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.removeOrphanedNetworks = exports.removeItemNetworks = exports.getNetworkHostConfig = exports.removeItemNetwork = exports.prepareItemNetwork = exports.checkNetworkLimitSupport = exports.getProxyNetworkName = exports.getRunNetworkName = exports.getItemNetworkName = void 0;
|
|
3
|
+
exports.removeOrphanedNetworks = exports.removeItemNetworks = exports.getNetworkHostConfig = exports.getPortBinding = exports.removeItemNetwork = exports.prepareItemNetwork = exports.checkNetworkLimitSupport = exports.getProxyNetworkName = exports.getRunNetworkName = exports.getItemNetworkName = void 0;
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const stream_1 = require("stream");
|
|
6
6
|
const work_service_1 = require("../planner/work-service");
|
|
@@ -258,7 +258,7 @@ options) {
|
|
|
258
258
|
await removeItemNetwork(docker, name);
|
|
259
259
|
throw e;
|
|
260
260
|
}
|
|
261
|
-
const publish = (options.publish ?? []).filter((port) =>
|
|
261
|
+
const publish = (options.publish ?? []).filter((port) => port.hostPort !== null);
|
|
262
262
|
return {
|
|
263
263
|
options: {
|
|
264
264
|
mode: name,
|
|
@@ -272,8 +272,9 @@ options) {
|
|
|
272
272
|
await docker.getNetwork(getProxyNetworkName(name)).connect({ Container: container.id });
|
|
273
273
|
}
|
|
274
274
|
if (publish.length > 0) {
|
|
275
|
-
await startPortRelay(docker, item, environment, name, container, publish, options);
|
|
275
|
+
return await startPortRelay(docker, item, environment, name, container, publish, options);
|
|
276
276
|
}
|
|
277
|
+
return undefined;
|
|
277
278
|
}
|
|
278
279
|
catch (e) {
|
|
279
280
|
await removeItemNetwork(docker, name);
|
|
@@ -374,12 +375,13 @@ async function startPortRelay(docker, item, environment, networkName, service, p
|
|
|
374
375
|
ExposedPorts: Object.fromEntries(ports.map((port) => [`${port.containerPort}/tcp`, {}])),
|
|
375
376
|
HostConfig: {
|
|
376
377
|
...HELPER_HOST_CONFIG,
|
|
377
|
-
PortBindings: Object.fromEntries(ports.map((port) => [`${port.containerPort}/tcp`, [
|
|
378
|
+
PortBindings: Object.fromEntries(ports.map((port) => [`${port.containerPort}/tcp`, [getPortBinding(port)]])),
|
|
378
379
|
},
|
|
379
380
|
});
|
|
380
381
|
await docker.getNetwork(networkName).connect({ Container: container.id });
|
|
381
382
|
await container.start();
|
|
382
|
-
item.status.write('debug', `ports ${ports.map((port) => port.hostPort).join(', ')} published by a relay`);
|
|
383
|
+
item.status.write('debug', `ports ${ports.map((port) => port.hostPort || 'any').join(', ')} published by a relay`);
|
|
384
|
+
return container;
|
|
383
385
|
}
|
|
384
386
|
// The network, its proxy network, its proxy and relay, and every endpoint on
|
|
385
387
|
// them: what an earlier run left behind, or what the item used.
|
|
@@ -429,6 +431,12 @@ async function removeNetwork(docker, name) {
|
|
|
429
431
|
}
|
|
430
432
|
}
|
|
431
433
|
}
|
|
434
|
+
// A port of the host for a port of a container: a free one for 0, on one address of it
|
|
435
|
+
// when the port names one.
|
|
436
|
+
function getPortBinding(port) {
|
|
437
|
+
return { HostPort: port.hostPort ? `${port.hostPort}` : '', ...(port.hostIp ? { HostIp: port.hostIp } : {}) };
|
|
438
|
+
}
|
|
439
|
+
exports.getPortBinding = getPortBinding;
|
|
432
440
|
function getNetworkHostConfig(network) {
|
|
433
441
|
return {
|
|
434
442
|
...(network.mode ? { NetworkMode: network.mode } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dockerService = exports.buildServiceCreateOptions = void 0;
|
|
3
|
+
exports.dockerService = exports.buildServiceCreateOptions = exports.readPublishedPorts = void 0;
|
|
4
4
|
const work_service_1 = require("../planner/work-service");
|
|
5
5
|
const run_labels_1 = require("../docker/run-labels");
|
|
6
6
|
const service_hostname_1 = require("../planner/utils/service-hostname");
|
|
@@ -22,6 +22,28 @@ const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
|
22
22
|
const docker_resources_1 = require("./docker-resources");
|
|
23
23
|
const egress_proxy_1 = require("../network/egress-proxy");
|
|
24
24
|
const HEALTHCHECK_INTERVAL_MS = 1000;
|
|
25
|
+
// Where docker published the ports of a container (or of its relay), for those that asked
|
|
26
|
+
// for a free one.
|
|
27
|
+
async function readPublishedPorts(container, ports) {
|
|
28
|
+
const wanted = ports.filter((port) => port.hostPort === 0);
|
|
29
|
+
if (wanted.length === 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const bound = (await container.inspect()).NetworkSettings.Ports ?? {};
|
|
33
|
+
return wanted.flatMap((port) => {
|
|
34
|
+
const binding = bound[`${port.containerPort}/tcp`]?.find((candidate) => candidate.HostPort);
|
|
35
|
+
return binding
|
|
36
|
+
? [
|
|
37
|
+
{
|
|
38
|
+
containerPort: port.containerPort,
|
|
39
|
+
host: port.hostIp ?? (binding.HostIp && binding.HostIp !== '0.0.0.0' ? binding.HostIp : '127.0.0.1'),
|
|
40
|
+
port: Number(binding.HostPort),
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
: [];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
exports.readPublishedPorts = readPublishedPorts;
|
|
25
47
|
function buildServiceCreateOptions(item, options, network, secrets, resources = {}) {
|
|
26
48
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
|
|
27
49
|
const command = (0, work_service_1.getServiceCommand)(item.data);
|
|
@@ -58,12 +80,9 @@ function buildServiceCreateOptions(item, options, network, secrets, resources =
|
|
|
58
80
|
// collide with another run on the same host. A limited service's are
|
|
59
81
|
// published by its relay.
|
|
60
82
|
PortBindings: options.publishPorts && !network.relayPorts
|
|
61
|
-
? item.data.ports
|
|
62
|
-
.filter((p) =>
|
|
63
|
-
.
|
|
64
|
-
map[`${port.containerPort}/tcp`] = [{ HostPort: `${port.hostPort}` }];
|
|
65
|
-
return map;
|
|
66
|
-
}, {})
|
|
83
|
+
? Object.fromEntries(item.data.ports
|
|
84
|
+
.filter((p) => p.hostPort !== null)
|
|
85
|
+
.map((port) => [`${port.containerPort}/tcp`, [(0, item_network_1.getPortBinding)(port)]]))
|
|
67
86
|
: {},
|
|
68
87
|
...resources,
|
|
69
88
|
},
|
|
@@ -90,16 +109,18 @@ async function dockerService(docker, item, options, environment) {
|
|
|
90
109
|
const resources = await (0, docker_resources_1.resolveDockerResources)(docker, item.data.resources, item.status);
|
|
91
110
|
await (0, execution_steps_1.refuseHostLinks)(item, environment);
|
|
92
111
|
container = await docker.createContainer(buildServiceCreateOptions(item, options, network.options, secrets, resources));
|
|
93
|
-
await network.connect(container);
|
|
112
|
+
const relay = await network.connect(container);
|
|
94
113
|
const stream = await container.attach({ stream: true, stdout: true, stderr: true });
|
|
95
114
|
(0, stream_1.logStream)(item.status, stream);
|
|
96
115
|
await container.start();
|
|
116
|
+
const published = options.publishPorts ? await readPublishedPorts(relay ?? container, item.data.ports) : [];
|
|
97
117
|
if (!item.data.healthcheck || !options.waitForReady) {
|
|
98
118
|
options.state.set({
|
|
99
119
|
type: 'running',
|
|
100
120
|
dns: { containerId: container.id },
|
|
101
121
|
stateKey: options.stateKey,
|
|
102
122
|
remote: null,
|
|
123
|
+
...(published.length > 0 ? { published } : {}),
|
|
103
124
|
});
|
|
104
125
|
}
|
|
105
126
|
else {
|
|
@@ -125,6 +146,7 @@ async function dockerService(docker, item, options, environment) {
|
|
|
125
146
|
dns: { containerId: container.id },
|
|
126
147
|
stateKey: options.stateKey,
|
|
127
148
|
remote: null,
|
|
149
|
+
...(published.length > 0 ? { published } : {}),
|
|
128
150
|
});
|
|
129
151
|
}
|
|
130
152
|
}
|
package/dist/harness/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getHarnessEnvironment = exports.connectSandbox = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
|
|
3
|
+
exports.getHarnessEnvironment = exports.connectSandbox = exports.connectServer = exports.hasShell = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
|
|
4
4
|
const get_file_context_1 = require("../file/get-file-context");
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const work_item_status_1 = require("../planner/work-item-status");
|
|
@@ -20,11 +20,21 @@ function harnessOptionsFromEnv(env) {
|
|
|
20
20
|
repo: value('HAMMERKIT_REPO'),
|
|
21
21
|
ref: value('HAMMERKIT_REF'),
|
|
22
22
|
build: value('HAMMERKIT_BUILD'),
|
|
23
|
-
shell
|
|
23
|
+
// set but empty is no shell, unset the default one
|
|
24
|
+
shell: env.HAMMERKIT_SHELL !== undefined ? env.HAMMERKIT_SHELL.trim() : undefined,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
exports.harnessOptionsFromEnv = harnessOptionsFromEnv;
|
|
27
|
-
|
|
28
|
+
// whether the clients offer a shell tool
|
|
29
|
+
function hasShell(options) {
|
|
30
|
+
return options.shell !== '';
|
|
31
|
+
}
|
|
32
|
+
exports.hasShell = hasShell;
|
|
33
|
+
/**
|
|
34
|
+
* The registered server a harness works on and the published version it runs, checked:
|
|
35
|
+
* what the clients need to list the tasks of the version before they take a sandbox.
|
|
36
|
+
*/
|
|
37
|
+
async function connectServer(environment, options) {
|
|
28
38
|
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
29
39
|
const name = options.server ?? config.run.on;
|
|
30
40
|
if (!name || !(0, host_config_1.hasServer)(config.servers, name)) {
|
|
@@ -40,7 +50,11 @@ async function connectSandbox(environment, options, log = () => undefined) {
|
|
|
40
50
|
throw new Error(`${options.build} is not the hash of a published version (sha256:...), name it as hammerkit remote publish printed it (HAMMERKIT_BUILD or --build)`);
|
|
41
51
|
}
|
|
42
52
|
const server = config.servers[name];
|
|
43
|
-
|
|
53
|
+
return { name, connection: { url: server.url, ...(0, token_source_1.getTokenSource)(environment, name, server) }, build: options.build };
|
|
54
|
+
}
|
|
55
|
+
exports.connectServer = connectServer;
|
|
56
|
+
async function connectSandbox(environment, options, log = () => undefined) {
|
|
57
|
+
const { name, connection, build } = await connectServer(environment, options);
|
|
44
58
|
let sandbox;
|
|
45
59
|
if (options.sandbox) {
|
|
46
60
|
sandbox = await sandbox_client_1.SandboxClient.get(connection, options.sandbox);
|
|
@@ -59,7 +73,7 @@ async function connectSandbox(environment, options, log = () => undefined) {
|
|
|
59
73
|
else {
|
|
60
74
|
throw new Error('name a sandbox (HAMMERKIT_SANDBOX or --sandbox), or a repository and ref to create one from (HAMMERKIT_REPO and HAMMERKIT_REF, or --repo and --ref)');
|
|
61
75
|
}
|
|
62
|
-
return new sandbox_client_1.SandboxClient(connection, sandbox, { hash:
|
|
76
|
+
return new sandbox_client_1.SandboxClient(connection, sandbox, { hash: build, shell: options.shell ?? exports.DEFAULT_SHELL_TASK });
|
|
63
77
|
}
|
|
64
78
|
exports.connectSandbox = connectSandbox;
|
|
65
79
|
// A client that starts with a repository and ref takes a sandbox of the caller's of the same
|
|
@@ -6,6 +6,15 @@ const log_1 = require("../log");
|
|
|
6
6
|
const version_1 = require("../version");
|
|
7
7
|
const connect_1 = require("./connect");
|
|
8
8
|
const mcp_1 = require("./mcp");
|
|
9
|
+
const task_tools_1 = require("./task-tools");
|
|
10
|
+
// the tasks run_task lists; a server that does not answer leaves them out, the tools still work
|
|
11
|
+
async function listTasks(sandbox, log) {
|
|
12
|
+
const listing = await (0, task_tools_1.readTaskListing)(() => sandbox.tasks());
|
|
13
|
+
if (!Array.isArray(listing)) {
|
|
14
|
+
log(`the tasks of ${sandbox.build.hash} could not be listed: ${listing.error}`);
|
|
15
|
+
}
|
|
16
|
+
return listing;
|
|
17
|
+
}
|
|
9
18
|
// `hammerkit remote mcp`: a Model Context Protocol server on stdio for one sandbox of a
|
|
10
19
|
// server. It lives under `remote`, which already is a command: a top-level `mcp` would hide a
|
|
11
20
|
// task named `mcp`, since `hammerkit <task>` runs a task. Like the other `remote` commands it
|
|
@@ -21,20 +30,27 @@ function registerMcpCommand(remote, environment, input = process.stdin) {
|
|
|
21
30
|
.addOption(new commander_1.Option('--repo <repo>', 'create a sandbox of this repository').env('HAMMERKIT_REPO'))
|
|
22
31
|
.addOption(new commander_1.Option('--ref <ref>', 'at this branch').env('HAMMERKIT_REF'))
|
|
23
32
|
.addOption(new commander_1.Option('--build <hash>', 'hash of the published version of the build file whose tasks run on it').env('HAMMERKIT_BUILD'))
|
|
24
|
-
.addOption(new commander_1.Option('--shell <task>',
|
|
33
|
+
.addOption(new commander_1.Option('--shell <task>', "its task whose last command is a shell, like sh -c; '' for no bash tool")
|
|
25
34
|
.env('HAMMERKIT_SHELL')
|
|
26
35
|
.default(connect_1.DEFAULT_SHELL_TASK))
|
|
27
36
|
.action(async (options) => {
|
|
28
37
|
try {
|
|
29
38
|
// stdout carries the protocol only, everything else goes to stderr
|
|
30
39
|
const log = (line) => environment.stderr.write(`hammerkit remote mcp: ${line}\n`);
|
|
31
|
-
|
|
40
|
+
// as the plugins read it: blank is no shell
|
|
41
|
+
const sandbox = await (0, connect_1.connectSandbox)(environment, { ...options, shell: options.shell.trim() }, log);
|
|
32
42
|
log(`sandbox ${sandbox.id} of ${sandbox.sandbox.repo} at ${sandbox.sandbox.commit}`);
|
|
43
|
+
const shell = sandbox.build.shell;
|
|
33
44
|
const instructions = `The tools work on hammerkit sandbox ${sandbox.id}, a checkout of ${sandbox.sandbox.repo} at ` +
|
|
34
|
-
`${sandbox.sandbox.commit} on a server. Paths are relative to its root.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
`${sandbox.sandbox.commit} on a server. Paths are relative to its root. ` +
|
|
46
|
+
(shell
|
|
47
|
+
? `bash runs a command as the task ${shell} of the published build file ${sandbox.build.hash}, ` +
|
|
48
|
+
'run_task runs its other tasks; only the files are kept between commands. '
|
|
49
|
+
: `run_task runs the tasks of the published build file ${sandbox.build.hash}; only the files are ` +
|
|
50
|
+
'kept between runs. ') +
|
|
51
|
+
'changes shows what changed since the fetch.';
|
|
52
|
+
const tasks = await listTasks(sandbox, log);
|
|
53
|
+
const served = (0, mcp_1.serveMcp)(input, environment.stdout, (0, mcp_1.createMcpTools)(async () => sandbox, { tasks, shell: shell || false }), {
|
|
38
54
|
name: 'hammerkit',
|
|
39
55
|
version: (0, version_1.getVersion)(),
|
|
40
56
|
instructions,
|
package/dist/harness/mcp.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.serveMcp = exports.createMcpTools = void 0;
|
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const format_1 = require("./format");
|
|
6
6
|
const sandbox_client_1 = require("./sandbox-client");
|
|
7
|
+
const task_tools_1 = require("./task-tools");
|
|
8
|
+
const service_tools_1 = require("./service-tools");
|
|
7
9
|
// the newest first; a client asking for an older one gets it
|
|
8
10
|
const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
9
11
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
@@ -25,40 +27,47 @@ const flag = (description) => ({ type: 'boolean', description });
|
|
|
25
27
|
function sandboxPath(path) {
|
|
26
28
|
return (path ?? '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
27
29
|
}
|
|
28
|
-
function readLog(run, task) {
|
|
29
|
-
return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
|
|
30
|
-
}
|
|
31
|
-
function describeRun(task, ran) {
|
|
32
|
-
const result = ran.result;
|
|
33
|
-
const lines = [`run ${ran.run.id}: ${ran.run.state}`];
|
|
34
|
-
if (result) {
|
|
35
|
-
lines.push(`task ${task}: ${result.status}` +
|
|
36
|
-
(result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
|
|
37
|
-
(result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
|
|
38
|
-
(result.cache?.hit ? ', from the cache' : ''));
|
|
39
|
-
if (result.error) {
|
|
40
|
-
lines.push(`error: ${result.error}`);
|
|
41
|
-
}
|
|
42
|
-
if (result.cache?.causes?.length) {
|
|
43
|
-
lines.push(`ran because: ${result.cache.causes.join('; ')}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else if (ran.run.error) {
|
|
47
|
-
lines.push(`error: ${ran.run.error}`);
|
|
48
|
-
}
|
|
49
|
-
if (result && result.lastLog.length > 0) {
|
|
50
|
-
lines.push('last lines:', ...result.lastLog);
|
|
51
|
-
}
|
|
52
|
-
lines.push(readLog(ran.run.id, task));
|
|
53
|
-
return lines.join('\n');
|
|
54
|
-
}
|
|
55
30
|
/**
|
|
56
31
|
* The tools of `hammerkit remote mcp`, for chat clients and agents without file and shell tools of
|
|
57
32
|
* their own. They take the shapes models are trained on: numbered reads with offset and
|
|
58
33
|
* limit, exact-string replace, `apply_patch` or a git diff, ripgrep's parameters, a shell
|
|
59
|
-
* command. Paths are relative to the root of the sandbox.
|
|
34
|
+
* command. Paths are relative to the root of the sandbox. `tasks` are the tasks the description
|
|
35
|
+
* of `run_task` lists; without `shell` there is no `bash`.
|
|
60
36
|
*/
|
|
61
|
-
function createMcpTools(client) {
|
|
37
|
+
function createMcpTools(client, options = {}) {
|
|
38
|
+
const [runTask, runLogs] = (0, task_tools_1.createTaskTools)(options.tasks, options.shell || undefined).map((tool) => fromTaskTool(tool, client));
|
|
39
|
+
const bash = {
|
|
40
|
+
name: 'bash',
|
|
41
|
+
description: 'Run a shell command on the sandbox and answer its output and exit code. It runs as the shell task of ' +
|
|
42
|
+
'the published build file, in its container and with its network; commands run one at a time, and ' +
|
|
43
|
+
'only the files are kept between them.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
command: text('The command'),
|
|
48
|
+
workdir: text('The directory to run it in, relative to the root of the sandbox'),
|
|
49
|
+
timeout: count('Stop the command after this many seconds', 1),
|
|
50
|
+
},
|
|
51
|
+
required: ['command'],
|
|
52
|
+
},
|
|
53
|
+
annotations: { title: 'Run a command' },
|
|
54
|
+
async call(args, signal) {
|
|
55
|
+
const sandbox = await client();
|
|
56
|
+
const ran = await sandbox.exec(args.command, sandboxPath(args.workdir), {
|
|
57
|
+
signal,
|
|
58
|
+
timeoutSeconds: args.timeout,
|
|
59
|
+
});
|
|
60
|
+
const output = (0, format_1.capOutput)(ran.output, (0, task_tools_1.readLogHint)(ran.run.id, sandbox.build.shell));
|
|
61
|
+
const lines = output ? [output, ''] : [];
|
|
62
|
+
if (ran.exitCode === null) {
|
|
63
|
+
lines.push(`hammerkit: ${ran.result?.error ?? ran.run.error ?? `the run ended ${ran.run.state}`}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push(`exit code ${ran.exitCode}`);
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
},
|
|
70
|
+
};
|
|
62
71
|
return inOrder([
|
|
63
72
|
{
|
|
64
73
|
name: 'read',
|
|
@@ -185,91 +194,9 @@ function createMcpTools(client) {
|
|
|
185
194
|
return (0, format_1.formatTree)(await (await client()).tree({ path, glob: args.pattern, limit: 500 }));
|
|
186
195
|
},
|
|
187
196
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
'the published build file, in its container and with its network; commands run one at a time, and ' +
|
|
192
|
-
'only the files are kept between them.',
|
|
193
|
-
inputSchema: {
|
|
194
|
-
type: 'object',
|
|
195
|
-
properties: {
|
|
196
|
-
command: text('The command'),
|
|
197
|
-
workdir: text('The directory to run it in, relative to the root of the sandbox'),
|
|
198
|
-
timeout: count('Stop the command after this many seconds', 1),
|
|
199
|
-
},
|
|
200
|
-
required: ['command'],
|
|
201
|
-
},
|
|
202
|
-
annotations: { title: 'Run a command' },
|
|
203
|
-
async call(args, signal) {
|
|
204
|
-
const sandbox = await client();
|
|
205
|
-
const ran = await sandbox.exec(args.command, sandboxPath(args.workdir), {
|
|
206
|
-
signal,
|
|
207
|
-
timeoutSeconds: args.timeout,
|
|
208
|
-
});
|
|
209
|
-
const output = (0, format_1.capOutput)(ran.output, readLog(ran.run.id, sandbox.build.shell));
|
|
210
|
-
const lines = output ? [output, ''] : [];
|
|
211
|
-
if (ran.exitCode === null) {
|
|
212
|
-
lines.push(`hammerkit: ${ran.result?.error ?? ran.run.error ?? `the run ended ${ran.run.state}`}`);
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
lines.push(`exit code ${ran.exitCode}`);
|
|
216
|
-
}
|
|
217
|
-
return lines.join('\n');
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
name: 'run_task',
|
|
222
|
-
description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
|
|
223
|
-
'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
|
|
224
|
-
'its last command, for example a test file.',
|
|
225
|
-
inputSchema: {
|
|
226
|
-
type: 'object',
|
|
227
|
-
properties: {
|
|
228
|
-
task: text('The name of the task'),
|
|
229
|
-
args: { type: 'array', items: { type: 'string' }, description: 'Arguments for its last command' },
|
|
230
|
-
},
|
|
231
|
-
required: ['task'],
|
|
232
|
-
},
|
|
233
|
-
annotations: { title: 'Run a task' },
|
|
234
|
-
async call(args, signal) {
|
|
235
|
-
const ran = await (await client()).runTask(args.task, { args: args.args, signal });
|
|
236
|
-
return describeRun(args.task, ran);
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'run_logs',
|
|
241
|
-
description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
|
|
242
|
-
'the lines that contain a text (grep, a fixed string) with lines of context around them.',
|
|
243
|
-
inputSchema: {
|
|
244
|
-
type: 'object',
|
|
245
|
-
properties: {
|
|
246
|
-
run: text('The id of the run'),
|
|
247
|
-
task: text('Only the lines of this task'),
|
|
248
|
-
tail: count('The last lines, or with grep the last matches', 1, 1000),
|
|
249
|
-
from: count('The first line to read', 1),
|
|
250
|
-
limit: count('How many lines to read', 1, 1000),
|
|
251
|
-
grep: text('Only lines that contain this text'),
|
|
252
|
-
ignore_case: flag('Ignore case in grep'),
|
|
253
|
-
context: count('Lines before and after each match', 0, 20),
|
|
254
|
-
},
|
|
255
|
-
required: ['run'],
|
|
256
|
-
},
|
|
257
|
-
annotations: { title: 'Read the log of a run', ...READ_ONLY },
|
|
258
|
-
async call(args) {
|
|
259
|
-
const page = await (await client()).logs(args.run, {
|
|
260
|
-
task: args.task,
|
|
261
|
-
tail: args.tail,
|
|
262
|
-
from: args.from,
|
|
263
|
-
limit: args.limit,
|
|
264
|
-
grep: args.grep,
|
|
265
|
-
i: args.ignore_case,
|
|
266
|
-
context: args.context,
|
|
267
|
-
});
|
|
268
|
-
const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
|
|
269
|
-
lines.push('', `(${page.total} lines${args.grep ? ` searched, ${page.lines.filter((line) => line.match !== false).length} match here` : ''}${page.next ? `, read on with from ${page.next}` : ''}${page.truncated ? ', the server cut the log off at its limit' : ''})`);
|
|
270
|
-
return lines.join('\n');
|
|
271
|
-
},
|
|
272
|
-
},
|
|
197
|
+
...(options.shell === false ? [] : [bash]),
|
|
198
|
+
runTask,
|
|
199
|
+
{ ...runLogs, annotations: { ...runLogs.annotations, ...READ_ONLY } },
|
|
273
200
|
{
|
|
274
201
|
name: 'changes',
|
|
275
202
|
description: 'What changed in the sandbox since the commit it was fetched at: a summary of the files, a diff, or a ' +
|
|
@@ -315,9 +242,30 @@ function createMcpTools(client) {
|
|
|
315
242
|
return `pushed ${pushed.commit} to ${pushed.branch}`;
|
|
316
243
|
},
|
|
317
244
|
},
|
|
245
|
+
...service_tools_1.SERVICE_TOOLS.map((tool) => ({
|
|
246
|
+
name: tool.name,
|
|
247
|
+
description: tool.description,
|
|
248
|
+
inputSchema: (0, service_tools_1.serviceToolSchema)(tool),
|
|
249
|
+
annotations: { title: tool.title, readOnlyHint: tool.name === 'endpoint_get', openWorldHint: false },
|
|
250
|
+
async call(args, signal) {
|
|
251
|
+
return tool.call(await client(), args, signal);
|
|
252
|
+
},
|
|
253
|
+
})),
|
|
318
254
|
]);
|
|
319
255
|
}
|
|
320
256
|
exports.createMcpTools = createMcpTools;
|
|
257
|
+
// a tool shared with the other harnesses, as an MCP tool
|
|
258
|
+
function fromTaskTool(tool, client) {
|
|
259
|
+
return {
|
|
260
|
+
name: tool.name,
|
|
261
|
+
description: tool.description,
|
|
262
|
+
inputSchema: (0, task_tools_1.taskToolSchema)(tool),
|
|
263
|
+
annotations: { title: tool.title },
|
|
264
|
+
async call(args, signal) {
|
|
265
|
+
return tool.call(await client(), args, signal);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
321
269
|
// the calls of a client go to the sandbox one at a time, in the order the client sent them
|
|
322
270
|
function inOrder(tools) {
|
|
323
271
|
const inTurn = (0, sandbox_client_1.oneAtATime)();
|