@oisincoveney/pipeline 1.16.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/config.d.ts +4 -4
- package/dist/config.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/index.js +6 -9
- package/dist/install-commands.js +8 -7
- package/dist/mcp/gateway.js +16 -20
- package/dist/pipeline-init.js +103 -2
- package/dist/runner.d.ts +1 -3
- package/dist/runner.js +5 -31
- package/dist/runtime/agent-node/agent-node.js +1 -1
- package/dist/schedule-planner.js +13 -1
- package/dist/toml.js +12 -0
- package/docs/config-architecture.md +1 -1
- package/docs/mcp-gateway.md +11 -15
- package/docs/mcp-host-isolation.md +8 -22
- package/docs/operator-guide.md +13 -10
- package/package.json +1 -1
- package/dist/mcp/launch-plan.js +0 -100
package/README.md
CHANGED
|
@@ -39,17 +39,16 @@ MCP is the Momokaya remote endpoint
|
|
|
39
39
|
|
|
40
40
|
The default GitHub MCP registration uses GitHub's official container in
|
|
41
41
|
read-only mode and reads `GITHUB_PERSONAL_ACCESS_TOKEN` from the environment.
|
|
42
|
-
The Momokaya
|
|
43
|
-
`
|
|
44
|
-
|
|
42
|
+
The Momokaya gateway endpoint is protected by Traefik HTTP Basic auth. Set
|
|
43
|
+
`PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP `Authorization` header
|
|
44
|
+
value before starting Codex or OpenCode:
|
|
45
45
|
|
|
46
46
|
```shell
|
|
47
|
-
export
|
|
47
|
+
export PIPELINE_MCP_GATEWAY_AUTHORIZATION="Basic $(printf '%s' 'user:password' | base64)"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
registration for that private endpoint.
|
|
50
|
+
`pipe init` writes project-level Codex and OpenCode config that points at the
|
|
51
|
+
singleton `pipeline-gateway` MCP server.
|
|
53
52
|
|
|
54
53
|
Check local prerequisites and config health:
|
|
55
54
|
|
package/dist/config.d.ts
CHANGED
|
@@ -223,8 +223,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
223
223
|
policy: z.ZodOptional<z.ZodObject<{
|
|
224
224
|
commands: z.ZodOptional<z.ZodEnum<{
|
|
225
225
|
allow: "allow";
|
|
226
|
-
deny: "deny";
|
|
227
226
|
"trusted-only": "trusted-only";
|
|
227
|
+
deny: "deny";
|
|
228
228
|
}>>;
|
|
229
229
|
modules: z.ZodOptional<z.ZodEnum<{
|
|
230
230
|
allow: "allow";
|
|
@@ -239,7 +239,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
239
239
|
local: "local";
|
|
240
240
|
}>;
|
|
241
241
|
provider: z.ZodLiteral<"toolhive">;
|
|
242
|
-
|
|
242
|
+
authorization_env: z.ZodDefault<z.ZodString>;
|
|
243
243
|
url: z.ZodOptional<z.ZodString>;
|
|
244
244
|
url_env: z.ZodDefault<z.ZodString>;
|
|
245
245
|
}, z.core.$strict>>;
|
|
@@ -279,10 +279,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
279
279
|
}, z.core.$strict>>;
|
|
280
280
|
output: z.ZodOptional<z.ZodObject<{
|
|
281
281
|
format: z.ZodEnum<{
|
|
282
|
-
json_schema: "json_schema";
|
|
283
282
|
text: "text";
|
|
284
283
|
json: "json";
|
|
285
284
|
jsonl: "jsonl";
|
|
285
|
+
json_schema: "json_schema";
|
|
286
286
|
}>;
|
|
287
287
|
repair: z.ZodOptional<z.ZodObject<{
|
|
288
288
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -326,10 +326,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
326
326
|
disabled: "disabled";
|
|
327
327
|
}>>>;
|
|
328
328
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
329
|
-
json_schema: "json_schema";
|
|
330
329
|
text: "text";
|
|
331
330
|
json: "json";
|
|
332
331
|
jsonl: "jsonl";
|
|
332
|
+
json_schema: "json_schema";
|
|
333
333
|
}>>>;
|
|
334
334
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
335
335
|
skills: z.ZodOptional<z.ZodBoolean>;
|
package/dist/config.js
CHANGED
|
@@ -132,7 +132,7 @@ const mcpGatewaySchema = z.object({
|
|
|
132
132
|
default_profile: z.string().min(1).optional(),
|
|
133
133
|
mode: z.enum(["hosted", "local"]),
|
|
134
134
|
provider: z.literal("toolhive"),
|
|
135
|
-
|
|
135
|
+
authorization_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_AUTHORIZATION"),
|
|
136
136
|
url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP gateway url must use http or https" }).optional(),
|
|
137
137
|
url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
|
|
138
138
|
}).strict();
|
package/dist/hooks.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
|
|
|
13
13
|
taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
14
14
|
}, z.core.$strict>>;
|
|
15
15
|
status: z.ZodEnum<{
|
|
16
|
-
fail: "fail";
|
|
17
16
|
pass: "pass";
|
|
17
|
+
fail: "fail";
|
|
18
18
|
skip: "skip";
|
|
19
19
|
}>;
|
|
20
20
|
summary: z.ZodOptional<z.ZodString>;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { PipelineConfigError, loadPipelineConfig, tryLoadPipelineConfig } from "./config.js";
|
|
3
|
+
import { configureGatewayHosts, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
|
|
3
4
|
import { compileWorkflowPlan } from "./workflow-planner.js";
|
|
4
5
|
import { formatInstallCommandsResult, installCommands, parseCommandHost } from "./install-commands.js";
|
|
5
|
-
import { configureGatewayHosts, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
|
|
6
6
|
import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "./runner.js";
|
|
7
7
|
import { formatConfigError, runPipelineFromConfig } from "./pipeline-runtime.js";
|
|
8
8
|
import { runKubernetesRunnerJob } from "./kubernetes-runner.js";
|
|
@@ -527,7 +527,6 @@ function formatWorkflowPlanNode(node, config, worktreePath) {
|
|
|
527
527
|
`kind=${node.kind}`,
|
|
528
528
|
`needs=${node.needs.join(",") || "none"}`,
|
|
529
529
|
launch ? `runner=${launch.runnerId}` : "",
|
|
530
|
-
launch ? `strategy=${launch.strategy}` : "",
|
|
531
530
|
node.gates?.length ? `gates=${node.gates.length}` : "gates=0",
|
|
532
531
|
node.artifacts?.length ? `artifacts=${node.artifacts.map((artifact) => artifact.path).join(",")}` : "artifacts=none"
|
|
533
532
|
].filter(Boolean).join(" ");
|
|
@@ -542,14 +541,12 @@ function resolveWorkflowSelection(config, workflowId, entrypointId) {
|
|
|
542
541
|
}
|
|
543
542
|
function formatOrchestratorPlan(config, worktreePath) {
|
|
544
543
|
const orchestrator = config.profiles[config.orchestrator.profile];
|
|
545
|
-
const launch = createOrchestratorLaunchPlan(config, {
|
|
546
|
-
nodeId: "orchestrator",
|
|
547
|
-
prompt: "<task>",
|
|
548
|
-
worktreePath
|
|
549
|
-
});
|
|
550
544
|
return [
|
|
551
|
-
`Orchestrator: runner=${
|
|
552
|
-
|
|
545
|
+
`Orchestrator: runner=${createOrchestratorLaunchPlan(config, {
|
|
546
|
+
nodeId: "orchestrator",
|
|
547
|
+
prompt: "<task>",
|
|
548
|
+
worktreePath
|
|
549
|
+
}).runnerId}`,
|
|
553
550
|
orchestrator.model ? `model=${orchestrator.model}` : "",
|
|
554
551
|
formatList("rules", orchestrator.rules),
|
|
555
552
|
formatList("skills", orchestrator.skills),
|
package/dist/install-commands.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveFileReference } from "./path-refs.js";
|
|
2
2
|
import { loadPipelineConfig } from "./config.js";
|
|
3
|
+
import { renderCodexGatewayConfig } from "./mcp/gateway.js";
|
|
3
4
|
import { compileWorkflowPlan } from "./workflow-planner.js";
|
|
4
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, join, relative } from "node:path";
|
|
@@ -173,6 +174,8 @@ function entrypointDispatchBlock(host, config, id, entrypoint) {
|
|
|
173
174
|
`Generate a schedule for entrypoint \`${id}\` and the user task.`,
|
|
174
175
|
`The schedule policy is \`${entrypoint.schedule}\`.`,
|
|
175
176
|
`Run \`pipe run --entrypoint ${id} <task description>\` to generate and execute the schedule artifact.`,
|
|
177
|
+
"The pipeline CLI runtime is the deterministic graph scheduler for scheduled entrypoints.",
|
|
178
|
+
"It launches configured Codex/OpenCode agent subprocesses as soon as their dependencies pass.",
|
|
176
179
|
"Use `pipe run --schedule <schedule.yaml>` only when rerunning an existing schedule artifact."
|
|
177
180
|
].join("\n");
|
|
178
181
|
}
|
|
@@ -358,7 +361,7 @@ function codexDefinitions(config, cwd) {
|
|
|
358
361
|
})),
|
|
359
362
|
projectAgentsMdDefinition(cwd, "codex"),
|
|
360
363
|
...nativeCodexProfiles.map(([id, profile]) => codexTomlAgentDefinition(config, cwd, id, profile)),
|
|
361
|
-
codexProjectConfigAgentDefinition()
|
|
364
|
+
codexProjectConfigAgentDefinition(config)
|
|
362
365
|
];
|
|
363
366
|
}
|
|
364
367
|
function projectAgentsMdDefinition(cwd, host) {
|
|
@@ -406,6 +409,7 @@ function codexTomlAgentDefinition(config, cwd, id, profile) {
|
|
|
406
409
|
};
|
|
407
410
|
const skillConfig = codexSkillConfig(config, cwd, profile);
|
|
408
411
|
const agentConfig = {
|
|
412
|
+
name: id,
|
|
409
413
|
description: profile.description ?? id,
|
|
410
414
|
...profileWithResolvedModel.model ? { model: profileWithResolvedModel.model } : {},
|
|
411
415
|
...profile.filesystem?.mode ? { sandbox_mode: profile.filesystem.mode } : {},
|
|
@@ -424,16 +428,12 @@ function codexTomlAgentDefinition(config, cwd, id, profile) {
|
|
|
424
428
|
].join("\n"),
|
|
425
429
|
skills: { config: skillConfig }
|
|
426
430
|
};
|
|
427
|
-
const mcpConfig = "mcp_servers" in agentConfig ? agentConfig : {
|
|
428
|
-
...agentConfig,
|
|
429
|
-
mcp_servers: {}
|
|
430
|
-
};
|
|
431
431
|
return {
|
|
432
432
|
content: [
|
|
433
433
|
"# Generated by @oisincoveney/pipeline.",
|
|
434
434
|
`${OWNER_YAML_MARKER_PREFIX}host=codex`,
|
|
435
435
|
"",
|
|
436
|
-
stringify(
|
|
436
|
+
stringify(agentConfig).trimEnd(),
|
|
437
437
|
""
|
|
438
438
|
].join("\n"),
|
|
439
439
|
host: "codex",
|
|
@@ -470,13 +470,14 @@ function codexSkillConfig(config, cwd, profile) {
|
|
|
470
470
|
}];
|
|
471
471
|
});
|
|
472
472
|
}
|
|
473
|
-
function codexProjectConfigAgentDefinition() {
|
|
473
|
+
function codexProjectConfigAgentDefinition(config) {
|
|
474
474
|
return {
|
|
475
475
|
block: {
|
|
476
476
|
end: CODEX_AGENT_CONFIG_END,
|
|
477
477
|
start: CODEX_AGENT_CONFIG_START
|
|
478
478
|
},
|
|
479
479
|
content: [
|
|
480
|
+
...config.mcp_gateway ? [renderCodexGatewayConfig(config).trimEnd(), ""] : [],
|
|
480
481
|
CODEX_AGENT_CONFIG_START,
|
|
481
482
|
stringify({ agents: { max_depth: 1 } }).trimEnd(),
|
|
482
483
|
CODEX_AGENT_CONFIG_END,
|
package/dist/mcp/gateway.js
CHANGED
|
@@ -5,8 +5,7 @@ import { execa } from "execa";
|
|
|
5
5
|
//#region src/mcp/gateway.ts
|
|
6
6
|
const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
|
|
7
7
|
const DEFAULT_LOCAL_GATEWAY_URL = "http://127.0.0.1:4483/mcp";
|
|
8
|
-
const
|
|
9
|
-
const LEGACY_CODEX_MCP_RE = /\[mcp_servers\./;
|
|
8
|
+
const LEGACY_CODEX_MCP_RE = /^\s*\[mcp_servers\.(?!pipeline-gateway(?:\]|\.env_http_headers\]))/m;
|
|
10
9
|
const LEGACY_OPENCODE_MCP_RE = /"mcp"\s*:\s*{(?!\s*"pipeline-gateway")/s;
|
|
11
10
|
const LEGACY_PIPELINE_MCP_RE = /path:\s*\.mcp\.json|uvx\s+mcpm|mcpm\s+run/;
|
|
12
11
|
var PipelineMcpGatewayError = class extends Error {
|
|
@@ -48,7 +47,7 @@ function renderGatewayConfig(config) {
|
|
|
48
47
|
`mode: ${gateway.mode}`,
|
|
49
48
|
gateway.url ? `url: ${gateway.url}` : "",
|
|
50
49
|
`url_env: ${gateway.url_env}`,
|
|
51
|
-
`
|
|
50
|
+
`authorization_env: ${gateway.authorization_env}`,
|
|
52
51
|
gateway.default_profile ? `default_profile: ${gateway.default_profile}` : "",
|
|
53
52
|
`resolved_url: ${gatewayUrl(gateway)}`
|
|
54
53
|
].filter(Boolean).join("\n");
|
|
@@ -57,10 +56,12 @@ function renderCodexGatewayConfig(config) {
|
|
|
57
56
|
const gateway = configuredGateway(config);
|
|
58
57
|
return [
|
|
59
58
|
"# Generated by @oisincoveney/pipeline.",
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
"",
|
|
60
|
+
`[mcp_servers.${PIPELINE_GATEWAY_SERVER_ID}]`,
|
|
61
|
+
`url = ${JSON.stringify(gatewayUrl(gateway))}`,
|
|
62
|
+
"",
|
|
63
|
+
`[mcp_servers.${PIPELINE_GATEWAY_SERVER_ID}.env_http_headers]`,
|
|
64
|
+
`Authorization = ${JSON.stringify(gateway.authorization_env)}`,
|
|
64
65
|
""
|
|
65
66
|
].join("\n");
|
|
66
67
|
}
|
|
@@ -164,23 +165,18 @@ function checkGatewayUrl(gateway) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
function checkGatewayToken(gateway) {
|
|
167
|
-
return process.env[gateway.
|
|
168
|
-
detail: gateway.
|
|
169
|
-
name: "gateway-
|
|
168
|
+
return process.env[gateway.authorization_env] ? {
|
|
169
|
+
detail: gateway.authorization_env,
|
|
170
|
+
name: "gateway-authorization",
|
|
170
171
|
passed: true
|
|
171
172
|
} : {
|
|
172
|
-
detail: `Set ${gateway.
|
|
173
|
-
name: "gateway-
|
|
173
|
+
detail: `Set ${gateway.authorization_env}.`,
|
|
174
|
+
name: "gateway-authorization",
|
|
174
175
|
passed: false
|
|
175
176
|
};
|
|
176
177
|
}
|
|
177
178
|
function gatewayAuthorizationHeader(gateway) {
|
|
178
|
-
return `
|
|
179
|
-
}
|
|
180
|
-
function resolveHeaderEnvPlaceholders(value, env = process.env) {
|
|
181
|
-
return value.replace(AUTHORIZATION_ENV_RE, (_match, envVar) => {
|
|
182
|
-
return env[envVar] || `{env:${envVar}}`;
|
|
183
|
-
});
|
|
179
|
+
return `{env:${gateway.authorization_env}}`;
|
|
184
180
|
}
|
|
185
181
|
function gatewayOpenCodeHeaders(gateway) {
|
|
186
182
|
return { Authorization: gatewayAuthorizationHeader(gateway) };
|
|
@@ -254,7 +250,7 @@ async function checkGatewayHealth(gateway) {
|
|
|
254
250
|
}
|
|
255
251
|
}
|
|
256
252
|
async function firstHealthyGatewayResponse(url, gateway) {
|
|
257
|
-
const authorization = process.env[gateway.
|
|
253
|
+
const authorization = process.env[gateway.authorization_env];
|
|
258
254
|
for (const healthUrl of gatewayHealthUrls(url)) {
|
|
259
255
|
const response = await fetch(healthUrl, {
|
|
260
256
|
headers: {
|
|
@@ -302,4 +298,4 @@ function legacyContentHit(cwd, path, pattern) {
|
|
|
302
298
|
return pattern.test(readFileSync(fullPath, "utf8")) ? path : void 0;
|
|
303
299
|
}
|
|
304
300
|
//#endregion
|
|
305
|
-
export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus,
|
|
301
|
+
export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, renderCodexGatewayConfig, renderGatewayConfig, runGatewayDoctor, startLocalGateway };
|
package/dist/pipeline-init.js
CHANGED
|
@@ -287,10 +287,11 @@ runners:
|
|
|
287
287
|
opencode:
|
|
288
288
|
type: opencode
|
|
289
289
|
command: opencode
|
|
290
|
+
model: opencode/deepseek-v4-flash-free
|
|
290
291
|
capabilities:
|
|
291
292
|
native_subagents: true
|
|
292
293
|
rules: true
|
|
293
|
-
skills:
|
|
294
|
+
skills: true
|
|
294
295
|
mcp_servers: true
|
|
295
296
|
tools: [read, list, grep, glob, bash, edit, write, task]
|
|
296
297
|
filesystem: [read-only, workspace-write]
|
|
@@ -353,7 +354,7 @@ mcp_gateway:
|
|
|
353
354
|
provider: toolhive
|
|
354
355
|
mode: local
|
|
355
356
|
url_env: PIPELINE_MCP_GATEWAY_URL
|
|
356
|
-
|
|
357
|
+
authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
|
|
357
358
|
default_profile: default
|
|
358
359
|
|
|
359
360
|
profiles:
|
|
@@ -563,6 +564,106 @@ profiles:
|
|
|
563
564
|
repair:
|
|
564
565
|
enabled: true
|
|
565
566
|
max_attempts: 1
|
|
567
|
+
pipeline-opencode-researcher:
|
|
568
|
+
runner: opencode
|
|
569
|
+
description: Research the requested task and produce structured findings with OpenCode.
|
|
570
|
+
instructions:
|
|
571
|
+
path: .pipeline/prompts/researcher.md
|
|
572
|
+
rules: [test-first]
|
|
573
|
+
skills: [research, spec, scope]
|
|
574
|
+
mcp_servers: [pipeline-gateway]
|
|
575
|
+
tools: [read, list, grep, glob, bash]
|
|
576
|
+
filesystem:
|
|
577
|
+
mode: read-only
|
|
578
|
+
allow: ["**/*"]
|
|
579
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
580
|
+
network:
|
|
581
|
+
mode: inherit
|
|
582
|
+
output:
|
|
583
|
+
format: json_schema
|
|
584
|
+
schema_path: .pipeline/schemas/research.schema.json
|
|
585
|
+
repair:
|
|
586
|
+
enabled: true
|
|
587
|
+
max_attempts: 1
|
|
588
|
+
pipeline-opencode-test-writer:
|
|
589
|
+
runner: opencode
|
|
590
|
+
description: Add focused failing tests for the requested behavior with OpenCode.
|
|
591
|
+
instructions:
|
|
592
|
+
path: .pipeline/prompts/test-writer.md
|
|
593
|
+
rules: [test-first]
|
|
594
|
+
skills: [test]
|
|
595
|
+
mcp_servers: [pipeline-gateway]
|
|
596
|
+
tools: [read, list, grep, glob, bash, edit, write]
|
|
597
|
+
filesystem:
|
|
598
|
+
mode: workspace-write
|
|
599
|
+
allow: ["**/*"]
|
|
600
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
601
|
+
network:
|
|
602
|
+
mode: inherit
|
|
603
|
+
output:
|
|
604
|
+
format: text
|
|
605
|
+
pipeline-opencode-code-writer:
|
|
606
|
+
runner: opencode
|
|
607
|
+
scheduling_roles: [implementation]
|
|
608
|
+
description: Implement production code until the failing tests pass with OpenCode.
|
|
609
|
+
instructions:
|
|
610
|
+
path: .pipeline/prompts/code-writer.md
|
|
611
|
+
rules: [test-first]
|
|
612
|
+
skills: [trace, test, fix, library-first-development]
|
|
613
|
+
mcp_servers: [pipeline-gateway]
|
|
614
|
+
tools: [read, list, grep, glob, bash, edit, write]
|
|
615
|
+
filesystem:
|
|
616
|
+
mode: workspace-write
|
|
617
|
+
allow: ["**/*"]
|
|
618
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
619
|
+
network:
|
|
620
|
+
mode: inherit
|
|
621
|
+
output:
|
|
622
|
+
format: text
|
|
623
|
+
pipeline-opencode-acceptance-reviewer:
|
|
624
|
+
runner: opencode
|
|
625
|
+
scheduling_roles: [coverage]
|
|
626
|
+
description: Audit the finished change against every acceptance criterion with OpenCode.
|
|
627
|
+
instructions:
|
|
628
|
+
path: .pipeline/prompts/acceptance-reviewer.md
|
|
629
|
+
rules: [verification]
|
|
630
|
+
skills: [critique, doubt]
|
|
631
|
+
mcp_servers: [pipeline-gateway]
|
|
632
|
+
tools: [read, list, grep, glob, bash]
|
|
633
|
+
filesystem:
|
|
634
|
+
mode: read-only
|
|
635
|
+
allow: ["**/*"]
|
|
636
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
637
|
+
network:
|
|
638
|
+
mode: inherit
|
|
639
|
+
output:
|
|
640
|
+
format: json_schema
|
|
641
|
+
schema_path: .pipeline/schemas/acceptance.schema.json
|
|
642
|
+
repair:
|
|
643
|
+
enabled: true
|
|
644
|
+
max_attempts: 1
|
|
645
|
+
pipeline-opencode-verifier:
|
|
646
|
+
runner: opencode
|
|
647
|
+
scheduling_roles: [coverage]
|
|
648
|
+
description: Verify checks, implementation fit, and final evidence with OpenCode.
|
|
649
|
+
instructions:
|
|
650
|
+
path: .pipeline/prompts/verifier.md
|
|
651
|
+
rules: [verification]
|
|
652
|
+
skills: [verify, critique, secure, optimize]
|
|
653
|
+
mcp_servers: [pipeline-gateway]
|
|
654
|
+
tools: [read, list, grep, glob, bash]
|
|
655
|
+
filesystem:
|
|
656
|
+
mode: read-only
|
|
657
|
+
allow: ["**/*"]
|
|
658
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
659
|
+
network:
|
|
660
|
+
mode: inherit
|
|
661
|
+
output:
|
|
662
|
+
format: json_schema
|
|
663
|
+
schema_path: .pipeline/schemas/verify.schema.json
|
|
664
|
+
repair:
|
|
665
|
+
enabled: true
|
|
666
|
+
max_attempts: 1
|
|
566
667
|
`;
|
|
567
668
|
const VERDICT_SCHEMA = z.enum(["PASS", "FAIL"]);
|
|
568
669
|
const STRING_ARRAY_SCHEMA = z.array(z.string());
|
package/dist/runner.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ interface AgentRunRequest {
|
|
|
25
25
|
interface AgentAdapter {
|
|
26
26
|
run(request: AgentRunRequest): Promise<AgentResult>;
|
|
27
27
|
}
|
|
28
|
-
type RunnerStrategy = "native" | "subprocess";
|
|
29
28
|
interface RunnerLaunchPlan {
|
|
30
29
|
args: string[];
|
|
31
30
|
command: string;
|
|
@@ -35,7 +34,6 @@ interface RunnerLaunchPlan {
|
|
|
35
34
|
outputFormat: string;
|
|
36
35
|
profileId?: string;
|
|
37
36
|
runnerId: string;
|
|
38
|
-
strategy: RunnerStrategy;
|
|
39
37
|
timeoutMs: number;
|
|
40
38
|
type: RunnerType;
|
|
41
39
|
}
|
|
@@ -65,4 +63,4 @@ declare function spawnAgent(harness: Harness, role: AgentRole, prompt: string, c
|
|
|
65
63
|
*/
|
|
66
64
|
declare const subprocessAgentAdapter: AgentAdapter;
|
|
67
65
|
//#endregion
|
|
68
|
-
export { AgentAdapter, AgentResult, AgentRole, AgentRunRequest, Harness, RunnerCapabilityError, RunnerExecutionOptions, RunnerLaunchInput, RunnerLaunchPlan,
|
|
66
|
+
export { AgentAdapter, AgentResult, AgentRole, AgentRunRequest, Harness, RunnerCapabilityError, RunnerExecutionOptions, RunnerLaunchInput, RunnerLaunchPlan, createOrchestratorLaunchPlan, createRunnerLaunchPlan, hardAgentAdapter, runLaunchPlan, spawnAgent, subprocessAgentAdapter };
|
package/dist/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveFileReference } from "./path-refs.js";
|
|
2
|
-
import {
|
|
2
|
+
import { tomlValue } from "./toml.js";
|
|
3
3
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { execa } from "execa";
|
|
@@ -41,13 +41,6 @@ function optionalModelArgs(harness, runner, actor) {
|
|
|
41
41
|
* Per-harness argv shape, excluding the leading harness binary name.
|
|
42
42
|
*/
|
|
43
43
|
function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
|
|
44
|
-
const mcp = buildMcpLaunchPlan({
|
|
45
|
-
actor: options.actor,
|
|
46
|
-
config: options.config,
|
|
47
|
-
nodeId: options.nodeId ?? "agent",
|
|
48
|
-
runnerType: harness,
|
|
49
|
-
worktreePath
|
|
50
|
-
});
|
|
51
44
|
const skillArgs = skillArgsFor(harness, options.config, options.actor, worktreePath);
|
|
52
45
|
switch (harness) {
|
|
53
46
|
case "codex": return [
|
|
@@ -56,7 +49,7 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
|
|
|
56
49
|
"-C",
|
|
57
50
|
worktreePath,
|
|
58
51
|
...optionalModelArgs(harness, options.runner, options.actor),
|
|
59
|
-
...
|
|
52
|
+
...options.config ? ["--ignore-user-config"] : [],
|
|
60
53
|
...skillArgs,
|
|
61
54
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
62
55
|
"--skip-git-repo-check",
|
|
@@ -67,7 +60,6 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
|
|
|
67
60
|
"--format",
|
|
68
61
|
"json",
|
|
69
62
|
...optionalModelArgs(harness, options.runner, options.actor),
|
|
70
|
-
...mcp.args,
|
|
71
63
|
...skillArgs,
|
|
72
64
|
"--dangerously-skip-permissions",
|
|
73
65
|
"--dir",
|
|
@@ -80,7 +72,6 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
|
|
|
80
72
|
"--format",
|
|
81
73
|
"json",
|
|
82
74
|
...optionalModelArgs(harness, options.runner, options.actor),
|
|
83
|
-
...mcp.args,
|
|
84
75
|
...skillArgs,
|
|
85
76
|
"--dangerously-skip-permissions",
|
|
86
77
|
"--dir",
|
|
@@ -145,16 +136,9 @@ function createActorLaunchPlan(config, input, actor, runnerId) {
|
|
|
145
136
|
if (runner.capabilities.output_formats && !runner.capabilities.output_formats.includes(outputFormat)) throw new RunnerCapabilityError(`runner '${runnerId}' does not support output format '${outputFormat}'`);
|
|
146
137
|
const command = runner.command ?? runner.type;
|
|
147
138
|
const timeoutMs = Number(process.env.PIPELINE_AGENT_TIMEOUT_MS ?? 3e5);
|
|
148
|
-
const env = buildMcpLaunchPlan({
|
|
149
|
-
actor,
|
|
150
|
-
config,
|
|
151
|
-
nodeId: input.nodeId,
|
|
152
|
-
runnerType: runner.type,
|
|
153
|
-
worktreePath: input.worktreePath
|
|
154
|
-
}).env;
|
|
155
139
|
const base = {
|
|
156
140
|
cwd: input.worktreePath,
|
|
157
|
-
env,
|
|
141
|
+
env: {},
|
|
158
142
|
nodeId: input.nodeId,
|
|
159
143
|
outputFormat,
|
|
160
144
|
profileId: input.profileId,
|
|
@@ -167,11 +151,9 @@ function createActorLaunchPlan(config, input, actor, runnerId) {
|
|
|
167
151
|
return {
|
|
168
152
|
...base,
|
|
169
153
|
args: renderArgv(runner.args ?? [], input.prompt, input.worktreePath),
|
|
170
|
-
command
|
|
171
|
-
strategy: "subprocess"
|
|
154
|
+
command
|
|
172
155
|
};
|
|
173
156
|
}
|
|
174
|
-
const strategy = nativeStrategy(config, input, runnerId);
|
|
175
157
|
return {
|
|
176
158
|
...base,
|
|
177
159
|
args: harnessArgv(runner.type, input.prompt, input.worktreePath, input.contextFile ?? null, {
|
|
@@ -180,8 +162,7 @@ function createActorLaunchPlan(config, input, actor, runnerId) {
|
|
|
180
162
|
nodeId: input.nodeId,
|
|
181
163
|
runner
|
|
182
164
|
}),
|
|
183
|
-
command
|
|
184
|
-
strategy
|
|
165
|
+
command
|
|
185
166
|
};
|
|
186
167
|
}
|
|
187
168
|
function skillArgsFor(runnerType, config, actor, worktreePath) {
|
|
@@ -199,13 +180,6 @@ function skillArgsFor(runnerType, config, actor, worktreePath) {
|
|
|
199
180
|
})))}`];
|
|
200
181
|
return [];
|
|
201
182
|
}
|
|
202
|
-
function nativeStrategy(config, input, runnerId) {
|
|
203
|
-
const runner = config.runners[runnerId];
|
|
204
|
-
const profile = input.profileId ? config.profiles[input.profileId] : void 0;
|
|
205
|
-
if (!(runner?.capabilities.native_subagents && profile)) return "subprocess";
|
|
206
|
-
if (runner.type === "command") return "subprocess";
|
|
207
|
-
return "native";
|
|
208
|
-
}
|
|
209
183
|
function renderArgv(args, prompt, cwd) {
|
|
210
184
|
return args.map((arg) => arg.replaceAll("{{prompt}}", prompt).replaceAll("{{cwd}}", cwd));
|
|
211
185
|
}
|
|
@@ -36,7 +36,7 @@ async function executeAgentNode(node, context, attempt) {
|
|
|
36
36
|
});
|
|
37
37
|
return {
|
|
38
38
|
evidence: [
|
|
39
|
-
`agent boundary node=${node.id} profile=${node.profile} runner=${plan.runnerId}
|
|
39
|
+
`agent boundary node=${node.id} profile=${node.profile} runner=${plan.runnerId}`,
|
|
40
40
|
...finalized.evidence,
|
|
41
41
|
...result.stderr ? [`stderr: ${result.stderr}`] : [],
|
|
42
42
|
...result.timedOut ? ["agent timed out"] : []
|
package/dist/schedule-planner.js
CHANGED
|
@@ -404,8 +404,20 @@ function plannerPrompt(entrypointId, task, baseline, config, planningContext) {
|
|
|
404
404
|
].join("\n");
|
|
405
405
|
}
|
|
406
406
|
function allowedProfilePromptLine(config, id) {
|
|
407
|
+
const profile = config.profiles[id];
|
|
408
|
+
const runner = config.runners[profile.runner];
|
|
407
409
|
const roles = effectiveSchedulingRoles(config, id);
|
|
408
|
-
|
|
410
|
+
const model = profile.model ?? runner?.model;
|
|
411
|
+
return `- ${id} (${[
|
|
412
|
+
`runner: ${profile.runner}`,
|
|
413
|
+
model ? `model: ${model}` : "",
|
|
414
|
+
roles.length > 0 ? `scheduling_roles: ${roles.join(", ")}` : "",
|
|
415
|
+
profile.description ? `description: ${profile.description}` : "",
|
|
416
|
+
profile.tools?.length ? `tools: ${profile.tools.join(", ")}` : "",
|
|
417
|
+
profile.filesystem?.mode ? `filesystem: ${profile.filesystem.mode}` : "",
|
|
418
|
+
profile.network?.mode ? `network: ${profile.network.mode}` : "",
|
|
419
|
+
`output: ${profile.output?.format ?? "text"}`
|
|
420
|
+
].filter(Boolean).join("; ")})`;
|
|
409
421
|
}
|
|
410
422
|
function effectiveSchedulingRoles(config, profileId) {
|
|
411
423
|
return [...new Set(config.profiles[profileId]?.scheduling_roles ?? [])].sort();
|
package/dist/toml.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/toml.ts
|
|
2
|
+
const TOML_BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
3
|
+
function tomlValue(value) {
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
5
|
+
if (value && typeof value === "object") return `{ ${Object.entries(value).map(([key, item]) => `${tomlKey(key)} = ${tomlValue(item)}`).join(", ")} }`;
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
}
|
|
8
|
+
function tomlKey(key) {
|
|
9
|
+
return TOML_BARE_KEY_PATTERN.test(key) ? key : JSON.stringify(key);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { tomlValue };
|
|
@@ -152,7 +152,7 @@ mcp_gateway:
|
|
|
152
152
|
mode: hosted
|
|
153
153
|
url: https://pipeline-mcp.momokaya.ee/mcp/
|
|
154
154
|
url_env: PIPELINE_MCP_GATEWAY_URL
|
|
155
|
-
|
|
155
|
+
authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
|
|
156
156
|
default_profile: default
|
|
157
157
|
|
|
158
158
|
profiles:
|
package/docs/mcp-gateway.md
CHANGED
|
@@ -11,7 +11,7 @@ Playwright, Qdrant, or Neon.
|
|
|
11
11
|
```text
|
|
12
12
|
Codex/OpenCode
|
|
13
13
|
|
|
|
14
|
-
|
|
|
14
|
+
| project-level MCP config
|
|
15
15
|
v
|
|
16
16
|
pipeline MCP gateway
|
|
17
17
|
|
|
|
@@ -21,9 +21,9 @@ Backlog / GitHub / Serena / Context7 / Playwright / Qdrant / Neon
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
The gateway owns long-lived upstream connections and process lifecycle. The
|
|
24
|
-
pipeline owns the host projection:
|
|
25
|
-
`pipeline-gateway`
|
|
26
|
-
|
|
24
|
+
pipeline owns the host projection: project host config declares only the
|
|
25
|
+
singleton `pipeline-gateway` remote MCP server. Agents inherit that project
|
|
26
|
+
config instead of receiving profile-scoped native MCP config.
|
|
27
27
|
|
|
28
28
|
## Folder Boundary
|
|
29
29
|
|
|
@@ -31,8 +31,6 @@ MCP-specific code belongs in `src/mcp`:
|
|
|
31
31
|
|
|
32
32
|
- `gateway.ts`: hosted/local gateway config, diagnostics, and host config
|
|
33
33
|
rewrites.
|
|
34
|
-
- `launch-plan.ts`: runtime host projection for Codex and OpenCode.
|
|
35
|
-
- `native-config.ts`: generated native Codex agent MCP config.
|
|
36
34
|
|
|
37
35
|
The rest of the runtime should consume those functions instead of hand-rendering
|
|
38
36
|
host-specific MCP config.
|
|
@@ -42,7 +40,8 @@ host-specific MCP config.
|
|
|
42
40
|
1. Run or deploy a gateway that exposes one remote MCP endpoint.
|
|
43
41
|
2. Configure the gateway with upstream servers and credentials.
|
|
44
42
|
3. Configure `mcp_gateway` in `.pipeline/profiles.yaml`.
|
|
45
|
-
4.
|
|
43
|
+
4. Run `pipe mcp gateway configure-host` to write the project Codex/OpenCode
|
|
44
|
+
host config.
|
|
46
45
|
5. Keep high-risk upstream capabilities controlled by gateway-side policy, not
|
|
47
46
|
by asking every agent host to independently start or filter servers.
|
|
48
47
|
|
|
@@ -54,7 +53,7 @@ mcp_gateway:
|
|
|
54
53
|
mode: hosted
|
|
55
54
|
url: https://pipeline-mcp.momokaya.ee/mcp/
|
|
56
55
|
url_env: PIPELINE_MCP_GATEWAY_URL
|
|
57
|
-
|
|
56
|
+
authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
|
|
58
57
|
default_profile: default
|
|
59
58
|
|
|
60
59
|
profiles:
|
|
@@ -73,10 +72,8 @@ orchestrator MCP set * subagent count * host config layers
|
|
|
73
72
|
```
|
|
74
73
|
|
|
75
74
|
With a gateway, the runtime launches zero local upstream MCP processes for
|
|
76
|
-
agents. Codex
|
|
77
|
-
`
|
|
78
|
-
XDG/config roots and receives inline `OPENCODE_CONFIG_CONTENT` containing only
|
|
79
|
-
`pipeline-gateway`.
|
|
75
|
+
agents. Codex and OpenCode read the same project-level host config, which
|
|
76
|
+
contains only `pipeline-gateway`.
|
|
80
77
|
|
|
81
78
|
## Candidate Gateway Implementations
|
|
82
79
|
|
|
@@ -87,6 +84,5 @@ Use an off-the-shelf aggregator when possible:
|
|
|
87
84
|
Use `pipe mcp gateway doctor` to check required environment variables, gateway
|
|
88
85
|
health, local ToolHive availability for local mode, and legacy direct MCP
|
|
89
86
|
entries. Use `pipe mcp gateway configure-host` to rewrite project or global
|
|
90
|
-
host config with a backup. For Codex, this removes
|
|
91
|
-
|
|
92
|
-
with `--config` only for pipeline-launched agents.
|
|
87
|
+
host config with a backup. For Codex and OpenCode, this removes direct
|
|
88
|
+
upstream MCP entries and writes the singleton `pipeline-gateway` remote entry.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# MCP Host Isolation
|
|
2
2
|
|
|
3
|
-
`oisin-pipeline` treats
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`oisin-pipeline` treats project host config as the MCP client boundary. The
|
|
4
|
+
generated Codex and OpenCode project configs declare exactly one remote MCP
|
|
5
|
+
server: `pipeline-gateway`.
|
|
6
6
|
|
|
7
7
|
## Codex
|
|
8
8
|
|
|
9
9
|
Codex `exec` supports `--ignore-user-config`, which skips
|
|
10
10
|
`$CODEX_HOME/config.toml` while continuing to use Codex auth. Pipeline-managed
|
|
11
|
-
Codex launches use that flag and
|
|
12
|
-
|
|
11
|
+
Codex launches use that flag and rely on the project `.codex/config.toml`
|
|
12
|
+
gateway entry.
|
|
13
13
|
|
|
14
14
|
This prevents user-config MCP fan-out for pipeline-launched Codex agents. It
|
|
15
15
|
does not claim to suppress every possible system, managed, plugin, or trusted
|
|
@@ -19,23 +19,9 @@ Reference: https://developers.openai.com/codex/config-basic
|
|
|
19
19
|
|
|
20
20
|
## OpenCode
|
|
21
21
|
|
|
22
|
-
OpenCode
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`XDG_STATE_HOME`, `XDG_CACHE_HOME`, and `OPENCODE_TEST_HOME` inside it, disable
|
|
26
|
-
project config with `OPENCODE_DISABLE_PROJECT_CONFIG=1`, and pass the
|
|
27
|
-
`pipeline-gateway` remote server through `OPENCODE_CONFIG_CONTENT`.
|
|
28
|
-
|
|
29
|
-
The generated inline config contains only `pipeline-gateway`. Direct upstream
|
|
30
|
-
MCP ids are omitted rather than rendered as disabled entries, because enabled
|
|
31
|
-
MCP servers can be started during OpenCode MCP service initialization before
|
|
32
|
-
per-message tool filtering.
|
|
33
|
-
|
|
34
|
-
Strict isolation means pipeline-managed OpenCode launches do not read the
|
|
35
|
-
user's normal OpenCode account or MCP auth files. Provider credentials should be
|
|
36
|
-
available through provider environment variables. Admin-managed OpenCode config
|
|
37
|
-
outside the user/project config layers may still require container isolation or
|
|
38
|
-
upstream host support.
|
|
22
|
+
OpenCode project config is generated at `.opencode/opencode.json` with only the
|
|
23
|
+
`pipeline-gateway` remote MCP server. Runtime launches do not set inline MCP
|
|
24
|
+
config environment variables; agents inherit the project-level config.
|
|
39
25
|
|
|
40
26
|
References:
|
|
41
27
|
- https://opencode.ai/docs/config/
|
package/docs/operator-guide.md
CHANGED
|
@@ -380,7 +380,7 @@ mcp_gateway:
|
|
|
380
380
|
mode: hosted
|
|
381
381
|
url: https://pipeline-mcp.momokaya.ee/mcp/
|
|
382
382
|
url_env: PIPELINE_MCP_GATEWAY_URL
|
|
383
|
-
|
|
383
|
+
authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
|
|
384
384
|
default_profile: default
|
|
385
385
|
```
|
|
386
386
|
|
|
@@ -413,17 +413,17 @@ pipe mcp gateway local-status
|
|
|
413
413
|
pipe mcp gateway local-start
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
-
`configure-host` removes
|
|
417
|
-
|
|
418
|
-
launch arguments so ordinary Codex sessions do not start pipeline MCP clients at
|
|
419
|
-
startup:
|
|
416
|
+
`configure-host` removes direct upstream MCP config and writes the singleton
|
|
417
|
+
gateway server into project host config. Codex receives:
|
|
420
418
|
|
|
421
419
|
```toml
|
|
422
420
|
# Generated by @oisincoveney/pipeline.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
421
|
+
|
|
422
|
+
[mcp_servers.pipeline-gateway]
|
|
423
|
+
url = "https://gateway.example/mcp"
|
|
424
|
+
|
|
425
|
+
[mcp_servers.pipeline-gateway.env_http_headers]
|
|
426
|
+
Authorization = "PIPELINE_MCP_GATEWAY_AUTHORIZATION"
|
|
427
427
|
```
|
|
428
428
|
|
|
429
429
|
OpenCode receives:
|
|
@@ -433,6 +433,9 @@ OpenCode receives:
|
|
|
433
433
|
"mcp": {
|
|
434
434
|
"pipeline-gateway": {
|
|
435
435
|
"type": "remote",
|
|
436
|
+
"headers": {
|
|
437
|
+
"Authorization": "{env:PIPELINE_MCP_GATEWAY_AUTHORIZATION}"
|
|
438
|
+
},
|
|
436
439
|
"url": "https://gateway.example/mcp"
|
|
437
440
|
}
|
|
438
441
|
}
|
|
@@ -529,7 +532,7 @@ mcp_gateway:
|
|
|
529
532
|
mode: hosted
|
|
530
533
|
url: https://pipeline-mcp.momokaya.ee/mcp/
|
|
531
534
|
url_env: PIPELINE_MCP_GATEWAY_URL
|
|
532
|
-
|
|
535
|
+
authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
|
|
533
536
|
|
|
534
537
|
profiles:
|
|
535
538
|
pipeline-router:
|
package/package.json
CHANGED
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"prepack": "bun run build:cli"
|
|
100
100
|
},
|
|
101
101
|
"type": "module",
|
|
102
|
-
"version": "1.
|
|
102
|
+
"version": "1.17.0",
|
|
103
103
|
"description": "Config-driven multi-agent pipeline runner for repository work",
|
|
104
104
|
"main": "./dist/index.js",
|
|
105
105
|
"types": "./dist/index.d.ts",
|
package/dist/mcp/launch-plan.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { gatewayServerForProfile, resolveHeaderEnvPlaceholders } from "./gateway.js";
|
|
2
|
-
import { mkdtempSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
//#region src/mcp/launch-plan.ts
|
|
6
|
-
const TOML_BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
7
|
-
function buildMcpLaunchPlan(input) {
|
|
8
|
-
const selectedServers = selectedMcpServers(input.config, input.actor);
|
|
9
|
-
return {
|
|
10
|
-
args: mcpArgsFor(input.runnerType, selectedServers, Boolean(input.config)),
|
|
11
|
-
env: mcpEnvFor(input, selectedServers),
|
|
12
|
-
selectedServers
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
function selectedMcpServers(config, actor) {
|
|
16
|
-
return gatewayServerForProfile(config, actor);
|
|
17
|
-
}
|
|
18
|
-
function mcpArgsFor(runnerType, servers, hasPipelineConfig) {
|
|
19
|
-
if (runnerType === "codex") return [...hasPipelineConfig ? ["--ignore-user-config"] : [], ...codexMcpArgs(servers)];
|
|
20
|
-
if (Object.keys(servers).length === 0) return [];
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
function mcpEnvFor(input, servers) {
|
|
24
|
-
if (input.runnerType !== "opencode" || !input.config) return {};
|
|
25
|
-
const config = toOpenCodeMcpConfig(servers);
|
|
26
|
-
const dir = mkdtempSync(join(tmpdir(), "pipeline-opencode-runtime-"));
|
|
27
|
-
return {
|
|
28
|
-
OPENCODE_AUTH_CONTENT: void 0,
|
|
29
|
-
OPENCODE_CONFIG: void 0,
|
|
30
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
31
|
-
OPENCODE_CONFIG_DIR: void 0,
|
|
32
|
-
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
33
|
-
OPENCODE_TEST_HOME: join(dir, "home"),
|
|
34
|
-
PIPELINE_OPENCODE_RUNTIME_DIR: dir,
|
|
35
|
-
PIPELINE_WORKTREE: input.worktreePath,
|
|
36
|
-
XDG_CACHE_HOME: join(dir, "cache"),
|
|
37
|
-
XDG_CONFIG_HOME: join(dir, "config"),
|
|
38
|
-
XDG_DATA_HOME: join(dir, "data"),
|
|
39
|
-
XDG_STATE_HOME: join(dir, "state")
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function isRemoteMcpServer(server) {
|
|
43
|
-
return typeof server.url === "string";
|
|
44
|
-
}
|
|
45
|
-
function headersWithBearerTokenEnv(server, renderTokenRef) {
|
|
46
|
-
const headers = { ...server.headers ?? {} };
|
|
47
|
-
if (server.bearer_token_env_var) headers.Authorization = `Bearer ${renderTokenRef(server.bearer_token_env_var)}`;
|
|
48
|
-
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
49
|
-
}
|
|
50
|
-
function toOpenCodeMcpConfig(selectedServers) {
|
|
51
|
-
return {
|
|
52
|
-
$schema: "https://opencode.ai/config.json",
|
|
53
|
-
mcp: { ...Object.fromEntries(Object.entries(selectedServers).map(([id, server]) => {
|
|
54
|
-
if (isRemoteMcpServer(server)) {
|
|
55
|
-
const headers = headersWithBearerTokenEnv(server, (envVar) => `{env:${envVar}}`);
|
|
56
|
-
return [id, {
|
|
57
|
-
enabled: true,
|
|
58
|
-
...headers ? { headers } : {},
|
|
59
|
-
type: "remote",
|
|
60
|
-
url: server.url
|
|
61
|
-
}];
|
|
62
|
-
}
|
|
63
|
-
return [id, {
|
|
64
|
-
command: [server.command, ...server.args ?? []],
|
|
65
|
-
enabled: true,
|
|
66
|
-
...server.env ? { environment: server.env } : {},
|
|
67
|
-
type: "local"
|
|
68
|
-
}];
|
|
69
|
-
})) }
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function codexMcpArgs(servers) {
|
|
73
|
-
return Object.entries(servers).flatMap(([id, server]) => {
|
|
74
|
-
if (isRemoteMcpServer(server)) return [
|
|
75
|
-
"--config",
|
|
76
|
-
`mcp_servers.${id}.url=${tomlValue(server.url)}`,
|
|
77
|
-
...server.headers ? ["--config", `mcp_servers.${id}.http_headers=${tomlValue(codexHttpHeaders(server.headers))}`] : [],
|
|
78
|
-
...server.bearer_token_env_var ? ["--config", `mcp_servers.${id}.bearer_token_env_var=${tomlValue(server.bearer_token_env_var)}`] : []
|
|
79
|
-
];
|
|
80
|
-
return [
|
|
81
|
-
"--config",
|
|
82
|
-
`mcp_servers.${id}.command=${tomlValue(server.command)}`,
|
|
83
|
-
...server.args ? ["--config", `mcp_servers.${id}.args=${tomlValue(server.args)}`] : [],
|
|
84
|
-
...server.env ? ["--config", `mcp_servers.${id}.env=${tomlValue(server.env)}`] : []
|
|
85
|
-
];
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
function codexHttpHeaders(headers) {
|
|
89
|
-
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, resolveHeaderEnvPlaceholders(value)]));
|
|
90
|
-
}
|
|
91
|
-
function tomlValue(value) {
|
|
92
|
-
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
93
|
-
if (value && typeof value === "object") return `{ ${Object.entries(value).map(([key, item]) => `${tomlKey(key)} = ${tomlValue(item)}`).join(", ")} }`;
|
|
94
|
-
return JSON.stringify(value);
|
|
95
|
-
}
|
|
96
|
-
function tomlKey(key) {
|
|
97
|
-
return TOML_BARE_KEY_PATTERN.test(key) ? key : JSON.stringify(key);
|
|
98
|
-
}
|
|
99
|
-
//#endregion
|
|
100
|
-
export { buildMcpLaunchPlan, tomlValue };
|