@oisincoveney/pipeline 1.16.1 → 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 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 Qdrant endpoint is protected by Traefik HTTP Basic auth. Set
43
- `MEMORY_MCP_BASIC_AUTH` to the base64 `user:password` payload before running
44
- `pipe init` if you want init to register that remote server with MCPM:
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 MEMORY_MCP_BASIC_AUTH="$(printf '%s' 'user:password' | base64)"
47
+ export PIPELINE_MCP_GATEWAY_AUTHORIZATION="Basic $(printf '%s' 'user:password' | base64)"
48
48
  ```
49
49
 
50
- When `MEMORY_MCP_BASIC_AUTH` is not set, `pipe init` still writes the default
51
- scaffold and keeps the generated `qdrant` MCP entry, but skips immediate MCPM
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
@@ -239,7 +239,8 @@ declare const configSchema: z.ZodObject<{
239
239
  local: "local";
240
240
  }>;
241
241
  provider: z.ZodLiteral<"toolhive">;
242
- token_env: z.ZodDefault<z.ZodString>;
242
+ authorization_env: z.ZodDefault<z.ZodString>;
243
+ url: z.ZodOptional<z.ZodString>;
243
244
  url_env: z.ZodDefault<z.ZodString>;
244
245
  }, z.core.$strict>>;
245
246
  mcp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
package/dist/config.js CHANGED
@@ -132,7 +132,8 @@ 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
- token_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_TOKEN"),
135
+ authorization_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_AUTHORIZATION"),
136
+ url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP gateway url must use http or https" }).optional(),
136
137
  url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
137
138
  }).strict();
138
139
  const instructionsSchema = z.object({
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=${launch.runnerId}`,
552
- `strategy=${launch.strategy}`,
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),
@@ -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(mcpConfig).trimEnd(),
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,
@@ -5,7 +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 LEGACY_CODEX_MCP_RE = /\[mcp_servers\./;
8
+ const LEGACY_CODEX_MCP_RE = /^\s*\[mcp_servers\.(?!pipeline-gateway(?:\]|\.env_http_headers\]))/m;
9
9
  const LEGACY_OPENCODE_MCP_RE = /"mcp"\s*:\s*{(?!\s*"pipeline-gateway")/s;
10
10
  const LEGACY_PIPELINE_MCP_RE = /path:\s*\.mcp\.json|uvx\s+mcpm|mcpm\s+run/;
11
11
  var PipelineMcpGatewayError = class extends Error {
@@ -25,7 +25,7 @@ function gatewayServer(config, env = process.env) {
25
25
  const gateway = configuredGateway(config);
26
26
  const url = gatewayUrl(gateway, env);
27
27
  return {
28
- bearer_token_env_var: gateway.token_env,
28
+ headers: { Authorization: gatewayAuthorizationHeader(gateway) },
29
29
  url
30
30
  };
31
31
  }
@@ -36,6 +36,7 @@ function configuredGateway(config) {
36
36
  function gatewayUrl(gateway, env = process.env) {
37
37
  const url = env[gateway.url_env];
38
38
  if (url) return url;
39
+ if (gateway.url) return gateway.url;
39
40
  if (gateway.mode === "local") return DEFAULT_LOCAL_GATEWAY_URL;
40
41
  throw new PipelineMcpGatewayError(`MCP gateway URL is required. Set ${gateway.url_env}.`);
41
42
  }
@@ -44,8 +45,9 @@ function renderGatewayConfig(config) {
44
45
  return [
45
46
  `provider: ${gateway.provider}`,
46
47
  `mode: ${gateway.mode}`,
48
+ gateway.url ? `url: ${gateway.url}` : "",
47
49
  `url_env: ${gateway.url_env}`,
48
- `token_env: ${gateway.token_env}`,
50
+ `authorization_env: ${gateway.authorization_env}`,
49
51
  gateway.default_profile ? `default_profile: ${gateway.default_profile}` : "",
50
52
  `resolved_url: ${gatewayUrl(gateway)}`
51
53
  ].filter(Boolean).join("\n");
@@ -54,10 +56,12 @@ function renderCodexGatewayConfig(config) {
54
56
  const gateway = configuredGateway(config);
55
57
  return [
56
58
  "# Generated by @oisincoveney/pipeline.",
57
- "# Codex pipeline runs receive MCP gateway config through runtime launch args.",
58
- "# Persistent Codex MCP config is intentionally left empty to avoid startup-time MCP fan-out.",
59
- `# gateway_url = ${JSON.stringify(gatewayUrl(gateway))}`,
60
- `# token_env = ${JSON.stringify(gateway.token_env)}`,
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)}`,
61
65
  ""
62
66
  ].join("\n");
63
67
  }
@@ -67,7 +71,7 @@ function renderOpenCodeGatewayConfig(config) {
67
71
  $schema: "https://opencode.ai/config.json",
68
72
  mcp: { [PIPELINE_GATEWAY_SERVER_ID]: {
69
73
  enabled: true,
70
- headers: { Authorization: `Bearer {env:${gateway.token_env}}` },
74
+ headers: gatewayOpenCodeHeaders(gateway),
71
75
  type: "remote",
72
76
  url: gatewayUrl(gateway)
73
77
  } }
@@ -161,16 +165,22 @@ function checkGatewayUrl(gateway) {
161
165
  }
162
166
  }
163
167
  function checkGatewayToken(gateway) {
164
- return process.env[gateway.token_env] ? {
165
- detail: gateway.token_env,
166
- name: "gateway-token",
168
+ return process.env[gateway.authorization_env] ? {
169
+ detail: gateway.authorization_env,
170
+ name: "gateway-authorization",
167
171
  passed: true
168
172
  } : {
169
- detail: `Set ${gateway.token_env}.`,
170
- name: "gateway-token",
173
+ detail: `Set ${gateway.authorization_env}.`,
174
+ name: "gateway-authorization",
171
175
  passed: false
172
176
  };
173
177
  }
178
+ function gatewayAuthorizationHeader(gateway) {
179
+ return `{env:${gateway.authorization_env}}`;
180
+ }
181
+ function gatewayOpenCodeHeaders(gateway) {
182
+ return { Authorization: gatewayAuthorizationHeader(gateway) };
183
+ }
174
184
  async function checkThv(cwd) {
175
185
  try {
176
186
  await execa("thv", ["version"], {
@@ -240,11 +250,12 @@ async function checkGatewayHealth(gateway) {
240
250
  }
241
251
  }
242
252
  async function firstHealthyGatewayResponse(url, gateway) {
253
+ const authorization = process.env[gateway.authorization_env];
243
254
  for (const healthUrl of gatewayHealthUrls(url)) {
244
255
  const response = await fetch(healthUrl, {
245
256
  headers: {
246
257
  Accept: "application/json, text/event-stream",
247
- ...process.env[gateway.token_env] ? { Authorization: `Bearer ${process.env[gateway.token_env]}` } : {}
258
+ ...authorization ? { Authorization: authorization } : {}
248
259
  },
249
260
  method: "GET"
250
261
  });
@@ -287,4 +298,4 @@ function legacyContentHit(cwd, path, pattern) {
287
298
  return pattern.test(readFileSync(fullPath, "utf8")) ? path : void 0;
288
299
  }
289
300
  //#endregion
290
- export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway };
301
+ export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, renderCodexGatewayConfig, renderGatewayConfig, runGatewayDoctor, startLocalGateway };
@@ -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: false
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
- token_env: PIPELINE_MCP_GATEWAY_TOKEN
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, RunnerStrategy, createOrchestratorLaunchPlan, createRunnerLaunchPlan, hardAgentAdapter, runLaunchPlan, spawnAgent, subprocessAgentAdapter };
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 { buildMcpLaunchPlan, tomlValue } from "./mcp/launch-plan.js";
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
- ...mcp.args,
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} strategy=${plan.strategy}`,
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"] : []
@@ -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
- return roles.length > 0 ? `- ${id} (scheduling_roles: ${roles.join(", ")})` : `- ${id}`;
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 };
@@ -150,8 +150,9 @@ MCP-enabled profiles use one gateway grant:
150
150
  mcp_gateway:
151
151
  provider: toolhive
152
152
  mode: hosted
153
+ url: https://pipeline-mcp.momokaya.ee/mcp/
153
154
  url_env: PIPELINE_MCP_GATEWAY_URL
154
- token_env: PIPELINE_MCP_GATEWAY_TOKEN
155
+ authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
155
156
  default_profile: default
156
157
 
157
158
  profiles:
@@ -11,7 +11,7 @@ Playwright, Qdrant, or Neon.
11
11
  ```text
12
12
  Codex/OpenCode
13
13
  |
14
- | profile-scoped MCP config
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: profiles that need MCP declare the singleton
25
- `pipeline-gateway` grant, and every Codex/OpenCode session receives only that
26
- remote MCP server.
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. Grant `pipeline-gateway` only to profiles that need MCP access.
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
 
@@ -52,8 +51,9 @@ Example profile config:
52
51
  mcp_gateway:
53
52
  provider: toolhive
54
53
  mode: hosted
54
+ url: https://pipeline-mcp.momokaya.ee/mcp/
55
55
  url_env: PIPELINE_MCP_GATEWAY_URL
56
- token_env: PIPELINE_MCP_GATEWAY_TOKEN
56
+ authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
57
57
  default_profile: default
58
58
 
59
59
  profiles:
@@ -72,10 +72,8 @@ orchestrator MCP set * subagent count * host config layers
72
72
  ```
73
73
 
74
74
  With a gateway, the runtime launches zero local upstream MCP processes for
75
- agents. Codex receives `--ignore-user-config` plus one
76
- `mcp_servers.pipeline-gateway` remote entry. OpenCode runs with isolated
77
- XDG/config roots and receives inline `OPENCODE_CONFIG_CONTENT` containing only
78
- `pipeline-gateway`.
75
+ agents. Codex and OpenCode read the same project-level host config, which
76
+ contains only `pipeline-gateway`.
79
77
 
80
78
  ## Candidate Gateway Implementations
81
79
 
@@ -86,6 +84,5 @@ Use an off-the-shelf aggregator when possible:
86
84
  Use `pipe mcp gateway doctor` to check required environment variables, gateway
87
85
  health, local ToolHive availability for local mode, and legacy direct MCP
88
86
  entries. Use `pipe mcp gateway configure-host` to rewrite project or global
89
- host config with a backup. For Codex, this removes persistent MCP entries and
90
- leaves gateway metadata as comments because the runtime injects the gateway
91
- 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 `.pipeline/profiles.yaml` as the MCP source of truth.
4
- Profiles that need MCP grant `pipeline-gateway`; runtime launch planning renders
5
- exactly one remote MCP server for the target host.
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 pass the singleton gateway explicitly with
12
- `--config mcp_servers.pipeline-gateway...` entries.
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 configuration is merged from multiple layers, so `OPENCODE_CONFIG`
23
- alone is not an isolation boundary. Pipeline-managed OpenCode launches create
24
- an isolated temporary runtime root, set `XDG_CONFIG_HOME`, `XDG_DATA_HOME`,
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/
@@ -378,8 +378,9 @@ Use the same client config shape for hosted and local gateways:
378
378
  mcp_gateway:
379
379
  provider: toolhive
380
380
  mode: hosted
381
+ url: https://pipeline-mcp.momokaya.ee/mcp/
381
382
  url_env: PIPELINE_MCP_GATEWAY_URL
382
- token_env: PIPELINE_MCP_GATEWAY_TOKEN
383
+ authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
383
384
  default_profile: default
384
385
  ```
385
386
 
@@ -412,17 +413,17 @@ pipe mcp gateway local-status
412
413
  pipe mcp gateway local-start
413
414
  ```
414
415
 
415
- `configure-host` removes persistent Codex MCP config and records the gateway
416
- metadata as comments. Codex pipeline runs receive the gateway through runtime
417
- launch arguments so ordinary Codex sessions do not start pipeline MCP clients at
418
- startup:
416
+ `configure-host` removes direct upstream MCP config and writes the singleton
417
+ gateway server into project host config. Codex receives:
419
418
 
420
419
  ```toml
421
420
  # Generated by @oisincoveney/pipeline.
422
- # Codex pipeline runs receive MCP gateway config through runtime launch args.
423
- # Persistent Codex MCP config is intentionally left empty to avoid startup-time MCP fan-out.
424
- # gateway_url = "https://gateway.example/mcp"
425
- # token_env = "PIPELINE_MCP_GATEWAY_TOKEN"
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"
426
427
  ```
427
428
 
428
429
  OpenCode receives:
@@ -432,6 +433,9 @@ OpenCode receives:
432
433
  "mcp": {
433
434
  "pipeline-gateway": {
434
435
  "type": "remote",
436
+ "headers": {
437
+ "Authorization": "{env:PIPELINE_MCP_GATEWAY_AUTHORIZATION}"
438
+ },
435
439
  "url": "https://gateway.example/mcp"
436
440
  }
437
441
  }
@@ -526,8 +530,9 @@ singleton gateway server to profiles that need MCP.
526
530
  mcp_gateway:
527
531
  provider: toolhive
528
532
  mode: hosted
533
+ url: https://pipeline-mcp.momokaya.ee/mcp/
529
534
  url_env: PIPELINE_MCP_GATEWAY_URL
530
- token_env: PIPELINE_MCP_GATEWAY_TOKEN
535
+ authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
531
536
 
532
537
  profiles:
533
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.16.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",
@@ -1,97 +0,0 @@
1
- import { gatewayServerForProfile } 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(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 tomlValue(value) {
89
- if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
90
- if (value && typeof value === "object") return `{ ${Object.entries(value).map(([key, item]) => `${tomlKey(key)} = ${tomlValue(item)}`).join(", ")} }`;
91
- return JSON.stringify(value);
92
- }
93
- function tomlKey(key) {
94
- return TOML_BARE_KEY_PATTERN.test(key) ? key : JSON.stringify(key);
95
- }
96
- //#endregion
97
- export { buildMcpLaunchPlan, tomlValue };