log-llm-config-staging 1.3.82 → 1.3.84

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/cli.js CHANGED
@@ -38,6 +38,12 @@ const main = async () => {
38
38
  await import('./compliance_check_runner.js');
39
39
  return;
40
40
  }
41
+ if (args[0] === 'dialog_prefs') {
42
+ const { runDialogPrefsCli } = await import('./dialog_prefs_cli.js');
43
+ await runDialogPrefsCli();
44
+ process.exit(0);
45
+ return;
46
+ }
41
47
  // `npx log-llm-config@latest <name>` runs this file (default bin) with args[0] set — not the named bin file.
42
48
  if (args[0] === 'execute-trusted-restarts') {
43
49
  const { runExecuteTrustedRestartsFromStdin } = await import('./execute_trusted_restarts.js');
@@ -6,9 +6,10 @@
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).
8
8
  */
9
- import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
9
+ import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, uploadSatisfiedManifestConfigs, } from './log_config_files/runtime/compliance_check.js';
10
+ import { isRemediationQuarantined, markRemediationApplyVerified, } from './log_config_files/runtime/remediation_apply_tracking.js';
10
11
  import { existsSync, readFileSync, statSync } from 'node:fs';
11
- import { getRemediationInstructionsPath, readRemediationInstructionsFile } from './log_config_files/runtime/management_storage.js';
12
+ import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
12
13
  import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
13
14
  import { isThisCliModule } from './cli_invocation_match.js';
14
15
  import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
@@ -16,6 +17,7 @@ import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
16
17
  import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid.js';
17
18
  import { syncRemediations } from './log_config_files/runtime/remediation_sync.js';
18
19
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
20
+ import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
19
21
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
20
22
  function parseIde() {
21
23
  const eq = process.argv.find((a) => a.startsWith('--ide='));
@@ -102,8 +104,11 @@ function uniquePolicyLabelsFromViolations(appliedViolations) {
102
104
  return policyNames.length > 0 ? policyNames.join('\n\n') : 'Agent security policy';
103
105
  }
104
106
  function formatPreventiveAutofixDialog(appliedViolations, applyLine) {
107
+ const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
108
+ const changePreviewBlock = changePreview ? `${changePreview}\n\n` : '';
105
109
  return ('Optimus Labs enforced a preventive agent security policy as determined by your security team:\n\n' +
106
110
  `${uniquePolicyLabelsFromViolations(appliedViolations)}\n\n` +
111
+ changePreviewBlock +
107
112
  `${applyLine}\n\n` +
108
113
  'Click OK to continue');
109
114
  }
@@ -165,20 +170,23 @@ export async function runCompliancePromptGate() {
165
170
  // unavailable server never delays the gate significantly.
166
171
  const hw = tryResolveHardwareUuid();
167
172
  if (hw) {
168
- const { remediations } = readRemediationInstructionsFile();
169
- if (Array.isArray(remediations) && remediations.length > 0) {
170
- try {
171
- await Promise.race([
172
- syncRemediations(loadEndpointBase(), hw),
173
- new Promise((resolve) => setTimeout(resolve, 3000)),
174
- ]);
175
- }
176
- catch {
177
- // Network or auth failure — fall back to local file state.
178
- }
173
+ try {
174
+ await Promise.race([
175
+ syncRemediations(loadEndpointBase(), hw),
176
+ new Promise((resolve) => setTimeout(resolve, 3000)),
177
+ ]);
178
+ }
179
+ catch {
180
+ // Network or auth failure — fall back to local file state.
179
181
  }
180
182
  }
181
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
+ }
182
190
  // Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
183
191
  // Upload those files fire-and-forget so the backend can resolve the finding immediately.
184
192
  for (const e of status.secondarySatisfied ?? []) {
@@ -198,7 +206,17 @@ export async function runCompliancePromptGate() {
198
206
  printAllowWithAdvisory(ide, advisory);
199
207
  return;
200
208
  }
201
- const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations, agent);
209
+ const actionableViolations = status.violations.filter((v) => !isRemediationQuarantined(v.uuid));
210
+ if (actionableViolations.length === 0 && status.violations.length > 0) {
211
+ hookRunLog(`compliance_prompt_gate: all ${status.violations.length} violation(s) quarantined — allowing prompt silently (backend notified on quarantine)`);
212
+ logRemediationApplyFailure('prompt_gate_quarantined_silent_allow', {
213
+ reason: 'all violations quarantined; no dialog — enforcement paused on this machine',
214
+ quarantined_count: status.violations.length,
215
+ });
216
+ printAllow(ide);
217
+ return;
218
+ }
219
+ const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(actionableViolations, agent);
202
220
  hookRunLog(`compliance_prompt_gate: autofix result ide=${ide} fixed=${fixed} applied=${appliedViolations.length} failed=${failedViolations.length} restart_commands=${restartCommands.length} deferred_sqlite_pending=${deferredSqlitePending}`);
203
221
  if (fixed > 0) {
204
222
  // Wait for all server reports before exiting so the POST lands.
@@ -231,13 +249,22 @@ export async function runCompliancePromptGate() {
231
249
  hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
232
250
  }
233
251
  if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
252
+ // For immediate (non-restart, non-deferred) fixes that the inline recheck confirmed,
253
+ // mark verified now so the next hook run does not false-quarantine due to pending flag.
254
+ if (recheckOk && restartCommands.length === 0 && !deferredSqlitePending) {
255
+ for (const v of appliedViolations) {
256
+ markRemediationApplyVerified(v.uuid);
257
+ }
258
+ }
259
+ const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
260
+ const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
234
261
  const autofixMessage = ide === 'cursor' && restartCommands.length > 0
235
262
  ? formatCursorRestartAutofixDialog(appliedViolations)
236
263
  : ide === 'claude'
237
264
  ? formatClaudeAutofixDialog(appliedViolations)
238
265
  : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
239
266
  .map((v) => autofixDialogLine(v))
240
- .join('\n')}`;
267
+ .join('\n')}${changePreviewSuffix}`;
241
268
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
242
269
  if (restartCommands.length > 0)
243
270
  payload.restart_commands = restartCommands;
@@ -283,6 +310,7 @@ export async function runCompliancePromptGate() {
283
310
  if (pruned.removed > 0) {
284
311
  await Promise.allSettled(pruned.reportPromises);
285
312
  }
313
+ await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
286
314
  printAllow(ide);
287
315
  }
288
316
  if (isRunAsCliModule()) {
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for macOS non-restart autofix dialogs and dialog preferences in ~/opt-ai-sec/management.
4
+ * Invoked from optimus-compliance-check.sh hooks.
5
+ */
6
+ import { execFileSync } from 'node:child_process';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { isNonRestartAutofixDialogSuppressed, setSuppressNonRestartAutofixDialogs, } from './log_config_files/runtime/dialog_preferences.js';
9
+ export const STOP_NOTIFICATIONS_BUTTON = 'Mute Alerts';
10
+ export const OK_BUTTON = 'OK';
11
+ function cliParts() {
12
+ const a = process.argv;
13
+ if (a[2] === 'dialog_prefs')
14
+ return a.slice(2);
15
+ return a.slice(2);
16
+ }
17
+ function parseArgValue(parts, flag) {
18
+ const eq = parts.find((p) => p.startsWith(`${flag}=`));
19
+ if (eq)
20
+ return eq.slice(flag.length + 1);
21
+ const idx = parts.indexOf(flag);
22
+ if (idx >= 0 && parts[idx + 1])
23
+ return parts[idx + 1];
24
+ return undefined;
25
+ }
26
+ function escapeAppleScriptString(text) {
27
+ return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
28
+ }
29
+ function icnsIconClause() {
30
+ const p = (process.env.OPTIMUS_DIALOG_ICNS ?? '').trim();
31
+ if (!p || !existsSync(p))
32
+ return 'with icon caution';
33
+ try {
34
+ const buf = readFileSync(p);
35
+ if (buf.length < 8 || buf.subarray(0, 4).toString('ascii') !== 'icns') {
36
+ return 'with icon caution';
37
+ }
38
+ }
39
+ catch {
40
+ return 'with icon caution';
41
+ }
42
+ return `with icon POSIX file "${escapeAppleScriptString(p)}"`;
43
+ }
44
+ export function buildNonRestartAutofixDialogScript(message, target) {
45
+ const msg = escapeAppleScriptString(message);
46
+ const icon = icnsIconClause();
47
+ const mute = escapeAppleScriptString(STOP_NOTIFICATIONS_BUTTON);
48
+ const body = `display dialog "${msg}" with title "Optimus Labs" buttons {"${mute}", "${OK_BUTTON}"} default button "${OK_BUTTON}" ${icon}`;
49
+ if (target === 'cursor') {
50
+ return `tell application "Cursor" to ${body}`;
51
+ }
52
+ if (target === 'claude-app') {
53
+ return `tell application "Claude" to ${body}`;
54
+ }
55
+ return body;
56
+ }
57
+ export function showNonRestartAutofixDialog(message, target) {
58
+ const script = buildNonRestartAutofixDialogScript(message, target);
59
+ let out = '';
60
+ try {
61
+ out = execFileSync('osascript', ['-'], { input: script, encoding: 'utf8' });
62
+ }
63
+ catch (err) {
64
+ const e = err;
65
+ out = typeof e.stdout === 'string' ? e.stdout : '';
66
+ // User dismissed or osascript failed — do not block callers; preference only set on explicit Stop.
67
+ if (!out.includes(STOP_NOTIFICATIONS_BUTTON)) {
68
+ throw err;
69
+ }
70
+ }
71
+ if (out.includes(STOP_NOTIFICATIONS_BUTTON)) {
72
+ setSuppressNonRestartAutofixDialogs(true);
73
+ return 'stop_notifications';
74
+ }
75
+ return 'ok';
76
+ }
77
+ function readMessageFile(path) {
78
+ if (!path || !existsSync(path)) {
79
+ throw new Error(`message file not found: ${path}`);
80
+ }
81
+ return readFileSync(path, 'utf8');
82
+ }
83
+ export async function runDialogPrefsCli() {
84
+ const parts = cliParts();
85
+ const sub = parts[1];
86
+ if (sub === 'is_suppressed') {
87
+ console.log(isNonRestartAutofixDialogSuppressed() ? 'true' : 'false');
88
+ return;
89
+ }
90
+ if (sub === 'set_suppress') {
91
+ setSuppressNonRestartAutofixDialogs(true);
92
+ return;
93
+ }
94
+ if (sub === 'show') {
95
+ const messageFile = parseArgValue(parts, '--message-file');
96
+ const targetRaw = parseArgValue(parts, '--target') ?? 'terminal';
97
+ if (targetRaw !== 'cursor' && targetRaw !== 'claude-app' && targetRaw !== 'terminal') {
98
+ throw new Error(`invalid --target: ${targetRaw}`);
99
+ }
100
+ const message = readMessageFile(messageFile ?? '');
101
+ showNonRestartAutofixDialog(message, targetRaw);
102
+ return;
103
+ }
104
+ throw new Error(`unknown dialog_prefs subcommand: ${sub ?? '(none)'}`);
105
+ }
@@ -0,0 +1,67 @@
1
+ import { CURSOR_SHADOW_KEYS_NESTED_WINS, coalesceCursorEnabledShadowValue, stripShadowKeysFromReactiveBlobUpload, } from './cursor_shadow_merge_policy.js';
2
+ const CURSOR_SHADOW_KEYS_MERGE_ENABLED_OR = new Set([
3
+ 'isWebSearchToolEnabled',
4
+ 'isWebSearchToolEnabled2',
5
+ ]);
6
+ /**
7
+ * Collect include_keys from composer_with_include read steps (backend-driven list).
8
+ */
9
+ export function shadowKeysFromReadQueries(readQueries) {
10
+ const keys = new Set();
11
+ for (const step of readQueries ?? []) {
12
+ if (step.value_kind === 'composer_with_include' && step.include_keys?.length) {
13
+ for (const k of step.include_keys) {
14
+ keys.add(k);
15
+ }
16
+ }
17
+ }
18
+ return [...keys];
19
+ }
20
+ /**
21
+ * Resolve one shadow key from reactive blob root + nested composerState (matches scan_targets).
22
+ */
23
+ export function mergeCursorShadowKeyValue(key, root, nested) {
24
+ const rootVal = Object.prototype.hasOwnProperty.call(root, key) ? root[key] : undefined;
25
+ const nestedVal = nested && Object.prototype.hasOwnProperty.call(nested, key) ? nested[key] : undefined;
26
+ if (CURSOR_SHADOW_KEYS_NESTED_WINS.has(key)) {
27
+ if (nestedVal !== undefined)
28
+ return nestedVal;
29
+ if (rootVal !== undefined)
30
+ return rootVal;
31
+ return undefined;
32
+ }
33
+ if (CURSOR_SHADOW_KEYS_MERGE_ENABLED_OR.has(key)) {
34
+ return coalesceCursorEnabledShadowValue(rootVal, nestedVal);
35
+ }
36
+ if (rootVal !== undefined)
37
+ return rootVal;
38
+ return nestedVal;
39
+ }
40
+ /**
41
+ * Merge shadow keys from the reactive blob into nested composerState (canonical for policy).
42
+ * Matches policy_engine.scan_targets; strips duplicate top-level keys from the upload shape.
43
+ */
44
+ export function mergeShadowKeysIntoComposerState(stateData, shadowKeys) {
45
+ if (!shadowKeys.length)
46
+ return;
47
+ const hasNested = stateData.composerState &&
48
+ typeof stateData.composerState === 'object' &&
49
+ !Array.isArray(stateData.composerState);
50
+ const hasRootKey = shadowKeys.some((k) => Object.prototype.hasOwnProperty.call(stateData, k));
51
+ if (!hasNested && !hasRootKey)
52
+ return;
53
+ let cs = stateData.composerState;
54
+ if (!hasNested) {
55
+ cs = {};
56
+ stateData.composerState = cs;
57
+ }
58
+ const composerState = cs;
59
+ const root = stateData;
60
+ for (const key of shadowKeys) {
61
+ const merged = mergeCursorShadowKeyValue(key, root, composerState);
62
+ if (merged !== undefined) {
63
+ composerState[key] = merged;
64
+ }
65
+ }
66
+ stripShadowKeysFromReactiveBlobUpload(stateData);
67
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Keep in sync with file_path_registry/cursor_composer_shadow_keys.py
3
+ * (CURSOR_REACTIVE_BLOB_SHADOW_KEYS + merge sets).
4
+ */
5
+ /** Keys Cursor duplicates on the reactive blob root and inside composerState. */
6
+ export const CURSOR_COMPOSER_SHADOW_KEYS = [
7
+ 'lastBrowserConnectionMode',
8
+ 'playwrightProtection',
9
+ 'isWebSearchToolEnabled',
10
+ 'isWebSearchToolEnabled2',
11
+ 'isWebSearchToolEnabled3',
12
+ 'isWebFetchToolEnabled',
13
+ 'autoAcceptWebSearchTool',
14
+ ];
15
+ /** Cursor Settings UI writes these on nested composerState; blob root copies are often stale. */
16
+ export const CURSOR_SHADOW_KEYS_NESTED_WINS = new Set([
17
+ 'playwrightProtection',
18
+ 'isWebFetchToolEnabled',
19
+ 'isWebSearchToolEnabled3',
20
+ 'autoAcceptWebSearchTool',
21
+ ]);
22
+ export function cursorShadowValueIsEnabled(value) {
23
+ return (value === true ||
24
+ value === 1 ||
25
+ (typeof value === 'string' && ['true', '1', 'yes'].includes(value.toLowerCase())));
26
+ }
27
+ /** Prefer nested when enabled (web search / auto-accept); else root. */
28
+ export function coalesceCursorEnabledShadowValue(root, nested) {
29
+ if (cursorShadowValueIsEnabled(nested))
30
+ return nested;
31
+ if (cursorShadowValueIsEnabled(root))
32
+ return root;
33
+ if (root !== undefined && root !== null)
34
+ return root;
35
+ return nested;
36
+ }
37
+ /**
38
+ * After merge, policy and uploads use composerState.<key> only — drop stale blob-root copies.
39
+ */
40
+ export function stripShadowKeysFromReactiveBlobUpload(stateData) {
41
+ if (!('composerState' in stateData))
42
+ return;
43
+ for (const key of CURSOR_COMPOSER_SHADOW_KEYS) {
44
+ delete stateData[key];
45
+ }
46
+ }
@@ -65,7 +65,9 @@ function buildRawContent(state, stateKey, value, includeKeys, ts) {
65
65
  else {
66
66
  raw[stateKey] = value;
67
67
  }
68
- if (includeKeys?.length) {
68
+ // Shadow keys are merged into composerState during readVSCDBState; do not copy stale blob-root
69
+ // copies onto the upload payload (policy paths are composerState.<key> only).
70
+ if (includeKeys?.length && stateKey !== 'composerState') {
69
71
  for (const k of includeKeys) {
70
72
  if (k in state && state[k] !== undefined)
71
73
  raw[k] = state[k];
@@ -0,0 +1,62 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readFileCollectionVscdbContract } from '../runtime/management_storage.js';
4
+ import { resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
5
+ import { CURSOR_COMPOSER_SHADOW_KEYS } from './cursor_shadow_merge_policy.js';
6
+ /** Suffix shared by Cursor reactive-storage ItemTable keys (see file_path_registry cursor agent). */
7
+ export const REACTIVE_STORAGE_ITEM_KEY_SUFFIX = 'reactiveStorageServiceImpl.persistentStorage.applicationUser';
8
+ function querySqliteValue(dbPath, key) {
9
+ const bin = resolveSqlite3Binary();
10
+ if (!bin)
11
+ return '';
12
+ const safe = key.replace(/'/g, "''");
13
+ const script = `.timeout 60000\nSELECT value FROM ItemTable WHERE key='${safe}';\n`;
14
+ return execFileSync(bin, ['-noheader', dbPath], {
15
+ input: script,
16
+ encoding: 'utf8',
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ }).trim();
19
+ }
20
+ /** When the cached contract is empty/stale, discover the live reactive blob row in state.vscdb. */
21
+ export function discoverReactiveStorageItemKeyInVscdb(dbPath) {
22
+ if (!existsSync(dbPath) || !resolveSqlite3Binary())
23
+ return undefined;
24
+ try {
25
+ const suffix = REACTIVE_STORAGE_ITEM_KEY_SUFFIX.replace(/'/g, "''");
26
+ const key = execFileSync(resolveSqlite3Binary(), [
27
+ dbPath,
28
+ `SELECT key FROM ItemTable WHERE key LIKE '%${suffix}%' LIMIT 1;`,
29
+ ], { encoding: 'utf8' }).trim();
30
+ if (!key || /['"\\]/.test(key))
31
+ return undefined;
32
+ const raw = querySqliteValue(dbPath, key);
33
+ if (!raw || raw === '{}')
34
+ return undefined;
35
+ return key;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ export function reactiveStorageItemKeyFromContract() {
42
+ const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
43
+ return typeof k === 'string' && k.trim() !== '' ? k.trim() : undefined;
44
+ }
45
+ export function reactiveStorageItemKeyForVscdb(dbPath) {
46
+ const fromContract = reactiveStorageItemKeyFromContract();
47
+ if (fromContract)
48
+ return fromContract;
49
+ if (dbPath)
50
+ return discoverReactiveStorageItemKeyInVscdb(dbPath);
51
+ return undefined;
52
+ }
53
+ export function composerShadowKeysFromContract() {
54
+ const fromContract = readFileCollectionVscdbContract()?.composer_shadow_keys ?? [];
55
+ if (fromContract.length > 0)
56
+ return [...fromContract];
57
+ return [...CURSOR_COMPOSER_SHADOW_KEYS];
58
+ }
59
+ export function reactiveBlobHasUsableContent(dbPath, reactiveKey) {
60
+ const raw = querySqliteValue(dbPath, reactiveKey).trim();
61
+ return raw !== '' && raw !== '{}';
62
+ }
@@ -1,7 +1,9 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
4
- import { readFileCollectionVscdbContract, sanitizeFileCollectionVscdbContract, sanitizeReactiveStorageItemKey, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
4
+ import { mergeCursorShadowKeyValue, mergeShadowKeysIntoComposerState, shadowKeysFromReadQueries, } from './composer_shadow_merge.js';
5
+ import { composerShadowKeysFromContract, reactiveStorageItemKeyForVscdb, } from './vscdb_reactive_storage.js';
6
+ import { readFileCollectionVscdbContract, sanitizeFileCollectionVscdbContract, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
5
7
  /**
6
8
  * ItemTable keys that store a bare JSON boolean; compliance paths use `${key}.${field}`.
7
9
  * Kept in sync with `coerceItemTableValueToObjectRoot` / `serializeItemTableValueForWrite` in remediation_sync.
@@ -63,8 +65,16 @@ export function normalizeVscdbComposerContractFromPatternsResponse(resp) {
63
65
  /** Persist backend-derived contract next to remediation_instructions.json. */
64
66
  export function persistVscdbComposerContractFromPatternsResponse(resp) {
65
67
  const norm = normalizeVscdbComposerContractFromPatternsResponse(resp);
66
- if (norm.reactive_storage_item_key || norm.composer_shadow_keys.length > 0) {
67
- writeFileCollectionVscdbContract(norm);
68
+ const existing = readFileCollectionVscdbContract();
69
+ const merged = {
70
+ version: 1,
71
+ reactive_storage_item_key: norm.reactive_storage_item_key ?? existing?.reactive_storage_item_key ?? undefined,
72
+ composer_shadow_keys: norm.composer_shadow_keys.length > 0
73
+ ? norm.composer_shadow_keys
74
+ : [...(existing?.composer_shadow_keys ?? [])],
75
+ };
76
+ if (merged.reactive_storage_item_key || merged.composer_shadow_keys.length > 0) {
77
+ writeFileCollectionVscdbContract(merged);
68
78
  }
69
79
  }
70
80
  /**
@@ -87,8 +97,7 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
87
97
  }
88
98
  try {
89
99
  const raw = querySqlite(dbPath, itemKey);
90
- const contract = readFileCollectionVscdbContract();
91
- const reactiveKey = sanitizeReactiveStorageItemKey(contract?.reactive_storage_item_key) ?? '';
100
+ const reactiveKey = reactiveStorageItemKeyForVscdb(dbPath) ?? '';
92
101
  if (itemKey === 'composerState' && (!raw || raw === '{}') && reactiveKey) {
93
102
  const reactive = querySqlite(dbPath, reactiveKey);
94
103
  if (reactive) {
@@ -137,6 +146,62 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
137
146
  return null;
138
147
  }
139
148
  }
149
+ /**
150
+ * Cursor stores web-tool toggles on the reactive blob root and nested `composerState`.
151
+ * Merge root shadow keys into nested composerState (matches policy_engine.scan_targets).
152
+ */
153
+ export function mergeComposerShadowKeysFromReactiveBlob(dbPath, merged) {
154
+ const reactiveKey = reactiveStorageItemKeyForVscdb(dbPath)?.trim();
155
+ const shadowKeys = composerShadowKeysFromContract();
156
+ if (!reactiveKey || shadowKeys.length === 0)
157
+ return;
158
+ const reactiveWrapped = readVscdbItemTableJson(dbPath, reactiveKey);
159
+ if (reactiveWrapped === null)
160
+ return;
161
+ const blob = reactiveWrapped[reactiveKey];
162
+ if (!blob || typeof blob !== 'object' || Array.isArray(blob))
163
+ return;
164
+ const root = blob;
165
+ let cs = merged.composerState;
166
+ if (!cs || typeof cs !== 'object' || Array.isArray(cs)) {
167
+ const nested = root.composerState;
168
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
169
+ cs = { ...nested };
170
+ }
171
+ else {
172
+ cs = {};
173
+ }
174
+ merged.composerState = cs;
175
+ }
176
+ const composerState = cs;
177
+ const nestedRoot = root.composerState && typeof root.composerState === 'object' && !Array.isArray(root.composerState)
178
+ ? root.composerState
179
+ : undefined;
180
+ for (const key of shadowKeys) {
181
+ const value = mergeCursorShadowKeyValue(key, root, nestedRoot ?? composerState);
182
+ if (value !== undefined) {
183
+ composerState[key] = value;
184
+ }
185
+ }
186
+ }
187
+ /**
188
+ * After a deferred state.vscdb write, upload the live reactive blob (not stale legacy composerState row).
189
+ * Return the full blob so the server can apply per-key shadow merge (same as log-config ingest).
190
+ */
191
+ export function buildVscdbPostApplyRawContent(dbPath, itemKeyFromPath) {
192
+ if (itemKeyFromPath === 'composerState') {
193
+ const reactiveKey = reactiveStorageItemKeyForVscdb(dbPath);
194
+ if (reactiveKey) {
195
+ const reactiveWrapped = readVscdbItemTableJson(dbPath, reactiveKey);
196
+ const blob = reactiveWrapped?.[reactiveKey];
197
+ if (blob && typeof blob === 'object' && !Array.isArray(blob)) {
198
+ return { ...blob };
199
+ }
200
+ }
201
+ return readVscdbItemTableJson(dbPath, itemKeyFromPath);
202
+ }
203
+ return readVscdbItemTableJson(dbPath, itemKeyFromPath);
204
+ }
140
205
  function setNested(obj, dotPath, value) {
141
206
  const parts = dotPath.split('.');
142
207
  const safe = (k) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype';
@@ -172,16 +237,15 @@ function runOneStep(dbPath, stateData, step) {
172
237
  stateData.composerState = composerState;
173
238
  }
174
239
  if (step.include_keys?.length) {
175
- const nested = composerState && typeof composerState === 'object'
240
+ const nested = composerState && typeof composerState === 'object' && !Array.isArray(composerState)
176
241
  ? composerState
177
242
  : undefined;
243
+ if (!nested)
244
+ return;
178
245
  for (const k of step.include_keys) {
179
- // Reactive blob root wins over nested composerState (matches policy_engine.scan_targets merge).
180
- if (k in obj && obj[k] !== undefined) {
181
- stateData[k] = obj[k];
182
- }
183
- else if (nested && k in nested && nested[k] !== undefined) {
184
- stateData[k] = nested[k];
246
+ const merged = mergeCursorShadowKeyValue(k, obj, nested);
247
+ if (merged !== undefined) {
248
+ nested[k] = merged;
185
249
  }
186
250
  }
187
251
  }
@@ -243,6 +307,7 @@ export function readVSCDBState(dbPath, readQueries, mergeFromComposerStateKeys)
243
307
  if (mergeFromComposerStateKeys?.length) {
244
308
  mergeFromComposerState(stateData, mergeFromComposerStateKeys);
245
309
  }
310
+ mergeShadowKeysIntoComposerState(stateData, shadowKeysFromReadQueries(readQueries));
246
311
  return Object.keys(stateData).length > 0 ? stateData : null;
247
312
  }
248
313
  catch (error) {