@oisincoveney/pipeline 1.13.0 → 1.15.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 +15 -2
- package/defaults/install-manifest.json +0 -66
- package/dist/config.d.ts +80 -35
- package/dist/config.js +108 -105
- package/dist/hooks.d.ts +60 -0
- package/dist/hooks.js +28 -0
- package/dist/index.js +46 -10
- package/dist/install-commands.js +1 -5
- package/dist/mcp/bootstrap.js +2 -213
- package/dist/mcp/gateway.js +288 -0
- package/dist/mcp/launch-plan.js +2 -4
- package/dist/mcp/native-config.js +2 -4
- package/dist/pipeline-init.js +48 -72
- package/dist/pipeline-runtime.d.ts +18 -4
- package/dist/pipeline-runtime.js +215 -101
- package/dist/runner-job-contract.d.ts +20 -1
- package/dist/runner-job-contract.js +16 -0
- package/dist/runtime-machines/workflow-machine.js +146 -93
- package/dist/schedule-planner.d.ts +0 -25
- package/dist/schedule-planner.js +48 -10
- package/dist/task-ref.js +12 -16
- package/dist/workflow-planner.d.ts +0 -1
- package/dist/workflow-planner.js +0 -1
- package/docs/config-architecture.md +17 -35
- package/docs/mcp-gateway.md +26 -28
- package/docs/mcp-host-isolation.md +10 -10
- package/docs/operator-guide.md +56 -73
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -188,7 +188,10 @@ default_workflow: default
|
|
|
188
188
|
|
|
189
189
|
orchestrator:
|
|
190
190
|
profile: orchestrator
|
|
191
|
-
|
|
191
|
+
|
|
192
|
+
hooks:
|
|
193
|
+
functions: {}
|
|
194
|
+
on: {}
|
|
192
195
|
|
|
193
196
|
workflows:
|
|
194
197
|
default:
|
|
@@ -353,7 +356,7 @@ runners:
|
|
|
353
356
|
- JSON Schema gates validate structure only. Use `verdict` and `acceptance`
|
|
354
357
|
gates to enforce semantic pass/fail and per-criterion coverage.
|
|
355
358
|
- Command hooks support host policy controls, sanitized environments, timeouts,
|
|
356
|
-
output limits, and JSON
|
|
359
|
+
output limits, and JSON file input/result payloads.
|
|
357
360
|
|
|
358
361
|
## App-Facing API
|
|
359
362
|
|
|
@@ -377,6 +380,16 @@ import {
|
|
|
377
380
|
} from "@oisincoveney/pipeline/runtime";
|
|
378
381
|
```
|
|
379
382
|
|
|
383
|
+
Hook modules can import the typed helper and result contract:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import {
|
|
387
|
+
defineHook,
|
|
388
|
+
type HookContext,
|
|
389
|
+
type HookResult,
|
|
390
|
+
} from "@oisincoveney/pipeline/hooks";
|
|
391
|
+
```
|
|
392
|
+
|
|
380
393
|
Runner Job producers can import the shared payload contract:
|
|
381
394
|
|
|
382
395
|
```ts
|
|
@@ -5,71 +5,5 @@
|
|
|
5
5
|
"source": "oisincoveney/skills",
|
|
6
6
|
"args": ["--agent", "codex", "--skill", "*", "--yes", "--copy"]
|
|
7
7
|
}
|
|
8
|
-
],
|
|
9
|
-
"mcps": [
|
|
10
|
-
{
|
|
11
|
-
"name": "oisin-pipeline-backlog",
|
|
12
|
-
"transport": "stdio",
|
|
13
|
-
"command": "backlog",
|
|
14
|
-
"args": ["mcp", "start"]
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"name": "oisin-pipeline-context7",
|
|
18
|
-
"transport": "remote",
|
|
19
|
-
"catalog": "context7"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"name": "oisin-pipeline-github-readonly",
|
|
23
|
-
"transport": "stdio",
|
|
24
|
-
"command": "docker",
|
|
25
|
-
"args": [
|
|
26
|
-
"run",
|
|
27
|
-
"-i",
|
|
28
|
-
"--rm",
|
|
29
|
-
"-e",
|
|
30
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
31
|
-
"ghcr.io/github/github-mcp-server",
|
|
32
|
-
"stdio",
|
|
33
|
-
"--read-only"
|
|
34
|
-
],
|
|
35
|
-
"env": {
|
|
36
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"name": "oisin-pipeline-playwright",
|
|
41
|
-
"transport": "stdio",
|
|
42
|
-
"catalog": "playwright"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "oisin-pipeline-qdrant",
|
|
46
|
-
"transport": "remote",
|
|
47
|
-
"url": "https://memory-mcp.momokaya.ee/mcp/",
|
|
48
|
-
"optionalRegistration": true,
|
|
49
|
-
"headers": {
|
|
50
|
-
"Authorization": {
|
|
51
|
-
"sources": [{ "env": "MEMORY_MCP_BASIC_AUTH", "prefix": "Basic " }]
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"name": "oisin-pipeline-semgrep",
|
|
57
|
-
"transport": "stdio",
|
|
58
|
-
"command": "docker",
|
|
59
|
-
"args": ["run", "-i", "--rm", "ghcr.io/semgrep/mcp", "-t", "stdio"]
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"name": "oisin-pipeline-serena",
|
|
63
|
-
"transport": "stdio",
|
|
64
|
-
"command": "uvx",
|
|
65
|
-
"args": [
|
|
66
|
-
"--from",
|
|
67
|
-
"git+https://github.com/oraios/serena",
|
|
68
|
-
"serena",
|
|
69
|
-
"start-mcp-server",
|
|
70
|
-
"--project-from-cwd",
|
|
71
|
-
"--context=codex"
|
|
72
|
-
]
|
|
73
|
-
}
|
|
74
8
|
]
|
|
75
9
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ declare const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
|
|
|
6
6
|
declare const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
|
|
7
7
|
declare const RUNNER_TYPES: readonly ["codex", "opencode", "command"];
|
|
8
8
|
declare const NODE_KINDS: readonly ["agent", "command", "builtin", "group", "parallel", "workflow"];
|
|
9
|
-
declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "gate.failure"];
|
|
9
|
+
declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "node.finish", "gate.failure"];
|
|
10
10
|
declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
|
|
11
11
|
declare const SCHEDULE_BASELINES: readonly ["epic", "pipe"];
|
|
12
12
|
type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_MISSING" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
|
|
@@ -88,7 +88,6 @@ declare const workflowNodeBaseSchema: z.ZodObject<{
|
|
|
88
88
|
field: z.ZodOptional<z.ZodString>;
|
|
89
89
|
kind: z.ZodLiteral<"verdict">;
|
|
90
90
|
}, z.core.$strict>], "kind">>>;
|
|
91
|
-
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
92
91
|
id: z.ZodString;
|
|
93
92
|
needs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
94
93
|
retries: z.ZodOptional<z.ZodObject<{
|
|
@@ -146,7 +145,6 @@ declare const workflowSchema: z.ZodObject<{
|
|
|
146
145
|
max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
|
|
147
146
|
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
148
147
|
}, z.core.$strict>>;
|
|
149
|
-
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
150
148
|
nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
|
|
151
149
|
}, z.core.$strict>;
|
|
152
150
|
declare const configSchema: z.ZodObject<{
|
|
@@ -164,36 +162,85 @@ declare const configSchema: z.ZodObject<{
|
|
|
164
162
|
}, z.core.$loose>>;
|
|
165
163
|
schedule: z.ZodString;
|
|
166
164
|
}, z.core.$strict>]>>>;
|
|
167
|
-
hooks: z.ZodDefault<z.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
165
|
+
hooks: z.ZodDefault<z.ZodObject<{
|
|
166
|
+
functions: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
167
|
+
kind: z.ZodLiteral<"module">;
|
|
168
|
+
module: z.ZodString;
|
|
169
|
+
permissions: z.ZodOptional<z.ZodObject<{
|
|
170
|
+
filesystem: z.ZodOptional<z.ZodEnum<{
|
|
171
|
+
"read-only": "read-only";
|
|
172
|
+
"workspace-write": "workspace-write";
|
|
173
|
+
}>>;
|
|
174
|
+
network: z.ZodOptional<z.ZodEnum<{
|
|
175
|
+
inherit: "inherit";
|
|
176
|
+
disabled: "disabled";
|
|
177
|
+
}>>;
|
|
178
|
+
}, z.core.$strict>>;
|
|
179
|
+
returns: z.ZodOptional<z.ZodObject<{
|
|
180
|
+
schema: z.ZodOptional<z.ZodString>;
|
|
181
|
+
}, z.core.$strict>>;
|
|
182
|
+
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
183
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
184
|
+
command: z.ZodArray<z.ZodString>;
|
|
185
|
+
env: z.ZodOptional<z.ZodObject<{
|
|
186
|
+
passthrough: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
187
|
+
set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
188
|
+
}, z.core.$strict>>;
|
|
189
|
+
kind: z.ZodLiteral<"command">;
|
|
190
|
+
output_limit_bytes: z.ZodOptional<z.ZodNumber>;
|
|
191
|
+
protocol: z.ZodDefault<z.ZodObject<{
|
|
192
|
+
input: z.ZodLiteral<"file">;
|
|
193
|
+
result: z.ZodLiteral<"file">;
|
|
194
|
+
}, z.core.$strict>>;
|
|
195
|
+
returns: z.ZodOptional<z.ZodObject<{
|
|
196
|
+
schema: z.ZodOptional<z.ZodString>;
|
|
197
|
+
}, z.core.$strict>>;
|
|
198
|
+
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
199
|
+
trusted: z.ZodOptional<z.ZodBoolean>;
|
|
200
|
+
}, z.core.$strict>], "kind">>>;
|
|
201
|
+
on: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
202
|
+
failure: z.ZodDefault<z.ZodEnum<{
|
|
203
|
+
fail: "fail";
|
|
204
|
+
ignore: "ignore";
|
|
205
|
+
}>>;
|
|
206
|
+
function: z.ZodString;
|
|
207
|
+
id: z.ZodString;
|
|
208
|
+
result: z.ZodOptional<z.ZodObject<{
|
|
209
|
+
pass_to: z.ZodOptional<z.ZodEnum<{
|
|
210
|
+
downstream: "downstream";
|
|
211
|
+
}>>;
|
|
212
|
+
publish: z.ZodOptional<z.ZodBoolean>;
|
|
213
|
+
save_as: z.ZodOptional<z.ZodString>;
|
|
214
|
+
}, z.core.$strict>>;
|
|
215
|
+
where: z.ZodOptional<z.ZodObject<{
|
|
216
|
+
gate: z.ZodOptional<z.ZodString>;
|
|
217
|
+
node: z.ZodOptional<z.ZodString>;
|
|
218
|
+
workflow: z.ZodOptional<z.ZodString>;
|
|
219
|
+
}, z.core.$strict>>;
|
|
220
|
+
with: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
221
|
+
}, z.core.$strict>>>>;
|
|
222
|
+
policy: z.ZodOptional<z.ZodObject<{
|
|
223
|
+
commands: z.ZodOptional<z.ZodEnum<{
|
|
224
|
+
allow: "allow";
|
|
225
|
+
deny: "deny";
|
|
226
|
+
"trusted-only": "trusted-only";
|
|
227
|
+
}>>;
|
|
228
|
+
modules: z.ZodOptional<z.ZodEnum<{
|
|
229
|
+
allow: "allow";
|
|
230
|
+
deny: "deny";
|
|
231
|
+
}>>;
|
|
174
232
|
}, z.core.$strict>>;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"node.success": "node.success";
|
|
182
|
-
"node.error": "node.error";
|
|
183
|
-
"gate.failure": "gate.failure";
|
|
184
|
-
}>;
|
|
185
|
-
kind: z.ZodEnum<{
|
|
186
|
-
builtin: "builtin";
|
|
187
|
-
command: "command";
|
|
233
|
+
}, z.core.$strict>>;
|
|
234
|
+
mcp_gateway: z.ZodOptional<z.ZodObject<{
|
|
235
|
+
default_profile: z.ZodOptional<z.ZodString>;
|
|
236
|
+
mode: z.ZodEnum<{
|
|
237
|
+
hosted: "hosted";
|
|
238
|
+
local: "local";
|
|
188
239
|
}>;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
required: z.ZodOptional<z.ZodBoolean>;
|
|
194
|
-
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
195
|
-
trusted: z.ZodOptional<z.ZodBoolean>;
|
|
196
|
-
}, z.core.$strict>>>;
|
|
240
|
+
provider: z.ZodLiteral<"toolhive">;
|
|
241
|
+
token_env: z.ZodDefault<z.ZodString>;
|
|
242
|
+
url_env: z.ZodDefault<z.ZodString>;
|
|
243
|
+
}, z.core.$strict>>;
|
|
197
244
|
mcp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
198
245
|
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
199
246
|
bearer_token_env_var: z.ZodOptional<z.ZodString>;
|
|
@@ -203,7 +250,6 @@ declare const configSchema: z.ZodObject<{
|
|
|
203
250
|
url: z.ZodOptional<z.ZodString>;
|
|
204
251
|
}, z.core.$strict>>>;
|
|
205
252
|
orchestrator: z.ZodObject<{
|
|
206
|
-
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
207
253
|
profile: z.ZodString;
|
|
208
254
|
}, z.core.$strict>;
|
|
209
255
|
profiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -231,10 +277,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
231
277
|
}, z.core.$strict>>;
|
|
232
278
|
output: z.ZodOptional<z.ZodObject<{
|
|
233
279
|
format: z.ZodEnum<{
|
|
280
|
+
json_schema: "json_schema";
|
|
234
281
|
text: "text";
|
|
235
282
|
json: "json";
|
|
236
283
|
jsonl: "jsonl";
|
|
237
|
-
json_schema: "json_schema";
|
|
238
284
|
}>;
|
|
239
285
|
repair: z.ZodOptional<z.ZodObject<{
|
|
240
286
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -274,10 +320,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
274
320
|
disabled: "disabled";
|
|
275
321
|
}>>>;
|
|
276
322
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
323
|
+
json_schema: "json_schema";
|
|
277
324
|
text: "text";
|
|
278
325
|
json: "json";
|
|
279
326
|
jsonl: "jsonl";
|
|
280
|
-
json_schema: "json_schema";
|
|
281
327
|
}>>>;
|
|
282
328
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
283
329
|
skills: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -324,7 +370,6 @@ declare const configSchema: z.ZodObject<{
|
|
|
324
370
|
max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
|
|
325
371
|
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
326
372
|
}, z.core.$strict>>;
|
|
327
|
-
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
328
373
|
nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
|
|
329
374
|
}, z.core.$strict>>>;
|
|
330
375
|
}, z.core.$strict>;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolveFileReference } from "./path-refs.js";
|
|
2
|
-
import { parseJson } from "./safe-json.js";
|
|
3
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { parseDocument } from "yaml";
|
|
@@ -23,6 +22,7 @@ const HOOK_EVENTS = [
|
|
|
23
22
|
"node.start",
|
|
24
23
|
"node.success",
|
|
25
24
|
"node.error",
|
|
25
|
+
"node.finish",
|
|
26
26
|
"gate.failure"
|
|
27
27
|
];
|
|
28
28
|
const TOOL_NAMES = [
|
|
@@ -55,6 +55,7 @@ const RETRY_REASONS = [
|
|
|
55
55
|
"timeout"
|
|
56
56
|
];
|
|
57
57
|
const SCHEDULE_BASELINES = ["epic", "pipe"];
|
|
58
|
+
const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
|
|
58
59
|
var PipelineConfigError = class extends Error {
|
|
59
60
|
code;
|
|
60
61
|
issues;
|
|
@@ -126,21 +127,13 @@ const mcpServerSchema = z.object({
|
|
|
126
127
|
path: ["bearer_token_env_var"]
|
|
127
128
|
});
|
|
128
129
|
});
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
const mcpGatewaySchema = z.object({
|
|
131
|
+
default_profile: z.string().min(1).optional(),
|
|
132
|
+
mode: z.enum(["hosted", "local"]),
|
|
133
|
+
provider: z.literal("toolhive"),
|
|
134
|
+
token_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_TOKEN"),
|
|
135
|
+
url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
|
|
133
136
|
}).strict();
|
|
134
|
-
const mcpServerDefinitionSchema = z.union([mcpServerSchema, z.object({ ref: mcpServerRefSchema }).strict()]);
|
|
135
|
-
const mcpJsonServerSchema = z.object({
|
|
136
|
-
args: z.array(z.string()).optional(),
|
|
137
|
-
bearer_token_env_var: z.string().min(1).optional(),
|
|
138
|
-
command: z.string().min(1).optional(),
|
|
139
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
140
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
141
|
-
url: z.string().min(1).optional()
|
|
142
|
-
}).passthrough();
|
|
143
|
-
const mcpJsonFileSchema = z.object({ mcpServers: strictRecord(mcpJsonServerSchema) }).passthrough();
|
|
144
137
|
const instructionsSchema = z.object({
|
|
145
138
|
inline: z.string().min(1).optional(),
|
|
146
139
|
path: z.string().min(1).optional()
|
|
@@ -237,27 +230,72 @@ const profileSchema = z.object({
|
|
|
237
230
|
skills: z.array(z.string()).optional(),
|
|
238
231
|
tools: z.array(z.enum(TOOL_NAMES)).optional()
|
|
239
232
|
}).strict();
|
|
240
|
-
const orchestratorSchema = z.object({
|
|
241
|
-
hooks: z.array(z.string()).optional(),
|
|
242
|
-
profile: z.string()
|
|
243
|
-
}).strict();
|
|
233
|
+
const orchestratorSchema = z.object({ profile: z.string() }).strict();
|
|
244
234
|
const hookEnvSchema = z.object({
|
|
245
235
|
passthrough: z.array(z.string()).optional(),
|
|
246
236
|
set: z.record(z.string(), z.string()).optional()
|
|
247
237
|
}).strict();
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
238
|
+
const hookPermissionsSchema = z.object({
|
|
239
|
+
filesystem: z.enum(FILESYSTEM_MODES).optional(),
|
|
240
|
+
network: z.enum(NETWORK_MODES).optional()
|
|
241
|
+
}).strict();
|
|
242
|
+
const hookReturnsSchema = z.object({ schema: z.string().min(1).optional() }).strict();
|
|
243
|
+
const moduleHookFunctionSchema = z.object({
|
|
244
|
+
kind: z.literal("module"),
|
|
245
|
+
module: z.string().min(1),
|
|
246
|
+
permissions: hookPermissionsSchema.optional(),
|
|
247
|
+
returns: hookReturnsSchema.optional(),
|
|
248
|
+
timeout_ms: z.number().int().positive().optional()
|
|
249
|
+
}).strict();
|
|
250
|
+
const commandHookProtocolSchema = z.object({
|
|
251
|
+
input: z.literal("file"),
|
|
252
|
+
result: z.literal("file")
|
|
253
|
+
}).strict();
|
|
254
|
+
const commandHookFunctionSchema = z.object({
|
|
255
|
+
command: z.array(z.string()).min(1),
|
|
252
256
|
env: hookEnvSchema.optional(),
|
|
253
|
-
|
|
254
|
-
kind: z.enum(["command", "builtin"]),
|
|
257
|
+
kind: z.literal("command"),
|
|
255
258
|
output_limit_bytes: z.number().int().positive().optional(),
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
protocol: commandHookProtocolSchema.default({
|
|
260
|
+
input: "file",
|
|
261
|
+
result: "file"
|
|
262
|
+
}),
|
|
263
|
+
returns: hookReturnsSchema.optional(),
|
|
258
264
|
timeout_ms: z.number().int().positive().optional(),
|
|
259
265
|
trusted: z.boolean().optional()
|
|
260
266
|
}).strict();
|
|
267
|
+
const hookFunctionSchema = z.discriminatedUnion("kind", [moduleHookFunctionSchema, commandHookFunctionSchema]);
|
|
268
|
+
const hookBindingWhereSchema = z.object({
|
|
269
|
+
gate: z.string().optional(),
|
|
270
|
+
node: z.string().optional(),
|
|
271
|
+
workflow: z.string().optional()
|
|
272
|
+
}).strict();
|
|
273
|
+
const hookBindingResultSchema = z.object({
|
|
274
|
+
pass_to: z.enum(["downstream"]).optional(),
|
|
275
|
+
publish: z.boolean().optional(),
|
|
276
|
+
save_as: z.string().min(1).optional()
|
|
277
|
+
}).strict();
|
|
278
|
+
const hookBindingSchema = z.object({
|
|
279
|
+
failure: z.enum(["fail", "ignore"]).default("ignore"),
|
|
280
|
+
function: z.string().min(1),
|
|
281
|
+
id: z.string().min(1),
|
|
282
|
+
result: hookBindingResultSchema.optional(),
|
|
283
|
+
where: hookBindingWhereSchema.optional(),
|
|
284
|
+
with: z.record(z.string(), z.unknown()).optional()
|
|
285
|
+
}).strict();
|
|
286
|
+
const hookPolicySchema = z.object({
|
|
287
|
+
commands: z.enum([
|
|
288
|
+
"allow",
|
|
289
|
+
"trusted-only",
|
|
290
|
+
"deny"
|
|
291
|
+
]).optional(),
|
|
292
|
+
modules: z.enum(["allow", "deny"]).optional()
|
|
293
|
+
}).strict();
|
|
294
|
+
const hooksConfigSchema = z.object({
|
|
295
|
+
functions: strictRecord(hookFunctionSchema).default({}),
|
|
296
|
+
on: strictRecord(z.array(hookBindingSchema)).default({}),
|
|
297
|
+
policy: hookPolicySchema.optional()
|
|
298
|
+
}).strict();
|
|
261
299
|
const taskContextResolverSchema = z.object({ type: z.string().min(1) }).passthrough();
|
|
262
300
|
const nodeTaskContextSchema = z.object({
|
|
263
301
|
acceptance_criteria: z.array(z.object({
|
|
@@ -282,7 +320,6 @@ const schedulePolicySchema = z.object({
|
|
|
282
320
|
const workflowNodeBaseSchema = z.object({
|
|
283
321
|
artifacts: z.array(artifactSchema).optional(),
|
|
284
322
|
gates: z.array(gateSchema).optional(),
|
|
285
|
-
hooks: z.array(z.string()).optional(),
|
|
286
323
|
id: z.string(),
|
|
287
324
|
needs: z.array(z.string()).optional(),
|
|
288
325
|
retries: retriesSchema.optional(),
|
|
@@ -319,7 +356,6 @@ const workflowNodeSchema = z.lazy(() => z.discriminatedUnion("kind", [
|
|
|
319
356
|
const workflowSchema = z.object({
|
|
320
357
|
description: z.string().optional(),
|
|
321
358
|
execution: workflowExecutionSchema.optional(),
|
|
322
|
-
hooks: z.array(z.string()).optional(),
|
|
323
359
|
nodes: z.array(workflowNodeSchema)
|
|
324
360
|
}).strict();
|
|
325
361
|
const runnersFileSchema = z.object({
|
|
@@ -327,7 +363,8 @@ const runnersFileSchema = z.object({
|
|
|
327
363
|
version: z.literal(1)
|
|
328
364
|
}).strict();
|
|
329
365
|
const profilesFileSchema = z.object({
|
|
330
|
-
|
|
366
|
+
mcp_gateway: mcpGatewaySchema.optional(),
|
|
367
|
+
mcp_servers: strictRecord(z.never()).default({}),
|
|
331
368
|
profiles: strictRecord(profileSchema).default({}),
|
|
332
369
|
rules: strictRecord(pathRefSchema).default({}),
|
|
333
370
|
skills: strictRecord(pathRefSchema).default({}),
|
|
@@ -336,7 +373,10 @@ const profilesFileSchema = z.object({
|
|
|
336
373
|
const pipelineFileSchema = z.object({
|
|
337
374
|
default_workflow: z.string(),
|
|
338
375
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
339
|
-
hooks:
|
|
376
|
+
hooks: hooksConfigSchema.default({
|
|
377
|
+
functions: {},
|
|
378
|
+
on: {}
|
|
379
|
+
}),
|
|
340
380
|
orchestrator: orchestratorSchema,
|
|
341
381
|
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
342
382
|
task_context: taskContextResolverSchema.optional(),
|
|
@@ -346,7 +386,11 @@ const pipelineFileSchema = z.object({
|
|
|
346
386
|
const configSchema = z.object({
|
|
347
387
|
default_workflow: z.string(),
|
|
348
388
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
349
|
-
hooks:
|
|
389
|
+
hooks: hooksConfigSchema.default({
|
|
390
|
+
functions: {},
|
|
391
|
+
on: {}
|
|
392
|
+
}),
|
|
393
|
+
mcp_gateway: mcpGatewaySchema.optional(),
|
|
350
394
|
mcp_servers: strictRecord(mcpServerSchema).default({}),
|
|
351
395
|
orchestrator: orchestratorSchema,
|
|
352
396
|
profiles: strictRecord(profileSchema).default({}),
|
|
@@ -464,12 +508,12 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
|
464
508
|
const runners = parseYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
|
|
465
509
|
const profiles = parseYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
|
|
466
510
|
const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
|
|
467
|
-
const mcpServers = resolveMcpServerDefinitions(profiles.mcp_servers, projectRoot);
|
|
468
511
|
return validatePipelineConfig({
|
|
469
512
|
default_workflow: pipeline.default_workflow,
|
|
470
513
|
entrypoints: pipeline.entrypoints,
|
|
471
514
|
hooks: pipeline.hooks,
|
|
472
|
-
|
|
515
|
+
...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
|
|
516
|
+
mcp_servers: profiles.mcp_servers,
|
|
473
517
|
orchestrator: pipeline.orchestrator,
|
|
474
518
|
profiles: profiles.profiles,
|
|
475
519
|
rules: profiles.rules,
|
|
@@ -497,61 +541,6 @@ function parseYamlAs(source, sourcePath, schema) {
|
|
|
497
541
|
})));
|
|
498
542
|
return parsed.data;
|
|
499
543
|
}
|
|
500
|
-
function resolveMcpServerDefinitions(registry, projectRoot) {
|
|
501
|
-
return Object.fromEntries(Object.entries(registry).map(([id, definition]) => [id, "ref" in definition ? resolveMcpServerRef(id, definition.ref, projectRoot) : definition]));
|
|
502
|
-
}
|
|
503
|
-
function resolveMcpServerRef(id, ref, projectRoot) {
|
|
504
|
-
if (!projectRoot) throw validationError([{
|
|
505
|
-
path: `mcp_servers.${id}.ref.path`,
|
|
506
|
-
message: "MCP server refs require a project root"
|
|
507
|
-
}]);
|
|
508
|
-
const filePath = resolveFileReference(projectRoot, ref.path);
|
|
509
|
-
if (!existsSync(filePath)) throw validationError([{
|
|
510
|
-
path: `mcp_servers.${id}.ref.path`,
|
|
511
|
-
message: `referenced MCP config file '${ref.path}' does not exist`
|
|
512
|
-
}]);
|
|
513
|
-
const parsed = parseMcpJsonFile(id, ref, filePath);
|
|
514
|
-
const importedId = ref.id ?? id;
|
|
515
|
-
const imported = parsed.mcpServers[importedId];
|
|
516
|
-
if (!imported) throw validationError([{
|
|
517
|
-
path: `mcp_servers.${id}.ref.id`,
|
|
518
|
-
message: `MCP config '${ref.path}' does not declare server '${importedId}'`
|
|
519
|
-
}]);
|
|
520
|
-
const normalized = normalizeMcpJsonServer(imported);
|
|
521
|
-
const result = mcpServerSchema.safeParse(normalized);
|
|
522
|
-
if (!result.success) throw validationError(result.error.issues.map((issue) => ({
|
|
523
|
-
path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
|
|
524
|
-
message: issue.message
|
|
525
|
-
})));
|
|
526
|
-
return result.data;
|
|
527
|
-
}
|
|
528
|
-
function parseMcpJsonFile(id, ref, filePath) {
|
|
529
|
-
let raw;
|
|
530
|
-
try {
|
|
531
|
-
raw = parseJson(readFileSync(filePath, "utf8"), `MCP config ${ref.path}`);
|
|
532
|
-
} catch (err) {
|
|
533
|
-
throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse MCP config ${ref.path}`, [{
|
|
534
|
-
path: `mcp_servers.${id}.ref.path`,
|
|
535
|
-
message: err instanceof Error ? err.message : String(err)
|
|
536
|
-
}]);
|
|
537
|
-
}
|
|
538
|
-
const parsed = mcpJsonFileSchema.safeParse(raw);
|
|
539
|
-
if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
|
|
540
|
-
path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
|
|
541
|
-
message: issue.message
|
|
542
|
-
})));
|
|
543
|
-
return parsed.data;
|
|
544
|
-
}
|
|
545
|
-
function normalizeMcpJsonServer(server) {
|
|
546
|
-
return {
|
|
547
|
-
...server.command ? { command: server.command } : {},
|
|
548
|
-
...server.args ? { args: server.args } : {},
|
|
549
|
-
...server.env ? { env: server.env } : {},
|
|
550
|
-
...server.url ? { url: server.url } : {},
|
|
551
|
-
...server.headers ? { headers: server.headers } : {},
|
|
552
|
-
...server.bearer_token_env_var ? { bearer_token_env_var: server.bearer_token_env_var } : {}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
544
|
function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
|
|
556
545
|
const parsed = configSchema.safeParse(rawConfig);
|
|
557
546
|
if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
|
|
@@ -565,11 +554,10 @@ function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
|
|
|
565
554
|
validateRegistryIds("rules", config.rules, issues);
|
|
566
555
|
validateRegistryIds("skills", config.skills, issues);
|
|
567
556
|
validateRegistryIds("mcp_servers", config.mcp_servers, issues);
|
|
568
|
-
validateRegistryIds("hooks", config.hooks, issues);
|
|
557
|
+
validateRegistryIds("hooks.functions", config.hooks.functions, issues);
|
|
569
558
|
validateRegistryIds("workflows", config.workflows, issues);
|
|
570
559
|
validateRegistryIds("entrypoints", config.entrypoints, issues);
|
|
571
|
-
if (config.profiles[config.orchestrator.profile])
|
|
572
|
-
else issues.push({
|
|
560
|
+
if (!config.profiles[config.orchestrator.profile]) issues.push({
|
|
573
561
|
path: "orchestrator.profile",
|
|
574
562
|
message: `orchestrator references missing profile '${config.orchestrator.profile}'`
|
|
575
563
|
});
|
|
@@ -584,16 +572,7 @@ function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
|
|
|
584
572
|
}
|
|
585
573
|
validateProfile(profileId, profile, runner, config, issues, projectRoot, options);
|
|
586
574
|
}
|
|
587
|
-
|
|
588
|
-
if (hook.kind === "command" && !hook.command) issues.push({
|
|
589
|
-
path: `hooks.${hookId}.command`,
|
|
590
|
-
message: `command hook '${hookId}' must declare command`
|
|
591
|
-
});
|
|
592
|
-
if (hook.kind === "builtin" && !hook.builtin) issues.push({
|
|
593
|
-
path: `hooks.${hookId}.builtin`,
|
|
594
|
-
message: `builtin hook '${hookId}' must declare builtin`
|
|
595
|
-
});
|
|
596
|
-
}
|
|
575
|
+
validateHookConfig(config, issues, projectRoot, options);
|
|
597
576
|
for (const [ruleId, rule] of Object.entries(config.rules)) validatePath(`rules.${ruleId}.path`, rule.path, projectRoot, issues, options);
|
|
598
577
|
for (const [skillId, skill] of Object.entries(config.skills)) validatePath(`skills.${skillId}.path`, skill.path, projectRoot, issues, options);
|
|
599
578
|
for (const [workflowId, workflow] of Object.entries(config.workflows)) validateWorkflow(workflowId, workflow, config, issues, projectRoot, options);
|
|
@@ -606,6 +585,29 @@ function validateRegistryIds(name, registry, issues) {
|
|
|
606
585
|
message: `registry id '${id}' must match ${ID_RE.source}`
|
|
607
586
|
});
|
|
608
587
|
}
|
|
588
|
+
function validateHookConfig(config, issues, projectRoot, options = {}) {
|
|
589
|
+
const allowedEvents = new Set(HOOK_EVENTS);
|
|
590
|
+
for (const [functionId, hookFunction] of Object.entries(config.hooks.functions)) validatePath(`hooks.functions.${functionId}.returns.schema`, hookFunction.returns?.schema, projectRoot, issues, options);
|
|
591
|
+
for (const [event, bindings] of Object.entries(config.hooks.on)) {
|
|
592
|
+
if (!allowedEvents.has(event)) {
|
|
593
|
+
issues.push({
|
|
594
|
+
path: `hooks.on.${event}`,
|
|
595
|
+
message: `unsupported hook event '${event}'`
|
|
596
|
+
});
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
for (const [index, binding] of bindings.entries()) {
|
|
600
|
+
if (!ID_RE.test(binding.id)) issues.push({
|
|
601
|
+
path: `hooks.on.${event}.${index}.id`,
|
|
602
|
+
message: `hook binding id '${binding.id}' must match ${ID_RE.source}`
|
|
603
|
+
});
|
|
604
|
+
if (!config.hooks.functions[binding.function]) issues.push({
|
|
605
|
+
path: `hooks.on.${event}.${index}.function`,
|
|
606
|
+
message: `hook binding '${binding.id}' references missing function '${binding.function}'`
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
609
611
|
function validateProfile(profileId, profile, runner, config, issues, projectRoot, options = {}) {
|
|
610
612
|
validateActor(`profile '${profileId}'`, `profiles.${profileId}`, profile, runner, config, issues, projectRoot, options);
|
|
611
613
|
validateListCapability(`profiles.${profileId}.output.format`, profile.output?.format ? [profile.output.format] : void 0, runner.capabilities.output_formats, "output format", issues);
|
|
@@ -629,7 +631,10 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
|
|
|
629
631
|
validatePath(`${path}.instructions.path`, actor.instructions.path, projectRoot, issues, options);
|
|
630
632
|
validateReferences(`${path}.rules`, actor.rules, config.rules, "rule", issues);
|
|
631
633
|
validateReferences(`${path}.skills`, actor.skills, config.skills, "skill", issues);
|
|
632
|
-
validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.
|
|
634
|
+
validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_gateway ? {
|
|
635
|
+
...config.mcp_servers,
|
|
636
|
+
[PIPELINE_GATEWAY_SERVER_ID]: {}
|
|
637
|
+
} : config.mcp_servers, "MCP server", issues);
|
|
633
638
|
validateBooleanCapability(`${path}.rules`, actor.rules, runner.capabilities.rules, "rules", issues);
|
|
634
639
|
validateBooleanCapability(`${path}.skills`, actor.skills, runner.capabilities.skills, "skills", issues);
|
|
635
640
|
validateBooleanCapability(`${path}.mcp_servers`, actor.mcp_servers, runner.capabilities.mcp_servers, "MCP servers", issues);
|
|
@@ -638,7 +643,6 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
|
|
|
638
643
|
validateListCapability(`${path}.network.mode`, actor.network?.mode ? [actor.network.mode] : void 0, runner.capabilities.network, "network mode", issues);
|
|
639
644
|
}
|
|
640
645
|
function validateWorkflow(workflowId, workflow, config, issues, projectRoot, options = {}) {
|
|
641
|
-
validateReferences(`workflows.${workflowId}.hooks`, workflow.hooks, config.hooks, "hook", issues);
|
|
642
646
|
const nodeIds = /* @__PURE__ */ new Set();
|
|
643
647
|
for (const node of workflow.nodes) {
|
|
644
648
|
if (nodeIds.has(node.id)) issues.push({
|
|
@@ -661,7 +665,6 @@ function validateWorkflowNode(workflowId, node, nodeIds, config, issues) {
|
|
|
661
665
|
path: `workflows.${workflowId}.nodes.${node.id}.needs`,
|
|
662
666
|
message: `node '${node.id}' references missing dependency '${need}'`
|
|
663
667
|
});
|
|
664
|
-
validateReferences(`workflows.${workflowId}.nodes.${node.id}.hooks`, node.hooks, config.hooks, "hook", issues);
|
|
665
668
|
validateWorkflowNodeKind(workflowId, node, config, issues);
|
|
666
669
|
if (node.kind === "parallel") validateParallelWorkflowNode(workflowId, node, config, issues);
|
|
667
670
|
}
|