@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.
- package/CHANGELOG.md +99 -0
- package/README.md +3 -0
- package/cockpit.schema.json +73 -3
- package/dist/autonomy.d.ts +3 -2
- package/dist/autonomy.js +30 -13
- package/dist/browser/commands.d.ts +57 -2
- package/dist/browser/commands.js +114 -10
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +4 -1
- package/dist/browser/surfaces.d.ts +7 -7
- package/dist/browser/surfaces.js +5 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-main.d.ts +40 -3
- package/dist/cli-main.js +241 -62
- package/dist/command-table.d.ts +19 -11
- package/dist/command-table.js +190 -38
- package/dist/config/load.d.ts +16 -4
- package/dist/config/load.js +25 -3
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/diagnostics.js +10 -1
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.js +6 -2
- package/dist/environment/application/doctor.d.ts +12 -7
- package/dist/environment/application/doctor.js +20 -8
- package/dist/environment/application/guard.d.ts +12 -6
- package/dist/environment/application/guard.js +87 -32
- package/dist/environment/application/health.d.ts +14 -5
- package/dist/environment/application/health.js +8 -2
- package/dist/environment/application/lifecycle-context.d.ts +10 -3
- package/dist/environment/application/lifecycle-state.js +4 -3
- package/dist/environment/application/lifecycle.d.ts +2 -1
- package/dist/environment/application/lifecycle.js +11 -3
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +10 -10
- package/dist/environment/claims.js +3 -11
- package/dist/environment/compose-topology.d.ts +15 -7
- package/dist/environment/compose-topology.js +100 -23
- package/dist/environment/contract.d.ts +42 -9
- package/dist/environment/contract.js +33 -0
- package/dist/environment/executable.d.ts +12 -0
- package/dist/environment/executable.js +46 -23
- package/dist/environment/generated-application/check.js +2 -0
- package/dist/environment/generated-application/prepare.js +2 -0
- package/dist/environment/generated-application/shared.d.ts +10 -2
- package/dist/environment/generated-application/shared.js +6 -1
- package/dist/environment/generated-application/surface-outputs.js +23 -12
- package/dist/environment/generated-application/surfaces.js +16 -11
- package/dist/environment/generated-application/sync.js +19 -4
- package/dist/environment/git-observation.d.ts +25 -6
- package/dist/environment/git-observation.js +37 -28
- package/dist/environment/health-composition.d.ts +3 -2
- package/dist/environment/health-composition.js +19 -2
- package/dist/environment/init-application.d.ts +39 -14
- package/dist/environment/init-application.js +39 -10
- package/dist/environment/lifecycle-composition.d.ts +4 -2
- package/dist/environment/lifecycle-composition.js +84 -17
- package/dist/environment/lifecycle-stage.d.ts +8 -3
- package/dist/environment/lifecycle-stage.js +11 -1
- package/dist/environment/logs.d.ts +1 -1
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +1 -1
- package/dist/environment/observations.js +2 -0
- package/dist/environment/operator-application.d.ts +35 -6
- package/dist/environment/operator-application.js +42 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +8 -3
- package/dist/environment/read-composition.js +53 -8
- package/dist/environment/readiness-composition.d.ts +20 -1
- package/dist/environment/readiness-composition.js +23 -2
- package/dist/environment/reclaim.d.ts +56 -19
- package/dist/environment/reclaim.js +147 -42
- package/dist/environment/runtime.d.ts +64 -18
- package/dist/environment/runtime.js +73 -3
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +48 -9
- package/dist/environment/state.js +142 -2
- package/dist/environment/tool-registration.js +3 -7
- package/dist/environment-exec/execute.d.ts +6 -3
- package/dist/environment-exec/execute.js +4 -0
- package/dist/fix-catalogue.d.ts +1 -1
- package/dist/generated-environment/adopt.js +4 -8
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +20 -3
- package/dist/guards/evaluator.js +39 -7
- package/dist/guards/local-identity.d.ts +6 -2
- package/dist/guards/local-identity.js +10 -4
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +1 -1
- package/dist/guards/policy-pin.js +14 -12
- package/dist/init/command.d.ts +4 -3
- package/dist/init/command.js +36 -16
- package/dist/init/derive.d.ts +8 -6
- package/dist/init/derive.js +140 -36
- package/dist/init/draft.d.ts +13 -11
- package/dist/init/draft.js +54 -31
- package/dist/init/render.d.ts +18 -6
- package/dist/init/render.js +31 -9
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema.d.ts +42 -75
- package/dist/schema.js +62 -11
- package/dist/surfaces/harness-registry.d.ts +80 -0
- package/dist/surfaces/harness-registry.js +29 -0
- package/dist/surfaces/mcp.d.ts +67 -17
- package/dist/surfaces/mcp.js +194 -31
- package/dist/surfaces/projection.d.ts +7 -7
- package/dist/surfaces/projection.js +41 -6
- package/dist/surfaces/render.d.ts +5 -44
- package/dist/surfaces/render.js +5 -2
- package/dist/team-metadata.js +2 -1
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +4 -97
- package/dist/work-orders/application.js +94 -254
- package/dist/work-orders/composition.d.ts +35 -58
- package/dist/work-orders/composition.js +216 -24
- package/dist/work-orders/contract.d.ts +22 -6
- package/dist/work-orders/contract.js +34 -16
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +54 -5
- package/dist/work-orders/store.js +118 -24
- package/docs/README.md +6 -3
- package/docs/capability-matrix.md +17 -9
- package/docs/cli-reference.md +209 -1
- package/docs/command-list.md +12 -1
- package/docs/concepts.md +6 -4
- package/docs/configuration-essentials.md +7 -2
- package/docs/configuration-reference.md +18 -3
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +194 -6
- package/docs/faq.md +5 -3
- package/docs/getting-started.md +109 -25
- package/docs/harness-surfaces.md +69 -2
- package/docs/safety-and-autonomy.md +56 -12
- package/docs/troubleshooting.md +29 -8
- package/docs/tutorial.md +3 -2
- package/docs/work-and-browser.md +180 -14
- package/libexec/browser-operation.cjs +86 -2
- package/package.json +2 -2
- package/surfaces/skills/cockpit-browser.skill.md +7 -2
- package/surfaces/skills/cockpit-explain.skill.md +1 -1
- package/surfaces/skills/cockpit-fix.skill.md +1 -1
- package/surfaces/skills/cockpit-setup.skill.md +3 -3
- package/surfaces/skills/cockpit-work.skill.md +1 -1
- package/surfaces/skills/procedures/explain.md +9 -4
- package/surfaces/skills/procedures/fix.md +25 -11
- package/surfaces/skills/procedures/setup.md +113 -40
- 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));
|
package/dist/surfaces/mcp.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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<
|
|
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;
|
package/dist/surfaces/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
23
|
+
const Command = ShellToken.refine(value => !value.startsWith('-'));
|
|
24
|
+
const StdioDeclaration = z.object({
|
|
24
25
|
name: ServerName,
|
|
25
|
-
command:
|
|
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
|
-
|
|
49
|
-
|
|
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,
|
|
54
|
-
`executable=$(command -v ${quote(
|
|
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
|
-
+ `${
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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:
|
|
119
|
-
name: server.name,
|
|
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:
|
|
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 =
|
|
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,
|
|
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 (
|
|
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
|
|
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,
|
|
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
|
}
|