log-llm-config-staging 1.3.81 → 1.3.83
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/compliance_prompt_gate.js +19 -2
- package/dist/log_config_files/collection/directory_collector.js +45 -25
- package/dist/log_config_files/readers/composer_shadow_merge.js +67 -0
- package/dist/log_config_files/readers/cursor_shadow_merge_policy.js +46 -0
- package/dist/log_config_files/readers/vscdb_config_builder.js +3 -1
- package/dist/log_config_files/readers/vscdb_reactive_storage.js +62 -0
- package/dist/log_config_files/readers/vscdb_reader.js +77 -12
- package/dist/log_config_files/runtime/compliance_check.js +179 -159
- package/dist/log_config_files/runtime/main_runner.js +19 -7
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +138 -0
- package/dist/log_config_files/runtime/remediation_sync.js +99 -23
- package/dist/log_config_files/runtime/worktree_absent.js +50 -0
- package/dist/log_config_files/runtime/worktree_scanner.js +137 -0
- package/dist/log_config_files/sender/batch_sender.js +12 -1
- package/package.json +1 -1
|
@@ -6,7 +6,8 @@
|
|
|
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, } from './log_config_files/runtime/compliance_check.js';
|
|
10
|
+
import { isRemediationQuarantined } from './log_config_files/runtime/remediation_apply_tracking.js';
|
|
10
11
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
11
12
|
import { getRemediationInstructionsPath, readRemediationInstructionsFile } from './log_config_files/runtime/management_storage.js';
|
|
12
13
|
import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
|
|
@@ -179,6 +180,12 @@ export async function runCompliancePromptGate() {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
const status = runLocalRemediationComplianceCheck(agent);
|
|
183
|
+
// Always process pending post-restart rows (even when compliance is ok — apply may have stuck).
|
|
184
|
+
const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
|
|
185
|
+
if (postRestartVerify.outcomes.length > 0) {
|
|
186
|
+
await Promise.allSettled(postRestartVerify.reportPromises);
|
|
187
|
+
hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
|
|
188
|
+
}
|
|
182
189
|
// Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
|
|
183
190
|
// Upload those files fire-and-forget so the backend can resolve the finding immediately.
|
|
184
191
|
for (const e of status.secondarySatisfied ?? []) {
|
|
@@ -198,7 +205,17 @@ export async function runCompliancePromptGate() {
|
|
|
198
205
|
printAllowWithAdvisory(ide, advisory);
|
|
199
206
|
return;
|
|
200
207
|
}
|
|
201
|
-
const
|
|
208
|
+
const actionableViolations = status.violations.filter((v) => !isRemediationQuarantined(v.uuid));
|
|
209
|
+
if (actionableViolations.length === 0 && status.violations.length > 0) {
|
|
210
|
+
hookRunLog(`compliance_prompt_gate: all ${status.violations.length} violation(s) quarantined — allowing prompt silently (backend notified on quarantine)`);
|
|
211
|
+
logRemediationApplyFailure('prompt_gate_quarantined_silent_allow', {
|
|
212
|
+
reason: 'all violations quarantined; no dialog — enforcement paused on this machine',
|
|
213
|
+
quarantined_count: status.violations.length,
|
|
214
|
+
});
|
|
215
|
+
printAllow(ide);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(actionableViolations, agent);
|
|
202
219
|
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
220
|
if (fixed > 0) {
|
|
204
221
|
// Wait for all server reports before exiting so the POST lands.
|
|
@@ -50,38 +50,58 @@ function collectDirectoryEntries(t) {
|
|
|
50
50
|
}
|
|
51
51
|
return results;
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
const METADATA_DIR_SCAN_MAX_DEPTH = 8;
|
|
54
|
+
function matchesDirGlob(name, glob) {
|
|
55
|
+
if (glob === '*')
|
|
56
|
+
return true;
|
|
57
|
+
// **/*.jsonl → match any extension suffix after **/
|
|
58
|
+
if (glob.startsWith('**/')) {
|
|
59
|
+
const suffix = glob.slice(3);
|
|
60
|
+
if (suffix.startsWith('*.'))
|
|
61
|
+
return name.endsWith(suffix.slice(1));
|
|
62
|
+
return name === suffix;
|
|
63
|
+
}
|
|
64
|
+
if (glob.startsWith('*.'))
|
|
65
|
+
return name.endsWith(glob.slice(1));
|
|
66
|
+
return name === glob;
|
|
67
|
+
}
|
|
68
|
+
/** Walk dir tree (bounded depth) and return the file with the highest mtime matching dir_glob. */
|
|
69
|
+
function findNewestMatchingFile(dirPath, glob, depth, maxDepth) {
|
|
70
|
+
let best = null;
|
|
58
71
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const glob = t.dir_glob ?? '*';
|
|
63
|
-
const matchName = (name) => {
|
|
64
|
-
if (glob === '*')
|
|
65
|
-
return true;
|
|
66
|
-
if (glob.startsWith('*.'))
|
|
67
|
-
return name.endsWith(glob.slice(1));
|
|
68
|
-
return name === glob;
|
|
69
|
-
};
|
|
70
|
-
try {
|
|
71
|
-
for (const entry of readdirSync(t.path, { withFileTypes: true })) {
|
|
72
|
-
if (!entry.isFile() || !matchName(entry.name))
|
|
73
|
-
continue;
|
|
72
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
73
|
+
const fullPath = join(dirPath, entry.name);
|
|
74
|
+
if (entry.isFile() && matchesDirGlob(entry.name, glob)) {
|
|
74
75
|
try {
|
|
75
|
-
const fileStat = statSync(
|
|
76
|
-
if (fileStat.mtime >
|
|
77
|
-
|
|
78
|
-
bestPath = join(t.path, entry.name);
|
|
76
|
+
const fileStat = statSync(fullPath);
|
|
77
|
+
if (!best || fileStat.mtime > best.mtime) {
|
|
78
|
+
best = { path: fullPath, mtime: fileStat.mtime };
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
catch { /* ignore unreadable files */ }
|
|
82
82
|
}
|
|
83
|
+
else if (entry.isDirectory() && depth < maxDepth) {
|
|
84
|
+
const nested = findNewestMatchingFile(fullPath, glob, depth + 1, maxDepth);
|
|
85
|
+
if (nested && (!best || nested.mtime > best.mtime)) {
|
|
86
|
+
best = nested;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
83
89
|
}
|
|
84
|
-
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore unreadable dir */ }
|
|
92
|
+
return best;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* For metadata-style DIR targets: recursively scan files matching dir_glob, return one entry
|
|
96
|
+
* with the highest mtime found. Falls back to the directory's own mtime if no files match.
|
|
97
|
+
*/
|
|
98
|
+
function collectDirectoryMetadata(t) {
|
|
99
|
+
try {
|
|
100
|
+
const dirStat = statSync(t.path);
|
|
101
|
+
const glob = t.dir_glob ?? '*';
|
|
102
|
+
const newest = findNewestMatchingFile(t.path, glob, 0, METADATA_DIR_SCAN_MAX_DEPTH);
|
|
103
|
+
const bestPath = newest?.path ?? t.path;
|
|
104
|
+
const bestMtime = newest?.mtime ?? dirStat.mtime;
|
|
85
105
|
return {
|
|
86
106
|
file_type: t.file_type,
|
|
87
107
|
file_path: bestPath,
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
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) {
|