@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.
Files changed (75) hide show
  1. package/README.md +32 -4
  2. package/package.json +2 -2
  3. package/packages/server/dist/app.js +14 -1
  4. package/packages/server/dist/claude-mcp.d.ts +16 -4
  5. package/packages/server/dist/claude-mcp.js +83 -61
  6. package/packages/server/dist/claude-plugins.d.ts +131 -0
  7. package/packages/server/dist/claude-plugins.js +394 -0
  8. package/packages/server/dist/cli/args.d.ts +1 -1
  9. package/packages/server/dist/cli/args.js +9 -0
  10. package/packages/server/dist/herdctl-agent-config.d.ts +7 -1
  11. package/packages/server/dist/herdctl-agent-config.js +20 -4
  12. package/packages/server/dist/herdctl.d.ts +7 -1
  13. package/packages/server/dist/herdctl.js +9 -2
  14. package/packages/server/dist/mcp-servers.d.ts +31 -12
  15. package/packages/server/dist/mcp-servers.js +127 -33
  16. package/packages/server/package.json +1 -1
  17. package/packages/web/dist/assets/{InstanceConfigPage-Ca905oey.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
  18. package/packages/web/dist/assets/{MarkdownRenderer-ChlC1ezH.js → MarkdownRenderer-B6gST_0U.js} +1 -1
  19. package/packages/web/dist/assets/{ProjectRedirect-CslqbhUb.js → ProjectRedirect-BCmKT4mf.js} +1 -1
  20. package/packages/web/dist/assets/{ProjectView-DkNdilGo.js → ProjectView-CCN98MWu.js} +3 -3
  21. package/packages/web/dist/assets/{ProjectsGrid-BrlvLc-Y.js → ProjectsGrid-CYzuN_yD.js} +1 -1
  22. package/packages/web/dist/assets/{arc-CGqCI5nQ.js → arc-BxvtMCZQ.js} +1 -1
  23. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cp4KTxE2.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
  24. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-yFxaTv04.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
  25. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-59lhWjRt.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
  26. package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
  27. package/packages/web/dist/assets/{chunk-2J33WTMH-BrpXGnJg.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
  28. package/packages/web/dist/assets/{chunk-4BX2VUAB-UxBDb5pi.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
  29. package/packages/web/dist/assets/{chunk-55IACEB6-D4ZJyUHW.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
  30. package/packages/web/dist/assets/{chunk-727SXJPM-DWdPj_8E.js → chunk-727SXJPM-bWEqh672.js} +1 -1
  31. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BIpNbflY.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
  32. package/packages/web/dist/assets/{chunk-FMBD7UC4-f6iJDfdI.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
  33. package/packages/web/dist/assets/{chunk-ND2GUHAM-Bz3TKIJk.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
  34. package/packages/web/dist/assets/{chunk-QZHKN3VN-buQmkafK.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
  35. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
  36. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
  37. package/packages/web/dist/assets/{core-TyFI6p0n.js → core-CSaNbmLO.js} +1 -1
  38. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CIkdi0XF.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
  39. package/packages/web/dist/assets/{dagre-BM42HDAG-BnLJCfNs.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
  40. package/packages/web/dist/assets/{diagram-2AECGRRQ-D0OqfDB9.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
  41. package/packages/web/dist/assets/{diagram-5GNKFQAL-DQ2spGrK.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
  42. package/packages/web/dist/assets/{diagram-KO2AKTUF-RDIVQ_S3.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
  43. package/packages/web/dist/assets/{diagram-LMA3HP47-DKoTfh2C.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
  44. package/packages/web/dist/assets/{diagram-OG6HWLK6-6k0OuSAL.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
  45. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B-oQaYAC.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
  46. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-B7qpYe9C.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
  47. package/packages/web/dist/assets/{format-PHdEBNOi.js → format-BSDZezwD.js} +1 -1
  48. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-SfBsqMmx.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
  49. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CuM4Rl_i.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
  50. package/packages/web/dist/assets/{index-D_1ogxg1.js → index-CUnFKMga.js} +2 -2
  51. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Dq-32Oj4.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
  52. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BLFHCj_o.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
  53. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CHP-Yu63.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
  54. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DRuehSqn.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
  55. package/packages/web/dist/assets/{linear-Bwz2vnH7.js → linear-DuyAaBu6.js} +1 -1
  56. package/packages/web/dist/assets/{mermaid.core-CeMMnCip.js → mermaid.core-BQlzuVvO.js} +4 -4
  57. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CEdqeoRW.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
  58. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BdDXBx87.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
  59. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bovd1Zhx.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
  60. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DiyrKnJ2.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
  61. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BuX7TRVw.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
  62. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BjetZWJo.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
  63. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-4AUBV6dF.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
  64. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
  65. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpQ8g8Bm.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
  66. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Cj99BWER.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
  67. package/packages/web/dist/assets/{wardley-L42UT6IY-S7AGTOWw.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
  68. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-LNXkUA7S.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
  69. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DGdTh4Xk.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
  70. package/packages/web/dist/index.html +1 -1
  71. package/packages/web/dist/sw.js +1 -1
  72. package/packages/web/dist/assets/channel-CZs6Z1i5.js +0 -2
  73. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +0 -2
  74. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +0 -2
  75. 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. Every diagnostic
52
- * names keys, variable names and server names only; a `url` is reported with its
53
- * query string and userinfo stripped, because that is where an API key rides.
54
- * {@link describeServer} is the single place a server is rendered for a human,
55
- * so there is one function to audit rather than a scattering of template strings.
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, and a `headers:` block that is silently dropped is an authentication
69
- * failure that looks like a broken server. So anything that cannot be carried is
70
- * an ERROR and that server is not attached — including an unknown key, which is
71
- * how a typo (`arg:` for `args:`) otherwise becomes a server that starts wrong.
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 streamable-HTTP server. Mutually exclusive with {@link command}. */
99
+ /** Endpoint for a remote server. Mutually exclusive with {@link command}. */
87
100
  url?: string;
88
- /** Accepted only as `stdio`/`http`, and only when it agrees with the above. */
101
+ /** `stdio` for a {@link command}; `http` or `sse` for a {@link url}. */
89
102
  type?: string;
90
- /** Present only in a MISCONFIGURED file: the engine cannot carry headers. */
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(`http: ${redactUrl(def.url)}`);
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 rejections are the interesting part, and each is a thing step 5 discovered
106
- * the engine strips silently:
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
- * - **`headers`** — there is no field for them, so a bearer-authenticated server
109
- * would arrive unauthenticated AND miss its stored OAuth token, which is keyed
110
- * on a hash that includes the headers. Step 5 passes such a host server with a
111
- * warning; a hand-written declaration gets an error instead, because the user
112
- * typed the header and can act on being told it does nothing.
113
- * - **`type: sse`** — every `url` is mapped to `type: "http"` downstream, so an
114
- * sse server is silently downgraded, with the same OAuth-key consequence.
115
- * - **an unknown key** — the only defence against `arg:`/`envs:`/`commands:`,
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(", ")} (supported: command, args, env, url) ` +
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
- if (type === "sse") {
159
- return fail(`${where}: \`type: sse\` cannot be carried — every \`url\` server is connected to as ` +
160
- `streamable HTTP downstream, so an sse server would be silently downgraded (and its ` +
161
- `stored OAuth token, keyed on a hash that includes the type, not found). Declare it ` +
162
- `as \`http\` if the server supports it`);
163
- }
164
- const expected = hasUrl ? "http" : "stdio";
165
- if (type !== expected) {
166
- return fail(`${where}: \`type: ${type}\` disagrees with the declaration (expected ${expected})`);
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 usual way a remote MCP endpoint is
182
- // authenticated now that `headers` cannot be carried, and this file is
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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-D_1ogxg1.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};
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