@edspencer/paddock 0.62.0 → 0.63.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 +32 -4
- package/package.json +2 -2
- package/packages/server/dist/app.js +14 -1
- package/packages/server/dist/claude-mcp.d.ts +16 -4
- package/packages/server/dist/claude-mcp.js +83 -61
- package/packages/server/dist/claude-plugins.d.ts +131 -0
- package/packages/server/dist/claude-plugins.js +394 -0
- package/packages/server/dist/cli/args.d.ts +1 -1
- package/packages/server/dist/cli/args.js +9 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +7 -1
- package/packages/server/dist/herdctl-agent-config.js +20 -4
- package/packages/server/dist/herdctl.d.ts +7 -1
- package/packages/server/dist/herdctl.js +9 -2
- package/packages/server/dist/mcp-servers.d.ts +31 -12
- package/packages/server/dist/mcp-servers.js +127 -33
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-Ca905oey.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-ChlC1ezH.js → MarkdownRenderer-B6gST_0U.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-CslqbhUb.js → ProjectRedirect-BCmKT4mf.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DkNdilGo.js → ProjectView-CCN98MWu.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BrlvLc-Y.js → ProjectsGrid-CYzuN_yD.js} +1 -1
- package/packages/web/dist/assets/{arc-CGqCI5nQ.js → arc-BxvtMCZQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cp4KTxE2.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-yFxaTv04.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-59lhWjRt.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
- package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-BrpXGnJg.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-UxBDb5pi.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-D4ZJyUHW.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DWdPj_8E.js → chunk-727SXJPM-bWEqh672.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BIpNbflY.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-f6iJDfdI.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-Bz3TKIJk.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-buQmkafK.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
- package/packages/web/dist/assets/{core-TyFI6p0n.js → core-CSaNbmLO.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CIkdi0XF.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-BnLJCfNs.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-D0OqfDB9.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-DQ2spGrK.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-RDIVQ_S3.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DKoTfh2C.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-6k0OuSAL.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B-oQaYAC.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-B7qpYe9C.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
- package/packages/web/dist/assets/{format-PHdEBNOi.js → format-BSDZezwD.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-SfBsqMmx.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CuM4Rl_i.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
- package/packages/web/dist/assets/{index-D_1ogxg1.js → index-CUnFKMga.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Dq-32Oj4.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BLFHCj_o.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CHP-Yu63.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DRuehSqn.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
- package/packages/web/dist/assets/{linear-Bwz2vnH7.js → linear-DuyAaBu6.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CeMMnCip.js → mermaid.core-BQlzuVvO.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CEdqeoRW.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BdDXBx87.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bovd1Zhx.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DiyrKnJ2.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BuX7TRVw.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BjetZWJo.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-4AUBV6dF.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpQ8g8Bm.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Cj99BWER.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-S7AGTOWw.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-LNXkUA7S.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DGdTh4Xk.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/channel-CZs6Z1i5.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.js +0 -2
|
@@ -48,33 +48,46 @@
|
|
|
48
48
|
* Unlike a management token, an MCP `env` entry is not necessarily secret
|
|
49
49
|
* (`NOTION_VERSION: "2022-06-28"` is not), so an inline value is a WARNING and
|
|
50
50
|
* not an error — but the warning is emitted for any key that looks like a
|
|
51
|
-
* credential, and nothing in this module ever prints a value.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* credential, and nothing in this module ever prints a value. `headers` follows
|
|
52
|
+
* the same rule and for a stronger reason: `Authorization` is the usual reason a
|
|
53
|
+
* header is declared at all. Every diagnostic names keys, variable names and
|
|
54
|
+
* server names only; a `url` is reported with its query string and userinfo
|
|
55
|
+
* stripped, because that is where an API key rides. {@link describeServer} is the
|
|
56
|
+
* single place a server is rendered for a human, so there is one function to
|
|
57
|
+
* audit rather than a scattering of template strings.
|
|
56
58
|
*
|
|
57
59
|
* The resolved values do land in the frozen `PaddockConfig` (as management tokens
|
|
58
60
|
* already do). They must never reach an API response: `instance-config.ts`
|
|
59
61
|
* publishes only the fields in its own `FIELDS` table, and this block is
|
|
60
62
|
* deliberately absent from it.
|
|
61
63
|
*
|
|
64
|
+
* One exposure the `env:` indirection does NOT close: under `driveMode: batch`
|
|
65
|
+
* the resolved value reaches a COMMAND LINE. See {@link argvExposure} — and note
|
|
66
|
+
* that carrying `headers` (#700) widens it, because an `Authorization` bearer is
|
|
67
|
+
* a likelier long-lived credential than an `env` entry.
|
|
68
|
+
*
|
|
62
69
|
* ## Validation is strict, and drops rather than degrades
|
|
63
70
|
*
|
|
64
71
|
* Step 5 passes a host server it cannot carry faithfully, with a warning, on the
|
|
65
72
|
* grounds that a user who declared a server elsewhere should get it plus a
|
|
66
73
|
* warning rather than nothing plus a warning. That reasoning inverts here,
|
|
67
74
|
* because the user typed this file: an unusable declaration is a mistake they can
|
|
68
|
-
* fix
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
75
|
+
* fix. So anything that cannot be carried is an ERROR and that server is not
|
|
76
|
+
* attached — including an unknown key, which is how a typo (`arg:` for `args:`)
|
|
77
|
+
* otherwise becomes a server that starts wrong.
|
|
78
|
+
*
|
|
79
|
+
* `headers:` and `type: sse` USED to be in that category, because herdctl's
|
|
80
|
+
* schema had no field for either. herdctl 5.32.0 (#446) carries both verbatim,
|
|
81
|
+
* so refusing them would now reject a declaration that works — they are accepted,
|
|
82
|
+
* and `headers` is a first-class secret-bearing field with the same `env:VAR`
|
|
83
|
+
* resolution and the same never-print rule as `env`.
|
|
72
84
|
*
|
|
73
85
|
* Never a boot failure, though: one bad server drops itself and the rest attach,
|
|
74
86
|
* matching `resolveManagementApiConfig`. A typo in a capability must not take an
|
|
75
87
|
* instance down.
|
|
76
88
|
*/
|
|
77
89
|
import type { McpServerDefs, McpServerDef } from "./claude-mcp.js";
|
|
90
|
+
import type { DriveMode } from "./models.js";
|
|
78
91
|
/** On-disk shape of one entry of the `mcpServers:` block. Every field optional. */
|
|
79
92
|
export interface McpServerConfigFile {
|
|
80
93
|
/** Executable for a stdio server. Mutually exclusive with {@link url}. */
|
|
@@ -83,11 +96,11 @@ export interface McpServerConfigFile {
|
|
|
83
96
|
args?: string[];
|
|
84
97
|
/** Environment for the server process. Values may be `env:VAR_NAME`. */
|
|
85
98
|
env?: Record<string, string>;
|
|
86
|
-
/** Endpoint for a
|
|
99
|
+
/** Endpoint for a remote server. Mutually exclusive with {@link command}. */
|
|
87
100
|
url?: string;
|
|
88
|
-
/**
|
|
101
|
+
/** `stdio` for a {@link command}; `http` or `sse` for a {@link url}. */
|
|
89
102
|
type?: string;
|
|
90
|
-
/**
|
|
103
|
+
/** Headers for a remote server. Values may be `env:VAR_NAME`. */
|
|
91
104
|
headers?: Record<string, string>;
|
|
92
105
|
}
|
|
93
106
|
/** On-disk shape of the whole block: a map of server name → declaration. */
|
|
@@ -165,4 +178,10 @@ export declare function declaredMcpNotices(opts: {
|
|
|
165
178
|
hostNames?: readonly string[];
|
|
166
179
|
/** Whether paddock's own browser server is attached (it wins a name clash). */
|
|
167
180
|
browserMcp?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* The instance's drive mode, which decides whether a declared `env` value ends
|
|
183
|
+
* up on a command line. Omitted ⇒ say nothing (the several callers that only
|
|
184
|
+
* want the inventory line).
|
|
185
|
+
*/
|
|
186
|
+
driveMode?: DriveMode;
|
|
168
187
|
}): DeclaredMcpNotice[];
|
|
@@ -65,12 +65,15 @@ export function describeServer(name, def) {
|
|
|
65
65
|
if (def.command)
|
|
66
66
|
parts.push(`stdio: ${def.command.split(/[\\/]/).pop()}`);
|
|
67
67
|
if (def.url)
|
|
68
|
-
parts.push(
|
|
68
|
+
parts.push(`${def.type ?? "http"}: ${redactUrl(def.url)}`);
|
|
69
69
|
if (def.args?.length)
|
|
70
70
|
parts.push(`${def.args.length} args`);
|
|
71
71
|
const envCount = Object.keys(def.env ?? {}).length;
|
|
72
72
|
if (envCount > 0)
|
|
73
73
|
parts.push(`${envCount} env ${envCount === 1 ? "entry" : "entries"}`);
|
|
74
|
+
const headerCount = Object.keys(def.headers ?? {}).length;
|
|
75
|
+
if (headerCount > 0)
|
|
76
|
+
parts.push(`${headerCount} ${headerCount === 1 ? "header" : "headers"}`);
|
|
74
77
|
return `${name} (${parts.join(", ")})`;
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
@@ -102,18 +105,18 @@ function resolveLeaf(raw, env, where) {
|
|
|
102
105
|
* Validate + resolve one declaration into the shape herdctl's `McpServerSchema`
|
|
103
106
|
* carries, or explain why it cannot be.
|
|
104
107
|
*
|
|
105
|
-
* The
|
|
106
|
-
* the
|
|
108
|
+
* The one rejection left that is about the engine rather than about the file is
|
|
109
|
+
* **an unknown key** — the only defence against `arg:`/`envs:`/`commands:`, which
|
|
110
|
+
* would otherwise yield a server that starts with the wrong argv.
|
|
107
111
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* which would otherwise yield a server that starts with the wrong argv.
|
|
112
|
+
* `headers` and `type: sse` were rejections too until herdctl 5.32.0, because
|
|
113
|
+
* the schema had no field for either: a bearer-authenticated server arrived
|
|
114
|
+
* unauthenticated and missed its stored OAuth token (keyed on a hash that
|
|
115
|
+
* includes the headers and the type), and an `sse` url was silently connected to
|
|
116
|
+
* as HTTP. Both are now carried verbatim, so both are accepted — `headers`
|
|
117
|
+
* through the same `env:VAR` resolution as `env`, and `type` checked only for
|
|
118
|
+
* agreeing with the declaration (`stdio` for a `command`, `http`/`sse` for a
|
|
119
|
+
* `url`).
|
|
117
120
|
*/
|
|
118
121
|
function narrowDeclaration(name, raw, env) {
|
|
119
122
|
const where = `mcpServers.${name}`;
|
|
@@ -135,16 +138,10 @@ function narrowDeclaration(name, raw, env) {
|
|
|
135
138
|
return fail(`${where}: is not a mapping`);
|
|
136
139
|
const unknown = Object.keys(raw).filter((k) => !KNOWN_KEYS.has(k));
|
|
137
140
|
if (unknown.length > 0) {
|
|
138
|
-
return fail(`${where}: unrecognised key(s) ${unknown.join(", ")}
|
|
141
|
+
return fail(`${where}: unrecognised key(s) ${unknown.join(", ")} ` +
|
|
142
|
+
`(supported: command, args, env, url, type, headers) ` +
|
|
139
143
|
`— a mistyped key would otherwise be dropped without a trace`);
|
|
140
144
|
}
|
|
141
|
-
if (raw.headers !== undefined) {
|
|
142
|
-
const keys = isRecord(raw.headers) ? Object.keys(raw.headers) : [];
|
|
143
|
-
return fail(`${where}: \`headers\`${keys.length > 0 ? ` (${keys.join(", ")})` : ""} cannot be ` +
|
|
144
|
-
`carried — the engine's MCP schema has no field for them, so the server would arrive ` +
|
|
145
|
-
`unauthenticated and its stored OAuth token (keyed on a hash that includes the headers) ` +
|
|
146
|
-
`would not be found either`);
|
|
147
|
-
}
|
|
148
145
|
const hasCommand = typeof raw.command === "string" && raw.command.trim().length > 0;
|
|
149
146
|
const hasUrl = typeof raw.url === "string" && raw.url.trim().length > 0;
|
|
150
147
|
if (hasCommand && hasUrl) {
|
|
@@ -153,20 +150,21 @@ function narrowDeclaration(name, raw, env) {
|
|
|
153
150
|
if (!hasCommand && !hasUrl) {
|
|
154
151
|
return fail(`${where}: declares neither a \`command\` nor a \`url\`, so there is nothing to start`);
|
|
155
152
|
}
|
|
153
|
+
const server = {};
|
|
156
154
|
if (raw.type !== undefined) {
|
|
157
155
|
const type = String(raw.type).trim().toLowerCase();
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
// `sse` joined `http` here in herdctl 5.32.0; an explicit type now wins over
|
|
157
|
+
// the bare-`url` ⇒ `http` inference rather than being stripped on the way
|
|
158
|
+
// down. It still has to agree with the rest of the declaration, because a
|
|
159
|
+
// `type` that disagrees is a typo and starting the wrong transport is a
|
|
160
|
+
// confusing failure rather than a loud one.
|
|
161
|
+
const expected = hasUrl ? ["http", "sse"] : ["stdio"];
|
|
162
|
+
if (!expected.includes(type)) {
|
|
163
|
+
return fail(`${where}: \`type: ${type}\` disagrees with the declaration ` +
|
|
164
|
+
`(expected ${expected.join(" or ")})`);
|
|
167
165
|
}
|
|
166
|
+
server.type = type;
|
|
168
167
|
}
|
|
169
|
-
const server = {};
|
|
170
168
|
if (hasCommand) {
|
|
171
169
|
const res = resolveLeaf(raw.command.trim(), env, `${where}.command`);
|
|
172
170
|
if (!res.ok)
|
|
@@ -178,9 +176,9 @@ function narrowDeclaration(name, raw, env) {
|
|
|
178
176
|
if (!res.ok)
|
|
179
177
|
return { errors, warnings: [...warnings, `${res.error} — server not attached`] };
|
|
180
178
|
server.url = res.value;
|
|
181
|
-
// A key in the query string is the
|
|
182
|
-
// authenticated
|
|
183
|
-
// git-tracked. Say so once; never echo the query itself.
|
|
179
|
+
// A key in the query string is one of the two ways a remote MCP endpoint is
|
|
180
|
+
// authenticated (the other, `headers`, is carryable again since #700), and
|
|
181
|
+
// this file is git-tracked. Say so once; never echo the query itself.
|
|
184
182
|
if (!raw.url.startsWith(ENV_REF_PREFIX)) {
|
|
185
183
|
let parsed;
|
|
186
184
|
try {
|
|
@@ -231,6 +229,34 @@ function narrowDeclaration(name, raw, env) {
|
|
|
231
229
|
if (Object.keys(resolved).length > 0)
|
|
232
230
|
server.env = resolved;
|
|
233
231
|
}
|
|
232
|
+
if (raw.headers !== undefined) {
|
|
233
|
+
if (!hasUrl) {
|
|
234
|
+
return fail(`${where}.headers: only a \`url\` server can carry headers`);
|
|
235
|
+
}
|
|
236
|
+
if (!isRecord(raw.headers)) {
|
|
237
|
+
return fail(`${where}.headers: must be a mapping of header name to value`);
|
|
238
|
+
}
|
|
239
|
+
const resolved = {};
|
|
240
|
+
for (const [key, value] of Object.entries(raw.headers)) {
|
|
241
|
+
if (typeof value !== "string") {
|
|
242
|
+
return fail(`${where}.headers.${key}: must be a string`);
|
|
243
|
+
}
|
|
244
|
+
const res = resolveLeaf(value, env, `${where}.headers.${key}`);
|
|
245
|
+
if (!res.ok)
|
|
246
|
+
return { errors, warnings: [...warnings, `${res.error} — server not attached`] };
|
|
247
|
+
resolved[key] = res.value;
|
|
248
|
+
// Same heuristic as `env`, one rule to learn — and `Authorization` is the
|
|
249
|
+
// usual reason a header is declared at all, so this fires on the common
|
|
250
|
+
// case rather than the exotic one.
|
|
251
|
+
if (!value.startsWith(ENV_REF_PREFIX) && SECRET_ISH_KEY_RE.test(key)) {
|
|
252
|
+
warnings.push(`${where}.headers.${key}: looks like a credential and is written into the config file ` +
|
|
253
|
+
`itself, which is git-tracked — prefer \`${key}: ${ENV_REF_PREFIX}VAR_NAME\` and set ` +
|
|
254
|
+
`the value in the environment`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (Object.keys(resolved).length > 0)
|
|
258
|
+
server.headers = resolved;
|
|
259
|
+
}
|
|
234
260
|
return { server, errors, warnings };
|
|
235
261
|
}
|
|
236
262
|
/**
|
|
@@ -265,6 +291,62 @@ export function resolveDeclaredMcpServers(file, env) {
|
|
|
265
291
|
}
|
|
266
292
|
return { servers, errors, warnings };
|
|
267
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* The one way a declared credential leaves paddock, said out loud.
|
|
296
|
+
*
|
|
297
|
+
* Everything else in this module is about keeping a resolved value out of the
|
|
298
|
+
* places paddock controls — the log, an error, the Settings API. It does that
|
|
299
|
+
* completely, and it is still not the whole story, because of what herdctl does
|
|
300
|
+
* with the record afterwards:
|
|
301
|
+
*
|
|
302
|
+
* ```js
|
|
303
|
+
* // @herdctl/core, runner/runtime/cli-runtime.js
|
|
304
|
+
* const mcpServers = transformMcpServers(options.agent.mcp_servers);
|
|
305
|
+
* const mcpConfig = JSON.stringify({ mcpServers });
|
|
306
|
+
* args.push("--mcp-config", mcpConfig); // ← env values and all
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* A process ARGUMENT is not private on Linux: `/proc/<pid>/cmdline` is
|
|
310
|
+
* world-readable by default (no `hidepid`), and `ps` prints it. So on the CLI
|
|
311
|
+
* runtime every declared server's `env` — and, since #700 made the field
|
|
312
|
+
* carryable, its `headers` — is legible to any local user for the lifetime of
|
|
313
|
+
* each `claude` invocation. Observed, not
|
|
314
|
+
* inferred: `test/integration/declared-mcp-argv.test.ts` drives a real turn and
|
|
315
|
+
* reads the token back out of the spawned process's argv.
|
|
316
|
+
*
|
|
317
|
+
* The SDK runtime does NOT do this. It hands the same record to the SDK
|
|
318
|
+
* in-process, and the stdio server it spawns receives the value in its
|
|
319
|
+
* environment, where `/proc/<pid>/environ` is owner-only — which is exactly what
|
|
320
|
+
* Claude Code itself does, so it is not a regression to fix here.
|
|
321
|
+
*
|
|
322
|
+
* Which runtime runs is `driveMode`: `session` (the default) is the SDK,
|
|
323
|
+
* `batch` is the CLI. Hence a WARNING on an instance that is on `batch`, and an
|
|
324
|
+
* informational note otherwise — because a single project can pin `driveMode:
|
|
325
|
+
* batch` for itself and bring the exposure back with it.
|
|
326
|
+
*
|
|
327
|
+
* Paddock cannot close this from here: the fix is upstream (the Claude CLI's
|
|
328
|
+
* `--mcp-config` also accepts a file path, which is not readable from another
|
|
329
|
+
* process's argv). Refusing to attach the server instead would break the feature
|
|
330
|
+
* for the deployments most likely to need it, so what this does is refuse to be
|
|
331
|
+
* silent — the same posture step 5 takes towards the fields the engine's schema
|
|
332
|
+
* cannot carry.
|
|
333
|
+
*/
|
|
334
|
+
function argvExposure(names, driveMode) {
|
|
335
|
+
const batch = driveMode === "batch";
|
|
336
|
+
return {
|
|
337
|
+
level: batch ? "warn" : "info",
|
|
338
|
+
message: `${names.join(", ")} ${names.length === 1 ? "declares" : "declare"} \`env\` values or ` +
|
|
339
|
+
`\`headers\`, and ` +
|
|
340
|
+
`under \`driveMode: batch\` the engine passes the whole server definition to \`claude\` ` +
|
|
341
|
+
`as a \`--mcp-config\` COMMAND-LINE argument — where any local process can read it via ` +
|
|
342
|
+
`/proc/<pid>/cmdline. ` +
|
|
343
|
+
(batch
|
|
344
|
+
? `This instance is on \`driveMode: batch\`, so that applies to every turn. Prefer ` +
|
|
345
|
+
`\`driveMode: session\` (the default) for a server that holds a credential.`
|
|
346
|
+
: `This instance is on \`driveMode: session\`, which passes them in-process instead — ` +
|
|
347
|
+
`but a project that pins \`driveMode: batch\` brings the exposure back.`),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
268
350
|
/**
|
|
269
351
|
* What to say at boot about the declared servers — the answer to "what does this
|
|
270
352
|
* instance actually have?", which should be readable off the log rather than
|
|
@@ -294,6 +376,18 @@ export function declaredMcpNotices(opts) {
|
|
|
294
376
|
`~/.claude.json (\`claude.mcpServers: host\`); this instance's own declaration wins.`,
|
|
295
377
|
});
|
|
296
378
|
}
|
|
379
|
+
// `headers` joined `env` here in #700: herdctl 5.32.0 carries them, so an
|
|
380
|
+
// `Authorization` bearer now rides in the same `--mcp-config` argv element that
|
|
381
|
+
// #702 found an `env` token in — and a bearer is the likelier long-lived
|
|
382
|
+
// credential of the two. A server declared with headers and no env would
|
|
383
|
+
// otherwise be the one case this warning missed.
|
|
384
|
+
const exposed = names.filter((n) => Object.keys(opts.servers[n].env ?? {}).length > 0 ||
|
|
385
|
+
Object.keys(opts.servers[n].headers ?? {}).length > 0);
|
|
386
|
+
if (exposed.length > 0 && opts.driveMode !== undefined) {
|
|
387
|
+
// The one place a declared credential escapes paddock, and it escapes
|
|
388
|
+
// downstream of every rule this module enforces — see {@link argvExposure}.
|
|
389
|
+
notices.push(argvExposure(exposed, opts.driveMode));
|
|
390
|
+
}
|
|
297
391
|
if (opts.browserMcp && names.includes("playwright")) {
|
|
298
392
|
notices.push({
|
|
299
393
|
level: "warn",
|
package/packages/web/dist/assets/{InstanceConfigPage-Ca905oey.js → InstanceConfigPage-CCk2hVT9.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-
|
|
1
|
+
import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-CUnFKMga.js";function $(){const[t,s]=o.useState(null),[l,i]=o.useState(!0),[r,c]=o.useState(null),[d,m]=o.useState({}),[h,k]=o.useState(!1),[j,p]=o.useState(null),[N,u]=o.useState(!1);o.useEffect(()=>{let a=!0;return i(!0),f.getInstanceConfig().then(n=>{a&&(s(n),c(null))}).catch(n=>a&&c(n instanceof Error?n.message:String(n))).finally(()=>a&&i(!1)),()=>{a=!1}},[]);const v=(a,n)=>{u(!1),p(null),m(g=>({...g,[a]:n}))},S=a=>Object.prototype.hasOwnProperty.call(d,a.key)?d[a.key]:a.value,y=o.useMemo(()=>t?t.groups.flatMap(a=>a.fields):[],[t]),x=o.useMemo(()=>y.filter(a=>a.editable&&!a.envOverridden).filter(a=>Object.prototype.hasOwnProperty.call(d,a.key)).filter(a=>!B(d[a.key],a.value)).map(a=>a.key),[y,d]),w=async()=>{if(x.length===0)return;const a={};for(const n of x)a[n]=d[n];k(!0),p(null);try{await f.updateInstanceConfig(a);const n=await f.getInstanceConfig();s(n),m({}),u(!0)}catch(n){p(n instanceof A||n instanceof Error?n.message:String(n))}finally{k(!1)}},C=()=>{m({}),p(null),u(!1)};return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:"mx-auto max-w-2xl",children:[e.jsx(L,{saved:N,configPath:t==null?void 0:t.configPath}),l&&e.jsx("p",{className:"text-sm text-paddock-500",children:"Loading…"}),r&&e.jsxs("p",{className:"text-sm text-red-600 dark:text-red-400",role:"alert",children:["Failed to load settings: ",r]}),t&&t.groups.map(a=>e.jsx(M,{title:a.label,description:a.description,children:e.jsx("div",{className:"grid grid-cols-1 gap-4",children:a.fields.map(n=>e.jsx(P,{field:n,value:S(n),onChange:g=>v(n.key,g)},n.key))})},a.id))]})}),t&&e.jsxs("footer",{className:"flex items-center gap-3 border-t border-paddock-200 px-4 py-3 dark:border-paddock-800 sm:px-6",children:[j&&e.jsxs("span",{className:"flex items-center gap-1.5 text-[13px] text-red-600 dark:text-red-400",role:"alert",children:[e.jsx(b,{width:14,height:14,className:"shrink-0"}),j]}),e.jsx("span",{className:"ml-auto text-[12px] text-paddock-400",children:x.length>0?`${x.length} unsaved change${x.length===1?"":"s"}`:"No changes"}),e.jsx("button",{type:"button",className:"btn-subtle",onClick:C,disabled:x.length===0||h,children:"Reset"}),e.jsx("button",{type:"button",className:"btn-primary",onClick:w,disabled:x.length===0||h,children:h?"Saving…":"Save changes"})]})]})}function L({saved:t,configPath:s}){return e.jsxs("div",{role:"status",className:`mb-6 flex items-start gap-2 rounded-lg border px-3 py-2.5 text-[13px] leading-snug ${t?"border-emerald-300 bg-emerald-50 text-emerald-800 dark:border-emerald-700/60 dark:bg-emerald-900/20 dark:text-emerald-300":"border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-300"}`,children:[t?e.jsx(E,{width:15,height:15,className:"mt-0.5 shrink-0"}):e.jsx(b,{width:15,height:15,className:"mt-0.5 shrink-0"}),e.jsx("span",{children:t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):e.jsxs(e.Fragment,{children:["Changes here are written to ",e.jsx("code",{className:"font-mono text-[12px]",children:T(s)})," ","and take effect only after the server restarts — the running instance keeps its current config until then."]})})]})}function M({title:t,description:s,children:l}){return e.jsxs("section",{className:"mb-6",children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wide text-paddock-500",children:t}),s&&e.jsx("p",{className:"mb-3 mt-0.5 text-[13px] text-paddock-500",children:s}),e.jsx("div",{className:`card ${s?"":"mt-2"}`,children:l})]})}function P({field:t,value:s,onChange:l}){const i=!t.editable||t.envOverridden,r=`cfg-${t.key}`;return e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("label",{htmlFor:i?void 0:r,className:"text-sm font-medium",children:[t.label,t.sensitive&&e.jsx("span",{className:"ml-1.5 align-middle text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400",children:"sensitive"})]}),t.type==="boolean"&&!i&&e.jsx("input",{id:r,type:"checkbox",className:"h-4 w-4 accent-accent",checked:!!s,onChange:c=>l(c.target.checked)})]}),t.help&&e.jsx("p",{className:"mt-0.5 text-[12px] leading-snug text-paddock-500",children:t.help}),i?e.jsx(V,{field:t,value:s}):t.type!=="boolean"&&e.jsx(R,{field:t,value:s,onChange:l,inputId:r}),t.envOverridden&&e.jsxs("p",{className:"mt-1 flex items-start gap-1.5 text-[12px] leading-snug text-amber-600 dark:text-amber-400",children:[e.jsx(b,{width:13,height:13,className:"mt-0.5 shrink-0"}),e.jsxs("span",{children:["Overridden by environment variable ",e.jsx("code",{className:"font-mono",children:t.envVar})," — edit that env var (and restart) to change it."]})]})]})}function R({field:t,value:s,onChange:l,inputId:i}){if(t.type==="enum")return e.jsx("select",{id:i,className:"input mt-1.5",value:String(s??""),onChange:r=>l(r.target.value),children:(t.enumValues??[]).map(r=>e.jsx("option",{value:r,children:r},r))});if(t.type==="number")return e.jsx("input",{id:i,type:"number",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"default":String(t.default),onChange:r=>l(r.target.value===""?null:Number(r.target.value))});if(t.type==="text"){const r=s==null||s===t.default,c=s==null?String(t.default??""):String(s),d=typeof t.default=="string"&&!r;return e.jsxs("div",{className:"mt-1.5",children:[e.jsx("textarea",{id:i,className:"input min-h-[9rem] resize-y font-mono text-[12px] leading-relaxed",rows:10,spellCheck:!1,value:c,onChange:m=>l(m.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-[12px] text-paddock-500",children:[e.jsx("span",{children:r?"Using the built-in default.":s===""?"Empty — nothing will be appended.":`${c.length.toLocaleString()} characters.`}),d&&e.jsx("button",{type:"button",className:"shrink-0 underline underline-offset-2 hover:text-paddock-700 dark:hover:text-paddock-300",onClick:()=>l(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const r=Array.isArray(s)?s.join(", "):String(s??"");return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:r,onChange:c=>l(c.target.value.split(",").map(d=>d.trim()).filter(Boolean))})}return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"":String(t.default),onChange:r=>l(r.target.value)})}function V({field:t,value:s}){return e.jsx("div",{className:"mt-1.5 rounded-md border border-dashed border-paddock-300 bg-paddock-50 px-2.5 py-1.5 text-[13px] text-paddock-600 dark:border-paddock-700 dark:bg-paddock-900/40 dark:text-paddock-300",children:t.type==="boolean"?e.jsx("span",{className:"font-mono",children:s?"true":"false"}):s==null||s===""?e.jsx("span",{className:"italic text-paddock-400",children:t.type==="text"&&s===""?"(empty — nothing appended)":"(not set)"}):t.type==="text"?e.jsx("span",{className:"block max-h-64 overflow-y-auto whitespace-pre-wrap font-mono text-[12px] leading-relaxed",children:String(s)}):e.jsx("span",{className:"break-all font-mono",children:Array.isArray(s)?s.join(", "):String(s)})})}function B(t,s){return Array.isArray(t)&&Array.isArray(s)?t.length===s.length&&t.every((l,i)=>l===s[i]):t===s}function T(t){if(!t)return"paddock.config.yaml";const s=t.split("/");return s[s.length-1]||t}function z(){const t=O(),s=(t==null?void 0:t.openNav)??(()=>{});return e.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[e.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-paddock-200 px-3 pb-2.5 dark:border-paddock-800 sm:px-6 lg:py-4",children:[e.jsx("button",{type:"button",onClick:s,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(F,{width:20,height:20})}),e.jsx(I,{width:18,height:18,className:"shrink-0 text-paddock-400"}),e.jsx("h1",{className:"text-[15px] font-semibold tracking-tight",children:"Config"})]}),e.jsx($,{})]})}export{z as InstanceConfigPage};
|
|
2
2
|
|