@edspencer/paddock 0.0.1 → 0.56.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 +69 -0
- package/install-notice.mjs +29 -0
- package/package.json +58 -5
- package/packages/server/dist/adoptable.d.ts +127 -0
- package/packages/server/dist/adoptable.js +315 -0
- package/packages/server/dist/app.d.ts +67 -0
- package/packages/server/dist/app.js +325 -0
- package/packages/server/dist/archive.d.ts +42 -0
- package/packages/server/dist/archive.js +125 -0
- package/packages/server/dist/attachments-config.d.ts +114 -0
- package/packages/server/dist/attachments-config.js +165 -0
- package/packages/server/dist/attachments-hint.d.ts +53 -0
- package/packages/server/dist/attachments-hint.js +140 -0
- package/packages/server/dist/attachments.d.ts +33 -0
- package/packages/server/dist/attachments.js +117 -0
- package/packages/server/dist/auth.d.ts +84 -0
- package/packages/server/dist/auth.js +247 -0
- package/packages/server/dist/background.d.ts +36 -0
- package/packages/server/dist/background.js +137 -0
- package/packages/server/dist/bind-safety.d.ts +56 -0
- package/packages/server/dist/bind-safety.js +51 -0
- package/packages/server/dist/brand.d.ts +44 -0
- package/packages/server/dist/brand.js +88 -0
- package/packages/server/dist/chat-dto.d.ts +210 -0
- package/packages/server/dist/chat-dto.js +308 -0
- package/packages/server/dist/cli/import-chats.d.ts +21 -0
- package/packages/server/dist/cli/import-chats.js +197 -0
- package/packages/server/dist/cli/paddock.d.ts +2 -0
- package/packages/server/dist/cli/paddock.js +247 -0
- package/packages/server/dist/concurrency.d.ts +57 -0
- package/packages/server/dist/concurrency.js +71 -0
- package/packages/server/dist/config.d.ts +512 -0
- package/packages/server/dist/config.js +593 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/event-bus.d.ts +87 -0
- package/packages/server/dist/event-bus.js +48 -0
- package/packages/server/dist/git.d.ts +128 -0
- package/packages/server/dist/git.js +446 -0
- package/packages/server/dist/github-auth.d.ts +44 -0
- package/packages/server/dist/github-auth.js +169 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
- package/packages/server/dist/herdctl-agent-config.js +331 -0
- package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
- package/packages/server/dist/herdctl-agent-names.js +228 -0
- package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
- package/packages/server/dist/herdctl-jobs-index.js +225 -0
- package/packages/server/dist/herdctl-jobs.d.ts +98 -0
- package/packages/server/dist/herdctl-jobs.js +225 -0
- package/packages/server/dist/herdctl.d.ts +572 -0
- package/packages/server/dist/herdctl.js +1218 -0
- package/packages/server/dist/hook-config.d.ts +217 -0
- package/packages/server/dist/hook-config.js +224 -0
- package/packages/server/dist/http-bytes.d.ts +24 -0
- package/packages/server/dist/http-bytes.js +52 -0
- package/packages/server/dist/index.d.ts +1 -0
- package/packages/server/dist/index.js +14 -0
- package/packages/server/dist/instance-config.d.ts +135 -0
- package/packages/server/dist/instance-config.js +345 -0
- package/packages/server/dist/localcommand.d.ts +23 -0
- package/packages/server/dist/localcommand.js +140 -0
- package/packages/server/dist/management-auth.d.ts +99 -0
- package/packages/server/dist/management-auth.js +165 -0
- package/packages/server/dist/management-config.d.ts +164 -0
- package/packages/server/dist/management-config.js +286 -0
- package/packages/server/dist/management-mcp-server.d.ts +20 -0
- package/packages/server/dist/management-mcp-server.js +95 -0
- package/packages/server/dist/management-metadata.d.ts +67 -0
- package/packages/server/dist/management-metadata.js +56 -0
- package/packages/server/dist/management-ops.d.ts +110 -0
- package/packages/server/dist/management-ops.js +449 -0
- package/packages/server/dist/management-policy.d.ts +172 -0
- package/packages/server/dist/management-policy.js +259 -0
- package/packages/server/dist/message-provenance.d.ts +112 -0
- package/packages/server/dist/message-provenance.js +260 -0
- package/packages/server/dist/models.d.ts +164 -0
- package/packages/server/dist/models.js +208 -0
- package/packages/server/dist/openapi.d.ts +66 -0
- package/packages/server/dist/openapi.js +183 -0
- package/packages/server/dist/parent-detach.d.ts +30 -0
- package/packages/server/dist/parent-detach.js +109 -0
- package/packages/server/dist/preload.d.ts +43 -0
- package/packages/server/dist/preload.js +55 -0
- package/packages/server/dist/project-files.d.ts +76 -0
- package/packages/server/dist/project-files.js +191 -0
- package/packages/server/dist/project-mime.d.ts +35 -0
- package/packages/server/dist/project-mime.js +55 -0
- package/packages/server/dist/project-paths.d.ts +88 -0
- package/packages/server/dist/project-paths.js +126 -0
- package/packages/server/dist/project-types.d.ts +316 -0
- package/packages/server/dist/project-types.js +48 -0
- package/packages/server/dist/projects.d.ts +202 -0
- package/packages/server/dist/projects.js +894 -0
- package/packages/server/dist/queued-message.d.ts +35 -0
- package/packages/server/dist/queued-message.js +116 -0
- package/packages/server/dist/read-state.d.ts +46 -0
- package/packages/server/dist/read-state.js +139 -0
- package/packages/server/dist/recovery-config.d.ts +111 -0
- package/packages/server/dist/recovery-config.js +105 -0
- package/packages/server/dist/recovery.d.ts +262 -0
- package/packages/server/dist/recovery.js +490 -0
- package/packages/server/dist/resume-drain.d.ts +122 -0
- package/packages/server/dist/resume-drain.js +158 -0
- package/packages/server/dist/route-context.d.ts +168 -0
- package/packages/server/dist/route-context.js +39 -0
- package/packages/server/dist/route-errors.d.ts +8 -0
- package/packages/server/dist/route-errors.js +13 -0
- package/packages/server/dist/routes/chats.d.ts +12 -0
- package/packages/server/dist/routes/chats.js +1406 -0
- package/packages/server/dist/routes/git.d.ts +13 -0
- package/packages/server/dist/routes/git.js +236 -0
- package/packages/server/dist/routes/mcp.d.ts +83 -0
- package/packages/server/dist/routes/mcp.js +250 -0
- package/packages/server/dist/routes/meta.d.ts +15 -0
- package/packages/server/dist/routes/meta.js +447 -0
- package/packages/server/dist/routes/projects.d.ts +25 -0
- package/packages/server/dist/routes/projects.js +736 -0
- package/packages/server/dist/routes/triggers.d.ts +21 -0
- package/packages/server/dist/routes/triggers.js +324 -0
- package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
- package/packages/server/dist/routes/workspace-mount.js +32 -0
- package/packages/server/dist/routes.d.ts +26 -0
- package/packages/server/dist/routes.js +29 -0
- package/packages/server/dist/run-provenance.d.ts +109 -0
- package/packages/server/dist/run-provenance.js +206 -0
- package/packages/server/dist/runs.d.ts +92 -0
- package/packages/server/dist/runs.js +72 -0
- package/packages/server/dist/schedule-config.d.ts +68 -0
- package/packages/server/dist/schedule-config.js +75 -0
- package/packages/server/dist/schedule-session.d.ts +34 -0
- package/packages/server/dist/schedule-session.js +120 -0
- package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
- package/packages/server/dist/self-mcp-descriptions.js +145 -0
- package/packages/server/dist/self-mcp-read.d.ts +15 -0
- package/packages/server/dist/self-mcp-read.js +97 -0
- package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
- package/packages/server/dist/self-mcp-triggers.js +144 -0
- package/packages/server/dist/self-mcp-types.d.ts +248 -0
- package/packages/server/dist/self-mcp-types.js +10 -0
- package/packages/server/dist/self-mcp-util.d.ts +73 -0
- package/packages/server/dist/self-mcp-util.js +157 -0
- package/packages/server/dist/self-mcp-write.d.ts +35 -0
- package/packages/server/dist/self-mcp-write.js +215 -0
- package/packages/server/dist/self-mcp.d.ts +95 -0
- package/packages/server/dist/self-mcp.js +448 -0
- package/packages/server/dist/send-file-mcp.d.ts +46 -0
- package/packages/server/dist/send-file-mcp.js +281 -0
- package/packages/server/dist/session-hub.d.ts +186 -0
- package/packages/server/dist/session-hub.js +291 -0
- package/packages/server/dist/spawn-capability.d.ts +99 -0
- package/packages/server/dist/spawn-capability.js +106 -0
- package/packages/server/dist/star.d.ts +26 -0
- package/packages/server/dist/star.js +94 -0
- package/packages/server/dist/start.d.ts +1 -0
- package/packages/server/dist/start.js +21 -0
- package/packages/server/dist/subagents.d.ts +337 -0
- package/packages/server/dist/subagents.js +626 -0
- package/packages/server/dist/sweep.d.ts +122 -0
- package/packages/server/dist/sweep.js +696 -0
- package/packages/server/dist/tooldetails.d.ts +33 -0
- package/packages/server/dist/tooldetails.js +466 -0
- package/packages/server/dist/transcribe.d.ts +69 -0
- package/packages/server/dist/transcribe.js +119 -0
- package/packages/server/dist/transcripts.d.ts +42 -0
- package/packages/server/dist/transcripts.js +164 -0
- package/packages/server/dist/trigger-config.d.ts +278 -0
- package/packages/server/dist/trigger-config.js +417 -0
- package/packages/server/dist/trigger-runtime.d.ts +94 -0
- package/packages/server/dist/trigger-runtime.js +82 -0
- package/packages/server/dist/trigger-session.d.ts +34 -0
- package/packages/server/dist/trigger-session.js +122 -0
- package/packages/server/dist/triggers.d.ts +83 -0
- package/packages/server/dist/triggers.js +124 -0
- package/packages/server/dist/trusted-proxy.d.ts +57 -0
- package/packages/server/dist/trusted-proxy.js +157 -0
- package/packages/server/dist/turn-notice.d.ts +154 -0
- package/packages/server/dist/turn-notice.js +271 -0
- package/packages/server/dist/unread.d.ts +36 -0
- package/packages/server/dist/unread.js +123 -0
- package/packages/server/dist/usage.d.ts +66 -0
- package/packages/server/dist/usage.js +243 -0
- package/packages/server/dist/wake-injection.d.ts +156 -0
- package/packages/server/dist/wake-injection.js +136 -0
- package/packages/server/dist/whisper-local.d.ts +9 -0
- package/packages/server/dist/whisper-local.js +122 -0
- package/packages/server/dist/ws-context.d.ts +158 -0
- package/packages/server/dist/ws-context.js +1 -0
- package/packages/server/dist/ws-protocol.d.ts +423 -0
- package/packages/server/dist/ws-protocol.js +251 -0
- package/packages/server/dist/ws-self-mcp.d.ts +52 -0
- package/packages/server/dist/ws-self-mcp.js +28 -0
- package/packages/server/dist/ws-triggers.d.ts +29 -0
- package/packages/server/dist/ws-triggers.js +261 -0
- package/packages/server/dist/ws-turn.d.ts +86 -0
- package/packages/server/dist/ws-turn.js +751 -0
- package/packages/server/dist/ws.d.ts +62 -0
- package/packages/server/dist/ws.js +1024 -0
- package/packages/server/package.json +6 -0
- package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
- package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
- package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
- package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
- package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
- package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
- package/packages/web/dist/assets/css-DazXZka4.js +2 -0
- package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
- package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
- package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
- package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
- package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
- package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
- package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
- package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
- package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
- package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
- package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
- package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
- package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
- package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
- package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
- package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
- package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
- package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
- package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
- package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
- package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
- package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
- package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
- package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
- package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
- package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/packages/web/dist/icons/apple-touch-icon.png +0 -0
- package/packages/web/dist/icons/favicon-16.png +0 -0
- package/packages/web/dist/icons/favicon-32.png +0 -0
- package/packages/web/dist/icons/icon-192.png +0 -0
- package/packages/web/dist/icons/icon-512.png +0 -0
- package/packages/web/dist/icons/maskable-512.png +0 -0
- package/packages/web/dist/index.html +56 -0
- package/packages/web/dist/manifest.webmanifest +32 -0
- package/packages/web/dist/sw.js +139 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock server configuration, sourced from an optional YAML instance-config
|
|
3
|
+
* file with environment overrides on top (precedence: file < env), and sane
|
|
4
|
+
* defaults beneath both.
|
|
5
|
+
*
|
|
6
|
+
* Everything is resolved once at startup so the rest of the app can import a
|
|
7
|
+
* frozen object. Paths are normalised to absolute.
|
|
8
|
+
*
|
|
9
|
+
* **Retired settings are IGNORED, never fatal** (#549). Config is pull-based on
|
|
10
|
+
* both layers: env vars are read by name via {@link envOr}, and the YAML file is
|
|
11
|
+
* parsed into a loose record whose keys are only ever read, never enumerated or
|
|
12
|
+
* validated against a schema. So a setting Paddock has since deleted is simply
|
|
13
|
+
* never looked at — boot succeeds and the value has no effect. This is a property
|
|
14
|
+
* of the resolution shape rather than a compatibility shim, so it needs no
|
|
15
|
+
* per-key upkeep as settings come and go. The cost is that a typo'd key is
|
|
16
|
+
* equally silent; that trade is deliberate — an operator should not be hard-failed
|
|
17
|
+
* out of a running instance by a stale line in an old env file. Pinned by
|
|
18
|
+
* `test/unit/config.test.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import YAML from "yaml";
|
|
25
|
+
import { DEFAULT_DRIVE_MODE, isKnownDriveMode, isKnownModel, } from "./models.js";
|
|
26
|
+
import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
27
|
+
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
28
|
+
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
29
|
+
import { resolveManagementApiConfig, } from "./management-config.js";
|
|
30
|
+
import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes, } from "./attachments-config.js";
|
|
31
|
+
function abs(p) {
|
|
32
|
+
return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Canonicalize a path: resolve symlinks in the deepest EXISTING ancestor, then
|
|
36
|
+
* re-append the not-yet-created tail. This matters because Claude Code records
|
|
37
|
+
* session transcripts under the *real* working directory (e.g. macOS maps
|
|
38
|
+
* `/tmp` -> `/private/tmp`), and SessionDiscoveryService encodes the configured
|
|
39
|
+
* working_directory to find them. Without canonicalization the configured path
|
|
40
|
+
* and the recorded path diverge and session discovery returns nothing.
|
|
41
|
+
*
|
|
42
|
+
* On Linux (the deploy target) this is typically a no-op, but it keeps paddock
|
|
43
|
+
* portable and robust against symlinked data roots.
|
|
44
|
+
*/
|
|
45
|
+
function canonical(p) {
|
|
46
|
+
const absolute = abs(p);
|
|
47
|
+
let dir = absolute;
|
|
48
|
+
const tail = [];
|
|
49
|
+
// Walk up to the first existing ancestor.
|
|
50
|
+
// eslint-disable-next-line no-constant-condition
|
|
51
|
+
while (true) {
|
|
52
|
+
try {
|
|
53
|
+
const real = fs.realpathSync(dir);
|
|
54
|
+
return tail.length ? path.join(real, ...tail.reverse()) : real;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir)
|
|
59
|
+
return absolute; // reached root without resolving
|
|
60
|
+
tail.push(path.basename(dir));
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function envOr(name, fallback) {
|
|
66
|
+
const v = process.env[name];
|
|
67
|
+
return v && v.trim().length > 0 ? v : fallback;
|
|
68
|
+
}
|
|
69
|
+
/** An env var's trimmed value, or undefined when unset/blank. */
|
|
70
|
+
function envOpt(name) {
|
|
71
|
+
const v = process.env[name];
|
|
72
|
+
return v && v.trim().length > 0 ? v.trim() : undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fold a config-file value in as the fallback beneath a hardcoded default. The
|
|
76
|
+
* file value is stringified — so the exact same parsing/coercion an env value
|
|
77
|
+
* would get applies — and used when present and non-blank; otherwise the
|
|
78
|
+
* hardcoded default. Env still wins because callers pass the result as the
|
|
79
|
+
* `envOr` fallback (which is only consulted when the env var is unset).
|
|
80
|
+
*/
|
|
81
|
+
function fileOr(fileVal, fallback) {
|
|
82
|
+
if (fileVal === undefined || fileVal === null)
|
|
83
|
+
return fallback;
|
|
84
|
+
const s = String(fileVal);
|
|
85
|
+
return s.trim().length > 0 ? s : fallback;
|
|
86
|
+
}
|
|
87
|
+
/** A config-file value as a trimmed string, or undefined when absent/blank. */
|
|
88
|
+
function fileOpt(fileVal) {
|
|
89
|
+
if (fileVal === undefined || fileVal === null)
|
|
90
|
+
return undefined;
|
|
91
|
+
const s = String(fileVal).trim();
|
|
92
|
+
return s.length > 0 ? s : undefined;
|
|
93
|
+
}
|
|
94
|
+
/** Default filename for the instance-config file, resolved under the data dir. */
|
|
95
|
+
const DEFAULT_CONFIG_FILENAME = "paddock.config.yaml";
|
|
96
|
+
/**
|
|
97
|
+
* Load the optional YAML instance-config file that provides the BASE layer for
|
|
98
|
+
* {@link PaddockConfig} (issue #270 / DD-5). Env vars still override every value
|
|
99
|
+
* (precedence file < env).
|
|
100
|
+
*
|
|
101
|
+
* Path resolution: an explicit `PADDOCK_CONFIG` env var wins; otherwise
|
|
102
|
+
* `<dataDir>/paddock.config.yaml`. When no file exists at the default location
|
|
103
|
+
* the result is an empty object, so an env-only deployment behaves exactly as
|
|
104
|
+
* before — this is a no-op for existing installs. An explicit `PADDOCK_CONFIG`
|
|
105
|
+
* that points at a missing file is treated as a misconfiguration (a clear error)
|
|
106
|
+
* rather than silently ignored.
|
|
107
|
+
*
|
|
108
|
+
* A present-but-malformed file (unparseable YAML, or a top-level scalar/list
|
|
109
|
+
* instead of a mapping) throws a clear Error rather than starting up with a
|
|
110
|
+
* half-empty config.
|
|
111
|
+
*/
|
|
112
|
+
export function loadConfigFile(dataDir) {
|
|
113
|
+
const explicit = envOpt("PADDOCK_CONFIG");
|
|
114
|
+
const configPath = explicit ? abs(explicit) : path.join(dataDir, DEFAULT_CONFIG_FILENAME);
|
|
115
|
+
let raw;
|
|
116
|
+
try {
|
|
117
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const code = err.code;
|
|
121
|
+
if (code === "ENOENT") {
|
|
122
|
+
// Missing default file → env-only, unchanged behaviour. A missing file
|
|
123
|
+
// that was EXPLICITLY requested is an operator error worth surfacing.
|
|
124
|
+
if (explicit) {
|
|
125
|
+
throw new Error(`PADDOCK_CONFIG points at a config file that does not exist: ${configPath}`);
|
|
126
|
+
}
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Failed to read Paddock config file ${configPath}: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = YAML.parse(raw);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
throw new Error(`Failed to parse Paddock config file ${configPath} as YAML: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
// An empty (or comments-only) file parses to null → treat as "no overrides".
|
|
139
|
+
if (parsed === null || parsed === undefined)
|
|
140
|
+
return {};
|
|
141
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
142
|
+
throw new Error(`Paddock config file ${configPath} must contain a YAML mapping (got ${Array.isArray(parsed) ? "a list" : typeof parsed}).`);
|
|
143
|
+
}
|
|
144
|
+
// A valueless key (`brand:` / `auth:` with nothing after it) parses to `null`.
|
|
145
|
+
// Treat such an empty section (or scalar) as ABSENT — drop it so it falls back
|
|
146
|
+
// to env/defaults — rather than passing `null` through to a loader that expects
|
|
147
|
+
// an object (which would crash with an unclear TypeError). Deeper `null`s and
|
|
148
|
+
// wrong-typed sections already degrade to defaults via fileOr/fileOpt.
|
|
149
|
+
const obj = parsed;
|
|
150
|
+
for (const key of Object.keys(obj)) {
|
|
151
|
+
if (obj[key] === null)
|
|
152
|
+
delete obj[key];
|
|
153
|
+
}
|
|
154
|
+
return obj;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the provider-agnostic auth config from `PADDOCK_AUTH_*` env vars.
|
|
158
|
+
* Everything is optional; the default is `none` (fully open). Validation of
|
|
159
|
+
* mode-specific requirements (e.g. a JWKS URL in jwt mode) happens in the auth
|
|
160
|
+
* plugin so a misconfig surfaces as a clear startup error rather than a
|
|
161
|
+
* silently-open server.
|
|
162
|
+
*/
|
|
163
|
+
function loadAuthConfig(file = {}) {
|
|
164
|
+
const rawMode = envOr("PADDOCK_AUTH_MODE", fileOr(file.mode, "none")).toLowerCase();
|
|
165
|
+
const mode = rawMode === "trusted-header" || rawMode === "jwt" ? rawMode : "none";
|
|
166
|
+
return {
|
|
167
|
+
mode,
|
|
168
|
+
userHeader: envOr("PADDOCK_AUTH_USER_HEADER", fileOr(file.userHeader, "X-Forwarded-User")),
|
|
169
|
+
emailHeader: envOpt("PADDOCK_AUTH_EMAIL_HEADER") ?? fileOpt(file.emailHeader),
|
|
170
|
+
groupsHeader: envOpt("PADDOCK_AUTH_GROUPS_HEADER") ?? fileOpt(file.groupsHeader),
|
|
171
|
+
jwtHeader: envOr("PADDOCK_AUTH_JWT_HEADER", fileOr(file.jwtHeader, "Authorization")),
|
|
172
|
+
jwksUrl: envOpt("PADDOCK_AUTH_JWKS_URL") ?? fileOpt(file.jwksUrl),
|
|
173
|
+
jwtIssuer: envOpt("PADDOCK_AUTH_JWT_ISSUER") ?? fileOpt(file.jwtIssuer),
|
|
174
|
+
jwtAudience: envOpt("PADDOCK_AUTH_JWT_AUDIENCE") ?? fileOpt(file.jwtAudience),
|
|
175
|
+
usernameClaim: envOpt("PADDOCK_AUTH_USERNAME_CLAIM") ?? fileOpt(file.usernameClaim),
|
|
176
|
+
groupsClaim: envOr("PADDOCK_AUTH_GROUPS_CLAIM", fileOr(file.groupsClaim, "groups")),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the dangerously-open opt-in from `PADDOCK_DANGEROUSLY_ALLOW_OPEN`
|
|
181
|
+
* (#435). Off by default; accepts the shared 1/true/yes truthy convention. When
|
|
182
|
+
* set, the bind-safety guard downgrades its refusal (non-loopback + auth=none)
|
|
183
|
+
* to a loud boot warning instead of failing closed.
|
|
184
|
+
*/
|
|
185
|
+
function loadDangerouslyAllowOpen() {
|
|
186
|
+
const raw = envOr("PADDOCK_DANGEROUSLY_ALLOW_OPEN", "false").toLowerCase();
|
|
187
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the voice-dictation (Whisper) capability from env. Defaults to `off`,
|
|
191
|
+
* so a plain instance never shows a mic button. The mode defaults to `remote`
|
|
192
|
+
* when an endpoint is configured (the common case — point it at a shared whisper
|
|
193
|
+
* server), and can be forced with `PADDOCK_WHISPER_MODE`. Accepts local/remote/off.
|
|
194
|
+
*/
|
|
195
|
+
function loadTranscriptionConfig(file = {}) {
|
|
196
|
+
const endpoint = envOpt("PADDOCK_WHISPER_ENDPOINT") ?? fileOpt(file.endpoint);
|
|
197
|
+
const rawMode = envOr("PADDOCK_WHISPER_MODE", fileOr(file.mode, endpoint ? "remote" : "off")).toLowerCase();
|
|
198
|
+
const mode = rawMode === "local" || rawMode === "remote" ? rawMode : "off";
|
|
199
|
+
return {
|
|
200
|
+
mode,
|
|
201
|
+
model: envOr("PADDOCK_WHISPER_MODEL", fileOr(file.model, "base")),
|
|
202
|
+
endpoint,
|
|
203
|
+
apiKey: envOpt("PADDOCK_WHISPER_API_KEY") ?? fileOpt(file.apiKey),
|
|
204
|
+
language: envOpt("PADDOCK_WHISPER_LANGUAGE") ?? fileOpt(file.language),
|
|
205
|
+
maxUploadBytes: Number(envOr("PADDOCK_WHISPER_MAX_UPLOAD_BYTES", fileOr(file.maxUploadBytes, String(25 * 1024 * 1024)))),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolve per-instance branding from env. Defaults preserve today's look, so a
|
|
210
|
+
* plain instance is unchanged; an operator running several instances from one
|
|
211
|
+
* image sets `PADDOCK_BRAND_*` in each instance's env to tell them apart.
|
|
212
|
+
*/
|
|
213
|
+
function loadBrandConfig(file = {}) {
|
|
214
|
+
return {
|
|
215
|
+
name: envOr("PADDOCK_BRAND_NAME", fileOr(file.name, "Paddock")),
|
|
216
|
+
logo: envOr("PADDOCK_BRAND_LOGO", fileOr(file.logo, "🐎")),
|
|
217
|
+
accent: envOr("PADDOCK_BRAND_ACCENT", fileOr(file.accent, "#c2603c")),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Default location of the built web SPA, resolved RELATIVE TO THIS MODULE:
|
|
222
|
+
* `packages/server/{src,dist}/config.js` -> `packages/web/dist`. The same hop
|
|
223
|
+
* works from both the `tsx src/` and `node dist/` layouts, so neither needs a
|
|
224
|
+
* `PADDOCK_WEB_DIST` override.
|
|
225
|
+
*
|
|
226
|
+
* Takes the module URL as a parameter purely so a test can exercise install
|
|
227
|
+
* paths this repo's own checkout does not have.
|
|
228
|
+
*
|
|
229
|
+
* **`fileURLToPath`, NOT `new URL(url).pathname`.** The raw pathname is
|
|
230
|
+
* percent-ENCODED, so an install path containing a space or any non-ASCII
|
|
231
|
+
* character (`/opt/my paddock/`, `~/Développement/`) yields a literal `%20`
|
|
232
|
+
* that resolves to a directory which does not exist. The failure is SILENT:
|
|
233
|
+
* `app.ts` degrades to API-only mode with only a log warning, so the user gets
|
|
234
|
+
* a blank page and no explanation of why. `fileURLToPath` also decodes the
|
|
235
|
+
* `/C:/...` drive-letter form on Windows. This is irrelevant when the path is
|
|
236
|
+
* the Docker image's fixed `/app`, and load-bearing the moment the package is
|
|
237
|
+
* installed under an arbitrary user directory (`npx`, global install).
|
|
238
|
+
*/
|
|
239
|
+
export function resolveDefaultWebDist(moduleUrl) {
|
|
240
|
+
return path.resolve(path.dirname(fileURLToPath(moduleUrl)), "../../web/dist");
|
|
241
|
+
}
|
|
242
|
+
export function loadPaddockConfig() {
|
|
243
|
+
// The optional YAML instance-config file provides the BASE layer; env vars
|
|
244
|
+
// override it (precedence file < env). The file is located under the data dir,
|
|
245
|
+
// so resolve a BOOTSTRAP data dir from env/default first to find it — the file
|
|
246
|
+
// may then re-base the data dir for the resolved config below.
|
|
247
|
+
const bootstrapDataDir = abs(envOr("PADDOCK_DATA_DIR", "./data"));
|
|
248
|
+
const file = loadConfigFile(bootstrapDataDir);
|
|
249
|
+
// Ensure the data root exists first so symlinks (e.g. /tmp -> /private/tmp on
|
|
250
|
+
// macOS) resolve consistently for every derived path below.
|
|
251
|
+
const dataRoot = abs(envOr("PADDOCK_DATA_DIR", fileOr(file.dataDir, "./data")));
|
|
252
|
+
try {
|
|
253
|
+
fs.mkdirSync(dataRoot, { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
/* best-effort; downstream mkdirs will surface real errors */
|
|
257
|
+
}
|
|
258
|
+
// working_directory of keeper agents MUST be canonical so session discovery
|
|
259
|
+
// (which encodes the real path) can find Claude transcripts.
|
|
260
|
+
const dataDir = canonical(dataRoot);
|
|
261
|
+
const projectsRoot = canonical(envOr("PADDOCK_PROJECTS_DIR", fileOr(file.projectsRoot, path.join(dataRoot, "projects"))));
|
|
262
|
+
const stateDir = canonical(envOr("PADDOCK_STATE_DIR", fileOr(file.stateDir, path.join(dataRoot, ".herdctl"))));
|
|
263
|
+
const herdctlConfigPath = canonical(envOr("PADDOCK_HERDCTL_CONFIG", fileOr(file.herdctlConfigPath, path.join(dataRoot, "herdctl.yaml"))));
|
|
264
|
+
// The ONE Claude home for this process (#588): paddock's transcript symlinks,
|
|
265
|
+
// the engine's session discovery and session adoption all resolve against this
|
|
266
|
+
// exact value. NOT canonical()-ed: `claudeHome()` is also what the CLI runtime
|
|
267
|
+
// and Claude Code itself see, and canonicalising would silently diverge from
|
|
268
|
+
// the literal path they encode into `<home>/projects/<encoded-cwd>`.
|
|
269
|
+
const resolvedClaudeHome = claudeHome();
|
|
270
|
+
const defaultWebDist = resolveDefaultWebDist(import.meta.url);
|
|
271
|
+
return Object.freeze({
|
|
272
|
+
port: Number(envOr("PORT", fileOr(file.port, "4000"))),
|
|
273
|
+
// Safe by default (#435): bind loopback unless explicitly told otherwise, so
|
|
274
|
+
// a fresh source/tarball run is network-closed. Non-loopback + no auth is then
|
|
275
|
+
// gated by the bind-safety guard (see bind-safety.ts). Existing deployments
|
|
276
|
+
// that set HOST/PADDOCK_HOST are unaffected — only the DEFAULT changed from
|
|
277
|
+
// 0.0.0.0. (The container image keeps ENV HOST=0.0.0.0: the network namespace
|
|
278
|
+
// is its boundary and Docker can't reach 127.0.0.1 inside the container.)
|
|
279
|
+
host: envOr("HOST", envOr("PADDOCK_HOST", fileOr(file.host, "127.0.0.1"))),
|
|
280
|
+
dangerouslyAllowOpen: loadDangerouslyAllowOpen(),
|
|
281
|
+
dataDir,
|
|
282
|
+
projectsRoot,
|
|
283
|
+
stateDir,
|
|
284
|
+
herdctlConfigPath,
|
|
285
|
+
webDist: abs(envOr("PADDOCK_WEB_DIST", fileOr(file.webDist, defaultWebDist))),
|
|
286
|
+
claudeHome: resolvedClaudeHome,
|
|
287
|
+
auth: loadAuthConfig(file.auth),
|
|
288
|
+
transcription: loadTranscriptionConfig(file.transcription),
|
|
289
|
+
brand: loadBrandConfig(file.brand),
|
|
290
|
+
models: loadModels(file.models),
|
|
291
|
+
driveMode: loadDriveMode(file.driveMode),
|
|
292
|
+
nativeSystemPrompt: loadNativeSystemPrompt(file.nativeSystemPrompt),
|
|
293
|
+
selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled),
|
|
294
|
+
selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) && loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled),
|
|
295
|
+
// Projects tool implies write implies read — the tool is appended in the write
|
|
296
|
+
// block, so both outer gates must be on for it to mean anything (issue #467).
|
|
297
|
+
selfMcpProjectsEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) &&
|
|
298
|
+
loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled) &&
|
|
299
|
+
loadSelfMcpProjectsEnabled(file.selfMcpProjectsEnabled),
|
|
300
|
+
maxSpawnDepth: loadMaxSpawnDepth(file.maxSpawnDepth),
|
|
301
|
+
scheduleMutationEnabled: loadScheduleMutationEnabled(file.scheduleMutationEnabled),
|
|
302
|
+
...(() => {
|
|
303
|
+
// #312 M1: resolved against the real environment (token material is only
|
|
304
|
+
// ever an `env:` reference). Diagnostics ride along for app.ts to log.
|
|
305
|
+
const { config, errors, warnings } = resolveManagementApiConfig(file.managementApi, process.env);
|
|
306
|
+
return { managementApi: config, managementApiDiagnostics: { errors, warnings } };
|
|
307
|
+
})(),
|
|
308
|
+
hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled),
|
|
309
|
+
recovery: loadRecoveryConfig(file.recovery),
|
|
310
|
+
attachments: loadAttachmentsConfig(file.attachments),
|
|
311
|
+
curation: loadCurationConfig(file.curation),
|
|
312
|
+
logLevel: envOr("LOG_LEVEL", fileOr(file.logLevel, "info")),
|
|
313
|
+
browserMcp: loadBrowserMcp(file.browserMcp),
|
|
314
|
+
openapi: loadOpenApiConfig(file.openapi),
|
|
315
|
+
sweepMinIntervalMs: loadSweepMinIntervalMs(file.sweepMinIntervalMs),
|
|
316
|
+
gitAuthor: {
|
|
317
|
+
name: envOr("PADDOCK_GIT_AUTHOR_NAME", fileOr(file.gitAuthor?.name, "Paddock")),
|
|
318
|
+
email: envOr("PADDOCK_GIT_AUTHOR_EMAIL", fileOr(file.gitAuthor?.email, "paddock@localhost")),
|
|
319
|
+
},
|
|
320
|
+
githubClientId: envOpt("PADDOCK_GITHUB_CLIENT_ID") ?? fileOpt(file.githubClientId),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// CurationConfig + DEFAULT_CURATION now live in ./curation-config.ts (issue #384),
|
|
324
|
+
// alongside the per-project override resolver — mirroring recovery-config.ts.
|
|
325
|
+
/** Resolve one positive-integer budget from env < file < built-in default. */
|
|
326
|
+
function loadCurationNum(envName, file, def) {
|
|
327
|
+
const raw = envOpt(envName) ?? fileOpt(file);
|
|
328
|
+
if (raw === undefined)
|
|
329
|
+
return def;
|
|
330
|
+
const n = Number(raw);
|
|
331
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : def;
|
|
332
|
+
}
|
|
333
|
+
/** Resolve the curation token budgets (issue #379), precedence env < file < default. */
|
|
334
|
+
function loadCurationConfig(file) {
|
|
335
|
+
return {
|
|
336
|
+
overviewMaxTokens: loadCurationNum("PADDOCK_CURATION_OVERVIEW_MAX_TOKENS", file?.overviewMaxTokens, DEFAULT_CURATION.overviewMaxTokens),
|
|
337
|
+
changelogMaxTokens: loadCurationNum("PADDOCK_CURATION_CHANGELOG_MAX_TOKENS", file?.changelogMaxTokens, DEFAULT_CURATION.changelogMaxTokens),
|
|
338
|
+
claudeMaxTokens: loadCurationNum("PADDOCK_CURATION_CLAUDEMD_MAX_TOKENS", file?.claudeMaxTokens, DEFAULT_CURATION.claudeMaxTokens),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Resolve the post-turn sweep's minimum interval from
|
|
343
|
+
* `PADDOCK_SWEEP_MIN_INTERVAL_MS` (issue #269 fold). Returns `undefined` when
|
|
344
|
+
* unset or invalid (non-finite / negative) so the SweepService default (5 min)
|
|
345
|
+
* applies — preserving the pre-fold `envIntervalMs()` semantics exactly.
|
|
346
|
+
*/
|
|
347
|
+
function loadSweepMinIntervalMs(file) {
|
|
348
|
+
const raw = envOpt("PADDOCK_SWEEP_MIN_INTERVAL_MS") ?? fileOpt(file);
|
|
349
|
+
if (raw === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
const n = Number(raw);
|
|
352
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Resolve whether keeper agents receive the Playwright browser MCP
|
|
356
|
+
* (issue #269 fold). The env var keeps its exact literal-'1' semantics (any
|
|
357
|
+
* other set value — including `true` — disables it, matching pre-loader
|
|
358
|
+
* behaviour); only when the env var is UNSET does the config file provide the
|
|
359
|
+
* base, using the 1/true/yes convention shared by the other boolean knobs.
|
|
360
|
+
*/
|
|
361
|
+
function loadBrowserMcp(file) {
|
|
362
|
+
const env = process.env.PADDOCK_BROWSER_MCP;
|
|
363
|
+
if (env !== undefined)
|
|
364
|
+
return env === "1";
|
|
365
|
+
const raw = fileOpt(file)?.toLowerCase();
|
|
366
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Resolve the OpenAPI reference config. `enabled` defaults OFF (opt-in):
|
|
370
|
+
* publishing the API surface is a deliberate choice, so it only mounts when
|
|
371
|
+
* `PADDOCK_OPENAPI_ENABLED` / YAML is a truthy value (1/true/yes/on). `path`
|
|
372
|
+
* (the UI route prefix) defaults `/open-api`; it is normalized to a leading
|
|
373
|
+
* slash with no trailing slash.
|
|
374
|
+
*/
|
|
375
|
+
function loadOpenApiConfig(file) {
|
|
376
|
+
const rawEnabled = (process.env.PADDOCK_OPENAPI_ENABLED ??
|
|
377
|
+
(file?.enabled === undefined ? undefined : String(file.enabled)))?.toLowerCase();
|
|
378
|
+
const enabled = rawEnabled === "1" || rawEnabled === "true" || rawEnabled === "yes" || rawEnabled === "on";
|
|
379
|
+
let path = envOpt("PADDOCK_OPENAPI_PATH") ?? fileOpt(file?.path) ?? "/open-api";
|
|
380
|
+
if (!path.startsWith("/"))
|
|
381
|
+
path = "/" + path;
|
|
382
|
+
if (path.length > 1 && path.endsWith("/"))
|
|
383
|
+
path = path.slice(0, -1);
|
|
384
|
+
return { enabled, path };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Resolve the instance-default max spawn depth from `PADDOCK_MAX_SPAWN_DEPTH`
|
|
388
|
+
* (issue #262). Defaults to {@link DEFAULT_MAX_SPAWN_DEPTH} (`1`); a
|
|
389
|
+
* missing/blank/out-of-range value falls back to the default rather than failing
|
|
390
|
+
* startup. A per-project override still wins at dispatch.
|
|
391
|
+
*/
|
|
392
|
+
function loadMaxSpawnDepth(file) {
|
|
393
|
+
const raw = envOpt("PADDOCK_MAX_SPAWN_DEPTH") ?? fileOpt(file);
|
|
394
|
+
if (raw === undefined)
|
|
395
|
+
return DEFAULT_MAX_SPAWN_DEPTH;
|
|
396
|
+
const n = Number(raw);
|
|
397
|
+
return isValidMaxSpawnDepth(n) ? n : DEFAULT_MAX_SPAWN_DEPTH;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Resolve a boolean recovery knob (issue #301) with the shared 1/true/yes
|
|
401
|
+
* convention, env over file over the built-in default. Mirrors the other boolean
|
|
402
|
+
* loaders; factored so each recovery flag reads identically.
|
|
403
|
+
*/
|
|
404
|
+
function loadRecoveryBool(env, file, fallback) {
|
|
405
|
+
const raw = envOr(env, fileOr(file, String(fallback))).toLowerCase();
|
|
406
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Resolve a non-negative-integer recovery knob (issue #301). A missing/blank/
|
|
410
|
+
* non-finite/negative value falls back to the default rather than failing
|
|
411
|
+
* startup — defensive, like {@link loadMaxSpawnDepth}.
|
|
412
|
+
*/
|
|
413
|
+
function loadRecoveryInt(env, file, fallback) {
|
|
414
|
+
const raw = envOpt(env) ?? fileOpt(file);
|
|
415
|
+
if (raw === undefined)
|
|
416
|
+
return fallback;
|
|
417
|
+
const n = Number(raw);
|
|
418
|
+
return Number.isInteger(n) && n >= 0 ? n : fallback;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Resolve the instance-default keeper-chat recovery config (issue #301). Layer 2
|
|
422
|
+
* (`surfaceKilledTask`) defaults ON, Layer 3 (`autoReDrive`) defaults OFF; the
|
|
423
|
+
* guards default per {@link DEFAULT_RECOVERY}. Each field is env-over-file-over-
|
|
424
|
+
* default, and a per-project `recovery` override still wins at dispatch
|
|
425
|
+
* (resolveRecoveryConfig). Malformed values degrade to the default, never fatal.
|
|
426
|
+
*/
|
|
427
|
+
function loadRecoveryConfig(file) {
|
|
428
|
+
const f = file ?? {};
|
|
429
|
+
return {
|
|
430
|
+
surfaceKilledTask: loadRecoveryBool("PADDOCK_RECOVERY_SURFACE", f.surfaceKilledTask, DEFAULT_RECOVERY.surfaceKilledTask),
|
|
431
|
+
autoReDrive: loadRecoveryBool("PADDOCK_RECOVERY_AUTODRIVE", f.autoReDrive, DEFAULT_RECOVERY.autoReDrive),
|
|
432
|
+
debounceMs: loadRecoveryInt("PADDOCK_RECOVERY_DEBOUNCE_MS", f.debounceMs, DEFAULT_RECOVERY.debounceMs),
|
|
433
|
+
maxRetries: loadRecoveryInt("PADDOCK_RECOVERY_MAX_RETRIES", f.maxRetries, DEFAULT_RECOVERY.maxRetries),
|
|
434
|
+
limboTimeoutMs: loadRecoveryInt("PADDOCK_RECOVERY_LIMBO_MS", f.limboTimeoutMs, DEFAULT_RECOVERY.limboTimeoutMs),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Resolve a positive-integer attachment knob (issue #328). A missing/blank/
|
|
439
|
+
* non-integer/< 1 value falls back to the default rather than failing startup —
|
|
440
|
+
* defensive, like {@link loadRecoveryInt} (but size/count must be ≥ 1).
|
|
441
|
+
*/
|
|
442
|
+
function loadAttachmentsInt(env, file, fallback) {
|
|
443
|
+
const raw = envOpt(env) ?? fileOpt(file);
|
|
444
|
+
if (raw === undefined)
|
|
445
|
+
return fallback;
|
|
446
|
+
const n = Number(raw);
|
|
447
|
+
return Number.isInteger(n) && n >= 1 ? n : fallback;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Resolve the `allowedTypes` list (issue #328). The env var is comma-separated
|
|
451
|
+
* (`PADDOCK_ATTACHMENTS_ALLOWED_TYPES=image/*,.pdf`); the YAML file takes a real
|
|
452
|
+
* array (or a comma-separated string, coerced the same way). An empty/invalid
|
|
453
|
+
* value falls back to the default (allow-all) — never "deny all".
|
|
454
|
+
*/
|
|
455
|
+
function loadAllowedTypes(file, fallback) {
|
|
456
|
+
const env = envOpt("PADDOCK_ATTACHMENTS_ALLOWED_TYPES");
|
|
457
|
+
const raw = env !== undefined ? env.split(",") : Array.isArray(file) ? file : typeof file === "string" ? file.split(",") : undefined;
|
|
458
|
+
return sanitizeAllowedTypes(raw) ?? fallback;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Resolve the instance-default inbound-attachment config (issue #328). Enabled +
|
|
462
|
+
* allow-all by default; each field is env-over-file-over-default, and a
|
|
463
|
+
* per-project `attachments` override still wins at request time
|
|
464
|
+
* (resolveAttachmentsConfig). Malformed values degrade to the default, never fatal.
|
|
465
|
+
*/
|
|
466
|
+
function loadAttachmentsConfig(file) {
|
|
467
|
+
const f = file ?? {};
|
|
468
|
+
const enabledRaw = envOr("PADDOCK_ATTACHMENTS_ENABLED", fileOr(f.enabled, String(DEFAULT_ATTACHMENTS.enabled))).toLowerCase();
|
|
469
|
+
return {
|
|
470
|
+
enabled: enabledRaw === "1" || enabledRaw === "true" || enabledRaw === "yes",
|
|
471
|
+
maxFileSizeMb: loadAttachmentsInt("PADDOCK_ATTACHMENTS_MAX_FILE_SIZE_MB", f.maxFileSizeMb, DEFAULT_ATTACHMENTS.maxFileSizeMb),
|
|
472
|
+
maxFilesPerMessage: loadAttachmentsInt("PADDOCK_ATTACHMENTS_MAX_FILES_PER_MESSAGE", f.maxFilesPerMessage, DEFAULT_ATTACHMENTS.maxFilesPerMessage),
|
|
473
|
+
allowedTypes: loadAllowedTypes(f.allowedTypes, DEFAULT_ATTACHMENTS.allowedTypes),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Resolve the per-deployment schedule-mutation gate (issue #265 / DD-7). Defaults
|
|
478
|
+
* OFF so a plain instance can't have its schedules mutated programmatically; an
|
|
479
|
+
* operator opts in with `PADDOCK_SCHEDULE_MUTATION=1` (or the config file). Accepts
|
|
480
|
+
* 1/true/yes. Static `project.yaml` schedules are armed regardless of this flag.
|
|
481
|
+
*/
|
|
482
|
+
function loadScheduleMutationEnabled(file) {
|
|
483
|
+
const raw = envOr("PADDOCK_SCHEDULE_MUTATION", fileOr(file, "false")).toLowerCase();
|
|
484
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Resolve the instance-default hook-management MCP gate (Epic G / G5, GG-4).
|
|
488
|
+
* Defaults OFF so a plain instance never advertises the hook tools; opt in with
|
|
489
|
+
* `PADDOCK_HOOKS_MCP=1` (or the config file), and a per-project `hooksMcpEnabled`
|
|
490
|
+
* override still wins at dispatch. Accepts 1/true/yes. Only meaningful when the
|
|
491
|
+
* self-MCP write tools are also enabled (the hook tools live on that server).
|
|
492
|
+
*/
|
|
493
|
+
function loadHooksMcpEnabled(file) {
|
|
494
|
+
const raw = envOr("PADDOCK_HOOKS_MCP", fileOr(file, "false")).toLowerCase();
|
|
495
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Resolve whether keepers additionally get the self-management MCP WRITE tools
|
|
499
|
+
* (issue #214 Phase 2). Defaults OFF; only takes effect when `PADDOCK_SELF_MCP`
|
|
500
|
+
* is also on (write implies read — enforced at the call site above). Accepts
|
|
501
|
+
* 1/true/yes.
|
|
502
|
+
*/
|
|
503
|
+
function loadSelfMcpWriteEnabled(file) {
|
|
504
|
+
const raw = envOr("PADDOCK_SELF_MCP_WRITE", fileOr(file, "false")).toLowerCase();
|
|
505
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Resolve whether keepers additionally get the self-management MCP PROJECT tools
|
|
509
|
+
* (issue #467 — `create_project`). Defaults OFF; only takes effect when
|
|
510
|
+
* `PADDOCK_SELF_MCP` and `PADDOCK_SELF_MCP_WRITE` are also on (enforced at the call
|
|
511
|
+
* site above). Accepts 1/true/yes.
|
|
512
|
+
*/
|
|
513
|
+
function loadSelfMcpProjectsEnabled(file) {
|
|
514
|
+
const raw = envOr("PADDOCK_SELF_MCP_PROJECTS", fileOr(file, "false")).toLowerCase();
|
|
515
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Resolve whether keepers get the read-only self-management MCP (issue #214).
|
|
519
|
+
* Defaults to OFF so a plain instance never advertises it; opt in per instance
|
|
520
|
+
* with `PADDOCK_SELF_MCP=1`. Accepts 1/true/yes.
|
|
521
|
+
*/
|
|
522
|
+
function loadSelfMcpEnabled(file) {
|
|
523
|
+
const raw = envOr("PADDOCK_SELF_MCP", fileOr(file, "false")).toLowerCase();
|
|
524
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Resolve whether keeper agents use the native system prompt + CLAUDE.md
|
|
528
|
+
* hierarchy (issue #176). Defaults to `true` (native) on every instance so a
|
|
529
|
+
* seeded instance-wide + per-project `CLAUDE.md` is auto-loaded; set
|
|
530
|
+
* `PADDOCK_NATIVE_PROMPT` to 0/false/no to fall back to the terse replace
|
|
531
|
+
* prompt.
|
|
532
|
+
*/
|
|
533
|
+
function loadNativeSystemPrompt(file) {
|
|
534
|
+
const raw = envOr("PADDOCK_NATIVE_PROMPT", fileOr(file, "true")).toLowerCase();
|
|
535
|
+
return !(raw === "0" || raw === "false" || raw === "no");
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Resolve the instance model allow-list (issue #457 Step 2), precedence
|
|
539
|
+
* env `PADDOCK_MODELS` (comma-separated ids) over YAML `models:` (a string array,
|
|
540
|
+
* or a comma-separated string coerced the same way) over the default (undefined ⇒
|
|
541
|
+
* every catalog model is offered). Each id is validated against the catalog with
|
|
542
|
+
* {@link isKnownModel}; unknown/blank/duplicate ids are dropped. If nothing valid
|
|
543
|
+
* survives — or the list was unset — the result is `undefined`, which downstream
|
|
544
|
+
* ({@link import("./models.js").resolveModels}) treats as "offer the full catalog",
|
|
545
|
+
* so an instance is never left with zero models.
|
|
546
|
+
*/
|
|
547
|
+
function loadModels(file) {
|
|
548
|
+
const env = envOpt("PADDOCK_MODELS");
|
|
549
|
+
const raw = env !== undefined
|
|
550
|
+
? env.split(",")
|
|
551
|
+
: Array.isArray(file)
|
|
552
|
+
? file.map((v) => String(v))
|
|
553
|
+
: typeof file === "string"
|
|
554
|
+
? file.split(",")
|
|
555
|
+
: undefined;
|
|
556
|
+
if (raw === undefined)
|
|
557
|
+
return undefined;
|
|
558
|
+
const seen = new Set();
|
|
559
|
+
const ids = [];
|
|
560
|
+
for (const item of raw) {
|
|
561
|
+
const id = item.trim();
|
|
562
|
+
if (!id || seen.has(id) || !isKnownModel(id))
|
|
563
|
+
continue;
|
|
564
|
+
seen.add(id);
|
|
565
|
+
ids.push(id);
|
|
566
|
+
}
|
|
567
|
+
// Empty after filtering (all unknown/blank) ⇒ fall back to the full catalog.
|
|
568
|
+
return ids.length > 0 ? ids : undefined;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Resolve the global keeper drive mode from `PADDOCK_DRIVE_MODE`. Defaults
|
|
572
|
+
* to `session` (DEFAULT_DRIVE_MODE, #316); an unrecognized value falls back
|
|
573
|
+
* to the default rather than failing startup. A per-project `driveMode` still
|
|
574
|
+
* overrides this at dispatch.
|
|
575
|
+
*/
|
|
576
|
+
function loadDriveMode(file) {
|
|
577
|
+
const raw = (envOpt("PADDOCK_DRIVE_MODE") ?? fileOpt(file))?.toLowerCase();
|
|
578
|
+
return raw && isKnownDriveMode(raw) ? raw : DEFAULT_DRIVE_MODE;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Resolve the Claude home from the environment — `CLAUDE_HOME`, else
|
|
582
|
+
* `~/.claude`.
|
|
583
|
+
*
|
|
584
|
+
* This is the RESOLVER, not the accessor. It is read once by
|
|
585
|
+
* {@link loadPaddockConfig} into {@link PaddockConfig.claudeHome}; runtime code
|
|
586
|
+
* should thread `cfg.claudeHome` rather than call this again, so paddock and the
|
|
587
|
+
* engine can never end up resolving two different homes (#588). It stays a
|
|
588
|
+
* function (not a constant) because `os.homedir()` must be read at call time —
|
|
589
|
+
* tests and processes that mutate `HOME` depend on that.
|
|
590
|
+
*/
|
|
591
|
+
export function claudeHome() {
|
|
592
|
+
return process.env.CLAUDE_HOME ?? path.join(os.homedir(), ".claude");
|
|
593
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sweeper curation budgets — the per-file token limits the post-turn curator
|
|
3
|
+
* keeps OVERVIEW.md / CHANGELOG.md / CLAUDE.md under (issue #379), with a
|
|
4
|
+
* per-project override layer (issue #384).
|
|
5
|
+
*
|
|
6
|
+
* ── Config discipline (the `recovery`/`driveMode` pattern) ────────────────────
|
|
7
|
+
* Every budget is an instance default (`PADDOCK_CURATION_*` env, YAML instance
|
|
8
|
+
* file beneath it — resolved in config.ts's `loadCurationConfig`) with an
|
|
9
|
+
* optional PER-PROJECT override (`project.yaml` →
|
|
10
|
+
* {@link import("./projects.js").ProjectYaml.curation}). An absent/invalid
|
|
11
|
+
* override inherits the instance default, resolved at sweep time by
|
|
12
|
+
* {@link resolveCurationConfig} — never baked into the DTO (mirrors how
|
|
13
|
+
* `recovery` resolves via `resolveRecoveryConfig`). A malformed override is
|
|
14
|
+
* ignored rather than fatal so a hand-edited `project.yaml` can't wedge a sweep.
|
|
15
|
+
*
|
|
16
|
+
* #379 shipped these instance-only; #384 adds the override so a docs-heavy
|
|
17
|
+
* project can raise its CHANGELOG budget (or a lean project tighten it) without
|
|
18
|
+
* changing the fleet-wide default.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Resolved curation budgets — all fields concrete. Held on {@link
|
|
22
|
+
* import("./config.js").PaddockConfig.curation} (instance defaults) and produced
|
|
23
|
+
* per-sweep by {@link resolveCurationConfig} (project override else instance).
|
|
24
|
+
*/
|
|
25
|
+
export interface CurationConfig {
|
|
26
|
+
/** Budget for OVERVIEW.md (regenerated wholesale each sweep). */
|
|
27
|
+
overviewMaxTokens: number;
|
|
28
|
+
/** Budget for CHANGELOG.md (injected into the preload; the biggest lever). */
|
|
29
|
+
changelogMaxTokens: number;
|
|
30
|
+
/** Budget for the CLAUDE.md curated-notes section (auto-loaded every turn). */
|
|
31
|
+
claudeMaxTokens: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A per-project curation override as stored in `project.yaml` — every field
|
|
35
|
+
* optional (an absent field inherits the instance default at sweep time).
|
|
36
|
+
*/
|
|
37
|
+
export type CurationOverride = Partial<CurationConfig>;
|
|
38
|
+
/** Built-in defaults when neither env nor YAML nor a project override sets a budget. */
|
|
39
|
+
export declare const DEFAULT_CURATION: CurationConfig;
|
|
40
|
+
/**
|
|
41
|
+
* Validate + normalise an untrusted value (from `project.yaml` or a PATCH body)
|
|
42
|
+
* into a {@link CurationOverride}, dropping any field that is missing or invalid,
|
|
43
|
+
* and returning `undefined` when nothing valid remains (so an empty override is
|
|
44
|
+
* never persisted). Each budget must be a positive integer. Defensive by design —
|
|
45
|
+
* a malformed hand-edit degrades to "inherit the instance default", never a
|
|
46
|
+
* sweep crash.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sanitizeCurationOverride(value: unknown): CurationOverride | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the effective curation budgets for a sweep: a valid per-project
|
|
51
|
+
* override wins field-by-field; every absent field inherits the instance default
|
|
52
|
+
* (which is itself `env > paddock.config.yaml > built-in default`). The override
|
|
53
|
+
* is re-sanitised so a corrupt on-disk value can't leak through.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveCurationConfig(override: CurationOverride | undefined, instanceDefault: CurationConfig): CurationConfig;
|