@dreb/coding-agent 2.40.2 → 2.42.0

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.
Files changed (45) hide show
  1. package/README.md +22 -9
  2. package/dist/core/agent-session.d.ts +2 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +8 -4
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/context-trust.d.ts +44 -0
  7. package/dist/core/context-trust.d.ts.map +1 -0
  8. package/dist/core/context-trust.js +117 -0
  9. package/dist/core/context-trust.js.map +1 -0
  10. package/dist/core/nested-context.d.ts +4 -3
  11. package/dist/core/nested-context.d.ts.map +1 -1
  12. package/dist/core/nested-context.js +34 -9
  13. package/dist/core/nested-context.js.map +1 -1
  14. package/dist/core/resource-loader.d.ts +3 -1
  15. package/dist/core/resource-loader.d.ts.map +1 -1
  16. package/dist/core/resource-loader.js +29 -11
  17. package/dist/core/resource-loader.js.map +1 -1
  18. package/dist/core/settings-manager.d.ts +38 -2
  19. package/dist/core/settings-manager.d.ts.map +1 -1
  20. package/dist/core/settings-manager.js +161 -6
  21. package/dist/core/settings-manager.js.map +1 -1
  22. package/dist/modes/index.d.ts +1 -1
  23. package/dist/modes/index.d.ts.map +1 -1
  24. package/dist/modes/index.js.map +1 -1
  25. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/settings-selector.js +2 -2
  27. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  28. package/dist/modes/rpc/index.d.ts +1 -1
  29. package/dist/modes/rpc/index.d.ts.map +1 -1
  30. package/dist/modes/rpc/index.js.map +1 -1
  31. package/dist/modes/rpc/rpc-client.d.ts +22 -8
  32. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  33. package/dist/modes/rpc/rpc-client.js +67 -6
  34. package/dist/modes/rpc/rpc-client.js.map +1 -1
  35. package/dist/modes/rpc/rpc-mode.d.ts +57 -7
  36. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  37. package/dist/modes/rpc/rpc-mode.js +341 -70
  38. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  39. package/dist/modes/rpc/rpc-types.d.ts +114 -3
  40. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  41. package/dist/modes/rpc/rpc-types.js.map +1 -1
  42. package/docs/dashboard.md +29 -9
  43. package/docs/rpc.md +184 -10
  44. package/docs/settings.md +32 -5
  45. package/package.json +1 -1
@@ -7,13 +7,14 @@
7
7
  * Protocol:
8
8
  * - Commands: JSON objects with `type` field, optional `id` for correlation
9
9
  * - Responses: JSON objects with `type: "response"`, `command`, `success`, and optional `data`/`error`
10
- * - Events: AgentSessionEvent objects streamed as they occur
10
+ * - Events: RpcEvent objects streamed as they occur
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import * as crypto from "node:crypto";
14
14
  import { basename } from "node:path";
15
15
  import { isValidThinkingLevel, VALID_THINKING_LEVELS } from "../../cli/args.js";
16
16
  import { VERSION } from "../../config.js";
17
+ import { canonicalizeTrustedRoots, matchContextTrust, validateTrustedContextFolder, validateTrustedContextFolders, } from "../../core/context-trust.js";
17
18
  import { DailyCostTracker } from "../../core/daily-cost-tracker.js";
18
19
  import { getGitBranch } from "../../core/git-branch.js";
19
20
  import { parseModelPattern } from "../../core/model-resolver.js";
@@ -87,6 +88,7 @@ export function getStateForRpc(session, modelFallbackMessage) {
87
88
  return {
88
89
  model: session.model,
89
90
  scopedModels: getScopedModelsForRpc(session),
91
+ tasks: session.tasks.map((task) => ({ ...task })),
90
92
  usingSubscription: session.model ? session.modelRegistry.isUsingOAuth(session.model) : false,
91
93
  thinkingLevel: session.thinkingLevel,
92
94
  isStreaming: session.isStreaming,
@@ -152,11 +154,14 @@ export function listAgentTypesForRpc(cwd) {
152
154
  /**
153
155
  * Handle the `get_settings` RPC command: snapshot the persistent default settings.
154
156
  *
155
- * Reads the SettingsManager's merged (global + project) view these are the values that
156
- * seed fresh runtimes, NOT the live session state (`get_state` reports that). Extracted
157
+ * Most fields read the SettingsManager's merged (global + project) view. Context trust
158
+ * fields intentionally read global settings only, so projects cannot expand the policy.
159
+ * These seed fresh runtimes, NOT the live session state (`get_state` reports that). Extracted
157
160
  * (like {@link deleteSessionForRpc}) so it is unit-testable without a live RPC session.
158
161
  */
159
162
  export function getSettingsForRpc(settingsManager) {
163
+ const contextTrust = settingsManager.getGlobalContextTrustPolicy();
164
+ const configuredTrustedFolders = settingsManager.getConfiguredTrustedContextFolders();
160
165
  return {
161
166
  defaultProvider: settingsManager.getDefaultProvider(),
162
167
  defaultModel: settingsManager.getDefaultModel(),
@@ -168,12 +173,62 @@ export function getSettingsForRpc(settingsManager) {
168
173
  imageAutoResize: settingsManager.getImageAutoResize(),
169
174
  blockImages: settingsManager.getBlockImages(),
170
175
  enableSkillCommands: settingsManager.getEnableSkillCommands(),
171
- autoLoadNestedContext: settingsManager.getAutoLoadNestedContext(),
176
+ autoLoadNestedContext: contextTrust.unrestricted,
177
+ trustedContextFolders: configuredTrustedFolders,
178
+ effectiveTrustedContextRoots: canonicalizeTrustedRoots(contextTrust.trustedFolders),
172
179
  transport: settingsManager.getTransport(),
173
180
  hideThinkingBlock: settingsManager.getHideThinkingBlock(),
174
181
  agentModels: settingsManager.getAgentModels(),
175
182
  };
176
183
  }
184
+ function formatSettingsErrors(errors) {
185
+ return errors.map((entry) => `${entry.scope}: ${entry.error.message}`).join("; ");
186
+ }
187
+ /**
188
+ * Reload durable global and project settings before serving `get_settings`.
189
+ *
190
+ * The shared settings-operation lock prevents a refresh from discarding another RPC
191
+ * settings operation's queued modifications. A failed queued write is returned before
192
+ * reload so its in-memory changes remain intact rather than being replaced by stale disk
193
+ * content. Reload failures leave SettingsManager's previous values in memory, therefore
194
+ * they must be surfaced as an RPC error instead of producing a stale snapshot.
195
+ */
196
+ export async function getFreshSettingsForRpc(settingsManager) {
197
+ return settingsWriteLock(async () => {
198
+ try {
199
+ // A runtime setter may have enqueued a write outside the RPC settings lock.
200
+ // Waiting for SettingsManager's own queue protects that update from reload().
201
+ await settingsManager.flush();
202
+ }
203
+ catch (error) {
204
+ return { ok: false, error: `Failed to refresh settings: ${error.message}` };
205
+ }
206
+ const writeErrors = settingsManager.drainErrors();
207
+ if (writeErrors.length > 0) {
208
+ return {
209
+ ok: false,
210
+ error: `Failed to refresh settings: pending settings write failed: ${formatSettingsErrors(writeErrors)}`,
211
+ };
212
+ }
213
+ try {
214
+ settingsManager.reload();
215
+ }
216
+ catch (error) {
217
+ return { ok: false, error: `Failed to reload settings: ${error.message}` };
218
+ }
219
+ const loadErrors = settingsManager.drainErrors();
220
+ if (loadErrors.length > 0 ||
221
+ settingsManager.hasGlobalSettingsLoadError() ||
222
+ settingsManager.hasProjectSettingsLoadError()) {
223
+ const detail = formatSettingsErrors(loadErrors);
224
+ return {
225
+ ok: false,
226
+ error: detail ? `Failed to reload settings: ${detail}` : "Failed to reload settings from durable storage",
227
+ };
228
+ }
229
+ return { ok: true, settings: getSettingsForRpc(settingsManager) };
230
+ });
231
+ }
177
232
  const SETTINGS_UPDATE_KEYS = [
178
233
  "defaultProvider",
179
234
  "defaultModel",
@@ -186,6 +241,7 @@ const SETTINGS_UPDATE_KEYS = [
186
241
  "blockImages",
187
242
  "enableSkillCommands",
188
243
  "autoLoadNestedContext",
244
+ "trustedContextFolders",
189
245
  "transport",
190
246
  "hideThinkingBlock",
191
247
  "agentModels",
@@ -223,15 +279,36 @@ function validateAgentModels(value) {
223
279
  }
224
280
  return { ok: true };
225
281
  }
282
+ /** Evaluate a strictly canonical, absolute directory through the core trust matcher. */
283
+ export function evaluateContextTrustForRpc(settingsManager, path) {
284
+ let canonicalTarget;
285
+ try {
286
+ canonicalTarget = validateTrustedContextFolder(path);
287
+ }
288
+ catch (error) {
289
+ return { ok: false, error: `Invalid context trust path: ${error.message}` };
290
+ }
291
+ const policy = settingsManager.getGlobalContextTrustPolicy();
292
+ const match = matchContextTrust(policy, canonicalTarget);
293
+ if (!match) {
294
+ return { ok: true, evaluation: { canonicalTarget, state: "untrusted" } };
295
+ }
296
+ if (policy.unrestricted) {
297
+ return { ok: true, evaluation: { canonicalTarget, state: "unrestricted" } };
298
+ }
299
+ return {
300
+ ok: true,
301
+ evaluation: { canonicalTarget, state: "trusted-root", grantingRoot: match.trustedRoot },
302
+ };
303
+ }
226
304
  /**
227
- * Simple promise-based mutex that serializes settings writes. RPC commands are dispatched
228
- * concurrently (`void handleInputLine(line)`) so two `set_settings` commands can overlap.
229
- * Without serialization, concurrent commands race on SettingsManager's shared error bucket
230
- * (`drainErrors()` clears the array for everyone): the first to drain takes all errors
231
- * (including the second's), and the second reports false success despite its write failing.
305
+ * Simple promise-based mutex that serializes durable settings operations. RPC commands are
306
+ * dispatched concurrently (`void handleInputLine(line)`) so settings writes and refreshes
307
+ * can overlap. Without serialization, a refresh could reload over queued modifications, and
308
+ * concurrent operations could race on SettingsManager's shared error bucket (`drainErrors()`
309
+ * clears the array for everyone).
232
310
  *
233
- * This lock ensures only one apply+flush+drain block runs at a time — each `set_settings`
234
- * gets an error window isolated from other `set_settings` commands and from stale errors.
311
+ * This lock ensures only one apply/flush/drain or flush/reload/drain block runs at a time.
235
312
  *
236
313
  * Known limitation: the lock does NOT cover the per-runtime commands (`set_model`,
237
314
  * `set_steering_mode`, etc.), which persist through the same SettingsManager write queue
@@ -249,6 +326,122 @@ function settingsWriteLock(fn) {
249
326
  settingsWriteQueue = next.catch(() => { });
250
327
  return next;
251
328
  }
329
+ async function persistContextTrustMutationForRpc(settingsManager, folders, targetPath, mutation) {
330
+ settingsManager.drainErrors();
331
+ if (settingsManager.hasGlobalSettingsLoadError()) {
332
+ return {
333
+ ok: false,
334
+ error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
335
+ };
336
+ }
337
+ try {
338
+ settingsManager.setTrustedContextFolders(folders);
339
+ await settingsManager.flush();
340
+ }
341
+ catch (error) {
342
+ // A security-policy mutation must not remain effective solely in memory when its
343
+ // durable write fails. Reload restores the last persisted global policy.
344
+ settingsManager.reload();
345
+ return { ok: false, error: `Failed to persist settings: ${error.message}` };
346
+ }
347
+ const writeErrors = settingsManager.drainErrors();
348
+ if (writeErrors.length > 0) {
349
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
350
+ // SettingsManager records queued write failures rather than rejecting flush(). Its
351
+ // in-memory policy has already changed, so restore it from durable storage before
352
+ // reporting failure rather than leaving an undurable trusted root effective.
353
+ settingsManager.reload();
354
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
355
+ }
356
+ const evaluated = evaluateContextTrustForRpc(settingsManager, targetPath);
357
+ if (!evaluated.ok)
358
+ return evaluated;
359
+ return {
360
+ ok: true,
361
+ result: { evaluation: evaluated.evaluation, settings: getSettingsForRpc(settingsManager), ...mutation },
362
+ };
363
+ }
364
+ /** Add a canonical directory to the global trusted roots and flush the change. */
365
+ export async function trustContextFolderForRpc(settingsManager, path) {
366
+ return settingsWriteLock(async () => {
367
+ const evaluated = evaluateContextTrustForRpc(settingsManager, path);
368
+ if (!evaluated.ok)
369
+ return evaluated;
370
+ // Existing malformed entries are fail-closed. A trust mutation rewrites the configured
371
+ // list as the canonical configured roots plus this new canonical root.
372
+ const roots = canonicalizeTrustedRoots(settingsManager.getConfiguredTrustedContextFolders());
373
+ let folders;
374
+ try {
375
+ folders = validateTrustedContextFolders([...roots, evaluated.evaluation.canonicalTarget]);
376
+ }
377
+ catch (error) {
378
+ return { ok: false, error: error.message };
379
+ }
380
+ return persistContextTrustMutationForRpc(settingsManager, folders, evaluated.evaluation.canonicalTarget, {
381
+ ...(folders.includes(evaluated.evaluation.canonicalTarget)
382
+ ? { addedRoot: evaluated.evaluation.canonicalTarget }
383
+ : {}),
384
+ });
385
+ });
386
+ }
387
+ /** Remove the root actually granting a directory access, rather than the supplied descendant. */
388
+ export async function untrustContextFolderForRpc(settingsManager, path) {
389
+ return settingsWriteLock(async () => {
390
+ const evaluated = evaluateContextTrustForRpc(settingsManager, path);
391
+ if (!evaluated.ok)
392
+ return evaluated;
393
+ if (evaluated.evaluation.state === "unrestricted") {
394
+ return {
395
+ ok: false,
396
+ error: "Cannot untrust a context folder while unrestricted nested context loading is enabled; disable autoLoadNestedContext first",
397
+ };
398
+ }
399
+ if (evaluated.evaluation.state === "untrusted") {
400
+ return {
401
+ ok: true,
402
+ result: { evaluation: evaluated.evaluation, settings: getSettingsForRpc(settingsManager) },
403
+ };
404
+ }
405
+ const removedRoot = evaluated.evaluation.grantingRoot;
406
+ const roots = canonicalizeTrustedRoots(settingsManager.getConfiguredTrustedContextFolders());
407
+ return persistContextTrustMutationForRpc(settingsManager, roots.filter((root) => root !== removedRoot), evaluated.evaluation.canonicalTarget, { removedRoot });
408
+ });
409
+ }
410
+ /** Remove a configured trusted-folder string exactly as stored and flush the change. */
411
+ export async function removeTrustedContextFolderForRpc(settingsManager, path) {
412
+ return settingsWriteLock(async () => {
413
+ if (typeof path !== "string" || path.length === 0) {
414
+ return { ok: false, error: "remove_trusted_context_folder requires a non-empty string path" };
415
+ }
416
+ settingsManager.drainErrors();
417
+ if (settingsManager.hasGlobalSettingsLoadError()) {
418
+ return {
419
+ ok: false,
420
+ error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
421
+ };
422
+ }
423
+ try {
424
+ settingsManager.removeTrustedContextFolder(path);
425
+ await settingsManager.flush();
426
+ }
427
+ catch (error) {
428
+ // A security-policy mutation must not remain effective solely in memory when its
429
+ // durable write fails. Reload restores the last persisted global policy.
430
+ settingsManager.reload();
431
+ return { ok: false, error: `Failed to persist settings: ${error.message}` };
432
+ }
433
+ const writeErrors = settingsManager.drainErrors();
434
+ if (writeErrors.length > 0) {
435
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
436
+ // SettingsManager records queued write failures rather than rejecting flush(). Its
437
+ // in-memory policy has already changed, so restore it from durable storage before
438
+ // reporting failure rather than leaving an undurable trust removal effective.
439
+ settingsManager.reload();
440
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
441
+ }
442
+ return { ok: true, result: { settings: getSettingsForRpc(settingsManager), removedFolder: path } };
443
+ });
444
+ }
252
445
  /**
253
446
  * Handle the `set_settings` RPC command: validate and persist default settings.
254
447
  *
@@ -329,6 +522,15 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
329
522
  return validation;
330
523
  }
331
524
  }
525
+ let trustedContextFolders;
526
+ if (update.trustedContextFolders !== undefined) {
527
+ try {
528
+ trustedContextFolders = validateTrustedContextFolders(update.trustedContextFolders);
529
+ }
530
+ catch (error) {
531
+ return { ok: false, error: error.message };
532
+ }
533
+ }
332
534
  const hasProvider = update.defaultProvider !== undefined;
333
535
  const hasModel = update.defaultModel !== undefined;
334
536
  if (hasProvider !== hasModel) {
@@ -350,6 +552,7 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
350
552
  };
351
553
  }
352
554
  }
555
+ const updatesContextTrustPolicy = update.autoLoadNestedContext !== undefined || trustedContextFolders !== undefined;
353
556
  // Serialize the apply+flush+drain block so concurrent set_settings commands
354
557
  // cannot race on the shared error bucket. See settingsWriteLock doc.
355
558
  return settingsWriteLock(async () => {
@@ -367,69 +570,95 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
367
570
  error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
368
571
  };
369
572
  }
370
- // --- Apply (validated) ---
371
- if (update.defaultProvider !== undefined && update.defaultModel !== undefined) {
372
- settingsManager.setDefaultModelAndProvider(update.defaultProvider, update.defaultModel);
373
- }
374
- if (update.defaultThinkingLevel !== undefined) {
375
- settingsManager.setDefaultThinkingLevel(update.defaultThinkingLevel);
376
- }
377
- if (update.steeringMode !== undefined) {
378
- settingsManager.setSteeringMode(update.steeringMode);
379
- }
380
- if (update.followUpMode !== undefined) {
381
- settingsManager.setFollowUpMode(update.followUpMode);
382
- }
383
- if (update.compactionEnabled !== undefined) {
384
- settingsManager.setCompactionEnabled(update.compactionEnabled);
385
- }
386
- if (update.retryEnabled !== undefined) {
387
- settingsManager.setRetryEnabled(update.retryEnabled);
388
- }
389
- if (update.imageAutoResize !== undefined) {
390
- settingsManager.setImageAutoResize(update.imageAutoResize);
391
- }
392
- if (update.blockImages !== undefined) {
393
- settingsManager.setBlockImages(update.blockImages);
394
- }
395
- if (update.enableSkillCommands !== undefined) {
396
- settingsManager.setEnableSkillCommands(update.enableSkillCommands);
397
- }
398
- if (update.autoLoadNestedContext !== undefined) {
399
- settingsManager.setAutoLoadNestedContext(update.autoLoadNestedContext);
400
- }
401
- if (update.transport !== undefined) {
402
- settingsManager.setTransport(update.transport);
403
- }
404
- if (update.hideThinkingBlock !== undefined) {
405
- settingsManager.setHideThinkingBlock(update.hideThinkingBlock);
406
- }
407
- const warnings = [];
408
- if (update.agentModels !== undefined) {
409
- for (const [agentName, models] of Object.entries(update.agentModels)) {
410
- if (settingsManager.hasProjectAgentModelOverride(agentName)) {
411
- warnings.push(`A project-level agentModels override for ${JSON.stringify(agentName)} (.dreb/settings.json) ` +
412
- "takes precedence this change to global settings will have no effect. " +
413
- "Edit the project settings file to change it.");
414
- }
415
- if (models.length > 0) {
416
- settingsManager.setAgentModelsForAgent(agentName, models);
573
+ try {
574
+ // Phase 1: durably persist every ordinary setting before touching the
575
+ // security-sensitive context trust policy.
576
+ if (update.defaultProvider !== undefined && update.defaultModel !== undefined) {
577
+ settingsManager.setDefaultModelAndProvider(update.defaultProvider, update.defaultModel);
578
+ }
579
+ if (update.defaultThinkingLevel !== undefined) {
580
+ settingsManager.setDefaultThinkingLevel(update.defaultThinkingLevel);
581
+ }
582
+ if (update.steeringMode !== undefined) {
583
+ settingsManager.setSteeringMode(update.steeringMode);
584
+ }
585
+ if (update.followUpMode !== undefined) {
586
+ settingsManager.setFollowUpMode(update.followUpMode);
587
+ }
588
+ if (update.compactionEnabled !== undefined) {
589
+ settingsManager.setCompactionEnabled(update.compactionEnabled);
590
+ }
591
+ if (update.retryEnabled !== undefined) {
592
+ settingsManager.setRetryEnabled(update.retryEnabled);
593
+ }
594
+ if (update.imageAutoResize !== undefined) {
595
+ settingsManager.setImageAutoResize(update.imageAutoResize);
596
+ }
597
+ if (update.blockImages !== undefined) {
598
+ settingsManager.setBlockImages(update.blockImages);
599
+ }
600
+ if (update.enableSkillCommands !== undefined) {
601
+ settingsManager.setEnableSkillCommands(update.enableSkillCommands);
602
+ }
603
+ if (update.transport !== undefined) {
604
+ settingsManager.setTransport(update.transport);
605
+ }
606
+ if (update.hideThinkingBlock !== undefined) {
607
+ settingsManager.setHideThinkingBlock(update.hideThinkingBlock);
608
+ }
609
+ const warnings = [];
610
+ if (update.agentModels !== undefined) {
611
+ for (const [agentName, models] of Object.entries(update.agentModels)) {
612
+ if (settingsManager.hasProjectAgentModelOverride(agentName)) {
613
+ warnings.push(`A project-level agentModels override for ${JSON.stringify(agentName)} (.dreb/settings.json) ` +
614
+ "takes precedence this change to global settings will have no effect. " +
615
+ "Edit the project settings file to change it.");
616
+ }
617
+ if (models.length > 0) {
618
+ settingsManager.setAgentModelsForAgent(agentName, models);
619
+ }
620
+ else {
621
+ settingsManager.removeAgentModelsForAgent(agentName);
622
+ }
417
623
  }
418
- else {
419
- settingsManager.removeAgentModelsForAgent(agentName);
624
+ }
625
+ await settingsManager.flush();
626
+ const writeErrors = settingsManager.drainErrors();
627
+ if (writeErrors.length > 0) {
628
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
629
+ // Context trust has not yet been modified, so do not reload: doing so
630
+ // could discard unrelated pending in-memory state.
631
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
632
+ }
633
+ // Phase 2: apply both context fields in one queued, atomic write only after
634
+ // phase 1 is durably successful.
635
+ if (updatesContextTrustPolicy) {
636
+ settingsManager.setContextTrust({
637
+ ...(update.autoLoadNestedContext !== undefined ? { autoLoadNested: update.autoLoadNestedContext } : {}),
638
+ ...(trustedContextFolders !== undefined ? { trustedFolders: trustedContextFolders } : {}),
639
+ });
640
+ await settingsManager.flush();
641
+ const ctxErrors = settingsManager.drainErrors();
642
+ if (ctxErrors.length > 0) {
643
+ const detail = ctxErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
644
+ // The context write failed, so durable storage retains its previous,
645
+ // fail-closed policy. Restore that policy in memory before returning.
646
+ settingsManager.reload();
647
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
420
648
  }
421
649
  }
650
+ return warnings.length > 0
651
+ ? { ok: true, settings: getSettingsForRpc(settingsManager), warnings }
652
+ : { ok: true, settings: getSettingsForRpc(settingsManager) };
422
653
  }
423
- // Ensure durability and surface write errors instead of losing them.
424
- await settingsManager.flush();
425
- const writeErrors = settingsManager.drainErrors();
426
- if (writeErrors.length > 0) {
427
- const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
428
- return { ok: false, error: `Failed to persist settings: ${detail}` };
654
+ catch (error) {
655
+ if (updatesContextTrustPolicy) {
656
+ // Setters and flush() can also fail synchronously. The same fail-closed
657
+ // rollback is required before returning that persistence failure.
658
+ settingsManager.reload();
659
+ }
660
+ return { ok: false, error: `Failed to persist settings: ${error.message}` };
429
661
  }
430
- return warnings.length > 0
431
- ? { ok: true, settings: getSettingsForRpc(settingsManager), warnings }
432
- : { ok: true, settings: getSettingsForRpc(settingsManager) };
433
662
  });
434
663
  }
435
664
  function normalizePreview(text) {
@@ -873,6 +1102,16 @@ export async function runRpcMode(session, modelFallbackMessage) {
873
1102
  case "get_state": {
874
1103
  return success(id, "get_state", getStateForRpc(session, modelFallbackMessage));
875
1104
  }
1105
+ case "get_dashboard_snapshot": {
1106
+ const snapshotId = id ?? crypto.randomUUID();
1107
+ const data = {
1108
+ snapshotId,
1109
+ state: getStateForRpc(session, modelFallbackMessage),
1110
+ messages: [...session.messages],
1111
+ backgroundAgents: getBackgroundAgents().map(toRpcBackgroundAgentInfo),
1112
+ };
1113
+ return success(id, "get_dashboard_snapshot", data);
1114
+ }
876
1115
  case "get_resources": {
877
1116
  return success(id, "get_resources", getResourcesForRpc(session));
878
1117
  }
@@ -1117,7 +1356,8 @@ export async function runRpcMode(session, modelFallbackMessage) {
1117
1356
  // Settings (persistent defaults)
1118
1357
  // =================================================================
1119
1358
  case "get_settings": {
1120
- return success(id, "get_settings", getSettingsForRpc(session.settingsManager));
1359
+ const result = await getFreshSettingsForRpc(session.settingsManager);
1360
+ return result.ok ? success(id, "get_settings", result.settings) : error(id, "get_settings", result.error);
1121
1361
  }
1122
1362
  case "set_settings": {
1123
1363
  const result = await setSettingsForRpc(session.settingsManager, session.modelRegistry, command.settings);
@@ -1128,6 +1368,30 @@ export async function runRpcMode(session, modelFallbackMessage) {
1128
1368
  ? { ...result.settings, warnings: result.warnings }
1129
1369
  : result.settings);
1130
1370
  }
1371
+ case "evaluate_context_trust": {
1372
+ const result = evaluateContextTrustForRpc(session.settingsManager, command.path);
1373
+ return result.ok
1374
+ ? success(id, "evaluate_context_trust", result.evaluation)
1375
+ : error(id, "evaluate_context_trust", result.error);
1376
+ }
1377
+ case "trust_context_folder": {
1378
+ const result = await trustContextFolderForRpc(session.settingsManager, command.path);
1379
+ return result.ok
1380
+ ? success(id, "trust_context_folder", result.result)
1381
+ : error(id, "trust_context_folder", result.error);
1382
+ }
1383
+ case "untrust_context_folder": {
1384
+ const result = await untrustContextFolderForRpc(session.settingsManager, command.path);
1385
+ return result.ok
1386
+ ? success(id, "untrust_context_folder", result.result)
1387
+ : error(id, "untrust_context_folder", result.error);
1388
+ }
1389
+ case "remove_trusted_context_folder": {
1390
+ const result = await removeTrustedContextFolderForRpc(session.settingsManager, command.path);
1391
+ return result.ok
1392
+ ? success(id, "remove_trusted_context_folder", result.result)
1393
+ : error(id, "remove_trusted_context_folder", result.error);
1394
+ }
1131
1395
  // =================================================================
1132
1396
  // Version
1133
1397
  // =================================================================
@@ -1211,6 +1475,13 @@ export async function runRpcMode(session, modelFallbackMessage) {
1211
1475
  // Handle regular commands
1212
1476
  const command = parsed;
1213
1477
  const response = await handleCommand(command);
1478
+ // Emit the barrier before the response. Stdout preserves byte order even
1479
+ // when these lines arrive in separate chunks, so RuntimePool always records
1480
+ // the EventHub sequence before the response promise can resume.
1481
+ if (command.type === "get_dashboard_snapshot" && response.success) {
1482
+ const snapshot = response;
1483
+ output({ type: "dashboard_snapshot_barrier", snapshotId: snapshot.data.snapshotId });
1484
+ }
1214
1485
  output(response);
1215
1486
  // Check for deferred shutdown request (idle between commands)
1216
1487
  await checkShutdownRequested();