hammerkit 1.27.0-next.13 → 1.27.0-next.15
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/dist/docker/item-network.js +13 -5
- package/dist/executer/docker-service.js +30 -8
- package/dist/harness/connect.js +19 -5
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +62 -114
- package/dist/harness/opencode.js +151 -50
- package/dist/harness/pi.js +14 -0
- package/dist/harness/sandbox-client.js +81 -2
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +210 -0
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/remote/remote-work-commands.js +98 -12
- package/dist/schema/work-scope-parser.js +6 -0
- package/dist/server/accounts-api.js +2 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +14 -1
- package/dist/server/kubernetes-backend.js +200 -33
- package/dist/server/plan.js +1 -0
- package/dist/server/policy.js +24 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +44 -2
- package/dist/server/run-worker.js +130 -9
- package/dist/server/runs.js +317 -24
- package/dist/server/server-config.js +160 -9
- package/dist/server/server.js +210 -14
- package/dist/server/service-runs.js +82 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.workerMain = exports.taskCauses = exports.runTasks = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
3
|
+
exports.workerMain = exports.readInput = exports.taskCauses = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const cli_1 = require("../cli");
|
|
@@ -21,6 +21,7 @@ const network_schema_1 = require("../schema/network-schema");
|
|
|
21
21
|
const work_network_1 = require("../planner/work-network");
|
|
22
22
|
const run_plan_1 = require("./run-plan");
|
|
23
23
|
const runs_1 = require("./runs");
|
|
24
|
+
const resources_1 = require("../kubernetes/resources");
|
|
24
25
|
// one log line of a task is cut here, the registry caps how many events a run keeps
|
|
25
26
|
const MAX_LOG_TEXT = 8 * 1024;
|
|
26
27
|
/**
|
|
@@ -59,6 +60,15 @@ exports.workerInputSchema = zod_1.z
|
|
|
59
60
|
.string()
|
|
60
61
|
.regex(/^[a-z]+-[0-9a-f]{24}$/)
|
|
61
62
|
.optional(),
|
|
63
|
+
// a service run: the services it keeps up instead of tasks, and the ports the server
|
|
64
|
+
// reaches as endpoints
|
|
65
|
+
up: zod_1.z
|
|
66
|
+
.object({
|
|
67
|
+
services: zod_1.z.array(zod_1.z.string()).min(1),
|
|
68
|
+
endpoints: zod_1.z.array(zod_1.z.object({ service: zod_1.z.string(), port: zod_1.z.number().int() }).strict()),
|
|
69
|
+
})
|
|
70
|
+
.strict()
|
|
71
|
+
.optional(),
|
|
62
72
|
})
|
|
63
73
|
.strict();
|
|
64
74
|
// The ceiling of the run, combined here and not in the server: the lower of two allow lists
|
|
@@ -68,6 +78,60 @@ function getRunNetwork(input) {
|
|
|
68
78
|
return input.sandboxNetwork ? (0, work_network_1.limitNetwork)((0, work_network_1.parseWorkNetwork)(input.sandboxNetwork), runs) : runs;
|
|
69
79
|
}
|
|
70
80
|
exports.getRunNetwork = getRunNetwork;
|
|
81
|
+
/**
|
|
82
|
+
* The ports of the services of a service run as the server publishes them: on a host the
|
|
83
|
+
* ports of endpoints go to a free port of the loopback interface, where only the server
|
|
84
|
+
* reaches them, and no other port goes to the host, whatever the build file says (`up`
|
|
85
|
+
* would publish them on every interface, where two runs collide). On a cluster nothing is
|
|
86
|
+
* published, the server reaches the Service of each.
|
|
87
|
+
*/
|
|
88
|
+
function publishEndpoints(workTree, endpoints, cluster) {
|
|
89
|
+
for (const service of Object.values(workTree.services)) {
|
|
90
|
+
if (!('ports' in service.data)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
service.data.ports = service.data.ports.map((port) => !cluster &&
|
|
94
|
+
endpoints.some((endpoint) => endpoint.service === service.name && endpoint.port === port.containerPort)
|
|
95
|
+
? { containerPort: port.containerPort, hostPort: 0, hostIp: '127.0.0.1' }
|
|
96
|
+
: { containerPort: port.containerPort, hostPort: null });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.publishEndpoints = publishEndpoints;
|
|
100
|
+
// Where the server reaches each endpoint, once every service the run keeps up is ready;
|
|
101
|
+
// null until then.
|
|
102
|
+
function getEndpointTargets(workTree, up, kubernetes) {
|
|
103
|
+
const services = Object.values(workTree.services).filter((service) => up.services.includes(service.name));
|
|
104
|
+
if (services.length < up.services.length || services.some((service) => service.state.current.type !== 'running')) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const targets = [];
|
|
108
|
+
for (const endpoint of up.endpoints) {
|
|
109
|
+
const service = services.find((candidate) => candidate.name === endpoint.service);
|
|
110
|
+
const state = service?.state.current;
|
|
111
|
+
if (!service || state?.type !== 'running') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (kubernetes) {
|
|
115
|
+
targets.push({
|
|
116
|
+
service: endpoint.service,
|
|
117
|
+
port: endpoint.port,
|
|
118
|
+
address: { host: `${(0, resources_1.getResourceName)(service, kubernetes)}.${kubernetes.namespace}.svc`, port: endpoint.port },
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const published = state.published?.find((candidate) => candidate.containerPort === endpoint.port);
|
|
123
|
+
if (!published) {
|
|
124
|
+
throw new Error(`${endpoint.service} is up, but its port ${endpoint.port} was not published`);
|
|
125
|
+
}
|
|
126
|
+
targets.push({
|
|
127
|
+
service: endpoint.service,
|
|
128
|
+
port: endpoint.port,
|
|
129
|
+
address: { host: published.host, port: published.port },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return targets;
|
|
133
|
+
}
|
|
134
|
+
exports.getEndpointTargets = getEndpointTargets;
|
|
71
135
|
/**
|
|
72
136
|
* Executes the tasks of a run with the engine of `hammerkit run`. The caller says which
|
|
73
137
|
* process this is in: it reports what happens through `emit` and stops when `abortCtrl`
|
|
@@ -75,7 +139,9 @@ exports.getRunNetwork = getRunNetwork;
|
|
|
75
139
|
*/
|
|
76
140
|
async function runTasks(input, cwd, processEnvs, abortCtrl,
|
|
77
141
|
// whether the run was stopped from outside; the engine aborts too when a task fails
|
|
78
|
-
cancelled, emit
|
|
142
|
+
cancelled, emit,
|
|
143
|
+
// a service run: its services are ready, at these addresses
|
|
144
|
+
up) {
|
|
79
145
|
const secrets = (0, redact_1.createSecretRegistry)();
|
|
80
146
|
const status = (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets);
|
|
81
147
|
// the process starts in a directory of the server, the tasks work on the run's files
|
|
@@ -126,7 +192,7 @@ cancelled, emit) {
|
|
|
126
192
|
// why a task was not taken from the cache goes into its result
|
|
127
193
|
explain: true,
|
|
128
194
|
};
|
|
129
|
-
for (const { name, scope } of (0, run_plan_1.getRunScopes)(input.tasks, options)) {
|
|
195
|
+
for (const { name, scope } of (0, run_plan_1.getRunScopes)(input.tasks, options, input.up?.services)) {
|
|
130
196
|
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
|
|
131
197
|
directory: input.directory,
|
|
132
198
|
});
|
|
@@ -135,17 +201,44 @@ cancelled, emit) {
|
|
|
135
201
|
if (input.volumeScope && !input.kubernetes) {
|
|
136
202
|
(0, hosted_volumes_1.scopeServiceVolumes)(workTree, input.volumeScope);
|
|
137
203
|
}
|
|
204
|
+
if (input.up) {
|
|
205
|
+
publishEndpoints(workTree, input.up.endpoints, !!input.kubernetes);
|
|
206
|
+
}
|
|
138
207
|
const cli = (0, cli_1.getCli)(workTree, environment);
|
|
139
|
-
if (cli.tasks().length === 0) {
|
|
208
|
+
if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
|
|
140
209
|
throw new Error(`no tasks found for ${name}`);
|
|
141
210
|
}
|
|
142
|
-
const execution =
|
|
211
|
+
const execution = input.up
|
|
212
|
+
? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
|
|
213
|
+
: cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
|
|
143
214
|
const seen = new Map();
|
|
144
|
-
|
|
215
|
+
let ready = false;
|
|
216
|
+
const report = () => {
|
|
217
|
+
reportTasks(emit, execution.state.current, seen, (text) => secrets.redact(text));
|
|
218
|
+
if (input.up && up && !ready) {
|
|
219
|
+
const targets = getEndpointTargets(execution.state.current, input.up, input.kubernetes);
|
|
220
|
+
if (targets) {
|
|
221
|
+
ready = true;
|
|
222
|
+
up(targets);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
145
226
|
const watch = execution.state.on('run-events', report);
|
|
146
227
|
try {
|
|
147
228
|
const result = await execution.start();
|
|
148
229
|
report();
|
|
230
|
+
// on a cluster the services stay in their Deployments once they are ready, until
|
|
231
|
+
// the namespace of the run goes
|
|
232
|
+
if (input.up && input.kubernetes && ready && result.success && !abortCtrl.signal.aborted) {
|
|
233
|
+
await new Promise((resolve) => abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true }));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (input.up && ready) {
|
|
237
|
+
if (!abortCtrl.signal.aborted) {
|
|
238
|
+
throw new Error('the services stopped on their own');
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
149
242
|
if (!result.success) {
|
|
150
243
|
throw new Error(cancelled() ? 'the run was cancelled' : `${name} failed`);
|
|
151
244
|
}
|
|
@@ -218,9 +311,13 @@ redact) {
|
|
|
218
311
|
// The run is the first line of the standard input. The server keeps it open and writes
|
|
219
312
|
// `cancel` on it to stop the run: that works the same on every platform, where a signal
|
|
220
313
|
// on Windows ends the process without letting it stop its tasks.
|
|
314
|
+
// `onEnd` is told when the input ends before the run does: the server is gone.
|
|
221
315
|
function readInput(stdin, cancel) {
|
|
222
316
|
const lines = (0, readline_1.createInterface)({ input: stdin, crlfDelay: Infinity });
|
|
223
317
|
let first;
|
|
318
|
+
let closing = false;
|
|
319
|
+
let ended = false;
|
|
320
|
+
let onEnd;
|
|
224
321
|
const input = new Promise((resolve) => {
|
|
225
322
|
lines.on('line', (line) => {
|
|
226
323
|
if (first === undefined) {
|
|
@@ -231,10 +328,29 @@ function readInput(stdin, cancel) {
|
|
|
231
328
|
cancel();
|
|
232
329
|
}
|
|
233
330
|
});
|
|
234
|
-
lines.on('close', () =>
|
|
331
|
+
lines.on('close', () => {
|
|
332
|
+
resolve(first ?? '');
|
|
333
|
+
if (!closing) {
|
|
334
|
+
ended = true;
|
|
335
|
+
onEnd?.();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
235
338
|
});
|
|
236
|
-
return {
|
|
339
|
+
return {
|
|
340
|
+
input,
|
|
341
|
+
close: () => {
|
|
342
|
+
closing = true;
|
|
343
|
+
lines.close();
|
|
344
|
+
},
|
|
345
|
+
onEnd: (listener) => {
|
|
346
|
+
onEnd = listener;
|
|
347
|
+
if (ended) {
|
|
348
|
+
listener();
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
};
|
|
237
352
|
}
|
|
353
|
+
exports.readInput = readInput;
|
|
238
354
|
/**
|
|
239
355
|
* The process of one run: reads its input, executes the tasks in the current directory
|
|
240
356
|
* and reports as lines of JSON. It exits 0 when it could say how the run ended, and the
|
|
@@ -261,8 +377,13 @@ async function workerMain(stdin, stdout, processEnvs, cwd, abortCtrl) {
|
|
|
261
377
|
say({ kind: 'result', ok: false, error: `the run could not be read: ${e instanceof Error ? e.message : e}` });
|
|
262
378
|
return;
|
|
263
379
|
}
|
|
380
|
+
if (input.up) {
|
|
381
|
+
// services stay until they are stopped: without the server that would stop them, at
|
|
382
|
+
// their ttl or on a cancel, nothing would
|
|
383
|
+
reader.onEnd(stop);
|
|
384
|
+
}
|
|
264
385
|
try {
|
|
265
|
-
await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }));
|
|
386
|
+
await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }), (targets) => say({ kind: 'up', targets }));
|
|
266
387
|
say({ kind: 'result', ok: true });
|
|
267
388
|
}
|
|
268
389
|
catch (e) {
|