@b00dea/cockpit 0.1.0 → 0.1.1

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 (196) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +3 -0
  3. package/cockpit.schema.json +73 -3
  4. package/dist/autonomy.d.ts +3 -2
  5. package/dist/autonomy.js +30 -13
  6. package/dist/browser/commands.d.ts +57 -2
  7. package/dist/browser/commands.js +114 -10
  8. package/dist/browser/runtime.d.ts +1 -0
  9. package/dist/browser/runtime.js +4 -1
  10. package/dist/browser/surfaces.d.ts +7 -7
  11. package/dist/browser/surfaces.js +5 -0
  12. package/dist/canonical-json.d.ts +2 -0
  13. package/dist/canonical-json.js +5 -0
  14. package/dist/cli-main.d.ts +40 -3
  15. package/dist/cli-main.js +241 -62
  16. package/dist/command-table.d.ts +19 -11
  17. package/dist/command-table.js +190 -38
  18. package/dist/config/load.d.ts +16 -4
  19. package/dist/config/load.js +25 -3
  20. package/dist/config/unconfirmed-toml.d.ts +9 -0
  21. package/dist/config/unconfirmed-toml.js +29 -0
  22. package/dist/diagnostics.js +10 -1
  23. package/dist/docs/check.d.ts +16 -0
  24. package/dist/docs/check.js +197 -0
  25. package/dist/docs/decision-record.d.ts +34 -0
  26. package/dist/docs/decision-record.js +135 -0
  27. package/dist/docs/decisions.d.ts +16 -0
  28. package/dist/docs/decisions.js +103 -0
  29. package/dist/docs/markdown.d.ts +36 -0
  30. package/dist/docs/markdown.js +88 -0
  31. package/dist/docs/references.d.ts +32 -0
  32. package/dist/docs/references.js +148 -0
  33. package/dist/environment/application/confirm.d.ts +35 -0
  34. package/dist/environment/application/confirm.js +122 -0
  35. package/dist/environment/application/destructive.js +6 -2
  36. package/dist/environment/application/doctor.d.ts +12 -7
  37. package/dist/environment/application/doctor.js +20 -8
  38. package/dist/environment/application/guard.d.ts +12 -6
  39. package/dist/environment/application/guard.js +87 -32
  40. package/dist/environment/application/health.d.ts +14 -5
  41. package/dist/environment/application/health.js +8 -2
  42. package/dist/environment/application/lifecycle-context.d.ts +10 -3
  43. package/dist/environment/application/lifecycle-state.js +4 -3
  44. package/dist/environment/application/lifecycle.d.ts +2 -1
  45. package/dist/environment/application/lifecycle.js +11 -3
  46. package/dist/environment/application/services.d.ts +60 -0
  47. package/dist/environment/application/services.js +89 -0
  48. package/dist/environment/application/siblings.d.ts +43 -0
  49. package/dist/environment/application/siblings.js +269 -0
  50. package/dist/environment/claims.d.ts +10 -10
  51. package/dist/environment/claims.js +3 -11
  52. package/dist/environment/compose-topology.d.ts +15 -7
  53. package/dist/environment/compose-topology.js +100 -23
  54. package/dist/environment/contract.d.ts +42 -9
  55. package/dist/environment/contract.js +33 -0
  56. package/dist/environment/executable.d.ts +12 -0
  57. package/dist/environment/executable.js +46 -23
  58. package/dist/environment/generated-application/check.js +2 -0
  59. package/dist/environment/generated-application/prepare.js +2 -0
  60. package/dist/environment/generated-application/shared.d.ts +10 -2
  61. package/dist/environment/generated-application/shared.js +6 -1
  62. package/dist/environment/generated-application/surface-outputs.js +23 -12
  63. package/dist/environment/generated-application/surfaces.js +16 -11
  64. package/dist/environment/generated-application/sync.js +19 -4
  65. package/dist/environment/git-observation.d.ts +25 -6
  66. package/dist/environment/git-observation.js +37 -28
  67. package/dist/environment/health-composition.d.ts +3 -2
  68. package/dist/environment/health-composition.js +19 -2
  69. package/dist/environment/init-application.d.ts +39 -14
  70. package/dist/environment/init-application.js +39 -10
  71. package/dist/environment/lifecycle-composition.d.ts +4 -2
  72. package/dist/environment/lifecycle-composition.js +84 -17
  73. package/dist/environment/lifecycle-stage.d.ts +8 -3
  74. package/dist/environment/lifecycle-stage.js +11 -1
  75. package/dist/environment/logs.d.ts +1 -1
  76. package/dist/environment/map-limit.d.ts +2 -0
  77. package/dist/environment/map-limit.js +13 -0
  78. package/dist/environment/mcp-observation.d.ts +9 -0
  79. package/dist/environment/mcp-observation.js +46 -0
  80. package/dist/environment/observations.d.ts +1 -1
  81. package/dist/environment/observations.js +2 -0
  82. package/dist/environment/operator-application.d.ts +35 -6
  83. package/dist/environment/operator-application.js +42 -0
  84. package/dist/environment/presence.d.ts +4 -0
  85. package/dist/environment/presence.js +26 -0
  86. package/dist/environment/read-composition.d.ts +8 -3
  87. package/dist/environment/read-composition.js +53 -8
  88. package/dist/environment/readiness-composition.d.ts +20 -1
  89. package/dist/environment/readiness-composition.js +23 -2
  90. package/dist/environment/reclaim.d.ts +56 -19
  91. package/dist/environment/reclaim.js +147 -42
  92. package/dist/environment/runtime.d.ts +64 -18
  93. package/dist/environment/runtime.js +73 -3
  94. package/dist/environment/served-checkout-grants.d.ts +53 -0
  95. package/dist/environment/served-checkout-grants.js +182 -0
  96. package/dist/environment/sibling-checkouts.d.ts +119 -0
  97. package/dist/environment/sibling-checkouts.js +254 -0
  98. package/dist/environment/sibling-observation.d.ts +27 -0
  99. package/dist/environment/sibling-observation.js +45 -0
  100. package/dist/environment/sibling-pull.d.ts +23 -0
  101. package/dist/environment/sibling-pull.js +95 -0
  102. package/dist/environment/stack-inputs.d.ts +18 -0
  103. package/dist/environment/stack-inputs.js +210 -0
  104. package/dist/environment/stack-inventory-prune.d.ts +31 -0
  105. package/dist/environment/stack-inventory-prune.js +129 -0
  106. package/dist/environment/stack-inventory-report.d.ts +44 -0
  107. package/dist/environment/stack-inventory-report.js +78 -0
  108. package/dist/environment/stack-inventory.d.ts +94 -0
  109. package/dist/environment/stack-inventory.js +418 -0
  110. package/dist/environment/state.d.ts +48 -9
  111. package/dist/environment/state.js +142 -2
  112. package/dist/environment/tool-registration.js +3 -7
  113. package/dist/environment-exec/execute.d.ts +6 -3
  114. package/dist/environment-exec/execute.js +4 -0
  115. package/dist/fix-catalogue.d.ts +1 -1
  116. package/dist/generated-environment/adopt.js +4 -8
  117. package/dist/guards/answer-confirmation.d.ts +41 -0
  118. package/dist/guards/answer-confirmation.js +109 -0
  119. package/dist/guards/evaluator.d.ts +20 -3
  120. package/dist/guards/evaluator.js +39 -7
  121. package/dist/guards/local-identity.d.ts +6 -2
  122. package/dist/guards/local-identity.js +10 -4
  123. package/dist/guards/policy-change.d.ts +43 -0
  124. package/dist/guards/policy-change.js +159 -0
  125. package/dist/guards/policy-pin.d.ts +1 -1
  126. package/dist/guards/policy-pin.js +14 -12
  127. package/dist/init/command.d.ts +4 -3
  128. package/dist/init/command.js +36 -16
  129. package/dist/init/derive.d.ts +8 -6
  130. package/dist/init/derive.js +140 -36
  131. package/dist/init/draft.d.ts +13 -11
  132. package/dist/init/draft.js +54 -31
  133. package/dist/init/render.d.ts +18 -6
  134. package/dist/init/render.js +31 -9
  135. package/dist/repository-path.d.ts +9 -0
  136. package/dist/repository-path.js +16 -0
  137. package/dist/schema.d.ts +42 -75
  138. package/dist/schema.js +62 -11
  139. package/dist/surfaces/harness-registry.d.ts +80 -0
  140. package/dist/surfaces/harness-registry.js +29 -0
  141. package/dist/surfaces/mcp.d.ts +67 -17
  142. package/dist/surfaces/mcp.js +194 -31
  143. package/dist/surfaces/projection.d.ts +7 -7
  144. package/dist/surfaces/projection.js +41 -6
  145. package/dist/surfaces/render.d.ts +5 -44
  146. package/dist/surfaces/render.js +5 -2
  147. package/dist/team-metadata.js +2 -1
  148. package/dist/testing/test-protection.d.ts +34 -0
  149. package/dist/testing/test-protection.js +137 -0
  150. package/dist/testing/weakening.d.ts +15 -0
  151. package/dist/testing/weakening.js +30 -0
  152. package/dist/work-orders/application-closure.d.ts +7 -0
  153. package/dist/work-orders/application-closure.js +76 -0
  154. package/dist/work-orders/application-context.d.ts +162 -0
  155. package/dist/work-orders/application-context.js +137 -0
  156. package/dist/work-orders/application-decisions.d.ts +27 -0
  157. package/dist/work-orders/application-decisions.js +121 -0
  158. package/dist/work-orders/application-grading.d.ts +11 -0
  159. package/dist/work-orders/application-grading.js +212 -0
  160. package/dist/work-orders/application.d.ts +4 -97
  161. package/dist/work-orders/application.js +94 -254
  162. package/dist/work-orders/composition.d.ts +35 -58
  163. package/dist/work-orders/composition.js +216 -24
  164. package/dist/work-orders/contract.d.ts +22 -6
  165. package/dist/work-orders/contract.js +34 -16
  166. package/dist/work-orders/decisions.d.ts +23 -0
  167. package/dist/work-orders/decisions.js +55 -0
  168. package/dist/work-orders/store.d.ts +54 -5
  169. package/dist/work-orders/store.js +118 -24
  170. package/docs/README.md +6 -3
  171. package/docs/capability-matrix.md +17 -9
  172. package/docs/cli-reference.md +209 -1
  173. package/docs/command-list.md +12 -1
  174. package/docs/concepts.md +6 -4
  175. package/docs/configuration-essentials.md +7 -2
  176. package/docs/configuration-reference.md +18 -3
  177. package/docs/docs-check.md +76 -0
  178. package/docs/environment.md +194 -6
  179. package/docs/faq.md +5 -3
  180. package/docs/getting-started.md +109 -25
  181. package/docs/harness-surfaces.md +69 -2
  182. package/docs/safety-and-autonomy.md +56 -12
  183. package/docs/troubleshooting.md +29 -8
  184. package/docs/tutorial.md +3 -2
  185. package/docs/work-and-browser.md +180 -14
  186. package/libexec/browser-operation.cjs +86 -2
  187. package/package.json +2 -2
  188. package/surfaces/skills/cockpit-browser.skill.md +7 -2
  189. package/surfaces/skills/cockpit-explain.skill.md +1 -1
  190. package/surfaces/skills/cockpit-fix.skill.md +1 -1
  191. package/surfaces/skills/cockpit-setup.skill.md +3 -3
  192. package/surfaces/skills/cockpit-work.skill.md +1 -1
  193. package/surfaces/skills/procedures/explain.md +9 -4
  194. package/surfaces/skills/procedures/fix.md +25 -11
  195. package/surfaces/skills/procedures/setup.md +113 -40
  196. package/surfaces/skills/procedures/work.md +45 -16
@@ -31,6 +31,22 @@ export declare const HARNESS_REGISTRY: readonly [Readonly<{
31
31
  measuredVersion: "2.1.317";
32
32
  provenance: "a SessionStart hook in .claude/settings.json ran under claude -p and the agent quoted its stdout back, while a hook under a misspelled event name never ran and Claude Code named it as ignored";
33
33
  }>;
34
+ remoteMcp: Readonly<{
35
+ evidence: 'not_applicable' | 'human_needed';
36
+ measuredVersion: string;
37
+ helper: boolean;
38
+ provenance: string;
39
+ state: 'instructed';
40
+ headers: true;
41
+ measuredOn: "2026-11-04";
42
+ }>;
43
+ servedCheckoutGrant: Readonly<{
44
+ state: 'enforced' | 'unavailable';
45
+ evidence: 'not_applicable' | 'human_needed';
46
+ measuredVersion: string | null;
47
+ provenance: string;
48
+ measuredOn: "2026-11-04";
49
+ }>;
34
50
  }>, Readonly<{
35
51
  target: "codexcli";
36
52
  type: 'agent';
@@ -64,6 +80,22 @@ export declare const HARNESS_REGISTRY: readonly [Readonly<{
64
80
  measuredVersion: "0.166.0";
65
81
  provenance: "only after a person trusts the hook, as for the guard: a trusted SessionStart hook in .codex/hooks.json ran under codex exec and the agent quoted its stdout back, while a hook under a misspelled event name never ran";
66
82
  }>;
83
+ remoteMcp: Readonly<{
84
+ evidence: 'not_applicable' | 'human_needed';
85
+ measuredVersion: string;
86
+ helper: boolean;
87
+ provenance: string;
88
+ state: 'instructed';
89
+ headers: true;
90
+ measuredOn: "2026-11-04";
91
+ }>;
92
+ servedCheckoutGrant: Readonly<{
93
+ state: 'enforced' | 'unavailable';
94
+ evidence: 'not_applicable' | 'human_needed';
95
+ measuredVersion: string | null;
96
+ provenance: string;
97
+ measuredOn: "2026-11-04";
98
+ }>;
67
99
  }>, Readonly<{
68
100
  target: "opencode";
69
101
  type: 'agent';
@@ -97,6 +129,22 @@ export declare const HARNESS_REGISTRY: readonly [Readonly<{
97
129
  measuredVersion: null;
98
130
  provenance: string;
99
131
  };
132
+ remoteMcp: Readonly<{
133
+ evidence: 'not_applicable' | 'human_needed';
134
+ measuredVersion: string;
135
+ helper: boolean;
136
+ provenance: string;
137
+ state: 'instructed';
138
+ headers: true;
139
+ measuredOn: "2026-11-04";
140
+ }>;
141
+ servedCheckoutGrant: Readonly<{
142
+ state: 'enforced' | 'unavailable';
143
+ evidence: 'not_applicable' | 'human_needed';
144
+ measuredVersion: string | null;
145
+ provenance: string;
146
+ measuredOn: "2026-11-04";
147
+ }>;
100
148
  }>, Readonly<{
101
149
  target: "cursor";
102
150
  type: 'agent';
@@ -126,6 +174,22 @@ export declare const HARNESS_REGISTRY: readonly [Readonly<{
126
174
  measuredVersion: null;
127
175
  provenance: string;
128
176
  };
177
+ remoteMcp: Readonly<{
178
+ state: 'unavailable';
179
+ evidence: 'not_applicable';
180
+ headers: false;
181
+ helper: false;
182
+ measuredOn: "2026-11-04";
183
+ measuredVersion: null;
184
+ provenance: "cockpit writes no remote MCP server for this tool: its remote transport and header spelling have not been checked";
185
+ }>;
186
+ servedCheckoutGrant: Readonly<{
187
+ state: 'enforced' | 'unavailable';
188
+ evidence: 'not_applicable' | 'human_needed';
189
+ measuredVersion: string | null;
190
+ provenance: string;
191
+ measuredOn: "2026-11-04";
192
+ }>;
129
193
  }>, Readonly<{
130
194
  target: "grokcli";
131
195
  type: 'agent';
@@ -155,6 +219,22 @@ export declare const HARNESS_REGISTRY: readonly [Readonly<{
155
219
  measuredVersion: null;
156
220
  provenance: string;
157
221
  };
222
+ remoteMcp: Readonly<{
223
+ state: 'unavailable';
224
+ evidence: 'not_applicable';
225
+ headers: false;
226
+ helper: false;
227
+ measuredOn: "2026-11-04";
228
+ measuredVersion: null;
229
+ provenance: "cockpit writes no remote MCP server for this tool: its remote transport and header spelling have not been checked";
230
+ }>;
231
+ servedCheckoutGrant: Readonly<{
232
+ state: 'enforced' | 'unavailable';
233
+ evidence: 'not_applicable' | 'human_needed';
234
+ measuredVersion: string | null;
235
+ provenance: string;
236
+ measuredOn: "2026-11-04";
237
+ }>;
158
238
  }>];
159
239
  export type HarnessRegistryRow = typeof HARNESS_REGISTRY[number];
160
240
  export type SurfaceTarget = HarnessRegistryRow['target'];
@@ -44,6 +44,21 @@ const codexSessionStart = Object.freeze({
44
44
  provenance: 'only after a person trusts the hook, as for the guard: a trusted SessionStart hook in .codex/hooks.json ran under codex exec and the agent quoted its stdout back, while a hook under a misspelled event name never ran',
45
45
  });
46
46
  const noSessionStart = (reason) => ({ state: 'unavailable', evidence: 'not_applicable', measuredOn: '2026-11-03', measuredVersion: null, provenance: reason });
47
+ // What each tool's native MCP file can say for a remote (streamable HTTP) server. A header must come
48
+ // from an environment reference or a headers helper, never a literal; a tool that cannot express a
49
+ // server's credential gets nothing for that server, because a server written without it would start,
50
+ // fail to authenticate and read as configured. Each run connected the tool to a loopback server that
51
+ // recorded the request headers, beside a control spelled the way the tool does not read.
52
+ const remoteMcpMeasured = (fact) => Object.freeze({ state: 'instructed', headers: true, measuredOn: '2026-11-04', ...fact });
53
+ const noRemoteMcp = Object.freeze({ state: 'unavailable', evidence: 'not_applicable', headers: false, helper: false,
54
+ measuredOn: '2026-11-04', measuredVersion: null,
55
+ provenance: 'cockpit writes no remote MCP server for this tool: its remote transport and header spelling have not been checked' });
56
+ // Whether `cockpit use` can grant a checkout served from outside the declared one through a
57
+ // machine-local file the tool reads. Each run wrote into the granted directory and into an
58
+ // ungranted control beside it.
59
+ const servedGrant = (fact) => Object.freeze({ measuredOn: '2026-11-04', ...fact });
60
+ const noServedGrant = servedGrant({ state: 'unavailable', evidence: 'not_applicable', measuredVersion: null,
61
+ provenance: 'the tool was not installed where this was measured, so cockpit use writes nothing for it' });
47
62
  const completeRulesyncFeatures = Object.freeze(['rules', 'mcp', 'subagents', 'skills', 'hooks', 'permissions']);
48
63
  const nonPermissionRulesyncFeatures = Object.freeze(['rules', 'mcp', 'subagents', 'skills', 'hooks']);
49
64
  export const HARNESS_REGISTRY = Object.freeze([
@@ -52,12 +67,20 @@ export const HARNESS_REGISTRY = Object.freeze([
52
67
  rulesyncFeatures: completeRulesyncFeatures,
53
68
  outputs: Object.freeze({ instruction: 'CLAUDE.md', skills: '.claude/skills', mcp: '.mcp.json', permissions: Object.freeze(['.claude/settings.json']) }),
54
69
  guard: Object.freeze({ capability: 'instructed', firing: claudeFiring }), native: reviewedNative, sessionStart: claudeSessionStart,
70
+ remoteMcp: remoteMcpMeasured({ evidence: 'human_needed', measuredVersion: '2.1.318', helper: true,
71
+ provenance: 'from a project .mcp.json, Claude Code sent the Authorization header a ${NAME} reference named while a $NAME control went out literally; it ran a headersHelper given with --mcp-config, but refused to run one from the project file until a person trusts the workspace, and a misspelled headerHelper sent nothing' }),
72
+ servedCheckoutGrant: servedGrant({ state: 'enforced', evidence: 'human_needed', measuredVersion: '2.1.318',
73
+ provenance: 'a directory in .claude/settings.local.json permissions.additionalDirectories was written and the control refused, and a malformed twin granted nothing; measured in an untrusted workspace, so whether it combines with the committed list once a person trusts the workspace is unmeasured' }),
55
74
  }),
56
75
  Object.freeze({
57
76
  target: 'codexcli', type: 'agent', displayName: 'Codex', rulesyncTarget: 'codexcli',
58
77
  rulesyncFeatures: completeRulesyncFeatures,
59
78
  outputs: Object.freeze({ instruction: 'AGENTS.md', skills: '.agents/skills', mcp: '.codex/config.toml', permissions: Object.freeze(['.codex/config.toml']) }),
60
79
  guard: Object.freeze({ capability: 'instructed', firing: codexFiring }), native: reviewedNative, sessionStart: codexSessionStart,
80
+ remoteMcp: remoteMcpMeasured({ evidence: 'not_applicable', measuredVersion: '0.166.0', helper: true,
81
+ provenance: 'from a project .codex/config.toml in a directory Codex had not been told to trust, codex exec sent the bearer_token_env_var and env_http_headers values and the http_headers_helper output, named a misspelled http_header_helper as ignored, and sent a ${NAME} written in http_headers literally' }),
82
+ servedCheckoutGrant: servedGrant({ state: 'unavailable', evidence: 'not_applicable', measuredVersion: '0.166.0',
83
+ provenance: 'Codex reads no machine-local project file; --add-dir at start made the worktree writable and without it the write was refused, so cockpit use prints that flag and writes nothing' }),
61
84
  }),
62
85
  Object.freeze({
63
86
  target: 'opencode', type: 'agent', displayName: 'OpenCode', rulesyncTarget: 'opencodev1',
@@ -67,6 +90,10 @@ export const HARNESS_REGISTRY = Object.freeze([
67
90
  // OpenCode 1.19.5 delivered session.created to a project plugin beside a misspelled hook that
68
91
  // never ran, but its model call failed, so no plugin hook was shown to put text in the agent's context.
69
92
  sessionStart: noSessionStart('OpenCode 1.19.5 delivers session.created to a plugin, but no run has shown a plugin putting text in the agent\'s context, so cockpit generates no session-start check'),
93
+ remoteMcp: remoteMcpMeasured({ evidence: 'not_applicable', measuredVersion: '1.19.5', helper: false,
94
+ provenance: 'opencode mcp list sent headers expanded from {env:NAME} in a project opencode.jsonc while a ${NAME} control went out literally; its remote server schema has no headers helper, so a server that declares one is not written for it' }),
95
+ servedCheckoutGrant: servedGrant({ state: 'enforced', evidence: 'not_applicable', measuredVersion: '1.19.5',
96
+ provenance: 'the machine-local grant file holding the worktree path allowed the write and the control was denied, and a pattern naming a missing grant file stopped OpenCode at start, naming the file' }),
70
97
  }),
71
98
  Object.freeze({
72
99
  target: 'cursor', type: 'agent', displayName: 'Cursor', rulesyncTarget: 'cursor',
@@ -74,12 +101,14 @@ export const HARNESS_REGISTRY = Object.freeze([
74
101
  // Rules overlays only: cockpit's own instructions reach Cursor through the root AGENTS.md.
75
102
  outputs: Object.freeze({ instruction: '.cursor/rules', skills: '.cursor/skills', mcp: '.cursor/mcp.json', permissions: Object.freeze(['.cursor/cli.json']) }),
76
103
  guard: noGuardAdapter, native: generatedNative, sessionStart: noSessionStart('cockpit generates no hook for this tool'),
104
+ remoteMcp: noRemoteMcp, servedCheckoutGrant: noServedGrant,
77
105
  }),
78
106
  Object.freeze({
79
107
  target: 'grokcli', type: 'agent', displayName: 'Grok Build', rulesyncTarget: 'grokcli',
80
108
  rulesyncFeatures: completeRulesyncFeatures,
81
109
  outputs: Object.freeze({ instruction: 'AGENTS.md', skills: '.grok/skills', mcp: '.grok/config.toml', permissions: Object.freeze(['.grok/config.toml']) }),
82
110
  guard: noGuardAdapter, native: generatedNative, sessionStart: noSessionStart('cockpit generates no hook for this tool'),
111
+ remoteMcp: noRemoteMcp, servedCheckoutGrant: noServedGrant,
83
112
  }),
84
113
  ]);
85
114
  export const SURFACE_TARGETS = Object.freeze(HARNESS_REGISTRY.map(row => row.target));
@@ -14,7 +14,7 @@ declare const Source: z.ZodDiscriminatedUnion<[z.ZodObject<{
14
14
  sha256: z.ZodString;
15
15
  slug: z.ZodString;
16
16
  }, z.core.$strict>], "kind">;
17
- declare const Declaration: z.ZodObject<{
17
+ declare const StdioDeclaration: z.ZodObject<{
18
18
  name: z.ZodString;
19
19
  command: z.ZodString;
20
20
  args: z.ZodArray<z.ZodString>;
@@ -35,25 +35,80 @@ declare const Declaration: z.ZodObject<{
35
35
  slug: z.ZodString;
36
36
  }, z.core.$strict>], "kind">;
37
37
  }, z.core.$strict>;
38
- export type McpSource = z.infer<typeof Source>;
39
- export type McpDeclaration = z.infer<typeof Declaration>;
38
+ declare const HttpDeclaration: z.ZodObject<{
39
+ name: z.ZodString;
40
+ transport: z.ZodLiteral<"http">;
41
+ url: z.ZodString;
42
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
43
+ helper: z.ZodNullable<z.ZodObject<{
44
+ command: z.ZodString;
45
+ args: z.ZodArray<z.ZodString>;
46
+ cwd: z.ZodString;
47
+ }, z.core.$strict>>;
48
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
49
+ kind: z.ZodLiteral<"core">;
50
+ path: z.ZodString;
51
+ sha256: z.ZodString;
52
+ }, z.core.$strict>, z.ZodObject<{
53
+ kind: z.ZodLiteral<"overlay">;
54
+ path: z.ZodString;
55
+ sha256: z.ZodString;
56
+ }, z.core.$strict>, z.ZodObject<{
57
+ kind: z.ZodLiteral<"sibling">;
58
+ path: z.ZodString;
59
+ sha256: z.ZodString;
60
+ slug: z.ZodString;
61
+ }, z.core.$strict>], "kind">;
62
+ }, z.core.$strict>;
63
+ type McpSource = z.infer<typeof Source>;
64
+ export type McpStdioDeclaration = z.infer<typeof StdioDeclaration>;
65
+ export type McpHttpDeclaration = z.infer<typeof HttpDeclaration>;
66
+ type McpDeclaration = McpStdioDeclaration | McpHttpDeclaration;
40
67
  export interface McpDiagnostic {
41
- readonly code: 'MCP_INPUT_INVALID' | 'MCP_DECLARATION_INVALID' | 'MCP_COMMAND_CONFLICT' | 'MCP_NOT_APPROVED' | 'MCP_APPROVAL_UNKNOWN';
68
+ readonly code: 'MCP_INPUT_INVALID' | 'MCP_DECLARATION_INVALID' | 'MCP_COMMAND_CONFLICT' | 'MCP_NOT_APPROVED' | 'MCP_APPROVAL_UNKNOWN' | 'MCP_TRANSPORT_UNAVAILABLE';
42
69
  readonly message: string;
43
70
  readonly server?: string;
44
71
  readonly sources?: readonly McpSource[];
45
72
  /** The approval digest of the server's current declaration, on MCP_NOT_APPROVED. */
46
73
  readonly sha256?: string;
74
+ /** The tools a server is not written for, on MCP_TRANSPORT_UNAVAILABLE. */
75
+ readonly targets?: readonly SurfaceTarget[];
47
76
  }
48
- interface McpServer {
49
- readonly name: string;
77
+ interface McpLaunch {
50
78
  readonly command: string;
51
79
  readonly args: readonly string[];
52
80
  readonly cwd: string;
53
- readonly envNames: readonly string[];
54
- readonly enabled: boolean;
55
- readonly sources: readonly McpSource[];
56
81
  }
82
+ export type McpServer = Readonly<{
83
+ name: string;
84
+ command: string;
85
+ args: readonly string[];
86
+ cwd: string;
87
+ envNames: readonly string[];
88
+ enabled: boolean;
89
+ sources: readonly McpSource[];
90
+ }> | Readonly<{
91
+ name: string;
92
+ transport: 'http';
93
+ url: string;
94
+ headers: Readonly<Record<string, string>>;
95
+ helper: McpLaunch | null;
96
+ envNames: readonly string[];
97
+ enabled: boolean;
98
+ sources: readonly McpSource[];
99
+ }>;
100
+ export type McpHarnessServer = Readonly<{
101
+ name: string;
102
+ command: string;
103
+ args: readonly string[];
104
+ enabled: boolean;
105
+ }> | Readonly<{
106
+ name: string;
107
+ url: string;
108
+ headers: Readonly<Record<string, string>>;
109
+ headersHelper: string | null;
110
+ enabled: boolean;
111
+ }>;
57
112
  export interface McpCollection {
58
113
  readonly servers: readonly McpServer[];
59
114
  readonly wrappers: readonly {
@@ -63,12 +118,7 @@ export interface McpCollection {
63
118
  }[];
64
119
  readonly harnesses: readonly {
65
120
  readonly target: SurfaceTarget;
66
- readonly servers: readonly {
67
- readonly name: string;
68
- readonly command: string;
69
- readonly args: readonly string[];
70
- readonly enabled: boolean;
71
- }[];
121
+ readonly servers: readonly McpHarnessServer[];
72
122
  }[];
73
123
  }
74
124
  export type McpCollectionOutcome = Readonly<{
@@ -79,14 +129,14 @@ export type McpCollectionOutcome = Readonly<{
79
129
  kind: 'refusal';
80
130
  diagnostic: McpDiagnostic;
81
131
  }>;
82
- export type McpParseOutcome = Readonly<{
132
+ type McpParseOutcome = Readonly<{
83
133
  kind: 'complete';
84
134
  declarations: readonly McpDeclaration[];
85
135
  }> | Readonly<{
86
136
  kind: 'refusal';
87
137
  diagnostic: McpDiagnostic;
88
138
  }>;
89
- type McpIdentity = Pick<McpDeclaration, 'command' | 'args' | 'cwd' | 'envNames'>;
139
+ type McpIdentity = Pick<McpStdioDeclaration, 'command' | 'args' | 'cwd' | 'envNames'> | Pick<McpHttpDeclaration, 'transport' | 'url' | 'headers' | 'helper'>;
90
140
  /** The digest `[mcp.approved]` records for a server, so a changed declaration is no longer approved. */
91
141
  export declare function mcpApprovalDigest(declaration: McpIdentity, cockpitDirectory: string): string;
92
142
  export declare function collectMcpDeclarations(input: unknown): McpCollectionOutcome;
@@ -1,4 +1,4 @@
1
- import { createHash } from 'node:crypto';
1
+ import { sha256Hex } from '../sha256.js';
2
2
  import { isAbsolute, relative, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { parseDocument } from 'yaml';
@@ -20,14 +20,69 @@ const Source = z.discriminatedUnion('kind', [
20
20
  ]);
21
21
  const Argument = ShellToken.refine(value => !/^--?(?:api[-_]?key|token|password|secret|authorization|credential)(?:=|$)/i.test(value)
22
22
  && !/^[a-z][a-z0-9+.-]*:\/\/[^/]*@/i.test(value));
23
- const Declaration = z.object({
23
+ const Command = ShellToken.refine(value => !value.startsWith('-'));
24
+ const StdioDeclaration = z.object({
24
25
  name: ServerName,
25
- command: ShellToken.refine(value => !value.startsWith('-')),
26
+ command: Command,
26
27
  args: z.array(Argument).max(256),
27
28
  cwd: WorkingDirectory,
28
29
  envNames: z.array(EnvironmentName).max(128).refine(names => new Set(names).size === names.length),
29
30
  source: Source,
30
31
  }).strict();
32
+ const LOOPBACK_HOSTS = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])$/;
33
+ const CREDENTIAL_NAME = /^(?:api[-_]?key|token|access[-_]?token|password|secret|authorization|credential)$/i;
34
+ /** Why a remote MCP URL is refused, or undefined when it is accepted. Never echoes the URL. */
35
+ function remoteMcpUrlProblem(value) {
36
+ // OpenCode expands {env:NAME} and {file:path} anywhere in opencode.jsonc, URL included, so a brace
37
+ // would let a declaration send an environment variable or a file's contents to the server.
38
+ if (/[{}]/.test(value))
39
+ return 'contains a brace, which OpenCode expands as {env:...} or {file:...} and Codex sends literally';
40
+ let url;
41
+ try {
42
+ url = new URL(value);
43
+ }
44
+ catch {
45
+ return 'is not an absolute URL';
46
+ }
47
+ if (url.username || url.password)
48
+ return 'carries credentials in its user information';
49
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && LOOPBACK_HOSTS.test(url.hostname))) {
50
+ return 'must use https, or http only to a loopback host';
51
+ }
52
+ if ([...url.searchParams.keys()].some(name => CREDENTIAL_NAME.test(name)))
53
+ return 'carries a credential-named query parameter';
54
+ return undefined;
55
+ }
56
+ // A header value is one environment reference, whole or after `Bearer ` in Authorization. Those are
57
+ // the only spellings all three tools resolve from the environment (Codex reads them as
58
+ // env_http_headers and bearer_token_env_var), so no credential literal reaches a tracked file.
59
+ const HEADER_REFERENCE = /^(Bearer )?\$\{([A-Z][A-Z0-9_]*)\}$/;
60
+ const HeaderName = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9-]*$/).max(128);
61
+ /** Why a declared header is refused, or undefined when it is an accepted environment reference. Never echoes the value. */
62
+ function remoteMcpHeaderProblem(name, value) {
63
+ if (!HeaderName.safeParse(name).success)
64
+ return 'has a name that is not an HTTP token';
65
+ const match = HEADER_REFERENCE.exec(value);
66
+ if (!match)
67
+ return 'must be exactly ${NAME}, or Bearer ${NAME} for Authorization, naming an upper-case environment variable; a literal value would commit the credential';
68
+ if (match[1] && name.toLowerCase() !== 'authorization')
69
+ return 'may use the Bearer form only in Authorization';
70
+ return undefined;
71
+ }
72
+ const Headers = z.record(z.string(), z.string().max(256))
73
+ .refine(headers => Object.keys(headers).length <= 32
74
+ && Object.entries(headers).every(([name, value]) => remoteMcpHeaderProblem(name, value) === undefined)
75
+ && new Set(Object.keys(headers).map(name => name.toLowerCase())).size === Object.keys(headers).length);
76
+ const HttpDeclaration = z.object({
77
+ name: ServerName,
78
+ transport: z.literal('http'),
79
+ url: z.string().max(2048).refine(url => remoteMcpUrlProblem(url) === undefined),
80
+ headers: Headers,
81
+ // The declared command that prints the request headers as a JSON object when the tool connects.
82
+ helper: z.object({ command: Command, args: z.array(Argument).max(64), cwd: WorkingDirectory }).strict().nullable(),
83
+ source: Source,
84
+ }).strict();
85
+ const Declaration = z.union([StdioDeclaration, HttpDeclaration]);
31
86
  const Input = z.object({
32
87
  cockpitDirectory: WorkingDirectory,
33
88
  declarations: z.array(Declaration).max(256),
@@ -45,29 +100,49 @@ function quote(value) {
45
100
  // server's directory is relative to it: the committed bytes are the same in every clone. The `./`
46
101
  // keeps CDPATH from choosing another directory and printing it into the JSON-RPC stream, and `-P`
47
102
  // resolves `..` from the real root, against which the relative path was computed.
48
- function wrapper(server, cockpitDirectory) {
49
- const inherited = ['PATH', ...INHERITED_TOOL_ENVIRONMENT].filter(name => !server.envNames.includes(name));
103
+ // A headers helper runs through the same kind of wrapper: Claude Code and Codex start it with
104
+ // `sh -c` from the directory the agent was launched in, and it prints the headers on stdout.
105
+ function wrapper(launch, envNames, cockpitDirectory) {
106
+ const inherited = ['PATH', ...INHERITED_TOOL_ENVIRONMENT].filter(name => !envNames.includes(name));
50
107
  return [
51
108
  '#!/bin/sh',
52
109
  'set -eu',
53
- `cd -P -- ${quote(`./${relative(cockpitDirectory, server.cwd) || '.'}`)}`,
54
- `executable=$(command -v ${quote(server.command)}) || { printf '%s\\n' 'MCP_EXECUTABLE_UNAVAILABLE' >&2; exit 2; }`,
110
+ `cd -P -- ${quote(`./${relative(cockpitDirectory, launch.cwd) || '.'}`)}`,
111
+ `executable=$(command -v ${quote(launch.command)}) || { printf '%s\\n' 'MCP_EXECUTABLE_UNAVAILABLE' >&2; exit 2; }`,
55
112
  '[ -f "$executable" ] && [ -x "$executable" ] || { printf \'%s\\n\' \'MCP_EXECUTABLE_UNAVAILABLE\' >&2; exit 2; }',
56
113
  // `npx`, `uvx` and most npm servers are `#!/usr/bin/env node` scripts: without PATH they
57
114
  // cannot start. The rest of the inherited set carries caches and locale, never a credential.
58
115
  `exec /usr/bin/env -i ${inherited.map(name => `\${${name}+"${name}=$${name}"}`).join(' ')}`
59
- + `${server.envNames.map(name => ` ${name}="\${${name}?required environment name ${name} is unset}"`).join('')} "$executable"${server.args.map(arg => ` ${quote(arg)}`).join('')}`,
116
+ + `${envNames.map(name => ` ${name}="\${${name}?required environment name ${name} is unset}"`).join('')} "$executable"${launch.args.map(arg => ` ${quote(arg)}`).join('')}`,
60
117
  '',
61
118
  ].join('\n');
62
119
  }
63
- // What a human reviews before approving a server: everything the wrapper runs. The working
64
- // directory is taken relative to the cockpit so the digest is the same in every clone.
120
+ /** The committed path of a server's stdio wrapper, or of its headers helper wrapper. */
121
+ function mcpWrapperPath(name, kind) {
122
+ return `.cockpit/mcp/${sha256Hex(name)}${kind === 'headers' ? '.headers' : ''}.sh`;
123
+ }
124
+ function isHttp(value) {
125
+ return 'transport' in value && value.transport === 'http';
126
+ }
127
+ function headerEnvironmentNames(headers) {
128
+ return [...new Set(Object.values(headers).map(value => HEADER_REFERENCE.exec(value)[2]))].sort();
129
+ }
130
+ // What a human reviews before approving a server: everything the wrapper runs, or the URL, the
131
+ // header references and the helper a remote server connects with. Working directories are taken
132
+ // relative to the cockpit so the digest is the same in every clone.
65
133
  function identity(declaration, cockpitDirectory) {
66
- return JSON.stringify({ command: declaration.command, args: declaration.args, cwd: relative(cockpitDirectory, declaration.cwd) || '.', envNames: [...declaration.envNames].sort() });
134
+ if (isHttp(declaration)) {
135
+ const helper = declaration.helper;
136
+ return JSON.stringify({ transport: 'http', url: declaration.url,
137
+ headers: Object.entries(declaration.headers).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0),
138
+ helper: helper && { command: helper.command, args: helper.args, cwd: relative(cockpitDirectory, helper.cwd) || '.' } });
139
+ }
140
+ const stdio = declaration;
141
+ return JSON.stringify({ command: stdio.command, args: stdio.args, cwd: relative(cockpitDirectory, stdio.cwd) || '.', envNames: [...stdio.envNames].sort() });
67
142
  }
68
143
  /** The digest `[mcp.approved]` records for a server, so a changed declaration is no longer approved. */
69
144
  export function mcpApprovalDigest(declaration, cockpitDirectory) {
70
- return createHash('sha256').update(identity(declaration, cockpitDirectory)).digest('hex');
145
+ return sha256Hex(identity(declaration, cockpitDirectory));
71
146
  }
72
147
  export function collectMcpDeclarations(input) {
73
148
  const captured = snapshotJson(input);
@@ -81,7 +156,7 @@ export function collectMcpDeclarations(input) {
81
156
  if (prior && identity(prior.declaration, cockpitDirectory) !== identity(declaration, cockpitDirectory)) {
82
157
  const conflict = snapshotJson({ kind: 'refusal', diagnostic: {
83
158
  code: 'MCP_COMMAND_CONFLICT', server: declaration.name,
84
- message: 'MCP server identity has divergent command, arguments, working directory or environment names',
159
+ message: 'MCP server identity has divergent declarations: command, arguments, working directory or environment names for a local server; URL, header references or headers helper for a remote one',
85
160
  sources: [...prior.sources, declaration.source],
86
161
  } });
87
162
  return conflict ? conflict
@@ -95,11 +170,17 @@ export function collectMcpDeclarations(input) {
95
170
  collected.set(declaration.name, { declaration, sources: [declaration.source] });
96
171
  }
97
172
  const digests = new Map([...collected].map(([name, { declaration }]) => [name, mcpApprovalDigest(declaration, cockpitDirectory)]));
98
- const servers = [...collected.values()].map(({ declaration, sources }) => ({
99
- name: declaration.name, command: declaration.command, args: declaration.args, cwd: declaration.cwd,
100
- envNames: [...declaration.envNames].sort(), enabled: Object.hasOwn(approved, declaration.name) && approved[declaration.name] === digests.get(declaration.name),
101
- sources: sources.sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0),
102
- })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
173
+ const servers = [...collected.values()].map(({ declaration, sources }) => {
174
+ const shared = {
175
+ enabled: Object.hasOwn(approved, declaration.name) && approved[declaration.name] === digests.get(declaration.name),
176
+ sources: sources.sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0),
177
+ };
178
+ return isHttp(declaration)
179
+ ? { name: declaration.name, transport: 'http', url: declaration.url, headers: declaration.headers, helper: declaration.helper,
180
+ envNames: headerEnvironmentNames(declaration.headers), ...shared }
181
+ : { name: declaration.name, command: declaration.command, args: declaration.args, cwd: declaration.cwd,
182
+ envNames: [...declaration.envNames].sort(), ...shared };
183
+ }).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
103
184
  const diagnostics = servers.filter(server => !server.enabled).map(server => ({
104
185
  code: 'MCP_NOT_APPROVED', server: server.name, sources: server.sources, sha256: digests.get(server.name),
105
186
  message: 'MCP server has no shared approval for its current declaration and remains disabled',
@@ -109,26 +190,61 @@ export function collectMcpDeclarations(input) {
109
190
  diagnostics.push({
110
191
  code: 'MCP_APPROVAL_UNKNOWN', server: name, message: 'The shared approval list names an undeclared MCP server',
111
192
  });
112
- const wrappers = servers.map(server => {
113
- const bytes = wrapper(server, cockpitDirectory);
114
- return { path: `.cockpit/mcp/${createHash('sha256').update(server.name).digest('hex')}.sh`, bytes, sha256: createHash('sha256').update(bytes).digest('hex') };
115
- });
193
+ // A tool that cannot carry a remote server's credential gets nothing for it: written without
194
+ // its headers, the server would start, fail to authenticate and still read as configured.
195
+ const writes = (server, target) => {
196
+ if (!isHttp(server))
197
+ return true;
198
+ const support = harnessFact(target).remoteMcp;
199
+ return support.state !== 'unavailable' && (Object.keys(server.headers).length === 0 || support.headers)
200
+ && (server.helper === null || support.helper);
201
+ };
202
+ const targets = parsed.data.targets;
203
+ for (const server of servers) {
204
+ const skipped = targets.filter(target => !writes(server, target));
205
+ if (skipped.length > 0)
206
+ diagnostics.push({
207
+ code: 'MCP_TRANSPORT_UNAVAILABLE', server: server.name, sources: server.sources, targets: skipped,
208
+ message: `MCP server is not written for ${skipped.join(', ')}: ${skipped.map(target => harnessFact(target).remoteMcp.provenance).filter((reason, index, all) => all.indexOf(reason) === index).join('; ')}`,
209
+ });
210
+ }
211
+ const wrappers = [];
212
+ const launchers = new Map();
213
+ for (const server of servers) {
214
+ const launch = isHttp(server) ? server.helper : server;
215
+ if (launch === null || !targets.some(target => writes(server, target)))
216
+ continue;
217
+ const path = mcpWrapperPath(server.name, isHttp(server) ? 'headers' : 'server');
218
+ const bytes = wrapper(launch, isHttp(server) ? [] : server.envNames, cockpitDirectory);
219
+ wrappers.push({ path, bytes, sha256: sha256Hex(bytes) });
220
+ launchers.set(server.name, path);
221
+ }
116
222
  const result = {
117
223
  servers, wrappers,
118
- harnesses: parsed.data.targets.map(target => ({ target, servers: servers.map((server, index) => ({
119
- name: server.name, command: '/bin/sh', args: [wrappers[index].path], enabled: server.enabled,
120
- })) })),
224
+ harnesses: targets.map(target => ({ target, servers: servers.filter(server => writes(server, target)).map((server) => isHttp(server)
225
+ ? { name: server.name, url: server.url, headers: server.headers,
226
+ headersHelper: launchers.has(server.name) ? `/bin/sh ${launchers.get(server.name)}` : null, enabled: server.enabled }
227
+ : { name: server.name, command: '/bin/sh', args: [launchers.get(server.name)], enabled: server.enabled }) })),
121
228
  };
122
229
  const outcome = snapshotJson({ kind: 'complete', result, diagnostics });
123
230
  return outcome ? outcome : refuse('MCP_INPUT_INVALID', 'Canonical MCP output exceeds the bounded data budget');
124
231
  }
125
232
  const NativeServer = z.object({
126
- type: z.literal('stdio').optional(), command: Declaration.shape.command,
233
+ type: z.literal('stdio').optional(), command: Command,
127
234
  args: z.array(Argument).max(256).default([]), cwd: RelativePath.or(z.literal('.')).optional(),
128
235
  env: z.record(EnvironmentName, z.string().max(128)).default({}),
129
236
  }).strict();
237
+ // Claude Code's `.mcp.json` spelling of a remote server; values are checked one by one below so a
238
+ // refusal can say which part is wrong without repeating it.
239
+ const NativeHttpServer = z.object({
240
+ type: z.literal('http'), url: z.string().max(2048),
241
+ headers: z.record(z.string(), z.string().max(256)).default({}), headersHelper: z.string().min(1).max(4096).optional(),
242
+ }).strict();
130
243
  // Absence is inferred only from cockpit's neutral shape; other source forms retain generator validation.
131
- const GeneratedServer = NativeServer.extend({ cwd: WorkingDirectory.optional(), disabled: z.boolean().optional() });
244
+ const GeneratedServer = z.union([
245
+ NativeServer.extend({ cwd: WorkingDirectory.optional(), disabled: z.boolean().optional() }),
246
+ NativeHttpServer.extend({ http_headers_helper: z.string().min(1).max(4096).optional(), disabled: z.boolean().optional() }),
247
+ ]);
132
248
  const GeneratedServers = z.record(ServerName, GeneratedServer);
133
249
  const TargetServers = z.object({ mcpServers: z.record(ServerName, GeneratedServer.nullable()).optional() }).strict();
134
250
  export function hasTargetMcpServers(sources, target) {
@@ -155,23 +271,70 @@ export function hasTargetMcpServers(sources, target) {
155
271
  return true;
156
272
  }
157
273
  }
158
- const NativeFile = z.object({ mcpServers: z.record(ServerName, NativeServer).refine(servers => Object.keys(servers).length <= 256) }).strict();
274
+ const NativeFile = z.object({ mcpServers: z.record(ServerName, z.unknown()).refine(servers => Object.keys(servers).length <= 256) }).strict();
159
275
  const ParseInput = z.object({ bytes: z.string(), source: Source, workingDirectory: WorkingDirectory }).strict();
276
+ // A helper is written as one command line in `.mcp.json`. Cockpit runs it without a shell, so each
277
+ // space-separated word must be plain: quoting, expansion and redirection belong in the helper script.
278
+ const HELPER_WORD = /^[A-Za-z0-9_@%+=:,.\/-]+$/;
279
+ function parseHttpServer(name, server, source, workingDirectory) {
280
+ const urlProblem = remoteMcpUrlProblem(server.url);
281
+ if (urlProblem)
282
+ return `MCP server ${name}: its url ${urlProblem}`;
283
+ const headerNames = Object.keys(server.headers);
284
+ if (headerNames.length > 32)
285
+ return `MCP server ${name} declares more than 32 headers`;
286
+ if (new Set(headerNames.map(header => header.toLowerCase())).size !== headerNames.length)
287
+ return `MCP server ${name} declares a header twice in different case`;
288
+ for (const [header, value] of Object.entries(server.headers)) {
289
+ const problem = remoteMcpHeaderProblem(header, value);
290
+ if (problem)
291
+ return `MCP server ${name}: header ${HeaderName.safeParse(header).success ? header : '(unnamed)'} ${problem}`;
292
+ }
293
+ let helper = null;
294
+ if (server.headersHelper !== undefined) {
295
+ const words = server.headersHelper.split(' ');
296
+ if (words.some(word => !HELPER_WORD.test(word)) || words[0].startsWith('-')) {
297
+ return `MCP server ${name}: headersHelper must be a command and arguments separated by single spaces, without quotes, variables or shell operators`;
298
+ }
299
+ helper = { command: words[0], args: words.slice(1), cwd: workingDirectory };
300
+ if (!z.array(Argument).safeParse(helper.args).success)
301
+ return `MCP server ${name}: headersHelper arguments must not carry a credential`;
302
+ }
303
+ return { name, transport: 'http', url: server.url, headers: server.headers, helper, source };
304
+ }
160
305
  export function parseMcpDeclarationFile(input) {
161
306
  const parsed = ParseInput.safeParse(snapshotJson(input));
162
307
  if (!parsed.success)
163
308
  return refuse('MCP_DECLARATION_INVALID', 'MCP source must be bounded authenticated declaration bytes');
164
309
  const { bytes, source, workingDirectory } = parsed.data;
165
- if (createHash('sha256').update(bytes).digest('hex') !== source.sha256)
310
+ if (sha256Hex(bytes) !== source.sha256)
166
311
  return refuse('MCP_DECLARATION_INVALID', 'MCP source digest does not match declaration bytes');
167
312
  const value = tryParseJson(bytes);
168
313
  if (value === undefined)
169
314
  return refuse('MCP_DECLARATION_INVALID', 'MCP declaration is not valid JSON');
170
315
  const file = NativeFile.safeParse(value);
171
316
  if (!file.success)
172
- return refuse('MCP_DECLARATION_INVALID', 'MCP declaration contains unsupported command, environment or transport fields');
317
+ return refuse('MCP_DECLARATION_INVALID', 'MCP declaration must be one mcpServers object of at most 256 named servers');
173
318
  const declarations = [];
174
- for (const [name, server] of Object.entries(file.data.mcpServers)) {
319
+ for (const [name, entry] of Object.entries(file.data.mcpServers)) {
320
+ const type = typeof entry === 'object' && entry !== null && 'type' in entry ? entry.type : undefined;
321
+ if (type === 'sse') {
322
+ return refuse('MCP_DECLARATION_INVALID', `MCP server ${name} uses the SSE transport, which Codex cannot connect to; declare the server's streamable HTTP endpoint with type http`);
323
+ }
324
+ if (type === 'http') {
325
+ const remote = NativeHttpServer.safeParse(entry);
326
+ if (!remote.success)
327
+ return refuse('MCP_DECLARATION_INVALID', `MCP server ${name} is a remote server with fields other than type, url, headers and headersHelper`);
328
+ const declaration = parseHttpServer(name, remote.data, source, workingDirectory);
329
+ if (typeof declaration === 'string')
330
+ return refuse('MCP_DECLARATION_INVALID', declaration);
331
+ declarations.push(declaration);
332
+ continue;
333
+ }
334
+ const local = NativeServer.safeParse(entry);
335
+ if (!local.success)
336
+ return refuse('MCP_DECLARATION_INVALID', 'MCP declaration contains unsupported command, environment or transport fields');
337
+ const server = local.data;
175
338
  if (Object.entries(server.env).some(([key, reference]) => reference !== `\${${key}}`)) {
176
339
  return refuse('MCP_DECLARATION_INVALID', 'MCP environment entries must reference their same declared environment name');
177
340
  }