@edspencer/paddock 0.0.1 → 0.57.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 +538 -0
- package/packages/server/dist/config.js +625 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -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 +623 -0
- package/packages/server/dist/herdctl.js +1279 -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 +143 -0
- package/packages/server/dist/instance-config.js +385 -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-Bypd_quL.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
- package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
- package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
- package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.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-C6Uuq-dq.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
- package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.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-B-YuzkF6.js +71 -0
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.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-Cs0D32Ry.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-Bc9fuI2i.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.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-BzGHkF9K.js +2 -0
- package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.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-CDmilo8S.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.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-BkUK73W0.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.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,143 @@
|
|
|
1
|
+
import { type PaddockConfig } from "./config.js";
|
|
2
|
+
/** Groups the Settings screen renders, in display order. */
|
|
3
|
+
export declare const GROUPS: {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}[];
|
|
8
|
+
/**
|
|
9
|
+
* How the Settings screen renders a field. `text` is `string` with a multi-line
|
|
10
|
+
* control (a `<textarea>`) — same wire shape, same coercion contract; the only
|
|
11
|
+
* difference is that the UI stops squashing paragraphs into one line.
|
|
12
|
+
*/
|
|
13
|
+
type FieldType = "number" | "boolean" | "string" | "text" | "enum" | "string-list";
|
|
14
|
+
/** Outcome of coercing a raw patch value into a persistable one. */
|
|
15
|
+
type Coerced = {
|
|
16
|
+
ok: true;
|
|
17
|
+
value: unknown;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One editable-or-displayed instance-config field. `key` is a stable dotted path
|
|
24
|
+
* used both to read the resolved value out of {@link PaddockConfig} and as the
|
|
25
|
+
* write path into the YAML document — the two shapes match by construction.
|
|
26
|
+
*/
|
|
27
|
+
interface FieldSpec {
|
|
28
|
+
/** Stable dotted id (also the YAML/`PaddockConfig` path). */
|
|
29
|
+
key: string;
|
|
30
|
+
group: string;
|
|
31
|
+
label: string;
|
|
32
|
+
help?: string;
|
|
33
|
+
type: FieldType;
|
|
34
|
+
/** Allowed values for `enum` fields. */
|
|
35
|
+
enumValues?: readonly string[];
|
|
36
|
+
/** Env var(s) that shadow this field; the first set one is reported. */
|
|
37
|
+
envVars: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Env-shadow semantics. Default (`false`) = the var shadows only when set to a
|
|
40
|
+
* non-blank value, matching `envOr`/`envOpt` in config.ts. Set `true` for the
|
|
41
|
+
* fields whose loaders key on `env !== undefined` instead, so a defined-but-
|
|
42
|
+
* blank var is still authoritative and must render read-only here too:
|
|
43
|
+
* - `browserMcp` (`loadBrowserMcp`) — a blank `PADDOCK_BROWSER_MCP` forces
|
|
44
|
+
* `false`;
|
|
45
|
+
* - `environmentPrompt` (`loadEnvironmentPrompt`, #635) — a blank
|
|
46
|
+
* `PADDOCK_ENVIRONMENT_PROMPT` IS the opt-out.
|
|
47
|
+
*/
|
|
48
|
+
envShadowWhenDefined?: boolean;
|
|
49
|
+
/** Built-in default (what you get with neither env nor file). `null` ⇒ unset. */
|
|
50
|
+
default: unknown;
|
|
51
|
+
/** Whether the UI may edit + PUT this field. Read-only fields are display-only. */
|
|
52
|
+
editable: boolean;
|
|
53
|
+
/** Semi-sensitive (shown with a caution note); never carries a secret value. */
|
|
54
|
+
sensitive?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Validate + coerce a raw JSON patch value into the value written to YAML.
|
|
57
|
+
* Only defined for editable fields. Mirrors the loader's acceptance so a UI
|
|
58
|
+
* write can't produce a file the loader would reject (it would just degrade).
|
|
59
|
+
*/
|
|
60
|
+
coerce?: (raw: unknown) => Coerced;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The single source of truth for the instance-settings surface. Order within a
|
|
64
|
+
* group is display order. Editable fields carry a `coerce`; read-only fields
|
|
65
|
+
* don't. NO field here holds a secret value (that's why auth JWT internals and
|
|
66
|
+
* the transcription API key are absent).
|
|
67
|
+
*/
|
|
68
|
+
export declare const FIELDS: readonly FieldSpec[];
|
|
69
|
+
/** One field's DTO in the GET response. */
|
|
70
|
+
export interface InstanceConfigFieldDto {
|
|
71
|
+
key: string;
|
|
72
|
+
group: string;
|
|
73
|
+
label: string;
|
|
74
|
+
help?: string;
|
|
75
|
+
type: FieldType;
|
|
76
|
+
enumValues?: readonly string[];
|
|
77
|
+
value: unknown;
|
|
78
|
+
default: unknown;
|
|
79
|
+
editable: boolean;
|
|
80
|
+
sensitive: boolean;
|
|
81
|
+
envOverridden: boolean;
|
|
82
|
+
/** The env var shadowing this field (only when `envOverridden`). */
|
|
83
|
+
envVar?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface InstanceConfigGroupDto {
|
|
86
|
+
id: string;
|
|
87
|
+
label: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
fields: InstanceConfigFieldDto[];
|
|
90
|
+
}
|
|
91
|
+
export interface InstanceConfigDto {
|
|
92
|
+
groups: InstanceConfigGroupDto[];
|
|
93
|
+
/** Absolute path of the file a PUT writes to (informational). */
|
|
94
|
+
configPath: string;
|
|
95
|
+
/** Always false — instance config is frozen at boot; edits need a restart. */
|
|
96
|
+
restartRequired: false;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
|
|
100
|
+
* (the same rule {@link import("./config.js").loadConfigFile} reads it back
|
|
101
|
+
* from), else `<dataDir>/paddock.config.yaml`.
|
|
102
|
+
*/
|
|
103
|
+
export declare function instanceConfigPath(cfg: PaddockConfig): string;
|
|
104
|
+
/**
|
|
105
|
+
* Build the grouped GET DTO from the resolved (frozen) config + live env. Each
|
|
106
|
+
* field reports its current `value`, built-in `default`, and whether an env var
|
|
107
|
+
* currently shadows it (so the UI renders env-shadowed fields read-only).
|
|
108
|
+
*/
|
|
109
|
+
export declare function buildInstanceConfig(cfg: PaddockConfig): InstanceConfigDto;
|
|
110
|
+
/** A rejected PUT: which field failed and why (surfaced as a 400). */
|
|
111
|
+
export declare class InstanceConfigError extends Error {
|
|
112
|
+
readonly field?: string | undefined;
|
|
113
|
+
constructor(message: string, field?: string | undefined);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Validate a patch of editable fields and coerce each value. Throws
|
|
117
|
+
* {@link InstanceConfigError} on the first offending field:
|
|
118
|
+
* - an unknown key,
|
|
119
|
+
* - a read-only / non-editable key,
|
|
120
|
+
* - a value the field's `coerce` rejects.
|
|
121
|
+
* Returns the coerced `{ key, value }` pairs ready to write. A field whose value
|
|
122
|
+
* is `null` (an editable optional being cleared) is returned with `value: null`
|
|
123
|
+
* so the writer can delete it from the file.
|
|
124
|
+
*/
|
|
125
|
+
export declare function validatePatch(patch: Record<string, unknown>): {
|
|
126
|
+
key: string;
|
|
127
|
+
value: unknown;
|
|
128
|
+
}[];
|
|
129
|
+
/**
|
|
130
|
+
* Write the validated pairs into `paddock.config.yaml`, preserving operator
|
|
131
|
+
* comments and unmanaged keys. Uses the `yaml` `Document` API to round-trip an
|
|
132
|
+
* existing file (or start a fresh document when none exists), then writes
|
|
133
|
+
* atomically (temp + rename). A `null` value deletes that key (clearing an
|
|
134
|
+
* optional back to its default). Returns the path written.
|
|
135
|
+
*
|
|
136
|
+
* Reuses no loader state — the caller passes the target path (from
|
|
137
|
+
* {@link instanceConfigPath}) so tests can point it anywhere.
|
|
138
|
+
*/
|
|
139
|
+
export declare function writeInstanceConfig(configPath: string, pairs: {
|
|
140
|
+
key: string;
|
|
141
|
+
value: unknown;
|
|
142
|
+
}[]): void;
|
|
143
|
+
export {};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance-wide settings surface (issue #385).
|
|
3
|
+
*
|
|
4
|
+
* Paddock's instance config (`PaddockConfig`) is resolved ONCE at boot and
|
|
5
|
+
* `Object.freeze`d — env over an optional `paddock.config.yaml` file over
|
|
6
|
+
* built-in defaults (see config.ts). Until now the only way to change a knob was
|
|
7
|
+
* to hand-edit that YAML (or an env var) and restart. This module backs a
|
|
8
|
+
* top-level admin Settings screen that reads the resolved config and writes the
|
|
9
|
+
* editable subset back to `paddock.config.yaml`.
|
|
10
|
+
*
|
|
11
|
+
* Three properties the screen depends on, enforced here:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Restart-required.** Writes go to the file only; the running process keeps
|
|
14
|
+
* its frozen config. Every field is `restart` in effect — the UI shows a
|
|
15
|
+
* persistent "takes effect after restart" banner.
|
|
16
|
+
* 2. **Env precedence.** `env > file > default`. A field also set by a
|
|
17
|
+
* `PADDOCK_*` env var is SHADOWED — writing it to the file has no effect
|
|
18
|
+
* while the env var is set. {@link buildInstanceConfig} reports `envOverridden`
|
|
19
|
+
* per field so the UI renders those read-only.
|
|
20
|
+
* 3. **Comment-preserving write.** {@link writeInstanceConfig} round-trips the
|
|
21
|
+
* file through the `yaml` `Document` API (not parse+stringify), so operator
|
|
22
|
+
* comments and any keys we don't manage survive. The write is atomic
|
|
23
|
+
* (temp + rename), and the file is created on first write if absent.
|
|
24
|
+
*
|
|
25
|
+
* Only the fields in {@link FIELDS} are exposed, and only those marked `editable`
|
|
26
|
+
* may be written — path/infra bindings (port, dataDir, …) and auth are read-only
|
|
27
|
+
* display. Secret values (a transcription API key, auth JWT internals) are never
|
|
28
|
+
* put in the descriptor table, so they can't leak into the API response.
|
|
29
|
+
*/
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { parseDocument } from "yaml";
|
|
33
|
+
import { DRIVE_MODES, DEFAULT_DRIVE_MODE, MODELS, isKnownModel } from "./models.js";
|
|
34
|
+
import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
35
|
+
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
36
|
+
import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes } from "./attachments-config.js";
|
|
37
|
+
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
38
|
+
import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
|
|
39
|
+
/** Groups the Settings screen renders, in display order. */
|
|
40
|
+
export const GROUPS = [
|
|
41
|
+
{ id: "curation", label: "Curation", description: "Per-file token budgets the post-turn sweeper keeps its curated files under." },
|
|
42
|
+
{ id: "sweeper", label: "Sweeper" },
|
|
43
|
+
{ id: "capabilities", label: "Capabilities", description: "What keeper agents are allowed to do. Most default off." },
|
|
44
|
+
{ id: "recovery", label: "Recovery" },
|
|
45
|
+
{ id: "attachments", label: "Attachments" },
|
|
46
|
+
{ id: "branding", label: "Branding" },
|
|
47
|
+
{ id: "transcription", label: "Transcription" },
|
|
48
|
+
{ id: "git", label: "Git identity" },
|
|
49
|
+
{ id: "logging", label: "Logging" },
|
|
50
|
+
{ id: "advanced", label: "Advanced (read-only)", description: "Process / filesystem bindings. Change these via env/redeploy, not the UI." },
|
|
51
|
+
];
|
|
52
|
+
// --- coercion helpers -------------------------------------------------------
|
|
53
|
+
const asBool = (raw) => typeof raw === "boolean" ? { ok: true, value: raw } : { ok: false, error: "must be a boolean" };
|
|
54
|
+
const posInt = (raw) => {
|
|
55
|
+
const n = Number(raw);
|
|
56
|
+
return Number.isFinite(n) && Number.isInteger(n) && n > 0
|
|
57
|
+
? { ok: true, value: n }
|
|
58
|
+
: { ok: false, error: "must be a positive integer" };
|
|
59
|
+
};
|
|
60
|
+
const nonNegInt = (raw) => {
|
|
61
|
+
const n = Number(raw);
|
|
62
|
+
return Number.isFinite(n) && Number.isInteger(n) && n >= 0
|
|
63
|
+
? { ok: true, value: n }
|
|
64
|
+
: { ok: false, error: "must be a non-negative integer" };
|
|
65
|
+
};
|
|
66
|
+
/** A non-negative number, OR null/empty-string to clear the override. */
|
|
67
|
+
const optNonNegNumber = (raw) => {
|
|
68
|
+
if (raw === null || raw === "" || raw === undefined)
|
|
69
|
+
return { ok: true, value: null };
|
|
70
|
+
const n = Number(raw);
|
|
71
|
+
return Number.isFinite(n) && n >= 0
|
|
72
|
+
? { ok: true, value: n }
|
|
73
|
+
: { ok: false, error: "must be a non-negative number (or blank to use the default)" };
|
|
74
|
+
};
|
|
75
|
+
const nonEmptyString = (raw) => typeof raw === "string" && raw.trim().length > 0
|
|
76
|
+
? { ok: true, value: raw.trim() }
|
|
77
|
+
: { ok: false, error: "must be a non-empty string" };
|
|
78
|
+
/** A string that may be blank (blank clears the override → falls back to default). */
|
|
79
|
+
const optString = (raw) => typeof raw === "string" ? { ok: true, value: raw.trim() } : { ok: false, error: "must be a string" };
|
|
80
|
+
/**
|
|
81
|
+
* Upper bound on a prompt-shaped field, in characters. Nothing enforces a limit
|
|
82
|
+
* downstream — the text is concatenated into the system prompt of every single
|
|
83
|
+
* turn — so an accidental paste of a whole file would quietly tax every request
|
|
84
|
+
* on the instance forever. 32 KiB is ~50× the built-in default and far more than
|
|
85
|
+
* any deliberate environment note; past that, a 400 is kinder than silence.
|
|
86
|
+
*/
|
|
87
|
+
const MAX_PROMPT_CHARS = 32 * 1024;
|
|
88
|
+
/**
|
|
89
|
+
* A free-text prompt body. Unlike {@link optString} this is **verbatim**: no
|
|
90
|
+
* trimming, because leading/trailing whitespace is content in a prompt and the
|
|
91
|
+
* operator's text must round-trip through YAML byte-for-byte.
|
|
92
|
+
*
|
|
93
|
+
* The three authoring states map onto the wire like this:
|
|
94
|
+
* - `null` → delete the key → fall back to Paddock's built-in default,
|
|
95
|
+
* - `""` → written as an empty string → append nothing (opt out),
|
|
96
|
+
* - any other string → written verbatim → appended instead of the default.
|
|
97
|
+
*
|
|
98
|
+
* Rejects a NUL byte: it survives neither the repo's no-NUL invariant
|
|
99
|
+
* (`scripts/check-no-nul-bytes.mjs`) nor any useful reading of "a prompt".
|
|
100
|
+
*/
|
|
101
|
+
const promptText = (raw) => {
|
|
102
|
+
if (raw === null || raw === undefined)
|
|
103
|
+
return { ok: true, value: null };
|
|
104
|
+
if (typeof raw !== "string")
|
|
105
|
+
return { ok: false, error: "must be a string (or null to restore the default)" };
|
|
106
|
+
if (raw.length > MAX_PROMPT_CHARS) {
|
|
107
|
+
return { ok: false, error: `must be at most ${MAX_PROMPT_CHARS} characters (got ${raw.length})` };
|
|
108
|
+
}
|
|
109
|
+
if (raw.includes("\0"))
|
|
110
|
+
return { ok: false, error: "must not contain NUL bytes" };
|
|
111
|
+
return { ok: true, value: raw };
|
|
112
|
+
};
|
|
113
|
+
const oneOf = (values) => (raw) => typeof raw === "string" && values.includes(raw)
|
|
114
|
+
? { ok: true, value: raw }
|
|
115
|
+
: { ok: false, error: `must be one of: ${values.join(", ")}` };
|
|
116
|
+
const spawnDepth = (raw) => {
|
|
117
|
+
const n = Number(raw);
|
|
118
|
+
return isValidMaxSpawnDepth(n)
|
|
119
|
+
? { ok: true, value: n }
|
|
120
|
+
: { ok: false, error: "must be a small non-negative integer" };
|
|
121
|
+
};
|
|
122
|
+
const hexColor = (raw) => typeof raw === "string" && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(raw.trim())
|
|
123
|
+
? { ok: true, value: raw.trim() }
|
|
124
|
+
: { ok: false, error: "must be a hex color like #c2603c" };
|
|
125
|
+
const stringList = (raw) => {
|
|
126
|
+
const list = sanitizeAllowedTypes(raw);
|
|
127
|
+
return list ? { ok: true, value: list } : { ok: false, error: "must be a non-empty list of type/extension strings" };
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Coerce a raw model allow-list (issue #457 Step 2) into a de-duped list of
|
|
131
|
+
* KNOWN catalog ids. Accepts a real array or a comma-separated string. Rejects
|
|
132
|
+
* (400) an unknown id — operators pick from the built-in catalog by id, so a typo
|
|
133
|
+
* surfaces rather than silently offering nothing — and rejects an empty result
|
|
134
|
+
* (write at least one, or clear the whole field to fall back to the full catalog).
|
|
135
|
+
*/
|
|
136
|
+
const modelList = (raw) => {
|
|
137
|
+
const arr = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(",") : null;
|
|
138
|
+
if (!arr)
|
|
139
|
+
return { ok: false, error: "must be a list of model ids" };
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const ids = [];
|
|
142
|
+
for (const item of arr) {
|
|
143
|
+
if (typeof item !== "string")
|
|
144
|
+
continue;
|
|
145
|
+
const id = item.trim();
|
|
146
|
+
if (!id || seen.has(id))
|
|
147
|
+
continue;
|
|
148
|
+
if (!isKnownModel(id))
|
|
149
|
+
return { ok: false, error: `unknown model id: ${id}` };
|
|
150
|
+
seen.add(id);
|
|
151
|
+
ids.push(id);
|
|
152
|
+
}
|
|
153
|
+
return ids.length > 0
|
|
154
|
+
? { ok: true, value: ids }
|
|
155
|
+
: { ok: false, error: "must include at least one known model id" };
|
|
156
|
+
};
|
|
157
|
+
const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
158
|
+
// --- field catalog ----------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* The single source of truth for the instance-settings surface. Order within a
|
|
161
|
+
* group is display order. Editable fields carry a `coerce`; read-only fields
|
|
162
|
+
* don't. NO field here holds a secret value (that's why auth JWT internals and
|
|
163
|
+
* the transcription API key are absent).
|
|
164
|
+
*/
|
|
165
|
+
export const FIELDS = [
|
|
166
|
+
// Curation — the motivating knobs (issue #379).
|
|
167
|
+
{ key: "curation.overviewMaxTokens", group: "curation", label: "OVERVIEW.md max tokens", type: "number", envVars: ["PADDOCK_CURATION_OVERVIEW_MAX_TOKENS"], default: DEFAULT_CURATION.overviewMaxTokens, editable: true, coerce: posInt },
|
|
168
|
+
{ key: "curation.changelogMaxTokens", group: "curation", label: "CHANGELOG.md max tokens", type: "number", envVars: ["PADDOCK_CURATION_CHANGELOG_MAX_TOKENS"], default: DEFAULT_CURATION.changelogMaxTokens, editable: true, coerce: posInt },
|
|
169
|
+
{ key: "curation.claudeMaxTokens", group: "curation", label: "CLAUDE.md max tokens", type: "number", envVars: ["PADDOCK_CURATION_CLAUDEMD_MAX_TOKENS"], default: DEFAULT_CURATION.claudeMaxTokens, editable: true, coerce: posInt },
|
|
170
|
+
// Sweeper.
|
|
171
|
+
{ key: "sweepMinIntervalMs", group: "sweeper", label: "Min sweep interval (ms)", help: "Minimum ms between post-turn sweeps for one project. Blank = default (5 min).", type: "number", envVars: ["PADDOCK_SWEEP_MIN_INTERVAL_MS"], default: null, editable: true, coerce: optNonNegNumber },
|
|
172
|
+
// Capabilities.
|
|
173
|
+
{ key: "driveMode", group: "capabilities", label: "Keeper drive mode", help: "session = persistent (streaming + cross-turn autonomy); batch = legacy one-shot.", type: "enum", enumValues: DRIVE_MODES, envVars: ["PADDOCK_DRIVE_MODE"], default: DEFAULT_DRIVE_MODE, editable: true, coerce: oneOf(DRIVE_MODES) },
|
|
174
|
+
{ key: "models", group: "capabilities", label: "Offered models", help: "Which built-in catalog models the picker offers, by id (e.g. claude-opus-5, claude-sonnet-5). Blank = offer all catalog models.", type: "string-list", envVars: ["PADDOCK_MODELS"], default: MODELS.map((m) => m.id), editable: true, coerce: modelList },
|
|
175
|
+
{ key: "nativeSystemPrompt", group: "capabilities", label: "Native system prompt", help: "Use Claude Code's native prompt + CLAUDE.md hierarchy (recommended).", type: "boolean", envVars: ["PADDOCK_NATIVE_PROMPT"], default: true, editable: true, coerce: asBool },
|
|
176
|
+
// Issue #635. `envShadowWhenDefined` because loadEnvironmentPrompt keys on
|
|
177
|
+
// definedness, not emptiness — a defined-but-empty PADDOCK_ENVIRONMENT_PROMPT
|
|
178
|
+
// IS the env-level opt-out, so it genuinely shadows the file and must render
|
|
179
|
+
// read-only. `default` carries the built-in text so the UI can offer a
|
|
180
|
+
// one-click restore (a `null` PUT deletes the key) without duplicating it.
|
|
181
|
+
{ key: "environmentPrompt", group: "capabilities", label: "Environment prompt", help: "Appended to every keeper turn's system prompt: what the agent should know about rendering into Paddock rather than a terminal. Clear it to append nothing.", type: "text", envVars: ["PADDOCK_ENVIRONMENT_PROMPT"], envShadowWhenDefined: true, default: DEFAULT_ENVIRONMENT_PROMPT, editable: true, coerce: promptText },
|
|
182
|
+
{ key: "selfMcpEnabled", group: "capabilities", label: "Self-management MCP (read)", help: "Let keepers list/read projects and other chats.", type: "boolean", envVars: ["PADDOCK_SELF_MCP"], default: false, editable: true, coerce: asBool },
|
|
183
|
+
{ key: "selfMcpWriteEnabled", group: "capabilities", label: "Self-management MCP (write)", help: "Let keepers create/fork/message chats (needs read enabled too).", type: "boolean", envVars: ["PADDOCK_SELF_MCP_WRITE"], default: false, editable: true, coerce: asBool },
|
|
184
|
+
{ key: "selfMcpProjectsEnabled", group: "capabilities", label: "Self-management MCP (projects)", help: "Let keepers create whole new projects, cloning a repo when repo-backed (needs self-MCP write).", type: "boolean", envVars: ["PADDOCK_SELF_MCP_PROJECTS"], default: false, editable: true, coerce: asBool },
|
|
185
|
+
{ key: "maxSpawnDepth", group: "capabilities", label: "Max spawn depth", help: "How deep a spawn tree may grow before children lose the self-MCP.", type: "number", envVars: ["PADDOCK_MAX_SPAWN_DEPTH"], default: DEFAULT_MAX_SPAWN_DEPTH, editable: true, coerce: spawnDepth },
|
|
186
|
+
{ key: "scheduleMutationEnabled", group: "capabilities", label: "Schedule mutation", help: "Allow programmatic schedule add/remove at runtime.", type: "boolean", envVars: ["PADDOCK_SCHEDULE_MUTATION"], default: false, editable: true, coerce: asBool },
|
|
187
|
+
{ key: "hooksMcpEnabled", group: "capabilities", label: "Hooks MCP", help: "Let agents declare/edit their own event hooks (needs self-MCP write).", type: "boolean", envVars: ["PADDOCK_HOOKS_MCP"], default: false, editable: true, coerce: asBool },
|
|
188
|
+
{ key: "browserMcp", group: "capabilities", label: "Browser MCP (Playwright)", help: "Give agents a headless Chromium browser MCP.", type: "boolean", envVars: ["PADDOCK_BROWSER_MCP"], envShadowWhenDefined: true, default: false, editable: true, coerce: asBool },
|
|
189
|
+
// Recovery (issue #301).
|
|
190
|
+
{ key: "recovery.surfaceKilledTask", group: "recovery", label: "Surface killed task", type: "boolean", envVars: ["PADDOCK_RECOVERY_SURFACE"], default: DEFAULT_RECOVERY.surfaceKilledTask, editable: true, coerce: asBool },
|
|
191
|
+
{ key: "recovery.autoReDrive", group: "recovery", label: "Auto re-drive", type: "boolean", envVars: ["PADDOCK_RECOVERY_AUTODRIVE"], default: DEFAULT_RECOVERY.autoReDrive, editable: true, coerce: asBool },
|
|
192
|
+
{ key: "recovery.debounceMs", group: "recovery", label: "Debounce (ms)", type: "number", envVars: ["PADDOCK_RECOVERY_DEBOUNCE_MS"], default: DEFAULT_RECOVERY.debounceMs, editable: true, coerce: nonNegInt },
|
|
193
|
+
{ key: "recovery.maxRetries", group: "recovery", label: "Max retries", type: "number", envVars: ["PADDOCK_RECOVERY_MAX_RETRIES"], default: DEFAULT_RECOVERY.maxRetries, editable: true, coerce: nonNegInt },
|
|
194
|
+
{ key: "recovery.limboTimeoutMs", group: "recovery", label: "Limbo timeout (ms)", type: "number", envVars: ["PADDOCK_RECOVERY_LIMBO_MS"], default: DEFAULT_RECOVERY.limboTimeoutMs, editable: true, coerce: nonNegInt },
|
|
195
|
+
// Attachments (issue #328).
|
|
196
|
+
{ key: "attachments.enabled", group: "attachments", label: "Enabled", type: "boolean", envVars: ["PADDOCK_ATTACHMENTS_ENABLED"], default: DEFAULT_ATTACHMENTS.enabled, editable: true, coerce: asBool },
|
|
197
|
+
{ key: "attachments.maxFileSizeMb", group: "attachments", label: "Max file size (MB)", type: "number", envVars: ["PADDOCK_ATTACHMENTS_MAX_FILE_SIZE_MB"], default: DEFAULT_ATTACHMENTS.maxFileSizeMb, editable: true, coerce: posInt },
|
|
198
|
+
{ key: "attachments.maxFilesPerMessage", group: "attachments", label: "Max files / message", type: "number", envVars: ["PADDOCK_ATTACHMENTS_MAX_FILES_PER_MESSAGE"], default: DEFAULT_ATTACHMENTS.maxFilesPerMessage, editable: true, coerce: posInt },
|
|
199
|
+
{ key: "attachments.allowedTypes", group: "attachments", label: "Allowed types", help: "MIME types / extensions (e.g. image/*, .pdf). * = allow all.", type: "string-list", envVars: ["PADDOCK_ATTACHMENTS_ALLOWED_TYPES"], default: [...DEFAULT_ATTACHMENTS.allowedTypes], editable: true, coerce: stringList },
|
|
200
|
+
// Branding (issue #34).
|
|
201
|
+
{ key: "brand.name", group: "branding", label: "Name", type: "string", envVars: ["PADDOCK_BRAND_NAME"], default: "Paddock", editable: true, coerce: nonEmptyString },
|
|
202
|
+
{ key: "brand.logo", group: "branding", label: "Logo", help: "An emoji/glyph, or a URL/path to an image.", type: "string", envVars: ["PADDOCK_BRAND_LOGO"], default: "🐎", editable: true, coerce: nonEmptyString },
|
|
203
|
+
{ key: "brand.accent", group: "branding", label: "Accent color", type: "string", envVars: ["PADDOCK_BRAND_ACCENT"], default: "#c2603c", editable: true, coerce: hexColor },
|
|
204
|
+
// Transcription (voice dictation). endpoint is semi-sensitive; apiKey is a
|
|
205
|
+
// secret and deliberately NOT surfaced here.
|
|
206
|
+
{ key: "transcription.mode", group: "transcription", label: "Mode", type: "enum", enumValues: ["off", "local", "remote"], envVars: ["PADDOCK_WHISPER_MODE"], default: "off", editable: true, coerce: oneOf(["off", "local", "remote"]) },
|
|
207
|
+
{ key: "transcription.model", group: "transcription", label: "Model", type: "string", envVars: ["PADDOCK_WHISPER_MODEL"], default: "base", editable: true, coerce: nonEmptyString },
|
|
208
|
+
{ key: "transcription.endpoint", group: "transcription", label: "Endpoint", help: "remote mode: OpenAI-compatible base URL.", type: "string", envVars: ["PADDOCK_WHISPER_ENDPOINT"], default: null, editable: true, sensitive: true, coerce: optString },
|
|
209
|
+
// Git identity.
|
|
210
|
+
{ key: "gitAuthor.name", group: "git", label: "Author name", type: "string", envVars: ["PADDOCK_GIT_AUTHOR_NAME"], default: "Paddock", editable: true, coerce: nonEmptyString },
|
|
211
|
+
{ key: "gitAuthor.email", group: "git", label: "Author email", type: "string", envVars: ["PADDOCK_GIT_AUTHOR_EMAIL"], default: "paddock@localhost", editable: true, coerce: nonEmptyString },
|
|
212
|
+
// Logging.
|
|
213
|
+
{ key: "logLevel", group: "logging", label: "Log level", type: "enum", enumValues: LOG_LEVELS, envVars: ["LOG_LEVEL"], default: "info", editable: true, coerce: oneOf(LOG_LEVELS) },
|
|
214
|
+
// Advanced — read-only display (process / filesystem bindings).
|
|
215
|
+
{ key: "port", group: "advanced", label: "Port", type: "number", envVars: ["PORT"], default: 4000, editable: false },
|
|
216
|
+
{ key: "host", group: "advanced", label: "Host", type: "string", envVars: ["HOST", "PADDOCK_HOST"], default: "127.0.0.1", editable: false },
|
|
217
|
+
{ key: "dataDir", group: "advanced", label: "Data dir", type: "string", envVars: ["PADDOCK_DATA_DIR"], default: null, editable: false },
|
|
218
|
+
{ key: "projectsRoot", group: "advanced", label: "Projects root", type: "string", envVars: ["PADDOCK_PROJECTS_DIR"], default: null, editable: false },
|
|
219
|
+
{ key: "stateDir", group: "advanced", label: "State dir", type: "string", envVars: ["PADDOCK_STATE_DIR"], default: null, editable: false },
|
|
220
|
+
{ key: "herdctlConfigPath", group: "advanced", label: "herdctl config path", type: "string", envVars: ["PADDOCK_HERDCTL_CONFIG"], default: null, editable: false },
|
|
221
|
+
{ key: "webDist", group: "advanced", label: "Web dist", type: "string", envVars: ["PADDOCK_WEB_DIST"], default: null, editable: false },
|
|
222
|
+
// Auth: read-only in v1 (misconfig can lock everyone out — issue #385). Only
|
|
223
|
+
// the mode is surfaced; JWT/JWKS internals stay out of the API.
|
|
224
|
+
{ key: "auth.mode", group: "advanced", label: "Auth mode", type: "string", envVars: ["PADDOCK_AUTH_MODE"], default: "none", editable: false, sensitive: true },
|
|
225
|
+
// GitHub client id: not a secret, but semi-sensitive — read-only display.
|
|
226
|
+
{ key: "githubClientId", group: "advanced", label: "GitHub client id", type: "string", envVars: ["PADDOCK_GITHUB_CLIENT_ID"], default: null, editable: false, sensitive: true },
|
|
227
|
+
];
|
|
228
|
+
/** Fast lookup + editable allowlist for the PUT path. */
|
|
229
|
+
const FIELD_BY_KEY = new Map(FIELDS.map((f) => [f.key, f]));
|
|
230
|
+
/** Read a dotted path out of an object, tolerating missing intermediates. */
|
|
231
|
+
function readPath(obj, dotted) {
|
|
232
|
+
let cur = obj;
|
|
233
|
+
for (const seg of dotted.split(".")) {
|
|
234
|
+
if (cur == null || typeof cur !== "object")
|
|
235
|
+
return undefined;
|
|
236
|
+
cur = cur[seg];
|
|
237
|
+
}
|
|
238
|
+
return cur;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The env var currently shadowing a field, or undefined. By default a var
|
|
242
|
+
* shadows only when set to a non-blank value (matching `envOr`/`envOpt`); when
|
|
243
|
+
* `whenDefined` is set, ANY defined value shadows (matching `loadBrowserMcp`'s
|
|
244
|
+
* `env !== undefined` semantics — a defined-but-blank var still forces `false`).
|
|
245
|
+
*/
|
|
246
|
+
function envOverride(envVars, whenDefined = false) {
|
|
247
|
+
for (const name of envVars) {
|
|
248
|
+
const v = process.env[name];
|
|
249
|
+
if (v === undefined)
|
|
250
|
+
continue;
|
|
251
|
+
if (whenDefined || v.trim().length > 0)
|
|
252
|
+
return name;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Default filename for the instance-config file, resolved under the data dir.
|
|
258
|
+
* Kept in sync with config.ts's private constant (both point at the same file);
|
|
259
|
+
* duplicated here so this module needn't reach into config.ts internals.
|
|
260
|
+
*/
|
|
261
|
+
const DEFAULT_CONFIG_FILENAME = "paddock.config.yaml";
|
|
262
|
+
/**
|
|
263
|
+
* Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
|
|
264
|
+
* (the same rule {@link import("./config.js").loadConfigFile} reads it back
|
|
265
|
+
* from), else `<dataDir>/paddock.config.yaml`.
|
|
266
|
+
*/
|
|
267
|
+
export function instanceConfigPath(cfg) {
|
|
268
|
+
const explicit = process.env.PADDOCK_CONFIG;
|
|
269
|
+
if (explicit && explicit.trim().length > 0) {
|
|
270
|
+
const p = explicit.trim();
|
|
271
|
+
return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
|
|
272
|
+
}
|
|
273
|
+
return path.join(cfg.dataDir, DEFAULT_CONFIG_FILENAME);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Build the grouped GET DTO from the resolved (frozen) config + live env. Each
|
|
277
|
+
* field reports its current `value`, built-in `default`, and whether an env var
|
|
278
|
+
* currently shadows it (so the UI renders env-shadowed fields read-only).
|
|
279
|
+
*/
|
|
280
|
+
export function buildInstanceConfig(cfg) {
|
|
281
|
+
const groups = GROUPS.map((g) => ({
|
|
282
|
+
id: g.id,
|
|
283
|
+
label: g.label,
|
|
284
|
+
description: g.description,
|
|
285
|
+
fields: [],
|
|
286
|
+
}));
|
|
287
|
+
const groupById = new Map(groups.map((g) => [g.id, g]));
|
|
288
|
+
for (const f of FIELDS) {
|
|
289
|
+
const shadow = envOverride(f.envVars, f.envShadowWhenDefined);
|
|
290
|
+
const raw = readPath(cfg, f.key);
|
|
291
|
+
const dto = {
|
|
292
|
+
key: f.key,
|
|
293
|
+
group: f.group,
|
|
294
|
+
label: f.label,
|
|
295
|
+
help: f.help,
|
|
296
|
+
type: f.type,
|
|
297
|
+
enumValues: f.enumValues,
|
|
298
|
+
// `undefined` (e.g. sweepMinIntervalMs unset, optional endpoint) → null so
|
|
299
|
+
// it JSON-serializes as an explicit absence rather than dropping the key.
|
|
300
|
+
value: raw === undefined ? null : raw,
|
|
301
|
+
default: f.default,
|
|
302
|
+
editable: f.editable,
|
|
303
|
+
sensitive: f.sensitive ?? false,
|
|
304
|
+
envOverridden: shadow !== undefined,
|
|
305
|
+
envVar: shadow,
|
|
306
|
+
};
|
|
307
|
+
groupById.get(f.group)?.fields.push(dto);
|
|
308
|
+
}
|
|
309
|
+
return { groups, configPath: instanceConfigPath(cfg), restartRequired: false };
|
|
310
|
+
}
|
|
311
|
+
/** A rejected PUT: which field failed and why (surfaced as a 400). */
|
|
312
|
+
export class InstanceConfigError extends Error {
|
|
313
|
+
field;
|
|
314
|
+
constructor(message, field) {
|
|
315
|
+
super(message);
|
|
316
|
+
this.field = field;
|
|
317
|
+
this.name = "InstanceConfigError";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Validate a patch of editable fields and coerce each value. Throws
|
|
322
|
+
* {@link InstanceConfigError} on the first offending field:
|
|
323
|
+
* - an unknown key,
|
|
324
|
+
* - a read-only / non-editable key,
|
|
325
|
+
* - a value the field's `coerce` rejects.
|
|
326
|
+
* Returns the coerced `{ key, value }` pairs ready to write. A field whose value
|
|
327
|
+
* is `null` (an editable optional being cleared) is returned with `value: null`
|
|
328
|
+
* so the writer can delete it from the file.
|
|
329
|
+
*/
|
|
330
|
+
export function validatePatch(patch) {
|
|
331
|
+
const out = [];
|
|
332
|
+
for (const [key, raw] of Object.entries(patch)) {
|
|
333
|
+
const spec = FIELD_BY_KEY.get(key);
|
|
334
|
+
if (!spec)
|
|
335
|
+
throw new InstanceConfigError(`Unknown setting: ${key}`, key);
|
|
336
|
+
if (!spec.editable || !spec.coerce) {
|
|
337
|
+
throw new InstanceConfigError(`Setting is read-only: ${key}`, key);
|
|
338
|
+
}
|
|
339
|
+
const res = spec.coerce(raw);
|
|
340
|
+
if (!res.ok)
|
|
341
|
+
throw new InstanceConfigError(`${spec.label} (${key}) ${res.error}`, key);
|
|
342
|
+
out.push({ key, value: res.value });
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Write the validated pairs into `paddock.config.yaml`, preserving operator
|
|
348
|
+
* comments and unmanaged keys. Uses the `yaml` `Document` API to round-trip an
|
|
349
|
+
* existing file (or start a fresh document when none exists), then writes
|
|
350
|
+
* atomically (temp + rename). A `null` value deletes that key (clearing an
|
|
351
|
+
* optional back to its default). Returns the path written.
|
|
352
|
+
*
|
|
353
|
+
* Reuses no loader state — the caller passes the target path (from
|
|
354
|
+
* {@link instanceConfigPath}) so tests can point it anywhere.
|
|
355
|
+
*/
|
|
356
|
+
export function writeInstanceConfig(configPath, pairs) {
|
|
357
|
+
let raw = "";
|
|
358
|
+
try {
|
|
359
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
if (err.code !== "ENOENT")
|
|
363
|
+
throw err;
|
|
364
|
+
// Missing file → create-on-first-write (a fresh, comment-free document).
|
|
365
|
+
}
|
|
366
|
+
const doc = parseDocument(raw);
|
|
367
|
+
// An empty / comments-only file parses to a null document body; give it a map
|
|
368
|
+
// so setIn has somewhere to place keys.
|
|
369
|
+
if (doc.contents == null)
|
|
370
|
+
doc.contents = doc.createNode({});
|
|
371
|
+
for (const { key, value } of pairs) {
|
|
372
|
+
const p = key.split(".");
|
|
373
|
+
if (value === null)
|
|
374
|
+
doc.deleteIn(p);
|
|
375
|
+
else
|
|
376
|
+
doc.setIn(p, value);
|
|
377
|
+
}
|
|
378
|
+
const serialized = doc.toString();
|
|
379
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
380
|
+
// Atomic write: same-dir temp file + rename, so a reader never sees a
|
|
381
|
+
// half-written config (rename is atomic on the same filesystem).
|
|
382
|
+
const tmp = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
383
|
+
fs.writeFileSync(tmp, serialized, "utf8");
|
|
384
|
+
fs.renameSync(tmp, configPath);
|
|
385
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EnrichedMessage } from "./subagents.js";
|
|
2
|
+
/** One recovered `local_command` stdout entry, anchored to its command echo. */
|
|
3
|
+
interface LocalCommandOutput {
|
|
4
|
+
/** The originating `<command-name>` echo's uuid (this entry's `parentUuid`). */
|
|
5
|
+
parentUuid?: string;
|
|
6
|
+
/** This entry's own uuid — a stable, reload-consistent key for the web. */
|
|
7
|
+
uuid?: string;
|
|
8
|
+
/** ISO timestamp copied from the source line (falls back to the anchor's). */
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
/** The raw `<local-command-stdout>…</local-command-stdout>` content. */
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/** Recover the dropped `local_command` stdout entries (file order) from a session. */
|
|
14
|
+
export declare function readLocalCommandOutputs(projectDir: string, sessionId: string): Promise<LocalCommandOutput[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Re-inject recovered `local_command` stdout as `role:"user"` messages (issue
|
|
17
|
+
* #158), each placed right after the `<command-name>` echo it belongs to (matched
|
|
18
|
+
* by `parentUuid` → the echo message's `uuid`). An output whose anchor isn't in the
|
|
19
|
+
* parsed messages (e.g. it was trimmed by a `limit`) is dropped rather than
|
|
20
|
+
* mis-positioned. A transcript with no local commands passes through untouched.
|
|
21
|
+
*/
|
|
22
|
+
export declare function enrichWithLocalCommands(projectDir: string, sessionId: string, messages: EnrichedMessage[]): Promise<EnrichedMessage[]>;
|
|
23
|
+
export {};
|