@edspencer/paddock 0.71.1 → 0.72.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/package.json +1 -1
- package/packages/server/dist/app.js +27 -0
- package/packages/server/dist/cli/args.d.ts +67 -8
- package/packages/server/dist/cli/args.js +213 -33
- package/packages/server/dist/cli/config.d.ts +157 -0
- package/packages/server/dist/cli/config.js +462 -0
- package/packages/server/dist/cli/paddock.js +53 -3
- package/packages/server/dist/cli/service/backend.d.ts +33 -0
- package/packages/server/dist/cli/service/index.d.ts +3 -3
- package/packages/server/dist/cli/service/index.js +124 -2
- package/packages/server/dist/cli/service/launchd.d.ts +27 -4
- package/packages/server/dist/cli/service/launchd.js +88 -14
- package/packages/server/dist/cli/service/systemd.d.ts +21 -2
- package/packages/server/dist/cli/service/systemd.js +58 -5
- package/packages/server/dist/config.d.ts +36 -1
- package/packages/server/dist/config.js +59 -44
- package/packages/server/dist/herdctl.d.ts +45 -0
- package/packages/server/dist/herdctl.js +58 -4
- package/packages/server/dist/instance-config.d.ts +149 -21
- package/packages/server/dist/instance-config.js +228 -5
- package/packages/server/dist/last-activity.d.ts +21 -0
- package/packages/server/dist/last-activity.js +7 -2
- package/packages/server/dist/profiles.d.ts +154 -0
- package/packages/server/dist/profiles.js +214 -0
- package/packages/server/dist/routes/transcripts.d.ts +61 -0
- package/packages/server/dist/routes/transcripts.js +676 -0
- package/packages/server/dist/routes.d.ts +3 -0
- package/packages/server/dist/routes.js +2 -0
- package/packages/server/dist/transcript-move.d.ts +62 -0
- package/packages/server/dist/transcript-move.js +179 -0
- package/packages/server/dist/transcripts-migration-execute.d.ts +144 -0
- package/packages/server/dist/transcripts-migration-execute.js +630 -0
- package/packages/server/dist/transcripts-migration.d.ts +336 -0
- package/packages/server/dist/transcripts-migration.js +917 -0
- package/packages/server/dist/transcripts.d.ts +46 -2
- package/packages/server/dist/transcripts.js +126 -3
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/DiscoverPage-DXI9FjmJ.js +2 -0
- package/packages/web/dist/assets/DiscoverView-D9oqVkT4.js +2 -0
- package/packages/web/dist/assets/InstanceConfigPage-DyH1j5oR.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-DdoNl0Ao.js → MarkdownRenderer-D95ofnQ8.js} +1 -1
- package/packages/web/dist/assets/ProjectMenu-BO70tGwy.js +2 -0
- package/packages/web/dist/assets/{ProjectRedirect-QuGeN-qx.js → ProjectRedirect-BxX_aADU.js} +1 -1
- package/packages/web/dist/assets/ProjectView-BbCyKoh5.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-CUFNVTif.js → ProjectsGrid-BafOHL43.js} +1 -1
- package/packages/web/dist/assets/RootHome-BGkMypUJ.js +2 -0
- package/packages/web/dist/assets/Surfaces-B8SyV4r3.js +2 -0
- package/packages/web/dist/assets/{arc-C0DRwCCT.js → arc-DGyr--oL.js} +1 -1
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-BNczNNmx.js +37 -0
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DKmCbgWk.js → blockDiagram-GPEHLZMM-DiTvGSrV.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CJ8LiLsj.js → c4Diagram-AAUBKEIU-CKZ_jy81.js} +1 -1
- package/packages/web/dist/assets/channel-CjwDi1A9.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-C3P-O4C3.js → chunk-2J33WTMH-5o96lthu.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Cs9u7x61.js → chunk-4BX2VUAB-38_pZfd8.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-C1st284U.js → chunk-55IACEB6-Db73LvlE.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-BjCAWo7B.js → chunk-727SXJPM-C-Q1-wUu.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BP6S9w2Q.js → chunk-AQP2D5EJ-ClJigITk.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-C3ySTaPs.js → chunk-FMBD7UC4-BVzSX2hw.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-eNPD4vzc.js → chunk-ND2GUHAM-DJlL-ELa.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-BbBpgHkO.js → chunk-QZHKN3VN-BC0QEk6w.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CxiNtY3i.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CxiNtY3i.js +2 -0
- package/packages/web/dist/assets/{core-ZwDDec0y.js → core-VUdoTe9w.js} +1 -1
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-DVqArcBy.js +2 -0
- package/packages/web/dist/assets/{dagre-BM42HDAG-DKIEUM4M.js → dagre-BM42HDAG-BgFGyUdv.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-CqlX7j8t.js → diagram-2AECGRRQ-BqdZmJt3.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Dph5apqo.js → diagram-5GNKFQAL-DeLPu8jT.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-TW6LyHGt.js → diagram-KO2AKTUF-BqTiTGay.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BxxIIv5a.js → diagram-LMA3HP47-CbpAvCgl.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-B8M0DeV7.js → diagram-OG6HWLK6-C4h0V97s.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Bfpy6UQa.js → erDiagram-TEJ5UH35-Dwz77uRx.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DstRzxek.js → flowDiagram-I6XJVG4X-DSikJnZR.js} +1 -1
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-DkAGNZb3.js +293 -0
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CX4IozE-.js → gitGraphDiagram-PVQCEYII-BQtzt9hP.js} +1 -1
- package/packages/web/dist/assets/index-BN3rLMpD.js +71 -0
- package/packages/web/dist/assets/index-Dfzb1DvL.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-CG2qCk6u.js → infoDiagram-5YYISTIA-YcaBUjeC.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-CPErvl3a.js → ishikawaDiagram-YF4QCWOH-BfLZB88u.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Bm6krUsf.js → journeyDiagram-JHISSGLW-DwaFhd6C.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-B4CC8CAx.js → kanban-definition-UN3LZRKU-e9P3WxsD.js} +1 -1
- package/packages/web/dist/assets/{linear-CJ1DbrOX.js → linear-CsXS5LTz.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-Dk2kkhf9.js → mermaid.core-DsA72Qyi.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CThvf_YD.js → mindmap-definition-RKZ34NQL-DyLOITDI.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DvkHcng6.js → pieDiagram-4H26LBE5-C36aM3D-.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-CqhrTh9q.js → quadrantDiagram-W4KKPZXB-Cq1zTk6a.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CZykiLym.js → requirementDiagram-4Y6WPE33-B6MctUk7.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-D-TzCKMx.js → sankeyDiagram-5OEKKPKP-CaNRUoPR.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C5pIItVu.js → sequenceDiagram-3UESZ5HK-DDj_tZiQ.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DE6G2JfB.js → stateDiagram-AJRCARHV-DDaMlLyK.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Bu6Swk2T.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C1VK2OfU.js → timeline-definition-PNZ67QCA-bMP-7Wam.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CtOozEwa.js → vennDiagram-CIIHVFJN-9nzKgc0Y.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DvHGK-lb.js → wardley-L42UT6IY-DIWbAFW5.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DxKwZ3-G.js → wardleyDiagram-YWT4CUSO-CVL3skbs.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-HM4j3l6y.js → xychartDiagram-2RQKCTM6-DHoVPvll.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/Chip-Db--PJFH.js +0 -2
- package/packages/web/dist/assets/DiscoverPage-BFo4FTzZ.js +0 -2
- package/packages/web/dist/assets/DiscoverView-C4hOB3nr.js +0 -2
- package/packages/web/dist/assets/InstanceConfigPage-U78M95nW.js +0 -2
- package/packages/web/dist/assets/ProjectMenu-BxQu14ze.js +0 -2
- package/packages/web/dist/assets/ProjectView-FUcyJMUf.js +0 -13
- package/packages/web/dist/assets/RootHome-SIxo-xX9.js +0 -2
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Wwpf4cvW.js +0 -37
- package/packages/web/dist/assets/channel-DEmtNKeB.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-dAGAuccL.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-dAGAuccL.js +0 -2
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-DiM4eeF5.js +0 -2
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-Bg0oFbAl.js +0 -293
- package/packages/web/dist/assets/index-ASW1Jl0Y.js +0 -71
- package/packages/web/dist/assets/index-bC5uCujx.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BP6oowh1.js +0 -2
|
@@ -24,12 +24,13 @@ import { fileURLToPath } from "node:url";
|
|
|
24
24
|
import YAML from "yaml";
|
|
25
25
|
import { DEFAULT_DRIVE_MODE, isKnownDriveMode, isKnownModel, } from "./models.js";
|
|
26
26
|
import { SCHEMA_VERSION_KEY, configSchemaRefusal } from "./schema-version.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
27
|
+
import { isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
28
|
+
import { posture as postureFor, resolveProfileName, } from "./profiles.js";
|
|
29
|
+
import { isKnownTranscriptsMode } from "./transcripts.js";
|
|
30
|
+
import { isKnownCredentialsMode } from "./claude-credentials.js";
|
|
31
|
+
import { isKnownInstructionsMode } from "./claude-instructions.js";
|
|
32
|
+
import { isKnownHooksMode } from "./claude-settings.js";
|
|
33
|
+
import { isKnownMcpServersMode, } from "./claude-mcp.js";
|
|
33
34
|
import { resolveDeclaredMcpServers } from "./mcp-servers.js";
|
|
34
35
|
import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
|
|
35
36
|
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
@@ -254,7 +255,7 @@ function loadBrandConfig(file = {}) {
|
|
|
254
255
|
export function resolveDefaultWebDist(moduleUrl) {
|
|
255
256
|
return path.resolve(path.dirname(fileURLToPath(moduleUrl)), "../../web/dist");
|
|
256
257
|
}
|
|
257
|
-
export function loadPaddockConfig() {
|
|
258
|
+
export function loadPaddockConfig(opts = {}) {
|
|
258
259
|
// The optional YAML instance-config file provides the BASE layer; env vars
|
|
259
260
|
// override it (precedence file < env). The file is located under the data dir,
|
|
260
261
|
// so resolve a BOOTSTRAP data dir from env/default first to find it — the file
|
|
@@ -264,11 +265,13 @@ export function loadPaddockConfig() {
|
|
|
264
265
|
// Ensure the data root exists first so symlinks (e.g. /tmp -> /private/tmp on
|
|
265
266
|
// macOS) resolve consistently for every derived path below.
|
|
266
267
|
const dataRoot = abs(envOr("PADDOCK_DATA_DIR", fileOr(file.dataDir, "./data")));
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
if (opts.createDataDir !== false) {
|
|
269
|
+
try {
|
|
270
|
+
fs.mkdirSync(dataRoot, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
/* best-effort; downstream mkdirs will surface real errors */
|
|
274
|
+
}
|
|
272
275
|
}
|
|
273
276
|
// working_directory of keeper agents MUST be canonical so session discovery
|
|
274
277
|
// (which encodes the real path) can find Claude transcripts.
|
|
@@ -283,7 +286,15 @@ export function loadPaddockConfig() {
|
|
|
283
286
|
const resolvedClaudeHome = resolveClaudeHome(dataDir, fileOpt(file.claudeHome));
|
|
284
287
|
const legacyClaudeHome = userClaudeHome();
|
|
285
288
|
const defaultWebDist = resolveDefaultWebDist(import.meta.url);
|
|
289
|
+
// The posture profile (#878), resolved BEFORE the levers it supplies defaults
|
|
290
|
+
// for. It is not a config layer of its own: `p` is handed to each posture
|
|
291
|
+
// loader as the fallback that used to be a hardcoded literal, so an individual
|
|
292
|
+
// file key or env var still wins over it. See `profiles.ts` for why an
|
|
293
|
+
// individual FILE key deliberately beats `PADDOCK_PROFILE` in the env.
|
|
294
|
+
const profile = resolveProfileName(file.profile);
|
|
295
|
+
const p = postureFor(profile);
|
|
286
296
|
return Object.freeze({
|
|
297
|
+
profile,
|
|
287
298
|
port: Number(envOr("PORT", fileOr(file.port, "7233"))),
|
|
288
299
|
// Safe by default (#435): bind loopback unless explicitly told otherwise, so
|
|
289
300
|
// a fresh source/tarball run is network-closed. Non-loopback + no auth is then
|
|
@@ -300,7 +311,7 @@ export function loadPaddockConfig() {
|
|
|
300
311
|
webDist: abs(envOr("PADDOCK_WEB_DIST", fileOr(file.webDist, defaultWebDist))),
|
|
301
312
|
claudeHome: resolvedClaudeHome,
|
|
302
313
|
legacyClaudeHome,
|
|
303
|
-
claude: loadClaudeConfig(file.claude),
|
|
314
|
+
claude: loadClaudeConfig(file.claude, p),
|
|
304
315
|
auth: loadAuthConfig(file.auth),
|
|
305
316
|
transcription: loadTranscriptionConfig(file.transcription),
|
|
306
317
|
brand: loadBrandConfig(file.brand),
|
|
@@ -308,15 +319,19 @@ export function loadPaddockConfig() {
|
|
|
308
319
|
driveMode: loadDriveMode(file.driveMode),
|
|
309
320
|
nativeSystemPrompt: loadNativeSystemPrompt(file.nativeSystemPrompt),
|
|
310
321
|
environmentPrompt: loadEnvironmentPrompt(file.environmentPrompt),
|
|
311
|
-
selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled),
|
|
312
|
-
selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) &&
|
|
322
|
+
selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled, p.selfMcpEnabled),
|
|
323
|
+
selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled, p.selfMcpEnabled) &&
|
|
324
|
+
loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled, p.selfMcpWriteEnabled),
|
|
313
325
|
// Projects tool implies write implies read — the tool is appended in the write
|
|
314
326
|
// block, so both outer gates must be on for it to mean anything (issue #467).
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
327
|
+
// The cascade is why a profile's three self-MCP values are never read in
|
|
328
|
+
// isolation: `profile: yolo` plus an explicit `selfMcpEnabled: false`
|
|
329
|
+
// correctly collapses write and projects too.
|
|
330
|
+
selfMcpProjectsEnabled: loadSelfMcpEnabled(file.selfMcpEnabled, p.selfMcpEnabled) &&
|
|
331
|
+
loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled, p.selfMcpWriteEnabled) &&
|
|
332
|
+
loadSelfMcpProjectsEnabled(file.selfMcpProjectsEnabled, p.selfMcpProjectsEnabled),
|
|
333
|
+
maxSpawnDepth: loadMaxSpawnDepth(file.maxSpawnDepth, p.maxSpawnDepth),
|
|
334
|
+
scheduleMutationEnabled: loadScheduleMutationEnabled(file.scheduleMutationEnabled, p.scheduleMutationEnabled),
|
|
320
335
|
...(() => {
|
|
321
336
|
// #312 M1: resolved against the real environment (token material is only
|
|
322
337
|
// ever an `env:` reference). Diagnostics ride along for app.ts to log.
|
|
@@ -330,12 +345,12 @@ export function loadPaddockConfig() {
|
|
|
330
345
|
const { servers, errors, warnings } = resolveDeclaredMcpServers(file.mcpServers, process.env);
|
|
331
346
|
return { mcpServers: servers, mcpServersDiagnostics: { errors, warnings } };
|
|
332
347
|
})(),
|
|
333
|
-
hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled),
|
|
348
|
+
hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled, p.hooksMcpEnabled),
|
|
334
349
|
recovery: loadRecoveryConfig(file.recovery),
|
|
335
350
|
attachments: loadAttachmentsConfig(file.attachments),
|
|
336
351
|
curation: loadCurationConfig(file.curation),
|
|
337
352
|
logLevel: envOr("LOG_LEVEL", fileOr(file.logLevel, "info")),
|
|
338
|
-
browserMcp: loadBrowserMcp(file.browserMcp),
|
|
353
|
+
browserMcp: loadBrowserMcp(file.browserMcp, p.browserMcp),
|
|
339
354
|
openapi: loadOpenApiConfig(file.openapi),
|
|
340
355
|
sweepMinIntervalMs: loadSweepMinIntervalMs(file.sweepMinIntervalMs),
|
|
341
356
|
gitAuthor: {
|
|
@@ -383,11 +398,13 @@ function loadSweepMinIntervalMs(file) {
|
|
|
383
398
|
* behaviour); only when the env var is UNSET does the config file provide the
|
|
384
399
|
* base, using the 1/true/yes convention shared by the other boolean knobs.
|
|
385
400
|
*/
|
|
386
|
-
function loadBrowserMcp(file) {
|
|
401
|
+
function loadBrowserMcp(file, fallback) {
|
|
387
402
|
const env = process.env.PADDOCK_BROWSER_MCP;
|
|
388
403
|
if (env !== undefined)
|
|
389
404
|
return env === "1";
|
|
390
405
|
const raw = fileOpt(file)?.toLowerCase();
|
|
406
|
+
if (raw === undefined)
|
|
407
|
+
return fallback;
|
|
391
408
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
392
409
|
}
|
|
393
410
|
/**
|
|
@@ -414,12 +431,12 @@ function loadOpenApiConfig(file) {
|
|
|
414
431
|
* missing/blank/out-of-range value falls back to the default rather than failing
|
|
415
432
|
* startup. A per-project override still wins at dispatch.
|
|
416
433
|
*/
|
|
417
|
-
function loadMaxSpawnDepth(file) {
|
|
434
|
+
function loadMaxSpawnDepth(file, fallback) {
|
|
418
435
|
const raw = envOpt("PADDOCK_MAX_SPAWN_DEPTH") ?? fileOpt(file);
|
|
419
436
|
if (raw === undefined)
|
|
420
|
-
return
|
|
437
|
+
return fallback;
|
|
421
438
|
const n = Number(raw);
|
|
422
|
-
return isValidMaxSpawnDepth(n) ? n :
|
|
439
|
+
return isValidMaxSpawnDepth(n) ? n : fallback;
|
|
423
440
|
}
|
|
424
441
|
/**
|
|
425
442
|
* Resolve a boolean recovery knob (issue #301) with the shared 1/true/yes
|
|
@@ -503,8 +520,8 @@ function loadAttachmentsConfig(file) {
|
|
|
503
520
|
* operator opts in with `PADDOCK_SCHEDULE_MUTATION=1` (or the config file). Accepts
|
|
504
521
|
* 1/true/yes. Static `project.yaml` schedules are armed regardless of this flag.
|
|
505
522
|
*/
|
|
506
|
-
function loadScheduleMutationEnabled(file) {
|
|
507
|
-
const raw = envOr("PADDOCK_SCHEDULE_MUTATION", fileOr(file,
|
|
523
|
+
function loadScheduleMutationEnabled(file, fallback) {
|
|
524
|
+
const raw = envOr("PADDOCK_SCHEDULE_MUTATION", fileOr(file, String(fallback))).toLowerCase();
|
|
508
525
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
509
526
|
}
|
|
510
527
|
/**
|
|
@@ -514,8 +531,8 @@ function loadScheduleMutationEnabled(file) {
|
|
|
514
531
|
* override still wins at dispatch. Accepts 1/true/yes. Only meaningful when the
|
|
515
532
|
* self-MCP write tools are also enabled (the hook tools live on that server).
|
|
516
533
|
*/
|
|
517
|
-
function loadHooksMcpEnabled(file) {
|
|
518
|
-
const raw = envOr("PADDOCK_HOOKS_MCP", fileOr(file,
|
|
534
|
+
function loadHooksMcpEnabled(file, fallback) {
|
|
535
|
+
const raw = envOr("PADDOCK_HOOKS_MCP", fileOr(file, String(fallback))).toLowerCase();
|
|
519
536
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
520
537
|
}
|
|
521
538
|
/**
|
|
@@ -524,8 +541,8 @@ function loadHooksMcpEnabled(file) {
|
|
|
524
541
|
* is also on (write implies read — enforced at the call site above). Accepts
|
|
525
542
|
* 1/true/yes.
|
|
526
543
|
*/
|
|
527
|
-
function loadSelfMcpWriteEnabled(file) {
|
|
528
|
-
const raw = envOr("PADDOCK_SELF_MCP_WRITE", fileOr(file,
|
|
544
|
+
function loadSelfMcpWriteEnabled(file, fallback) {
|
|
545
|
+
const raw = envOr("PADDOCK_SELF_MCP_WRITE", fileOr(file, String(fallback))).toLowerCase();
|
|
529
546
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
530
547
|
}
|
|
531
548
|
/**
|
|
@@ -534,8 +551,8 @@ function loadSelfMcpWriteEnabled(file) {
|
|
|
534
551
|
* `PADDOCK_SELF_MCP` and `PADDOCK_SELF_MCP_WRITE` are also on (enforced at the call
|
|
535
552
|
* site above). Accepts 1/true/yes.
|
|
536
553
|
*/
|
|
537
|
-
function loadSelfMcpProjectsEnabled(file) {
|
|
538
|
-
const raw = envOr("PADDOCK_SELF_MCP_PROJECTS", fileOr(file,
|
|
554
|
+
function loadSelfMcpProjectsEnabled(file, fallback) {
|
|
555
|
+
const raw = envOr("PADDOCK_SELF_MCP_PROJECTS", fileOr(file, String(fallback))).toLowerCase();
|
|
539
556
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
540
557
|
}
|
|
541
558
|
/**
|
|
@@ -543,8 +560,8 @@ function loadSelfMcpProjectsEnabled(file) {
|
|
|
543
560
|
* Defaults to OFF so a plain instance never advertises it; opt in per instance
|
|
544
561
|
* with `PADDOCK_SELF_MCP=1`. Accepts 1/true/yes.
|
|
545
562
|
*/
|
|
546
|
-
function loadSelfMcpEnabled(file) {
|
|
547
|
-
const raw = envOr("PADDOCK_SELF_MCP", fileOr(file,
|
|
563
|
+
function loadSelfMcpEnabled(file, fallback) {
|
|
564
|
+
const raw = envOr("PADDOCK_SELF_MCP", fileOr(file, String(fallback))).toLowerCase();
|
|
548
565
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
549
566
|
}
|
|
550
567
|
/**
|
|
@@ -660,20 +677,18 @@ export function userClaudeHome() {
|
|
|
660
677
|
* reason — an MCP server is a process paddock spawns, so a typo that attaches
|
|
661
678
|
* nothing is better than one that attaches the user's whole set unasked.
|
|
662
679
|
*/
|
|
663
|
-
function loadClaudeConfig(file = {}) {
|
|
680
|
+
function loadClaudeConfig(file = {}, p) {
|
|
664
681
|
const transcripts = (envOpt("PADDOCK_CLAUDE_TRANSCRIPTS") ?? fileOpt(file.transcripts))?.toLowerCase();
|
|
665
682
|
const credentials = (envOpt("PADDOCK_CLAUDE_CREDENTIALS") ?? fileOpt(file.credentials))?.toLowerCase();
|
|
666
683
|
const instructions = (envOpt("PADDOCK_CLAUDE_INSTRUCTIONS") ?? fileOpt(file.instructions))?.toLowerCase();
|
|
667
684
|
const hooks = (envOpt("PADDOCK_CLAUDE_HOOKS") ?? fileOpt(file.hooks))?.toLowerCase();
|
|
668
685
|
const mcpServers = (envOpt("PADDOCK_CLAUDE_MCP_SERVERS") ?? fileOpt(file.mcpServers))?.toLowerCase();
|
|
669
686
|
return {
|
|
670
|
-
transcripts: transcripts && isKnownTranscriptsMode(transcripts) ? transcripts :
|
|
671
|
-
credentials: credentials && isKnownCredentialsMode(credentials) ? credentials :
|
|
672
|
-
instructions: instructions && isKnownInstructionsMode(instructions)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
hooks: hooks && isKnownHooksMode(hooks) ? hooks : DEFAULT_HOOKS_MODE,
|
|
676
|
-
mcpServers: mcpServers && isKnownMcpServersMode(mcpServers) ? mcpServers : DEFAULT_MCP_SERVERS_MODE,
|
|
687
|
+
transcripts: transcripts && isKnownTranscriptsMode(transcripts) ? transcripts : p.transcripts,
|
|
688
|
+
credentials: credentials && isKnownCredentialsMode(credentials) ? credentials : p.credentials,
|
|
689
|
+
instructions: instructions && isKnownInstructionsMode(instructions) ? instructions : p.instructions,
|
|
690
|
+
hooks: hooks && isKnownHooksMode(hooks) ? hooks : p.hooks,
|
|
691
|
+
mcpServers: mcpServers && isKnownMcpServersMode(mcpServers) ? mcpServers : p.mcpServers,
|
|
677
692
|
};
|
|
678
693
|
}
|
|
679
694
|
/**
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
import { FleetManager, type DiscoveredSession, type ChatMessage, type SDKMessage, type TriggerResult, type AgentInfo, type FleetStatus, type SlashCommand, type InjectedMcpServerDef, type SessionWakeHandler, type ResolveInjectedMcpServers, type ScheduleTriggerHandler, type ScheduleInfo, type JobMetadata, type AdoptionPlacementMode, type AdoptableSession, type AdoptSkippedSession } from "@herdctl/core";
|
|
50
50
|
import type { PaddockConfig } from "./config.js";
|
|
51
51
|
import type { Project } from "./projects.js";
|
|
52
|
+
import { type UnplantedChatsLink } from "./transcripts.js";
|
|
52
53
|
import { type PaddockTrigger } from "./trigger-config.js";
|
|
53
54
|
import { type McpSources } from "./claude-mcp.js";
|
|
54
55
|
import { type HostPluginSource } from "./claude-plugins.js";
|
|
@@ -190,6 +191,22 @@ export declare class HerdctlService {
|
|
|
190
191
|
* fix's residue gate — see {@link residueDepthFor} + `./resume-drain.ts`).
|
|
191
192
|
*/
|
|
192
193
|
private agentWorkingDirs;
|
|
194
|
+
/**
|
|
195
|
+
* Each agent's `.chats` HOST dir — `project.dir`, which for a repo-backed
|
|
196
|
+
* project is NOT `workingDir` (#187). Recorded so {@link deleteSession} can ask
|
|
197
|
+
* whether this project's store is still a planted symlink before it runs an
|
|
198
|
+
* `rm` through it (#708).
|
|
199
|
+
*/
|
|
200
|
+
private agentChatsHostDirs;
|
|
201
|
+
/**
|
|
202
|
+
* Stale `.chats` symlinks this service unplanted while bringing the on-disk
|
|
203
|
+
* layout in line with `claude.transcripts` (#708) — i.e. an instance that used
|
|
204
|
+
* to run `host` and now runs `own`. Drained by `app.ts` into the boot log:
|
|
205
|
+
* the transcripts written during the `host` period are still in the user's own
|
|
206
|
+
* Claude home and are no longer in paddock's chat list, so the operator needs
|
|
207
|
+
* the paths to decide what to import. Empty on every normal boot.
|
|
208
|
+
*/
|
|
209
|
+
readonly unplantedChatsLinks: UnplantedChatsLink[];
|
|
193
210
|
/**
|
|
194
211
|
* Live session-mode turns keyed by the synthetic turn id we hand back as the
|
|
195
212
|
* `jobId` (Paddock#111). `openChatSession` creates no herdctl job record, so
|
|
@@ -557,6 +574,26 @@ export declare class HerdctlService {
|
|
|
557
574
|
*
|
|
558
575
|
* The chat leaves paddock's list either way — which is what the user asked for
|
|
559
576
|
* — so `retained` is how a caller tells "gone" from "no longer shown here".
|
|
577
|
+
*
|
|
578
|
+
* ## The `own` branch's one precondition (#708)
|
|
579
|
+
*
|
|
580
|
+
* `manager.deleteSession` `rm`s `<claudeHome>/projects/<enc>/<id>.jsonl`, which
|
|
581
|
+
* paddock has symlinked at `<project.dir>/.chats`. That is only paddock's store
|
|
582
|
+
* if `.chats` is a real directory. On an instance flipped `host` → `own` it was
|
|
583
|
+
* a leftover symlink into the user's own Claude home, so the `own` branch —
|
|
584
|
+
* chosen precisely because "these transcripts are ours to delete" — resolved
|
|
585
|
+
* through it and unlinked the user's real terminal history.
|
|
586
|
+
*
|
|
587
|
+
* `ensureProjectChats` clears that symlink on every boot now, so this should
|
|
588
|
+
* never fire. It is here anyway because that healing is best-effort by
|
|
589
|
+
* construction: the whole module swallows its own errors so a layout problem
|
|
590
|
+
* can never block chat, and the single operation that must not run against an
|
|
591
|
+
* unhealed layout is this `rm`. Checking is one `lstat` that never follows the
|
|
592
|
+
* link, so the guard itself cannot touch `~/.claude` either.
|
|
593
|
+
*
|
|
594
|
+
* Refusing reports the same `{removed: false, retained: true}` the `host`
|
|
595
|
+
* branch does, for the same reason: nothing was deleted, so do not claim it
|
|
596
|
+
* was. The next boot heals the layout and the delete then works normally.
|
|
560
597
|
*/
|
|
561
598
|
deleteSession(agentName: string, sessionId: string): Promise<ChatDeletion>;
|
|
562
599
|
/**
|
|
@@ -983,6 +1020,14 @@ export declare class HerdctlService {
|
|
|
983
1020
|
* errors, and a missing symlink only means Claude keeps its default location.
|
|
984
1021
|
*/
|
|
985
1022
|
private ensureSweeperHome;
|
|
1023
|
+
/**
|
|
1024
|
+
* {@link ensureProjectChats} for every call site, so the stale-`.chats`-symlink
|
|
1025
|
+
* findings it reports (#708) are accumulated in one place rather than dropped
|
|
1026
|
+
* by three independent callers. Sweeper homes are included deliberately: they
|
|
1027
|
+
* inherited the same stale link, so they wrote their transcripts into the
|
|
1028
|
+
* user's real Claude home too.
|
|
1029
|
+
*/
|
|
1030
|
+
private ensureChats;
|
|
986
1031
|
private triggerAgentConfig;
|
|
987
1032
|
private ensureConfigFile;
|
|
988
1033
|
}
|
|
@@ -53,7 +53,7 @@ import { promises as fs } from "node:fs";
|
|
|
53
53
|
import { randomUUID } from "node:crypto";
|
|
54
54
|
import path from "node:path";
|
|
55
55
|
import { DEFAULT_MODEL } from "./models.js";
|
|
56
|
-
import { ensureProjectChats, projectChatsDir } from "./transcripts.js";
|
|
56
|
+
import { chatsDirIsPlanted, ensureProjectChats, projectChatsDir, } from "./transcripts.js";
|
|
57
57
|
import { triggerRunsOnOwnAgent, isCuratorTrigger, } from "./trigger-config.js";
|
|
58
58
|
import { buildAgentConfig, buildSweeperConfig, sweeperWorkingDir, buildTriggerConfig, ensureConfigFile as writeBootConfigFile, } from "./herdctl-agent-config.js";
|
|
59
59
|
import { EMPTY_MCP_SOURCES } from "./claude-mcp.js";
|
|
@@ -254,6 +254,22 @@ export class HerdctlService {
|
|
|
254
254
|
* fix's residue gate — see {@link residueDepthFor} + `./resume-drain.ts`).
|
|
255
255
|
*/
|
|
256
256
|
agentWorkingDirs = new Map();
|
|
257
|
+
/**
|
|
258
|
+
* Each agent's `.chats` HOST dir — `project.dir`, which for a repo-backed
|
|
259
|
+
* project is NOT `workingDir` (#187). Recorded so {@link deleteSession} can ask
|
|
260
|
+
* whether this project's store is still a planted symlink before it runs an
|
|
261
|
+
* `rm` through it (#708).
|
|
262
|
+
*/
|
|
263
|
+
agentChatsHostDirs = new Map();
|
|
264
|
+
/**
|
|
265
|
+
* Stale `.chats` symlinks this service unplanted while bringing the on-disk
|
|
266
|
+
* layout in line with `claude.transcripts` (#708) — i.e. an instance that used
|
|
267
|
+
* to run `host` and now runs `own`. Drained by `app.ts` into the boot log:
|
|
268
|
+
* the transcripts written during the `host` period are still in the user's own
|
|
269
|
+
* Claude home and are no longer in paddock's chat list, so the operator needs
|
|
270
|
+
* the paths to decide what to import. Empty on every normal boot.
|
|
271
|
+
*/
|
|
272
|
+
unplantedChatsLinks = [];
|
|
257
273
|
/**
|
|
258
274
|
* Live session-mode turns keyed by the synthetic turn id we hand back as the
|
|
259
275
|
* `jobId` (Paddock#111). `openChatSession` creates no herdctl job record, so
|
|
@@ -420,7 +436,7 @@ export class HerdctlService {
|
|
|
420
436
|
// at the .chats store in the metadata dir — so repo-backed transcripts stay
|
|
421
437
|
// out of the external checkout's working tree (issue #187). For a notebook
|
|
422
438
|
// project workingDir === dir, so this is the classic behavior.
|
|
423
|
-
await
|
|
439
|
+
await this.ensureChats(project.workingDir, project.dir);
|
|
424
440
|
await this.ensureSweeperHome(project);
|
|
425
441
|
await this.fleet.addAgent(this.keeperAgentConfig(project), { replace: true });
|
|
426
442
|
await this.fleet.addAgent(this.sweeperAgentConfig(project), { replace: true });
|
|
@@ -430,6 +446,7 @@ export class HerdctlService {
|
|
|
430
446
|
await this.registerTriggerAgents(project);
|
|
431
447
|
this.agentModels.set(keeperAgentName(project.slug), project.model ?? DEFAULT_MODEL);
|
|
432
448
|
this.agentWorkingDirs.set(keeperAgentName(project.slug), project.workingDir);
|
|
449
|
+
this.agentChatsHostDirs.set(keeperAgentName(project.slug), project.dir);
|
|
433
450
|
}
|
|
434
451
|
}
|
|
435
452
|
/** Start the scheduler (keeper agents currently have no schedules, but this
|
|
@@ -494,7 +511,7 @@ export class HerdctlService {
|
|
|
494
511
|
async ensureProjectAgent(project) {
|
|
495
512
|
if (!this.fleet)
|
|
496
513
|
return;
|
|
497
|
-
await
|
|
514
|
+
await this.ensureChats(project.workingDir, project.dir);
|
|
498
515
|
await this.ensureSweeperHome(project);
|
|
499
516
|
await this.fleet.addAgent(this.keeperAgentConfig(project), { replace: true });
|
|
500
517
|
await this.fleet.addAgent(this.sweeperAgentConfig(project), { replace: true });
|
|
@@ -507,6 +524,7 @@ export class HerdctlService {
|
|
|
507
524
|
// default), so a model change via PATCH takes effect here too.
|
|
508
525
|
this.agentModels.set(keeperAgentName(project.slug), project.model ?? DEFAULT_MODEL);
|
|
509
526
|
this.agentWorkingDirs.set(keeperAgentName(project.slug), project.workingDir);
|
|
527
|
+
this.agentChatsHostDirs.set(keeperAgentName(project.slug), project.dir);
|
|
510
528
|
}
|
|
511
529
|
/**
|
|
512
530
|
* Register (or replace) every trigger a project declares that runs on its OWN scoped
|
|
@@ -922,9 +940,33 @@ export class HerdctlService {
|
|
|
922
940
|
*
|
|
923
941
|
* The chat leaves paddock's list either way — which is what the user asked for
|
|
924
942
|
* — so `retained` is how a caller tells "gone" from "no longer shown here".
|
|
943
|
+
*
|
|
944
|
+
* ## The `own` branch's one precondition (#708)
|
|
945
|
+
*
|
|
946
|
+
* `manager.deleteSession` `rm`s `<claudeHome>/projects/<enc>/<id>.jsonl`, which
|
|
947
|
+
* paddock has symlinked at `<project.dir>/.chats`. That is only paddock's store
|
|
948
|
+
* if `.chats` is a real directory. On an instance flipped `host` → `own` it was
|
|
949
|
+
* a leftover symlink into the user's own Claude home, so the `own` branch —
|
|
950
|
+
* chosen precisely because "these transcripts are ours to delete" — resolved
|
|
951
|
+
* through it and unlinked the user's real terminal history.
|
|
952
|
+
*
|
|
953
|
+
* `ensureProjectChats` clears that symlink on every boot now, so this should
|
|
954
|
+
* never fire. It is here anyway because that healing is best-effort by
|
|
955
|
+
* construction: the whole module swallows its own errors so a layout problem
|
|
956
|
+
* can never block chat, and the single operation that must not run against an
|
|
957
|
+
* unhealed layout is this `rm`. Checking is one `lstat` that never follows the
|
|
958
|
+
* link, so the guard itself cannot touch `~/.claude` either.
|
|
959
|
+
*
|
|
960
|
+
* Refusing reports the same `{removed: false, retained: true}` the `host`
|
|
961
|
+
* branch does, for the same reason: nothing was deleted, so do not claim it
|
|
962
|
+
* was. The next boot heals the layout and the delete then works normally.
|
|
925
963
|
*/
|
|
926
964
|
async deleteSession(agentName, sessionId) {
|
|
927
965
|
if (this.cfg.claude.transcripts === "own") {
|
|
966
|
+
const chatsHostDir = this.agentChatsHostDirs.get(agentName);
|
|
967
|
+
if (chatsHostDir !== undefined && (await chatsDirIsPlanted(chatsHostDir))) {
|
|
968
|
+
return { removed: false, retained: true };
|
|
969
|
+
}
|
|
928
970
|
const removed = await this.manager.deleteSession(agentName, sessionId);
|
|
929
971
|
await this.dropAgentSessionPointer(agentName, sessionId);
|
|
930
972
|
return { removed, retained: false };
|
|
@@ -1831,7 +1873,19 @@ export class HerdctlService {
|
|
|
1831
1873
|
*/
|
|
1832
1874
|
async ensureSweeperHome(project) {
|
|
1833
1875
|
const dir = sweeperWorkingDir(this.cfg, project.slug);
|
|
1834
|
-
await
|
|
1876
|
+
await this.ensureChats(dir, dir);
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* {@link ensureProjectChats} for every call site, so the stale-`.chats`-symlink
|
|
1880
|
+
* findings it reports (#708) are accumulated in one place rather than dropped
|
|
1881
|
+
* by three independent callers. Sweeper homes are included deliberately: they
|
|
1882
|
+
* inherited the same stale link, so they wrote their transcripts into the
|
|
1883
|
+
* user's real Claude home too.
|
|
1884
|
+
*/
|
|
1885
|
+
async ensureChats(workingDir, chatsHostDir) {
|
|
1886
|
+
const unplanted = await ensureProjectChats(workingDir, chatsHostDir, this.claudeHomeTarget);
|
|
1887
|
+
if (unplanted)
|
|
1888
|
+
this.unplantedChatsLinks.push(unplanted);
|
|
1835
1889
|
}
|
|
1836
1890
|
triggerAgentConfig(project, triggerName, trigger) {
|
|
1837
1891
|
return buildTriggerConfig(this.cfg, project, triggerName, trigger);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ProfileName } from "./profiles.js";
|
|
2
|
+
import { type TranscriptsMode } from "./transcripts.js";
|
|
1
3
|
import { type PaddockConfig } from "./config.js";
|
|
2
4
|
/** Groups the Settings screen renders, in display order. */
|
|
3
5
|
export declare const GROUPS: {
|
|
@@ -91,6 +93,14 @@ export interface InstanceConfigFieldDto {
|
|
|
91
93
|
envOverridden: boolean;
|
|
92
94
|
/** The env var shadowing this field (only when `envOverridden`). */
|
|
93
95
|
envVar?: string;
|
|
96
|
+
/**
|
|
97
|
+
* The instance's posture profile is what put `value` here (#878): this field
|
|
98
|
+
* is one of the profile's levers, and neither the config file nor an env var
|
|
99
|
+
* overrode it. False on every non-posture field, and — importantly — false on
|
|
100
|
+
* a posture field the environment or the file set, so the UI never credits the
|
|
101
|
+
* profile for a value it did not choose. See {@link isFromProfile}.
|
|
102
|
+
*/
|
|
103
|
+
fromProfile: boolean;
|
|
94
104
|
}
|
|
95
105
|
export interface InstanceConfigGroupDto {
|
|
96
106
|
id: string;
|
|
@@ -129,33 +139,29 @@ export interface InstanceConfigDto {
|
|
|
129
139
|
* than trusting a cached value: the point is to notice a write we did not make.
|
|
130
140
|
*/
|
|
131
141
|
export declare function instanceConfigVersion(configPath: string): string | null;
|
|
142
|
+
/**
|
|
143
|
+
* The transcripts mode a RESTART would resolve — read from the config FILE,
|
|
144
|
+
* falling back to the profile's default when the file is silent about it.
|
|
145
|
+
*
|
|
146
|
+
* NOT `cfg.claude.transcripts`, which is what THIS process froze at boot. The
|
|
147
|
+
* two differ for the whole window between a successful `own → host` migration
|
|
148
|
+
* (#882) and the restart it asks for, and that window is precisely when a
|
|
149
|
+
* second POST arrives: without this the idempotency check would see `own`,
|
|
150
|
+
* decide nothing had happened, and re-run a migration that already had.
|
|
151
|
+
*
|
|
152
|
+
* The environment is deliberately not consulted. `PADDOCK_CLAUDE_TRANSCRIPTS`
|
|
153
|
+
* beats the file, so when it is set the true answer to "what would a restart
|
|
154
|
+
* resolve" is the env var — but a caller in that situation is already refusing
|
|
155
|
+
* with `env_shadowed`, and folding it in here would make a shadowed instance
|
|
156
|
+
* look permanently migrated.
|
|
157
|
+
*/
|
|
158
|
+
export declare function pendingTranscriptsMode(cfg: PaddockConfig): TranscriptsMode;
|
|
132
159
|
/**
|
|
133
160
|
* Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
|
|
134
161
|
* (the same rule {@link import("./config.js").loadConfigFile} reads it back
|
|
135
162
|
* from), else `<dataDir>/paddock.config.yaml`.
|
|
136
163
|
*/
|
|
137
164
|
export declare function instanceConfigPath(cfg: PaddockConfig): string;
|
|
138
|
-
/**
|
|
139
|
-
* Build the grouped GET DTO from the resolved (frozen) config, the live env AND
|
|
140
|
-
* the config file on disk. Each field reports:
|
|
141
|
-
*
|
|
142
|
-
* - `value` — EFFECTIVE now, out of the frozen boot config;
|
|
143
|
-
* - `pendingValue` — what a restart would resolve, out of the file this instant;
|
|
144
|
-
* - `pendingRestart` — those two differ.
|
|
145
|
-
*
|
|
146
|
-
* Reading the file is the load-bearing part (#722). While this built the DTO
|
|
147
|
-
* from the frozen config alone, `GET` could not observe ANY write — including
|
|
148
|
-
* the one the caller had just made — so a successful save appeared to revert,
|
|
149
|
-
* two tabs clobbered each other with nothing able to notice, and
|
|
150
|
-
* `restartRequired` had no choice but to be a hardcoded `false`.
|
|
151
|
-
*
|
|
152
|
-
* Pending values are computed for EDITABLE, non-env-shadowed fields only:
|
|
153
|
-
* - an env-shadowed field resolves to the same env value after a restart, so
|
|
154
|
-
* the file cannot make it diverge;
|
|
155
|
-
* - the read-only `advanced` bindings are normalised at boot (`dataDir` and
|
|
156
|
-
* friends are canonicalised, `port` is `Number()`-ed), so comparing them
|
|
157
|
-
* against raw file text would manufacture divergence that isn't there.
|
|
158
|
-
*/
|
|
159
165
|
export declare function buildInstanceConfig(cfg: PaddockConfig): InstanceConfigDto;
|
|
160
166
|
/** A rejected PUT: which field failed and why (surfaced as a 400). */
|
|
161
167
|
export declare class InstanceConfigError extends Error {
|
|
@@ -191,4 +197,126 @@ export declare function writeInstanceConfig(configPath: string, pairs: {
|
|
|
191
197
|
key: string;
|
|
192
198
|
value: unknown;
|
|
193
199
|
}[]): void;
|
|
200
|
+
/**
|
|
201
|
+
* WHICH LAYER supplied a field's effective value.
|
|
202
|
+
*
|
|
203
|
+
* Paddock resolves config as `profile < file < env` beneath a static code
|
|
204
|
+
* default, and #878's argument for a `config show --resolved` is that naming the
|
|
205
|
+
* layer is the honest answer to "I want it all written down" — unlike a
|
|
206
|
+
* hand-maintained file, a printed resolution cannot drift from what the process
|
|
207
|
+
* actually did.
|
|
208
|
+
*
|
|
209
|
+
* `profile` and `default` are deliberately separate, and that distinction is the
|
|
210
|
+
* whole point of the command for the twelve posture keys. Those keys no longer
|
|
211
|
+
* HAVE a code default of their own: the profile supplies it (see
|
|
212
|
+
* {@link defaultFor}). Collapsing the two would leave an operator unable to tell
|
|
213
|
+
* "Paddock has always shipped this" from "your profile chose this, and picking a
|
|
214
|
+
* different one would change it".
|
|
215
|
+
*/
|
|
216
|
+
export type ConfigSource = "env" | "file" | "profile" | "default";
|
|
217
|
+
/** One field's effective value and where it came from. */
|
|
218
|
+
export interface ResolvedConfigField {
|
|
219
|
+
key: string;
|
|
220
|
+
group: string;
|
|
221
|
+
label: string;
|
|
222
|
+
help?: string;
|
|
223
|
+
type: FieldType;
|
|
224
|
+
/**
|
|
225
|
+
* The EFFECTIVE value, normalised the way {@link buildInstanceConfig} does it:
|
|
226
|
+
* a field the frozen config leaves unset reads as the fallback it implies (the
|
|
227
|
+
* profile's, or the code default), never as a bare `null`. Printing `null` for
|
|
228
|
+
* `models` — which resolves to "offer the whole catalog" — would be a lie
|
|
229
|
+
* dressed as precision.
|
|
230
|
+
*/
|
|
231
|
+
value: unknown;
|
|
232
|
+
source: ConfigSource;
|
|
233
|
+
/** `env` → the variable name; `profile` → the profile name; `file` → its path. */
|
|
234
|
+
origin?: string;
|
|
235
|
+
sensitive: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Whether `paddock config eject` may materialise this key into the config file
|
|
238
|
+
* (#878). Decided here rather than in the CLI because it is a property of the
|
|
239
|
+
* field catalog, and a lever added to {@link FIELDS} tomorrow should land on
|
|
240
|
+
* the right side of it without anyone remembering the eject command exists.
|
|
241
|
+
*
|
|
242
|
+
* The rule is `(editable || posture key) && !sensitive`, which draws the line
|
|
243
|
+
* at **what an ejected file can honestly promise to be**:
|
|
244
|
+
*
|
|
245
|
+
* - **Posture and editable keys are in.** These are the settings a config file
|
|
246
|
+
* is for — the twelve levers a profile governs, plus everything the Settings
|
|
247
|
+
* screen already round-trips through this same file. Writing them changes no
|
|
248
|
+
* effective value, only where it comes from.
|
|
249
|
+
* - **Process/filesystem bindings are out** — `port`, `host`, `dataDir`,
|
|
250
|
+
* `projectsRoot`, `stateDir`, `herdctlConfigPath`, `webDist`. Their resolved
|
|
251
|
+
* values are absolute, canonicalised, machine-specific paths; `dataDir` in
|
|
252
|
+
* particular resolves to the directory the file itself lives in, so ejecting
|
|
253
|
+
* it writes a self-reference that survives a move and silently points the
|
|
254
|
+
* copy back at the original. An ejected config should stay portable and
|
|
255
|
+
* stay mountable, and freezing this instance's port into it is how a second
|
|
256
|
+
* instance started from the same file collides on boot.
|
|
257
|
+
* - **`sensitive` fields are out**, unconditionally and with no flag to
|
|
258
|
+
* override. `transcription.endpoint` is an operator-supplied URL that can
|
|
259
|
+
* read `https://user:token@host`; bulk-materialising a possibly
|
|
260
|
+
* credential-bearing value into a file on disk is not a thing to make one
|
|
261
|
+
* keystroke away. Writing that key by hand stays available and is a
|
|
262
|
+
* deliberate act, which is the right shape for it.
|
|
263
|
+
*
|
|
264
|
+
* Excluding sensitive fields costs nothing in fidelity, which is worth stating
|
|
265
|
+
* because it is not obvious: a sensitive field's value can only have come from
|
|
266
|
+
* the file (in which case it is already there and eject leaves it alone) or
|
|
267
|
+
* from the environment (which eject skips anyway) or from its default (in which
|
|
268
|
+
* case there is nothing to write). So there is no case where skipping one makes
|
|
269
|
+
* the ejected file resolve differently.
|
|
270
|
+
*/
|
|
271
|
+
ejectable: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* The config file names this key, but a higher layer won and this value is
|
|
274
|
+
* inert. Worth surfacing rather than silently ignoring: an operator who edited
|
|
275
|
+
* the file and saw nothing change is exactly the person running this command.
|
|
276
|
+
*/
|
|
277
|
+
shadowedFileValue?: unknown;
|
|
278
|
+
}
|
|
279
|
+
/** The resolved profile, and which layer named it. */
|
|
280
|
+
export interface ResolvedProfileInfo {
|
|
281
|
+
name: ProfileName;
|
|
282
|
+
source: "env" | "file" | "default";
|
|
283
|
+
/** The variable name, or the config file path. Absent on `default`. */
|
|
284
|
+
origin?: string;
|
|
285
|
+
/**
|
|
286
|
+
* A profile name that WAS set and is not one of the three. `resolveProfileName`
|
|
287
|
+
* falls back to the default rather than failing the boot, which is the right
|
|
288
|
+
* call for a boot and a terrible one for a diagnostic — so the typo is reported
|
|
289
|
+
* here instead of vanishing.
|
|
290
|
+
*/
|
|
291
|
+
unrecognised?: {
|
|
292
|
+
value: string;
|
|
293
|
+
origin: string;
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Everything `paddock config show` needs, with no formatting decisions taken. */
|
|
297
|
+
export interface ResolvedConfigReport {
|
|
298
|
+
dataDir: string;
|
|
299
|
+
configPath: string;
|
|
300
|
+
configFileExists: boolean;
|
|
301
|
+
/** The file exists but could not be read/parsed (the loader is the strict reader). */
|
|
302
|
+
configFileError?: string;
|
|
303
|
+
profile: ResolvedProfileInfo;
|
|
304
|
+
fields: ResolvedConfigField[];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Resolve every field in {@link FIELDS} against a loaded config, reporting the
|
|
308
|
+
* layer each value came from.
|
|
309
|
+
*
|
|
310
|
+
* Built on the same descriptor table the Config screen uses rather than a
|
|
311
|
+
* parallel list, so a lever added for the UI is printed by the CLI for free and
|
|
312
|
+
* the two can never disagree about what a default is. What it does NOT reuse is
|
|
313
|
+
* `buildInstanceConfig`'s DTO: that answers "what would a restart change?", which
|
|
314
|
+
* needs the file compared against the process. This answers "where did each of
|
|
315
|
+
* these values come from?", which needs the file compared against the LAYERS.
|
|
316
|
+
*
|
|
317
|
+
* No value here is a secret — {@link FIELDS} excludes them by construction — but
|
|
318
|
+
* `sensitive` rides along so a caller can decide what to put on a terminal that
|
|
319
|
+
* ends up pasted into an issue.
|
|
320
|
+
*/
|
|
321
|
+
export declare function resolveConfigReport(cfg: PaddockConfig): ResolvedConfigReport;
|
|
194
322
|
export {};
|