hammerkit 1.27.0-next.3 → 1.27.0-next.30
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/build.schema.json +188 -0
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +10 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +4 -1
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +168 -17
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/await-running-state.js +59 -13
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +35 -12
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/main.js +2 -0
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +19 -1
- package/dist/planner/utils/append-work-task.js +18 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +124 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +31 -10
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +30 -7
- package/dist/remote/remote-work-commands.js +426 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +8 -1
- package/dist/schema/build-file-container-service-schema.js +12 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +120 -11
- package/dist/schema/schema-parser.js +92 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +739 -0
- package/dist/server/executor.js +57 -5
- package/dist/server/kubernetes-backend.js +367 -65
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +188 -103
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +245 -12
- package/dist/server/runs.js +410 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +255 -14
- package/dist/server/server.js +269 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SERVICE_TOOLS = exports.describeServiceRun = exports.parseEndpoint = exports.serviceToolArgs = exports.serviceToolSchema = void 0;
|
|
4
|
+
// the most of a page an agent gets back
|
|
5
|
+
const MAX_PAGE_CHARS = 20000;
|
|
6
|
+
// The parameters as a JSON schema, for MCP, pi and opencode 2.
|
|
7
|
+
function serviceToolSchema(tool) {
|
|
8
|
+
return {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => [
|
|
11
|
+
name,
|
|
12
|
+
parameter.type === 'string'
|
|
13
|
+
? { type: 'string', description: parameter.description }
|
|
14
|
+
: { type: 'array', items: { type: 'string' }, description: parameter.description },
|
|
15
|
+
])),
|
|
16
|
+
required: tool.required,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
exports.serviceToolSchema = serviceToolSchema;
|
|
20
|
+
// The parameters with the zod of opencode 1.
|
|
21
|
+
function serviceToolArgs(tool, z) {
|
|
22
|
+
return Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
|
|
23
|
+
const type = parameter.type === 'string' ? z.string() : z.array(z.string());
|
|
24
|
+
return [name, (tool.required.includes(name) ? type : type.optional()).describe(parameter.description)];
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
exports.serviceToolArgs = serviceToolArgs;
|
|
28
|
+
// `web:8080` as the server takes an endpoint
|
|
29
|
+
function parseEndpoint(value) {
|
|
30
|
+
const index = value.lastIndexOf(':');
|
|
31
|
+
const port = Number(value.slice(index + 1));
|
|
32
|
+
if (index <= 0 || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
33
|
+
throw new Error(`${value} is not an endpoint, write it as service:port, such as web:8080`);
|
|
34
|
+
}
|
|
35
|
+
return { service: value.slice(0, index), port };
|
|
36
|
+
}
|
|
37
|
+
exports.parseEndpoint = parseEndpoint;
|
|
38
|
+
// How a service run stands, for an agent: where its endpoints are and until when.
|
|
39
|
+
function describeServiceRun(run) {
|
|
40
|
+
const lines = [`run ${run.id}: ${run.state}${run.up ? ` (${run.up.name})` : ''}`];
|
|
41
|
+
if (run.state === 'up') {
|
|
42
|
+
lines.push(`up until ${run.expiresAt}, stop it with services_stop when it is no longer needed`);
|
|
43
|
+
if (run.up?.sleepTtl) {
|
|
44
|
+
lines.push('without a request for a while the services sleep, the next request wakes them');
|
|
45
|
+
}
|
|
46
|
+
for (const endpoint of run.up?.endpoints ?? []) {
|
|
47
|
+
lines.push(`${endpoint.service}:${endpoint.port} -> ${endpoint.url}`);
|
|
48
|
+
}
|
|
49
|
+
if ((run.up?.endpoints ?? []).length > 0) {
|
|
50
|
+
lines.push('a person opens these addresses in a browser and signs in; read a page yourself with endpoint_get');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (run.error) {
|
|
54
|
+
lines.push(`error: ${run.error}`);
|
|
55
|
+
}
|
|
56
|
+
for (const [task, result] of Object.entries(run.results ?? {})) {
|
|
57
|
+
if (result.status === 'failed') {
|
|
58
|
+
lines.push(`${task} failed${result.error ? `: ${result.error}` : ''}`, ...result.lastLog);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (run.state !== 'up') {
|
|
62
|
+
lines.push(`read the log with run_logs { "run": "${run.id}" }`);
|
|
63
|
+
}
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
exports.describeServiceRun = describeServiceRun;
|
|
67
|
+
exports.SERVICE_TOOLS = [
|
|
68
|
+
{
|
|
69
|
+
name: 'services_up',
|
|
70
|
+
title: 'Start services',
|
|
71
|
+
description: 'Start services of the published build file on the hammerkit server, with the files of the sandbox (or of ' +
|
|
72
|
+
'a branch), and keep them up for a person to look at: each endpoint (service:port) gets an address of its ' +
|
|
73
|
+
'own behind the sign-in of the server. Answers once they are up, with the addresses and until when they ' +
|
|
74
|
+
'stay. A second start with the same name replaces the first, so its addresses stay the same. A server on ' +
|
|
75
|
+
'Kubernetes starts services from a branch only: push the sandbox and pass branch.',
|
|
76
|
+
parameters: {
|
|
77
|
+
services: { type: 'string[]', description: 'The services to start, as the build file names them' },
|
|
78
|
+
endpoints: { type: 'string[]', description: 'The ports to publish, as service:port, such as web:8080' },
|
|
79
|
+
ttl: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'How long they stay once ready, such as 2h, then they are removed; the server default when left out',
|
|
82
|
+
},
|
|
83
|
+
sleepTtl: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'How long they stay up without a request, such as 30m, then they sleep until the next request wakes ' +
|
|
86
|
+
'them (from a branch only); the server default when left out',
|
|
87
|
+
},
|
|
88
|
+
name: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Lower case letters, digits and -; the addresses carry it. The branch when left out',
|
|
91
|
+
},
|
|
92
|
+
branch: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Start from this pushed branch of the repository instead of the files of the sandbox',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ['services'],
|
|
98
|
+
async call(client, args, signal) {
|
|
99
|
+
const followed = await client.up({
|
|
100
|
+
services: args.services,
|
|
101
|
+
endpoints: (args.endpoints ?? []).map(parseEndpoint),
|
|
102
|
+
ttl: args.ttl || undefined,
|
|
103
|
+
sleepTtl: args.sleepTtl || undefined,
|
|
104
|
+
name: args.name || undefined,
|
|
105
|
+
branch: args.branch || undefined,
|
|
106
|
+
}, { signal });
|
|
107
|
+
return describeServiceRun(followed.run);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'endpoint_get',
|
|
112
|
+
title: 'Read a page of an endpoint',
|
|
113
|
+
description: 'GET a path of an endpoint of a service run started with services_up, signed in as you, and answer its ' +
|
|
114
|
+
'status and body: to check that the app answers before a person opens it.',
|
|
115
|
+
parameters: {
|
|
116
|
+
run: { type: 'string', description: 'The id of the service run' },
|
|
117
|
+
path: { type: 'string', description: 'The path, such as / or /api/health' },
|
|
118
|
+
service: { type: 'string', description: 'The service of the endpoint, the first endpoint when left out' },
|
|
119
|
+
},
|
|
120
|
+
required: ['run', 'path'],
|
|
121
|
+
async call(client, args, signal) {
|
|
122
|
+
const page = await client.fetchEndpoint(args.run, args.path, args.service || undefined, signal);
|
|
123
|
+
const body = page.body.length > MAX_PAGE_CHARS
|
|
124
|
+
? `${page.body.slice(0, MAX_PAGE_CHARS)}\n… (${page.body.length} characters, the first ${MAX_PAGE_CHARS} shown)`
|
|
125
|
+
: page.body;
|
|
126
|
+
return [`GET ${page.url}: ${page.status}${page.type ? ` (${page.type})` : ''}`, '', body].join('\n');
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'services_stop',
|
|
131
|
+
title: 'Stop services',
|
|
132
|
+
description: 'Stop a service run started with services_up: its services and endpoints go.',
|
|
133
|
+
parameters: { run: { type: 'string', description: 'The id of the service run' } },
|
|
134
|
+
required: ['run'],
|
|
135
|
+
async call(client, args) {
|
|
136
|
+
const run = await client.stop(args.run);
|
|
137
|
+
return `run ${run.id}: ${run.state === 'up' ? 'stopping' : run.state}`;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
//# sourceMappingURL=service-tools.js.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.taskToolArgs = exports.taskToolSchema = exports.createTaskTools = exports.describeTasks = exports.describeRun = exports.readLogHint = exports.readTaskListing = exports.LIST_TASKS_MS = void 0;
|
|
4
|
+
const sandbox_client_1 = require("./sandbox-client");
|
|
5
|
+
// the most tasks a description lists, and of a task's description or command it shows
|
|
6
|
+
const MAX_LISTED = 40;
|
|
7
|
+
const MAX_DESCRIPTION_CHARS = 200;
|
|
8
|
+
const MAX_COMMAND_CHARS = 100;
|
|
9
|
+
// how long a client waits for the tasks of the build file when it starts, it goes on without them
|
|
10
|
+
exports.LIST_TASKS_MS = 15000;
|
|
11
|
+
// The tasks of the published version, or why they could not be read within `ms`.
|
|
12
|
+
async function readTaskListing(list, ms = exports.LIST_TASKS_MS) {
|
|
13
|
+
let timer;
|
|
14
|
+
const timeout = new Promise((_, reject) => {
|
|
15
|
+
timer = setTimeout(() => reject(new Error(`no answer within ${ms / 1000} s`)), ms);
|
|
16
|
+
timer.unref?.();
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.race([list(), timeout]);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.readTaskListing = readTaskListing;
|
|
29
|
+
// the hint at the end of a cut output or a result, for the model
|
|
30
|
+
function readLogHint(run, task) {
|
|
31
|
+
return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
|
|
32
|
+
}
|
|
33
|
+
exports.readLogHint = readLogHint;
|
|
34
|
+
// A run of a task in a few lines: how it went, why it ran, its last lines.
|
|
35
|
+
function describeRun(task, ran) {
|
|
36
|
+
const result = ran.result;
|
|
37
|
+
const lines = [`run ${ran.run.id}: ${ran.run.state}`];
|
|
38
|
+
if (result) {
|
|
39
|
+
lines.push(`task ${task}: ${result.status}` +
|
|
40
|
+
(result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
|
|
41
|
+
(result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
|
|
42
|
+
(result.cache?.hit ? ', from the cache' : ''));
|
|
43
|
+
if (result.error) {
|
|
44
|
+
lines.push(`error: ${result.error}`);
|
|
45
|
+
}
|
|
46
|
+
if (result.cache?.causes?.length) {
|
|
47
|
+
lines.push(`ran because: ${result.cache.causes.join('; ')}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (ran.run.error) {
|
|
51
|
+
lines.push(`error: ${ran.run.error}`);
|
|
52
|
+
}
|
|
53
|
+
if (result && result.lastLog.length > 0) {
|
|
54
|
+
lines.push('last lines:', ...result.lastLog);
|
|
55
|
+
}
|
|
56
|
+
lines.push(readLogHint(ran.run.id, task));
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
exports.describeRun = describeRun;
|
|
60
|
+
/**
|
|
61
|
+
* The tasks for the model, one line each: what it is for (or its command, when the build
|
|
62
|
+
* file says nothing) and whether it takes arguments. `shell` is left out, the shell tool runs it.
|
|
63
|
+
*/
|
|
64
|
+
function describeTasks(listing, shell) {
|
|
65
|
+
if (listing === undefined) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(listing)) {
|
|
69
|
+
return `The tasks of the build file could not be listed (${listing.error}); name one as the build file does.`;
|
|
70
|
+
}
|
|
71
|
+
const tasks = listing.filter((task) => !shell || task.name !== shell);
|
|
72
|
+
if (tasks.length === 0) {
|
|
73
|
+
return 'The published build file has no tasks to run.';
|
|
74
|
+
}
|
|
75
|
+
const lines = tasks.slice(0, MAX_LISTED).map((task) => {
|
|
76
|
+
const parts = [`- ${task.name}`];
|
|
77
|
+
if (task.description) {
|
|
78
|
+
parts.push(`: ${cut(oneLine(task.description), MAX_DESCRIPTION_CHARS)}`);
|
|
79
|
+
}
|
|
80
|
+
else if (task.command) {
|
|
81
|
+
parts.push(`: runs \`${cut(oneLine(task.command), MAX_COMMAND_CHARS)}\``);
|
|
82
|
+
}
|
|
83
|
+
if (task.args === null) {
|
|
84
|
+
parts.push(' (takes args)');
|
|
85
|
+
}
|
|
86
|
+
else if (task.args !== undefined || task.command === null) {
|
|
87
|
+
parts.push(' (takes no args)');
|
|
88
|
+
}
|
|
89
|
+
return parts.join('');
|
|
90
|
+
});
|
|
91
|
+
if (tasks.length > MAX_LISTED) {
|
|
92
|
+
lines.push(`- and ${tasks.length - MAX_LISTED} more`);
|
|
93
|
+
}
|
|
94
|
+
return ['The tasks of the published build file:', ...lines].join('\n');
|
|
95
|
+
}
|
|
96
|
+
exports.describeTasks = describeTasks;
|
|
97
|
+
function oneLine(text) {
|
|
98
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
99
|
+
}
|
|
100
|
+
function cut(text, max) {
|
|
101
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `run_task`, `run_logs` and `check`, with the tasks the description of `run_task` lists. Without a
|
|
105
|
+
* listing it names none, as the MCP tools before a version could be read.
|
|
106
|
+
*/
|
|
107
|
+
function createTaskTools(listing, shell) {
|
|
108
|
+
const tasks = describeTasks(listing, shell);
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
name: 'run_task',
|
|
112
|
+
title: 'Run a task',
|
|
113
|
+
description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
|
|
114
|
+
'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
|
|
115
|
+
'its last command, for example a test file. It runs in the container, with the network and the ' +
|
|
116
|
+
'options the build file gives the task.' +
|
|
117
|
+
(tasks ? `\n\n${tasks}` : ''),
|
|
118
|
+
parameters: {
|
|
119
|
+
task: { type: 'string', description: 'The name of the task' },
|
|
120
|
+
args: { type: 'string[]', description: 'Arguments for its last command' },
|
|
121
|
+
},
|
|
122
|
+
required: ['task'],
|
|
123
|
+
async call(client, args, signal) {
|
|
124
|
+
const ran = await client.runTask(args.task, { args: args.args?.length ? args.args : undefined, signal });
|
|
125
|
+
return describeRun(args.task, ran);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'run_logs',
|
|
130
|
+
title: 'Read the log of a run',
|
|
131
|
+
description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
|
|
132
|
+
'the lines that contain a text (grep, a fixed string) with lines of context around them.',
|
|
133
|
+
parameters: {
|
|
134
|
+
run: { type: 'string', description: 'The id of the run' },
|
|
135
|
+
task: { type: 'string', description: 'Only the lines of this task' },
|
|
136
|
+
tail: {
|
|
137
|
+
type: 'integer',
|
|
138
|
+
description: 'The last lines, or with grep the last matches',
|
|
139
|
+
minimum: 1,
|
|
140
|
+
maximum: 1000,
|
|
141
|
+
},
|
|
142
|
+
from: { type: 'integer', description: 'The first line to read', minimum: 1 },
|
|
143
|
+
limit: { type: 'integer', description: 'How many lines to read', minimum: 1, maximum: 1000 },
|
|
144
|
+
grep: { type: 'string', description: 'Only lines that contain this text' },
|
|
145
|
+
ignore_case: { type: 'boolean', description: 'Ignore case in grep' },
|
|
146
|
+
context: { type: 'integer', description: 'Lines before and after each match', minimum: 0, maximum: 20 },
|
|
147
|
+
},
|
|
148
|
+
required: ['run'],
|
|
149
|
+
async call(client, args) {
|
|
150
|
+
const page = await client.logs(args.run, {
|
|
151
|
+
task: args.task || undefined,
|
|
152
|
+
tail: args.tail || undefined,
|
|
153
|
+
from: args.from || undefined,
|
|
154
|
+
limit: args.limit || undefined,
|
|
155
|
+
grep: args.grep || undefined,
|
|
156
|
+
i: args.ignore_case || undefined,
|
|
157
|
+
context: args.context || undefined,
|
|
158
|
+
});
|
|
159
|
+
const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
|
|
160
|
+
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' : ''})`);
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'check',
|
|
166
|
+
title: 'Run a check',
|
|
167
|
+
description: sandbox_client_1.CHECK_DESCRIPTION,
|
|
168
|
+
parameters: { task: { type: 'string', description: 'The name of the task' } },
|
|
169
|
+
required: ['task'],
|
|
170
|
+
call(client, args, signal) {
|
|
171
|
+
return (0, sandbox_client_1.runCheck)(client, args.task, signal);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
exports.createTaskTools = createTaskTools;
|
|
177
|
+
// The parameters as a JSON schema, for MCP and opencode 2.
|
|
178
|
+
function taskToolSchema(tool) {
|
|
179
|
+
return {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
|
|
182
|
+
if (parameter.type === 'string[]') {
|
|
183
|
+
return [name, { type: 'array', items: { type: 'string' }, description: parameter.description }];
|
|
184
|
+
}
|
|
185
|
+
if (parameter.type === 'integer') {
|
|
186
|
+
const { maximum } = parameter;
|
|
187
|
+
return [
|
|
188
|
+
name,
|
|
189
|
+
{
|
|
190
|
+
type: 'integer',
|
|
191
|
+
description: parameter.description,
|
|
192
|
+
minimum: parameter.minimum,
|
|
193
|
+
...(maximum !== undefined ? { maximum } : {}),
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
return [name, { type: parameter.type, description: parameter.description }];
|
|
198
|
+
})),
|
|
199
|
+
required: tool.required,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
exports.taskToolSchema = taskToolSchema;
|
|
203
|
+
// The parameters with the zod of opencode 1.
|
|
204
|
+
function taskToolArgs(tool, z) {
|
|
205
|
+
return Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
|
|
206
|
+
let type;
|
|
207
|
+
if (parameter.type === 'string[]') {
|
|
208
|
+
type = z.array(z.string());
|
|
209
|
+
}
|
|
210
|
+
else if (parameter.type === 'integer') {
|
|
211
|
+
type = z.number().int().min(parameter.minimum);
|
|
212
|
+
type = parameter.maximum !== undefined ? type.max(parameter.maximum) : type;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
type = parameter.type === 'boolean' ? z.boolean() : z.string();
|
|
216
|
+
}
|
|
217
|
+
return [name, (tool.required.includes(name) ? type : type.optional()).describe(parameter.description)];
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
exports.taskToolArgs = taskToolArgs;
|
|
221
|
+
//# sourceMappingURL=task-tools.js.map
|
package/dist/kubernetes/apply.js
CHANGED
|
@@ -36,28 +36,39 @@ exports.apply = apply;
|
|
|
36
36
|
* of a policy).
|
|
37
37
|
*/
|
|
38
38
|
async function replace(instance, spec) {
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
for (let attempt = 1;; attempt++) {
|
|
40
|
+
let current;
|
|
41
|
+
try {
|
|
42
|
+
current = await instance.objectApi.read(spec);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (statusCodeOf(e) !== 404) {
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return await instance.objectApi.create(spec);
|
|
50
|
+
}
|
|
51
|
+
catch (createError) {
|
|
52
|
+
if (statusCodeOf(createError) !== 409) {
|
|
53
|
+
throw createError;
|
|
54
|
+
}
|
|
55
|
+
current = await instance.objectApi.read(spec);
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
58
|
try {
|
|
48
|
-
return await instance.objectApi.
|
|
59
|
+
return await instance.objectApi.replace({
|
|
60
|
+
...spec,
|
|
61
|
+
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
62
|
+
});
|
|
49
63
|
}
|
|
50
|
-
catch (
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
catch (e) {
|
|
65
|
+
// replaced by someone else since it was read (the server sets up a caller's
|
|
66
|
+
// namespace for a run and for a sandbox at the same time): read it again
|
|
67
|
+
if (statusCodeOf(e) !== 409 || attempt >= 3) {
|
|
68
|
+
throw e;
|
|
53
69
|
}
|
|
54
|
-
current = await instance.objectApi.read(spec);
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
|
-
return await instance.objectApi.replace({
|
|
58
|
-
...spec,
|
|
59
|
-
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
60
|
-
});
|
|
61
72
|
}
|
|
62
73
|
exports.replace = replace;
|
|
63
74
|
//# sourceMappingURL=apply.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.stopJob = exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
|
|
3
|
+
exports.stopJob = exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.PASSING_FAILURE_MS = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
|
|
4
4
|
const abort_1 = require("../executer/abort");
|
|
5
5
|
const abort_event_1 = require("../utils/abort-event");
|
|
6
6
|
const apply_1 = require("./apply");
|
|
7
7
|
const sleep_1 = require("../utils/sleep");
|
|
8
|
+
const cluster_state_1 = require("./cluster-state");
|
|
8
9
|
// The waits below poll instead of using the client's Watch: Watch aborts every
|
|
9
10
|
// request after 30s, which a pod or deployment still pulling its image outlasts.
|
|
10
11
|
async function awaitRunningState(instance, env, name, phase, pollInterval = 1000) {
|
|
@@ -76,29 +77,74 @@ async function deleteJob(instance, env, name) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
exports.deleteJob = deleteJob;
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
80
|
+
// how long a reason that may still pass (an image pull a registry refused for now, a
|
|
81
|
+
// container the runtime could not create) may last before it fails the service
|
|
82
|
+
exports.PASSING_FAILURE_MS = 60000;
|
|
83
|
+
// Waits for an available replica: a ready one, for a service without a healthcheck one that
|
|
84
|
+
// stayed ready for a while. The deadline only starts once the container runs, and again
|
|
85
|
+
// at each start of it, so an image pull or a back-off doesn't count against it (as on
|
|
86
|
+
// docker, where it starts after the container started). A pod that won't get there without a change (an image
|
|
87
|
+
// that isn't there, a container that crashed once more than its
|
|
88
|
+
// service's `restarts`, 2 by default) fails it at once, with
|
|
89
|
+
// the reason the cluster gives; one that may still pass fails once it lasted a minute.
|
|
90
|
+
// Only the pods of this Deployment count: not the ones of one deleted before it was
|
|
91
|
+
// created again, which may still be there, terminating or not yet.
|
|
82
92
|
async function awaitDeployRunningState(instance, env, name, readiness, pollInterval = 1000, now = Date.now) {
|
|
83
93
|
let runningSince = null;
|
|
94
|
+
// how often the container was restarted when last looked at
|
|
95
|
+
let restarts = 0;
|
|
96
|
+
let said;
|
|
97
|
+
// since when a pod has had a failure that may still pass, by pod
|
|
98
|
+
const failingSince = new Map();
|
|
84
99
|
for (;;) {
|
|
85
100
|
const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
|
|
86
|
-
|
|
101
|
+
// available: ready for the Deployment's minReadySeconds
|
|
102
|
+
if ((deployment.status?.availableReplicas ?? 0) > 0) {
|
|
87
103
|
return;
|
|
88
104
|
}
|
|
105
|
+
const created = deployment.metadata?.creationTimestamp;
|
|
106
|
+
const listed = await instance.coreApi.listNamespacedPod({
|
|
107
|
+
namespace: env.namespace,
|
|
108
|
+
labelSelector: readiness.podSelector,
|
|
109
|
+
});
|
|
110
|
+
const pods = listed.items.filter((pod) => !pod.metadata?.deletionTimestamp &&
|
|
111
|
+
!(created && pod.metadata?.creationTimestamp && pod.metadata.creationTimestamp < created));
|
|
112
|
+
for (const [index, pod] of pods.entries()) {
|
|
113
|
+
const key = pod.metadata?.name ?? `${index}`;
|
|
114
|
+
const failure = (0, cluster_state_1.getPodFailure)(pod, readiness.container, readiness.restarts);
|
|
115
|
+
if (!failure) {
|
|
116
|
+
failingSince.delete(key);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const since = failingSince.get(key) ?? now();
|
|
120
|
+
failingSince.set(key, since);
|
|
121
|
+
if (failure.final || now() - since >= exports.PASSING_FAILURE_MS) {
|
|
122
|
+
throw new Error(`${readiness.subject ?? name} can not become ready, ${failure.text}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const waiting = pods.map((pod) => (0, cluster_state_1.getPodWaiting)(pod, readiness.container)).find((text) => !!text);
|
|
126
|
+
if (waiting && waiting !== said) {
|
|
127
|
+
readiness.onWaiting?.(waiting);
|
|
128
|
+
}
|
|
129
|
+
said = waiting;
|
|
89
130
|
if (readiness.timeout !== null) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
// the deadline bounds each start of the container, as on Docker: a crash is counted by
|
|
132
|
+
// `restarts`, and the back-off before the next start doesn't count against it
|
|
133
|
+
const statuses = pods.flatMap((pod) => (pod.status?.containerStatuses ?? []).filter((status) => !readiness.container || status.name === readiness.container));
|
|
134
|
+
const running = statuses.length > 0
|
|
135
|
+
? statuses.some((status) => status.state?.running)
|
|
136
|
+
: pods.some((pod) => pod.status?.phase === 'Running');
|
|
137
|
+
const restarted = Math.max(0, ...statuses.map((status) => status.restartCount ?? 0));
|
|
138
|
+
if (!running) {
|
|
139
|
+
runningSince = null;
|
|
140
|
+
}
|
|
141
|
+
else if (runningSince === null || restarted !== restarts) {
|
|
142
|
+
runningSince = now();
|
|
98
143
|
}
|
|
99
144
|
else if (now() - runningSince >= readiness.timeout) {
|
|
100
145
|
throw new Error(readiness.timeoutMessage);
|
|
101
146
|
}
|
|
147
|
+
restarts = restarted;
|
|
102
148
|
}
|
|
103
149
|
await (0, sleep_1.sleep)(pollInterval);
|
|
104
150
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasOutputsFlatteningAlike = exports.hasPathsEndingAlike = exports.byDepth = exports.getClaimSubPath = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const portable_path_1 = require("../planner/utils/portable-path");
|
|
7
|
+
// The paths a work item keeps in its claim. Its consumers mount its outputs by the same
|
|
8
|
+
// paths, so they find the same folders.
|
|
9
|
+
function getClaimPaths(work) {
|
|
10
|
+
const data = work.data;
|
|
11
|
+
const paths = [];
|
|
12
|
+
if ('mounts' in data) {
|
|
13
|
+
paths.push(...data.mounts.map((mount) => mount.containerPath));
|
|
14
|
+
}
|
|
15
|
+
if ('src' in data) {
|
|
16
|
+
paths.push(...data.src.filter((src) => !src.inherited).map((src) => src.absolutePath));
|
|
17
|
+
}
|
|
18
|
+
if ('generates' in data) {
|
|
19
|
+
paths.push(...data.generates.filter((generate) => !generate.inherited).map((generate) => generate.path));
|
|
20
|
+
}
|
|
21
|
+
if ('volumes' in data) {
|
|
22
|
+
paths.push(...data.volumes.filter((volume) => !volume.inherited).map((volume) => volume.containerPath));
|
|
23
|
+
}
|
|
24
|
+
return paths;
|
|
25
|
+
}
|
|
26
|
+
// Where `path` is in the claim of `work`, relative to its root. A path is named after its
|
|
27
|
+
// last segment, so claims without two such paths are laid out as they always were (a
|
|
28
|
+
// service's data among them). Of the paths with the same last segment, the least deep one
|
|
29
|
+
// (then the first in sorted order) keeps the name, and the others also carry a hash of
|
|
30
|
+
// their path. Both go by the path
|
|
31
|
+
// relative to the project, as the task's id does, so a run in another checkout directory
|
|
32
|
+
// (each run of a server has its own) or with its `generates` in another order finds the
|
|
33
|
+
// same folders in a kept claim.
|
|
34
|
+
function getClaimSubPath(work, path) {
|
|
35
|
+
const name = (0, path_1.basename)(path) || 'root';
|
|
36
|
+
const alike = getClaimPaths(work).filter((other) => other !== path && (0, path_1.basename)(other) === (0, path_1.basename)(path));
|
|
37
|
+
if (alike.length === 0) {
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
const own = (0, portable_path_1.portablePath)(work.data.projectRoot, path);
|
|
41
|
+
const first = [own, ...alike.map((other) => (0, portable_path_1.portablePath)(work.data.projectRoot, other))].sort(byDepth)[0];
|
|
42
|
+
if (first === own) {
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
return `${name}-${(0, crypto_1.createHash)('sha256').update(own).digest('hex').slice(0, 12)}`;
|
|
46
|
+
}
|
|
47
|
+
exports.getClaimSubPath = getClaimSubPath;
|
|
48
|
+
function byDepth(a, b) {
|
|
49
|
+
return a.split('/').length - b.split('/').length || (a < b ? -1 : a > b ? 1 : 0);
|
|
50
|
+
}
|
|
51
|
+
exports.byDepth = byDepth;
|
|
52
|
+
// Whether two different paths of the claim of `work` end alike. Before getClaimSubPath
|
|
53
|
+
// named them apart, they shared one folder, and outputs made then are mixed.
|
|
54
|
+
function hasPathsEndingAlike(work) {
|
|
55
|
+
const paths = new Set(getClaimPaths(work));
|
|
56
|
+
return new Set([...paths].map((path) => (0, path_1.basename)(path))).size < paths.size;
|
|
57
|
+
}
|
|
58
|
+
exports.hasPathsEndingAlike = hasPathsEndingAlike;
|
|
59
|
+
// Whether two outputs of `work` flatten to the same name of a cache archive (`a-b` and
|
|
60
|
+
// `a/b`, see getArchivePaths). Before they were named apart, one archive held both.
|
|
61
|
+
function hasOutputsFlatteningAlike(work) {
|
|
62
|
+
const data = work.data;
|
|
63
|
+
if (!('generates' in data) || !('cwd' in data)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const paths = new Set(data.generates.filter((generate) => !generate.inherited).map((generate) => generate.path));
|
|
67
|
+
const names = new Set([...paths].map((path) => (0, path_1.relative)(data.cwd, path).split(path_1.sep).join('-')));
|
|
68
|
+
return names.size < paths.size;
|
|
69
|
+
}
|
|
70
|
+
exports.hasOutputsFlatteningAlike = hasOutputsFlatteningAlike;
|
|
71
|
+
//# sourceMappingURL=claim-layout.js.map
|