@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.
- package/README.md +22 -9
- package/dist/core/agent-session.d.ts +2 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -4
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/context-trust.d.ts +44 -0
- package/dist/core/context-trust.d.ts.map +1 -0
- package/dist/core/context-trust.js +117 -0
- package/dist/core/context-trust.js.map +1 -0
- package/dist/core/nested-context.d.ts +4 -3
- package/dist/core/nested-context.d.ts.map +1 -1
- package/dist/core/nested-context.js +34 -9
- package/dist/core/nested-context.js.map +1 -1
- package/dist/core/resource-loader.d.ts +3 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +29 -11
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +38 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +161 -6
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +2 -2
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/rpc/index.d.ts +1 -1
- package/dist/modes/rpc/index.d.ts.map +1 -1
- package/dist/modes/rpc/index.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +22 -8
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +67 -6
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +57 -7
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +341 -70
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +114 -3
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/dashboard.md +29 -9
- package/docs/rpc.md +184 -10
- package/docs/settings.md +32 -5
- 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:
|
|
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
|
-
*
|
|
156
|
-
*
|
|
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:
|
|
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
|
|
228
|
-
* concurrently (`void handleInputLine(line)`) so
|
|
229
|
-
* Without serialization,
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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();
|