@dreb/coding-agent 2.40.2 → 2.41.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 +20 -7
  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 +36 -2
  19. package/dist/core/settings-manager.d.ts.map +1 -1
  20. package/dist/core/settings-manager.js +157 -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 +9 -1
  32. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  33. package/dist/modes/rpc/rpc-client.js +20 -0
  34. package/dist/modes/rpc/rpc-client.js.map +1 -1
  35. package/dist/modes/rpc/rpc-mode.d.ts +38 -5
  36. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  37. package/dist/modes/rpc/rpc-mode.js +266 -61
  38. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  39. package/dist/modes/rpc/rpc-types.d.ts +73 -2
  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 +10 -2
  43. package/docs/rpc.md +159 -8
  44. package/docs/settings.md +32 -5
  45. package/package.json +1 -1
@@ -14,6 +14,7 @@ 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";
@@ -152,11 +153,14 @@ export function listAgentTypesForRpc(cwd) {
152
153
  /**
153
154
  * Handle the `get_settings` RPC command: snapshot the persistent default settings.
154
155
  *
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
156
+ * Most fields read the SettingsManager's merged (global + project) view. Context trust
157
+ * fields intentionally read global settings only, so projects cannot expand the policy.
158
+ * These seed fresh runtimes, NOT the live session state (`get_state` reports that). Extracted
157
159
  * (like {@link deleteSessionForRpc}) so it is unit-testable without a live RPC session.
158
160
  */
159
161
  export function getSettingsForRpc(settingsManager) {
162
+ const contextTrust = settingsManager.getGlobalContextTrustPolicy();
163
+ const configuredTrustedFolders = settingsManager.getConfiguredTrustedContextFolders();
160
164
  return {
161
165
  defaultProvider: settingsManager.getDefaultProvider(),
162
166
  defaultModel: settingsManager.getDefaultModel(),
@@ -168,7 +172,9 @@ export function getSettingsForRpc(settingsManager) {
168
172
  imageAutoResize: settingsManager.getImageAutoResize(),
169
173
  blockImages: settingsManager.getBlockImages(),
170
174
  enableSkillCommands: settingsManager.getEnableSkillCommands(),
171
- autoLoadNestedContext: settingsManager.getAutoLoadNestedContext(),
175
+ autoLoadNestedContext: contextTrust.unrestricted,
176
+ trustedContextFolders: configuredTrustedFolders,
177
+ effectiveTrustedContextRoots: canonicalizeTrustedRoots(contextTrust.trustedFolders),
172
178
  transport: settingsManager.getTransport(),
173
179
  hideThinkingBlock: settingsManager.getHideThinkingBlock(),
174
180
  agentModels: settingsManager.getAgentModels(),
@@ -186,6 +192,7 @@ const SETTINGS_UPDATE_KEYS = [
186
192
  "blockImages",
187
193
  "enableSkillCommands",
188
194
  "autoLoadNestedContext",
195
+ "trustedContextFolders",
189
196
  "transport",
190
197
  "hideThinkingBlock",
191
198
  "agentModels",
@@ -223,6 +230,28 @@ function validateAgentModels(value) {
223
230
  }
224
231
  return { ok: true };
225
232
  }
233
+ /** Evaluate a strictly canonical, absolute directory through the core trust matcher. */
234
+ export function evaluateContextTrustForRpc(settingsManager, path) {
235
+ let canonicalTarget;
236
+ try {
237
+ canonicalTarget = validateTrustedContextFolder(path);
238
+ }
239
+ catch (error) {
240
+ return { ok: false, error: `Invalid context trust path: ${error.message}` };
241
+ }
242
+ const policy = settingsManager.getGlobalContextTrustPolicy();
243
+ const match = matchContextTrust(policy, canonicalTarget);
244
+ if (!match) {
245
+ return { ok: true, evaluation: { canonicalTarget, state: "untrusted" } };
246
+ }
247
+ if (policy.unrestricted) {
248
+ return { ok: true, evaluation: { canonicalTarget, state: "unrestricted" } };
249
+ }
250
+ return {
251
+ ok: true,
252
+ evaluation: { canonicalTarget, state: "trusted-root", grantingRoot: match.trustedRoot },
253
+ };
254
+ }
226
255
  /**
227
256
  * Simple promise-based mutex that serializes settings writes. RPC commands are dispatched
228
257
  * concurrently (`void handleInputLine(line)`) so two `set_settings` commands can overlap.
@@ -249,6 +278,122 @@ function settingsWriteLock(fn) {
249
278
  settingsWriteQueue = next.catch(() => { });
250
279
  return next;
251
280
  }
281
+ async function persistContextTrustMutationForRpc(settingsManager, folders, targetPath, mutation) {
282
+ settingsManager.drainErrors();
283
+ if (settingsManager.hasGlobalSettingsLoadError()) {
284
+ return {
285
+ ok: false,
286
+ error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
287
+ };
288
+ }
289
+ try {
290
+ settingsManager.setTrustedContextFolders(folders);
291
+ await settingsManager.flush();
292
+ }
293
+ catch (error) {
294
+ // A security-policy mutation must not remain effective solely in memory when its
295
+ // durable write fails. Reload restores the last persisted global policy.
296
+ settingsManager.reload();
297
+ return { ok: false, error: `Failed to persist settings: ${error.message}` };
298
+ }
299
+ const writeErrors = settingsManager.drainErrors();
300
+ if (writeErrors.length > 0) {
301
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
302
+ // SettingsManager records queued write failures rather than rejecting flush(). Its
303
+ // in-memory policy has already changed, so restore it from durable storage before
304
+ // reporting failure rather than leaving an undurable trusted root effective.
305
+ settingsManager.reload();
306
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
307
+ }
308
+ const evaluated = evaluateContextTrustForRpc(settingsManager, targetPath);
309
+ if (!evaluated.ok)
310
+ return evaluated;
311
+ return {
312
+ ok: true,
313
+ result: { evaluation: evaluated.evaluation, settings: getSettingsForRpc(settingsManager), ...mutation },
314
+ };
315
+ }
316
+ /** Add a canonical directory to the global trusted roots and flush the change. */
317
+ export async function trustContextFolderForRpc(settingsManager, path) {
318
+ return settingsWriteLock(async () => {
319
+ const evaluated = evaluateContextTrustForRpc(settingsManager, path);
320
+ if (!evaluated.ok)
321
+ return evaluated;
322
+ // Existing malformed entries are fail-closed. A trust mutation rewrites the configured
323
+ // list as the canonical configured roots plus this new canonical root.
324
+ const roots = canonicalizeTrustedRoots(settingsManager.getConfiguredTrustedContextFolders());
325
+ let folders;
326
+ try {
327
+ folders = validateTrustedContextFolders([...roots, evaluated.evaluation.canonicalTarget]);
328
+ }
329
+ catch (error) {
330
+ return { ok: false, error: error.message };
331
+ }
332
+ return persistContextTrustMutationForRpc(settingsManager, folders, evaluated.evaluation.canonicalTarget, {
333
+ ...(folders.includes(evaluated.evaluation.canonicalTarget)
334
+ ? { addedRoot: evaluated.evaluation.canonicalTarget }
335
+ : {}),
336
+ });
337
+ });
338
+ }
339
+ /** Remove the root actually granting a directory access, rather than the supplied descendant. */
340
+ export async function untrustContextFolderForRpc(settingsManager, path) {
341
+ return settingsWriteLock(async () => {
342
+ const evaluated = evaluateContextTrustForRpc(settingsManager, path);
343
+ if (!evaluated.ok)
344
+ return evaluated;
345
+ if (evaluated.evaluation.state === "unrestricted") {
346
+ return {
347
+ ok: false,
348
+ error: "Cannot untrust a context folder while unrestricted nested context loading is enabled; disable autoLoadNestedContext first",
349
+ };
350
+ }
351
+ if (evaluated.evaluation.state === "untrusted") {
352
+ return {
353
+ ok: true,
354
+ result: { evaluation: evaluated.evaluation, settings: getSettingsForRpc(settingsManager) },
355
+ };
356
+ }
357
+ const removedRoot = evaluated.evaluation.grantingRoot;
358
+ const roots = canonicalizeTrustedRoots(settingsManager.getConfiguredTrustedContextFolders());
359
+ return persistContextTrustMutationForRpc(settingsManager, roots.filter((root) => root !== removedRoot), evaluated.evaluation.canonicalTarget, { removedRoot });
360
+ });
361
+ }
362
+ /** Remove a configured trusted-folder string exactly as stored and flush the change. */
363
+ export async function removeTrustedContextFolderForRpc(settingsManager, path) {
364
+ return settingsWriteLock(async () => {
365
+ if (typeof path !== "string" || path.length === 0) {
366
+ return { ok: false, error: "remove_trusted_context_folder requires a non-empty string path" };
367
+ }
368
+ settingsManager.drainErrors();
369
+ if (settingsManager.hasGlobalSettingsLoadError()) {
370
+ return {
371
+ ok: false,
372
+ error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
373
+ };
374
+ }
375
+ try {
376
+ settingsManager.removeTrustedContextFolder(path);
377
+ await settingsManager.flush();
378
+ }
379
+ catch (error) {
380
+ // A security-policy mutation must not remain effective solely in memory when its
381
+ // durable write fails. Reload restores the last persisted global policy.
382
+ settingsManager.reload();
383
+ return { ok: false, error: `Failed to persist settings: ${error.message}` };
384
+ }
385
+ const writeErrors = settingsManager.drainErrors();
386
+ if (writeErrors.length > 0) {
387
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
388
+ // SettingsManager records queued write failures rather than rejecting flush(). Its
389
+ // in-memory policy has already changed, so restore it from durable storage before
390
+ // reporting failure rather than leaving an undurable trust removal effective.
391
+ settingsManager.reload();
392
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
393
+ }
394
+ return { ok: true, result: { settings: getSettingsForRpc(settingsManager), removedFolder: path } };
395
+ });
396
+ }
252
397
  /**
253
398
  * Handle the `set_settings` RPC command: validate and persist default settings.
254
399
  *
@@ -329,6 +474,15 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
329
474
  return validation;
330
475
  }
331
476
  }
477
+ let trustedContextFolders;
478
+ if (update.trustedContextFolders !== undefined) {
479
+ try {
480
+ trustedContextFolders = validateTrustedContextFolders(update.trustedContextFolders);
481
+ }
482
+ catch (error) {
483
+ return { ok: false, error: error.message };
484
+ }
485
+ }
332
486
  const hasProvider = update.defaultProvider !== undefined;
333
487
  const hasModel = update.defaultModel !== undefined;
334
488
  if (hasProvider !== hasModel) {
@@ -350,6 +504,7 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
350
504
  };
351
505
  }
352
506
  }
507
+ const updatesContextTrustPolicy = update.autoLoadNestedContext !== undefined || trustedContextFolders !== undefined;
353
508
  // Serialize the apply+flush+drain block so concurrent set_settings commands
354
509
  // cannot race on the shared error bucket. See settingsWriteLock doc.
355
510
  return settingsWriteLock(async () => {
@@ -367,69 +522,95 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
367
522
  error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
368
523
  };
369
524
  }
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);
525
+ try {
526
+ // Phase 1: durably persist every ordinary setting before touching the
527
+ // security-sensitive context trust policy.
528
+ if (update.defaultProvider !== undefined && update.defaultModel !== undefined) {
529
+ settingsManager.setDefaultModelAndProvider(update.defaultProvider, update.defaultModel);
530
+ }
531
+ if (update.defaultThinkingLevel !== undefined) {
532
+ settingsManager.setDefaultThinkingLevel(update.defaultThinkingLevel);
533
+ }
534
+ if (update.steeringMode !== undefined) {
535
+ settingsManager.setSteeringMode(update.steeringMode);
536
+ }
537
+ if (update.followUpMode !== undefined) {
538
+ settingsManager.setFollowUpMode(update.followUpMode);
539
+ }
540
+ if (update.compactionEnabled !== undefined) {
541
+ settingsManager.setCompactionEnabled(update.compactionEnabled);
542
+ }
543
+ if (update.retryEnabled !== undefined) {
544
+ settingsManager.setRetryEnabled(update.retryEnabled);
545
+ }
546
+ if (update.imageAutoResize !== undefined) {
547
+ settingsManager.setImageAutoResize(update.imageAutoResize);
548
+ }
549
+ if (update.blockImages !== undefined) {
550
+ settingsManager.setBlockImages(update.blockImages);
551
+ }
552
+ if (update.enableSkillCommands !== undefined) {
553
+ settingsManager.setEnableSkillCommands(update.enableSkillCommands);
554
+ }
555
+ if (update.transport !== undefined) {
556
+ settingsManager.setTransport(update.transport);
557
+ }
558
+ if (update.hideThinkingBlock !== undefined) {
559
+ settingsManager.setHideThinkingBlock(update.hideThinkingBlock);
560
+ }
561
+ const warnings = [];
562
+ if (update.agentModels !== undefined) {
563
+ for (const [agentName, models] of Object.entries(update.agentModels)) {
564
+ if (settingsManager.hasProjectAgentModelOverride(agentName)) {
565
+ warnings.push(`A project-level agentModels override for ${JSON.stringify(agentName)} (.dreb/settings.json) ` +
566
+ "takes precedence this change to global settings will have no effect. " +
567
+ "Edit the project settings file to change it.");
568
+ }
569
+ if (models.length > 0) {
570
+ settingsManager.setAgentModelsForAgent(agentName, models);
571
+ }
572
+ else {
573
+ settingsManager.removeAgentModelsForAgent(agentName);
574
+ }
417
575
  }
418
- else {
419
- settingsManager.removeAgentModelsForAgent(agentName);
576
+ }
577
+ await settingsManager.flush();
578
+ const writeErrors = settingsManager.drainErrors();
579
+ if (writeErrors.length > 0) {
580
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
581
+ // Context trust has not yet been modified, so do not reload: doing so
582
+ // could discard unrelated pending in-memory state.
583
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
584
+ }
585
+ // Phase 2: apply both context fields in one queued, atomic write only after
586
+ // phase 1 is durably successful.
587
+ if (updatesContextTrustPolicy) {
588
+ settingsManager.setContextTrust({
589
+ ...(update.autoLoadNestedContext !== undefined ? { autoLoadNested: update.autoLoadNestedContext } : {}),
590
+ ...(trustedContextFolders !== undefined ? { trustedFolders: trustedContextFolders } : {}),
591
+ });
592
+ await settingsManager.flush();
593
+ const ctxErrors = settingsManager.drainErrors();
594
+ if (ctxErrors.length > 0) {
595
+ const detail = ctxErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
596
+ // The context write failed, so durable storage retains its previous,
597
+ // fail-closed policy. Restore that policy in memory before returning.
598
+ settingsManager.reload();
599
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
420
600
  }
421
601
  }
602
+ return warnings.length > 0
603
+ ? { ok: true, settings: getSettingsForRpc(settingsManager), warnings }
604
+ : { ok: true, settings: getSettingsForRpc(settingsManager) };
422
605
  }
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}` };
606
+ catch (error) {
607
+ if (updatesContextTrustPolicy) {
608
+ // Setters and flush() can also fail synchronously. The same fail-closed
609
+ // rollback is required before returning that persistence failure.
610
+ settingsManager.reload();
611
+ }
612
+ return { ok: false, error: `Failed to persist settings: ${error.message}` };
429
613
  }
430
- return warnings.length > 0
431
- ? { ok: true, settings: getSettingsForRpc(settingsManager), warnings }
432
- : { ok: true, settings: getSettingsForRpc(settingsManager) };
433
614
  });
434
615
  }
435
616
  function normalizePreview(text) {
@@ -1128,6 +1309,30 @@ export async function runRpcMode(session, modelFallbackMessage) {
1128
1309
  ? { ...result.settings, warnings: result.warnings }
1129
1310
  : result.settings);
1130
1311
  }
1312
+ case "evaluate_context_trust": {
1313
+ const result = evaluateContextTrustForRpc(session.settingsManager, command.path);
1314
+ return result.ok
1315
+ ? success(id, "evaluate_context_trust", result.evaluation)
1316
+ : error(id, "evaluate_context_trust", result.error);
1317
+ }
1318
+ case "trust_context_folder": {
1319
+ const result = await trustContextFolderForRpc(session.settingsManager, command.path);
1320
+ return result.ok
1321
+ ? success(id, "trust_context_folder", result.result)
1322
+ : error(id, "trust_context_folder", result.error);
1323
+ }
1324
+ case "untrust_context_folder": {
1325
+ const result = await untrustContextFolderForRpc(session.settingsManager, command.path);
1326
+ return result.ok
1327
+ ? success(id, "untrust_context_folder", result.result)
1328
+ : error(id, "untrust_context_folder", result.error);
1329
+ }
1330
+ case "remove_trusted_context_folder": {
1331
+ const result = await removeTrustedContextFolderForRpc(session.settingsManager, command.path);
1332
+ return result.ok
1333
+ ? success(id, "remove_trusted_context_folder", result.result)
1334
+ : error(id, "remove_trusted_context_folder", result.error);
1335
+ }
1131
1336
  // =================================================================
1132
1337
  // Version
1133
1338
  // =================================================================