log-llm-config-staging 1.3.79 → 1.3.81

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.
@@ -176,13 +176,13 @@ function runOneStep(dbPath, stateData, step) {
176
176
  ? composerState
177
177
  : undefined;
178
178
  for (const k of step.include_keys) {
179
- // Prefer the value inside composerState when present; blob root can be stale vs the UI.
180
- if (nested && k in nested && nested[k] !== undefined) {
181
- stateData[k] = nested[k];
182
- }
183
- else if (k in obj && obj[k] !== undefined) {
179
+ // Reactive blob root wins over nested composerState (matches policy_engine.scan_targets merge).
180
+ if (k in obj && obj[k] !== undefined) {
184
181
  stateData[k] = obj[k];
185
182
  }
183
+ else if (nested && k in nested && nested[k] !== undefined) {
184
+ stateData[k] = nested[k];
185
+ }
186
186
  }
187
187
  }
188
188
  return;
@@ -14,7 +14,7 @@ import { existsSync, readFileSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
17
- import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
17
+ import { readFileCollectionVscdbContract, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
18
18
  import { resolveRemediationConfigPath } from './remediation_config_path.js';
19
19
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
20
20
  import { loadEndpointBase } from '../sender/endpoint_config.js';
@@ -174,6 +174,47 @@ function verifyOpsApplied(configJson, settingPath, ops) {
174
174
  }
175
175
  return { ok: true, expected: null };
176
176
  }
177
+ function shouldMergeComposerShadowKeys(itemKeyFromPath, checkSettingPaths) {
178
+ if (itemKeyFromPath === 'composerState')
179
+ return true;
180
+ return checkSettingPaths.some((p) => p === 'composerState' || p.startsWith('composerState.'));
181
+ }
182
+ /**
183
+ * Cursor stores web-tool toggles on the reactive blob root and nested `composerState`.
184
+ * Policy scan merges root shadow keys into composerState; compliance must match or autofix
185
+ * never runs when nested values are stale (e.g. lastBrowserConnectionMode none vs root editor).
186
+ */
187
+ export function mergeComposerShadowKeysFromReactiveBlob(dbPath, merged) {
188
+ const contract = readFileCollectionVscdbContract();
189
+ const reactiveKey = contract?.reactive_storage_item_key?.trim();
190
+ const shadowKeys = contract?.composer_shadow_keys ?? [];
191
+ if (!reactiveKey || shadowKeys.length === 0)
192
+ return;
193
+ const reactiveWrapped = readVscdbItemTableJson(dbPath, reactiveKey);
194
+ if (reactiveWrapped === null)
195
+ return;
196
+ const blob = reactiveWrapped[reactiveKey];
197
+ if (!blob || typeof blob !== 'object' || Array.isArray(blob))
198
+ return;
199
+ const root = blob;
200
+ let cs = merged.composerState;
201
+ if (!cs || typeof cs !== 'object' || Array.isArray(cs)) {
202
+ const nested = root.composerState;
203
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
204
+ cs = { ...nested };
205
+ }
206
+ else {
207
+ cs = {};
208
+ }
209
+ merged.composerState = cs;
210
+ }
211
+ const composerState = cs;
212
+ for (const key of shadowKeys) {
213
+ if (Object.prototype.hasOwnProperty.call(root, key) && root[key] !== undefined) {
214
+ composerState[key] = root[key];
215
+ }
216
+ }
217
+ }
177
218
  /** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
178
219
  function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
179
220
  const resolvedPath = resolveRemediationConfigPath(configFilePath);
@@ -193,6 +234,9 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
193
234
  return { ok: false, reason: 'vscdb_read_failed' };
194
235
  Object.assign(merged, wrapped);
195
236
  }
237
+ if (shouldMergeComposerShadowKeys(itemKeyFromPath, checkSettingPaths)) {
238
+ mergeComposerShadowKeysFromReactiveBlob(dbPath, merged);
239
+ }
196
240
  return { ok: true, json: merged };
197
241
  }
198
242
  if (!existsSync(resolvedPath))
@@ -101,9 +101,13 @@ async function sendAllConfigFiles(configFiles, hardwareUuid, authKey) {
101
101
  const ok = await sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestId, manifest);
102
102
  hookRunLog(`hook-request update manifest result=${ok ? 'ok' : 'fail'}`);
103
103
  }
104
+ // Report failed uploads on finish so the backend holds those paths from OpenClaw prune.
104
105
  if (ingestSessionId && batchResult.accepted > 0) {
105
- const ok = await sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId);
106
- hookRunLog(`ingest-session finish result=${ok ? 'ok' : 'fail'}`);
106
+ const ok = await sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId, batchResult.failedArtifacts);
107
+ hookRunLog(`ingest-session finish result=${ok ? 'ok' : 'fail'}`
108
+ + (batchResult.failedArtifacts.length > 0
109
+ ? ` held=${batchResult.failedArtifacts.length} failed upload(s)`
110
+ : ''));
107
111
  }
108
112
  // Exit 0 if anything was persisted so the shell hook keeps last_log and throttles. Exit 1 only when
109
113
  // every item failed (e.g. SQLite locked on server) — otherwise partial success used to return 1,
@@ -11,6 +11,17 @@ import path from 'node:path';
11
11
  export const BATCH_CHUNK_SIZE = 20;
12
12
  const MAX_BATCH_SIZE_BYTES = 500 * 1024; // 500KB
13
13
  export { resolveRepoFromPath } from '../runtime/workspace_repo.js';
14
+ function recordFailedArtifacts(target, files, seen) {
15
+ for (const file of files) {
16
+ const file_path = canonicalCursorUserStateVscdbPath(file.file_path);
17
+ const file_type = file.file_type || '';
18
+ const key = `${file_type}\t${file_path}`;
19
+ if (!file_type || seen.has(key))
20
+ continue;
21
+ seen.add(key);
22
+ target.push({ file_path, file_type });
23
+ }
24
+ }
14
25
  function resolveApiBase(endpoint) {
15
26
  try {
16
27
  return new URL(endpoint).origin;
@@ -92,6 +103,8 @@ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequ
92
103
  const basePayloadSize = JSON.stringify({ hardware_uuid: hardwareUuid, metadata }).length + 800;
93
104
  const chunks = buildBatchChunks(configFiles, basePayloadSize);
94
105
  const totals = { accepted: 0, failed: 0 };
106
+ const failedArtifacts = [];
107
+ const failedArtifactKeys = new Set();
95
108
  let chunkIndex = 0;
96
109
  while (chunkIndex < chunks.length) {
97
110
  let chunk = chunks[chunkIndex];
@@ -118,11 +131,21 @@ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequ
118
131
  totals.accepted += typeof response.accepted === 'number' ? response.accepted : chunk.length;
119
132
  const failedList = Array.isArray(response.failed) ? response.failed : [];
120
133
  totals.failed += failedList.length;
121
- if (failedList.length > 0)
134
+ if (failedList.length > 0) {
122
135
  hookRunLog(`batch chunk failed items: ${JSON.stringify(failedList.slice(0, 3))}`);
136
+ for (const item of failedList) {
137
+ const idx = typeof item.index === 'number'
138
+ ? item.index
139
+ : -1;
140
+ if (idx >= 0 && idx < chunk.length) {
141
+ recordFailedArtifacts(failedArtifacts, [chunk[idx]], failedArtifactKeys);
142
+ }
143
+ }
144
+ }
123
145
  }
124
146
  else {
125
147
  totals.failed += chunk.length;
148
+ recordFailedArtifacts(failedArtifacts, chunk, failedArtifactKeys);
126
149
  hookRunLog(`batch chunk failed: ${response.error || response.message || response.status}`);
127
150
  }
128
151
  }
@@ -130,10 +153,11 @@ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequ
130
153
  const errorMessage = error instanceof Error ? error.message : String(error);
131
154
  hookRunLog(`batch chunk error: ${errorMessage}`);
132
155
  totals.failed += chunk.length;
156
+ recordFailedArtifacts(failedArtifacts, chunk, failedArtifactKeys);
133
157
  }
134
158
  chunkIndex++;
135
159
  }
136
- return totals;
160
+ return { ...totals, failedArtifacts };
137
161
  }
138
162
  async function sendIngestSessionStart(hardwareUuid, authKey) {
139
163
  const endpoint = loadEndpointBase();
@@ -153,10 +177,20 @@ async function sendIngestSessionStart(hardwareUuid, authKey) {
153
177
  return null;
154
178
  }
155
179
  }
156
- async function sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId) {
180
+ async function sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId, failedUploads = []) {
157
181
  const endpoint = loadEndpointBase();
158
182
  const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/ingest-session/finish/`;
159
- const payload = { hardware_uuid: hardwareUuid, action: 'ingest_session_finish', ingest_session_id: ingestSessionId };
183
+ const failed_uploads = [...failedUploads]
184
+ .map(({ file_path, file_type }) => ({ file_path, file_type }))
185
+ .sort((a, b) => a.file_path.localeCompare(b.file_path) || a.file_type.localeCompare(b.file_type));
186
+ const payload = {
187
+ hardware_uuid: hardwareUuid,
188
+ action: 'ingest_session_finish',
189
+ ingest_session_id: ingestSessionId,
190
+ };
191
+ if (failed_uploads.length > 0) {
192
+ payload.failed_uploads = failed_uploads;
193
+ }
160
194
  const signature = createSignature(payload, authKey.key);
161
195
  const body = { ...payload, signature, key_id: authKey.key_id || '' };
162
196
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config-staging",
3
- "version": "1.3.79",
3
+ "version": "1.3.81",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {