hammerkit 1.26.0 → 1.27.0-next.2
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 +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/log.js +1 -1
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +3 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +41 -0
- package/dist/planner/work-runtime-kubernetes.js +335 -102
- package/dist/program.js +23 -1
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +5 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +26 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +562 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +51 -2
- package/dist/server/run-request.js +23 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +184 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
package/dist/server/server.js
CHANGED
|
@@ -13,7 +13,9 @@ const runs_1 = require("./runs");
|
|
|
13
13
|
const git_checkout_1 = require("./git-checkout");
|
|
14
14
|
const repo_access_1 = require("./repo-access");
|
|
15
15
|
const executor_1 = require("./executor");
|
|
16
|
+
const kubernetes_backend_1 = require("./kubernetes-backend");
|
|
16
17
|
const run_plan_1 = require("./run-plan");
|
|
18
|
+
const work_network_1 = require("../planner/work-network");
|
|
17
19
|
const run_environment_1 = require("./run-environment");
|
|
18
20
|
const accounts_api_1 = require("./accounts-api");
|
|
19
21
|
const run_request_1 = require("./run-request");
|
|
@@ -25,7 +27,13 @@ const proposals_api_1 = require("./proposals-api");
|
|
|
25
27
|
const proposals_1 = require("./proposals");
|
|
26
28
|
const sources_1 = require("./sources");
|
|
27
29
|
const sources_api_1 = require("./sources-api");
|
|
30
|
+
const sandboxes_1 = require("./sandboxes");
|
|
31
|
+
const sandbox_storage_1 = require("./sandbox-storage");
|
|
32
|
+
const kubernetes_sandbox_storage_1 = require("./kubernetes-sandbox-storage");
|
|
33
|
+
const sandbox_docker_1 = require("./sandbox-docker");
|
|
34
|
+
const sandboxes_api_1 = require("./sandboxes-api");
|
|
28
35
|
const units_1 = require("../utils/units");
|
|
36
|
+
const run_logs_1 = require("./run-logs");
|
|
29
37
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
30
38
|
const path_1 = require("path");
|
|
31
39
|
var discovery_2 = require("./discovery");
|
|
@@ -42,11 +50,20 @@ function getDiscoveryDocument(config) {
|
|
|
42
50
|
...(i.clientId ? { clientId: i.clientId } : {}),
|
|
43
51
|
scope: i.scope,
|
|
44
52
|
})),
|
|
45
|
-
backend:
|
|
53
|
+
backend: config.backend.type === 'kubernetes'
|
|
54
|
+
? {
|
|
55
|
+
type: 'kubernetes',
|
|
56
|
+
platform: config.backend.pools[0].platform,
|
|
57
|
+
pools: config.backend.pools.map((pool) => ({ name: pool.name, platform: pool.platform })),
|
|
58
|
+
}
|
|
59
|
+
: { type: 'host', platform: config.backend.platform ?? (0, server_config_1.detectPlatform)() },
|
|
46
60
|
};
|
|
47
61
|
}
|
|
48
62
|
exports.getDiscoveryDocument = getDiscoveryDocument;
|
|
49
63
|
const HEARTBEAT_MS = 15000;
|
|
64
|
+
// the longest `?wait=` of a run: below the 60 seconds after which a proxy in front of the
|
|
65
|
+
// server (nginx by default) closes a connection that says nothing
|
|
66
|
+
const MAX_WAIT_MS = 50000;
|
|
50
67
|
// Server-sent events: `id` is the sequence number, so a client that lost the
|
|
51
68
|
// connection continues with Last-Event-ID (or ?after=) and misses nothing.
|
|
52
69
|
function streamEvents(registry, config, principal, id, req, res, url) {
|
|
@@ -79,8 +96,8 @@ function streamEvents(registry, config, principal, id, req, res, url) {
|
|
|
79
96
|
watch.stop();
|
|
80
97
|
});
|
|
81
98
|
}
|
|
82
|
-
async function handleRuns(config, registry, definitions, sources, planning, repos, log, principal, req, res, url) {
|
|
83
|
-
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events))?)?$/.exec(url.pathname);
|
|
99
|
+
async function handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res, url) {
|
|
100
|
+
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs))?)?$/.exec(url.pathname);
|
|
84
101
|
if (!match) {
|
|
85
102
|
return false;
|
|
86
103
|
}
|
|
@@ -88,10 +105,17 @@ async function handleRuns(config, registry, definitions, sources, planning, repo
|
|
|
88
105
|
const method = req.method;
|
|
89
106
|
if (!id) {
|
|
90
107
|
if (method === 'GET') {
|
|
91
|
-
|
|
108
|
+
// the results of each task are read one run at a time
|
|
109
|
+
(0, http_2.send)(res, 200, {
|
|
110
|
+
runs: registry.list(principal).map((run) => {
|
|
111
|
+
const { results, ...listed } = toRunResponse(run);
|
|
112
|
+
void results;
|
|
113
|
+
return listed;
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
92
116
|
}
|
|
93
117
|
else if (method === 'POST') {
|
|
94
|
-
await submitRun(config, registry, definitions, sources, planning, repos, log, principal, req, res);
|
|
118
|
+
await submitRun(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res);
|
|
95
119
|
}
|
|
96
120
|
else {
|
|
97
121
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET, POST' });
|
|
@@ -116,9 +140,39 @@ async function handleRuns(config, registry, definitions, sources, planning, repo
|
|
|
116
140
|
else if (action === 'events') {
|
|
117
141
|
streamEvents(registry, config, principal, id, req, res, url);
|
|
118
142
|
}
|
|
143
|
+
else if (action === 'logs') {
|
|
144
|
+
let query;
|
|
145
|
+
try {
|
|
146
|
+
query = (0, run_logs_1.parseLogQuery)(url.searchParams);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
(0, http_2.send)(res, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
const logs = registry.logs(principal, id, accountsOf(config, principal));
|
|
153
|
+
logs
|
|
154
|
+
? (0, http_2.send)(res, 200, (0, run_logs_1.readLogs)(logs.events, logs.index, query, logs.truncated))
|
|
155
|
+
: (0, http_2.send)(res, 404, { error: 'not found' });
|
|
156
|
+
}
|
|
119
157
|
else {
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
let wait = 0;
|
|
159
|
+
const waitParam = url.searchParams.get('wait');
|
|
160
|
+
if (waitParam !== null) {
|
|
161
|
+
try {
|
|
162
|
+
wait = Math.min((0, units_1.parseDuration)(waitParam), MAX_WAIT_MS);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
(0, http_2.send)(res, 400, { error: `wait is a duration up to ${(0, units_1.formatDuration)(MAX_WAIT_MS)}, for example 30s` });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// a caller that hangs up stops waiting
|
|
170
|
+
const gone = new AbortController();
|
|
171
|
+
res.on('close', () => gone.abort());
|
|
172
|
+
const run = await registry.wait(principal, id, wait, accountsOf(config, principal), gone.signal);
|
|
173
|
+
if (!gone.signal.aborted) {
|
|
174
|
+
run ? (0, http_2.send)(res, 200, toRunResponse(run)) : (0, http_2.send)(res, 404, { error: 'not found' });
|
|
175
|
+
}
|
|
122
176
|
}
|
|
123
177
|
return true;
|
|
124
178
|
}
|
|
@@ -130,9 +184,9 @@ function accountsOf(config, principal) {
|
|
|
130
184
|
function toRunResponse(run) {
|
|
131
185
|
const { owner, ...visible } = run;
|
|
132
186
|
void owner;
|
|
133
|
-
return visible;
|
|
187
|
+
return { ...visible, results: run.results ?? {} };
|
|
134
188
|
}
|
|
135
|
-
async function submitRun(config, registry, definitions, sources, planning, repos, log, principal, req, res) {
|
|
189
|
+
async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res) {
|
|
136
190
|
const parsed = run_request_1.runRequestSchema.safeParse(await (0, http_2.readJson)(req));
|
|
137
191
|
if (!parsed.success) {
|
|
138
192
|
const issue = parsed.error.issues[0];
|
|
@@ -147,21 +201,26 @@ async function submitRun(config, registry, definitions, sources, planning, repos
|
|
|
147
201
|
(0, http_2.send)(res, 403, { error: 'you are not entitled to run anything on this server' });
|
|
148
202
|
return;
|
|
149
203
|
}
|
|
150
|
-
const version = definitions.find(request.definition
|
|
204
|
+
const version = definitions.find(request.definition);
|
|
151
205
|
if (!version) {
|
|
152
|
-
(0, http_2.send)(res, 400, { error: `there is no build file ${request.definition
|
|
206
|
+
(0, http_2.send)(res, 400, { error: `there is no published build file ${request.definition}` });
|
|
153
207
|
return;
|
|
154
208
|
}
|
|
155
209
|
if (version.yanked) {
|
|
156
210
|
(0, http_2.send)(res, 409, {
|
|
157
|
-
error: `${version.
|
|
211
|
+
error: `${version.hash} was yanked${version.yanked.reason ? `: ${version.yanked.reason}` : ''}`,
|
|
158
212
|
});
|
|
159
213
|
return;
|
|
160
214
|
}
|
|
161
215
|
const known = [...version.tasks, ...version.services];
|
|
162
216
|
const unknown = request.tasks.filter((task) => !known.includes(task));
|
|
163
217
|
if (unknown.length > 0) {
|
|
164
|
-
(0, http_2.send)(res, 400, { error: `${version.
|
|
218
|
+
(0, http_2.send)(res, 400, { error: `${version.hash} has no task ${unknown.map(oauth_1.quote).join(', ')}` });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const argued = (request.options.args ?? []).length > 0 ? request.tasks[0] : undefined;
|
|
222
|
+
if (argued !== undefined && version.services.includes(argued)) {
|
|
223
|
+
(0, http_2.send)(res, 400, { error: `${argued} is a service, arguments go to a task` });
|
|
165
224
|
return;
|
|
166
225
|
}
|
|
167
226
|
if (request.source.type === 'upload') {
|
|
@@ -180,29 +239,57 @@ async function submitRun(config, registry, definitions, sources, planning, repos
|
|
|
180
239
|
return;
|
|
181
240
|
}
|
|
182
241
|
}
|
|
242
|
+
const sandbox = request.source.type === 'sandbox' ? sandboxes.registry.get(principal, request.source.id) : undefined;
|
|
243
|
+
if (request.source.type === 'sandbox') {
|
|
244
|
+
if (!sandbox) {
|
|
245
|
+
(0, http_2.send)(res, 400, { error: `there is no sandbox ${request.source.id}, create one with POST /v1/sandboxes` });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (sandboxes.policy.runtimes?.length === 0) {
|
|
249
|
+
(0, http_2.send)(res, 403, {
|
|
250
|
+
error: 'this server runs only local tasks, which it runs on no sandbox unless sandboxes.runtimes names local',
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await sandboxes.registry.measured(principal);
|
|
255
|
+
const over = sandboxes.registry.overLimit(principal);
|
|
256
|
+
if (over) {
|
|
257
|
+
(0, http_2.send)(res, 413, { error: over });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
183
261
|
// the accounts of a run are read from the files it is pinned to, with the engine
|
|
184
262
|
const bundle = version.plan ? await definitions.files(version) : undefined;
|
|
185
263
|
if (!version.plan || !bundle) {
|
|
186
264
|
(0, http_2.send)(res, 409, {
|
|
187
|
-
error: `${version.
|
|
265
|
+
error: `${version.hash} was published before accounts were recorded, publish it again`,
|
|
188
266
|
});
|
|
189
267
|
return;
|
|
190
268
|
}
|
|
191
269
|
let planned;
|
|
192
270
|
try {
|
|
193
|
-
|
|
271
|
+
// a run on a sandbox may only use the runtimes of `sandboxes.runtimes`
|
|
272
|
+
planned = await (0, run_plan_1.planRun)(bundle, request, planning.environment, sandbox ? sandboxes.policy : planning.policy);
|
|
194
273
|
}
|
|
195
274
|
catch (e) {
|
|
196
275
|
(0, http_2.send)(res, 400, { error: `the run does not load: ${e instanceof Error ? e.message : String(e)}` });
|
|
197
276
|
return;
|
|
198
277
|
}
|
|
278
|
+
const refused = (0, run_plan_1.refuseArguments)(request, planned.plan);
|
|
279
|
+
if (refused) {
|
|
280
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${refused}`);
|
|
281
|
+
(0, http_2.send)(res, 400, { error: refused });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
199
284
|
const decision = (0, policy_1.decideRun)({
|
|
200
285
|
grant,
|
|
201
286
|
accounts: planned.accounts,
|
|
202
287
|
catalog: config.accounts,
|
|
203
288
|
source: request.source.type === 'ref'
|
|
204
289
|
? { type: 'ref', repo: request.source.repo, ref: request.source.ref }
|
|
205
|
-
:
|
|
290
|
+
: sandbox
|
|
291
|
+
? { type: 'sandbox', repo: sandbox.repo, branch: sandbox.ref }
|
|
292
|
+
: { type: 'upload' },
|
|
206
293
|
approvable: (account) => config.entitlements.some((entitlement) => entitlement.accounts[account] === 'use'),
|
|
207
294
|
readable: (repo) => repos.readable(repo),
|
|
208
295
|
});
|
|
@@ -228,19 +315,34 @@ async function submitRun(config, registry, definitions, sources, planning, repos
|
|
|
228
315
|
return;
|
|
229
316
|
}
|
|
230
317
|
}
|
|
231
|
-
else {
|
|
318
|
+
else if (request.source.type === 'upload') {
|
|
232
319
|
source = request.source;
|
|
233
320
|
}
|
|
321
|
+
const submit = (runSource) => registry.submit(principal, {
|
|
322
|
+
definition: version.hash,
|
|
323
|
+
tasks: request.tasks,
|
|
324
|
+
accounts: planned.accounts,
|
|
325
|
+
plan: planned.plan,
|
|
326
|
+
source: runSource,
|
|
327
|
+
options: request.options,
|
|
328
|
+
}, approvals);
|
|
234
329
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
330
|
+
// a sandbox is read and the run submitted while nothing else changes it: the run
|
|
331
|
+
// records what the sandbox held, and holds it from now on
|
|
332
|
+
const run = source
|
|
333
|
+
? submit(source)
|
|
334
|
+
: await sandboxes.registry.withSandbox(principal, sandbox.id, async (current) => {
|
|
335
|
+
const state = await sandboxes.registry.state(current.id);
|
|
336
|
+
return submit({
|
|
337
|
+
type: 'sandbox',
|
|
338
|
+
id: current.id,
|
|
339
|
+
repo: current.repo,
|
|
340
|
+
commit: state.commit,
|
|
341
|
+
tree: state.tree,
|
|
342
|
+
clean: state.clean,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub} accounts=[${planned.accounts.join(',')}] source=${run.source.type}${run.source.type === 'sandbox' ? ` sandbox=${run.source.id}` : ''} tasks=${run.tasks.length}${approvals.length > 0 ? ` approval=[${approvals.join(',')}]` : ''}`);
|
|
244
346
|
(0, http_2.send)(res, 202, toRunResponse(run));
|
|
245
347
|
}
|
|
246
348
|
catch (e) {
|
|
@@ -248,6 +350,12 @@ async function submitRun(config, registry, definitions, sources, planning, repos
|
|
|
248
350
|
(0, http_2.send)(res, e.status, { error: e.message });
|
|
249
351
|
return;
|
|
250
352
|
}
|
|
353
|
+
if (e instanceof sandboxes_1.SandboxError) {
|
|
354
|
+
(0, http_2.send)(res, e.status === 404 ? 400 : e.status, {
|
|
355
|
+
error: e.status === 404 ? `there is no sandbox ${sandbox.id}` : e.message,
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
251
359
|
throw e;
|
|
252
360
|
}
|
|
253
361
|
}
|
|
@@ -271,7 +379,7 @@ async function authenticate(verifier, log, req, res) {
|
|
|
271
379
|
return null;
|
|
272
380
|
}
|
|
273
381
|
}
|
|
274
|
-
async function handle(config, registry, definitions, proposals, sources, planning, repos, verifier, log, req, res) {
|
|
382
|
+
async function handle(config, registry, definitions, proposals, sources, sandboxes, planning, repos, verifier, log, req, res) {
|
|
275
383
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
276
384
|
const path = url.pathname;
|
|
277
385
|
if (path === discovery_1.DISCOVERY_PATH || path === '/healthz') {
|
|
@@ -295,7 +403,10 @@ async function handle(config, registry, definitions, proposals, sources, plannin
|
|
|
295
403
|
if (await (0, sources_api_1.handleSources)(config, sources, log, principal, req, res, url)) {
|
|
296
404
|
return;
|
|
297
405
|
}
|
|
298
|
-
if (await
|
|
406
|
+
if (await (0, sandboxes_api_1.handleSandboxes)(config, sandboxes.registry, repos, log, principal, req, res, url)) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (await handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res, url)) {
|
|
299
410
|
return;
|
|
300
411
|
}
|
|
301
412
|
if (await (0, approvals_api_1.handleApprovals)(config, registry, log, principal, req, res, url)) {
|
|
@@ -315,7 +426,7 @@ async function startServer(config, environment, options = {}) {
|
|
|
315
426
|
const write = options.log ?? (() => undefined);
|
|
316
427
|
// request urls and subjects come from callers: no control characters, so no forged log lines
|
|
317
428
|
const log = (line) => write(line.replace(/\p{Cc}/gu, '?').slice(0, 1000));
|
|
318
|
-
if (config.runtimes.includes('local')) {
|
|
429
|
+
if (config.runtimes.includes('local') && config.backend.type === 'host') {
|
|
319
430
|
log('warning: the local runtime is allowed, local tasks run as the user of this server and read all it can read; ' +
|
|
320
431
|
'an account is then an approval class, not a credential boundary');
|
|
321
432
|
}
|
|
@@ -327,7 +438,13 @@ async function startServer(config, environment, options = {}) {
|
|
|
327
438
|
processEnvs: (0, run_environment_1.runEnvironment)(config, environment.processEnvs),
|
|
328
439
|
hosted: { catalog: (0, server_config_1.getSecretCatalog)(config) },
|
|
329
440
|
},
|
|
330
|
-
policy: {
|
|
441
|
+
policy: {
|
|
442
|
+
envs: config.runs.env.allow,
|
|
443
|
+
reserved: (0, server_config_1.getSourceAccounts)(config),
|
|
444
|
+
runtimes: config.runtimes,
|
|
445
|
+
backend: config.backend.type,
|
|
446
|
+
network: (0, work_network_1.parseWorkNetwork)(config.runs.network),
|
|
447
|
+
},
|
|
331
448
|
};
|
|
332
449
|
const storage = config.storage.directory ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'server');
|
|
333
450
|
const definitions = new definitions_1.DefinitionStore((0, path_1.join)(storage, 'definitions'), planning.environment, {
|
|
@@ -335,15 +452,59 @@ async function startServer(config, environment, options = {}) {
|
|
|
335
452
|
});
|
|
336
453
|
await definitions.load();
|
|
337
454
|
const repos = new repo_access_1.RepoAccess(config, planning.environment, options.repos);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
455
|
+
const sandboxRuntimes = (0, server_config_1.getSandboxRuntimes)(config);
|
|
456
|
+
const sandboxLimits = {
|
|
457
|
+
count: config.sandboxes.limits.count,
|
|
458
|
+
bytes: (0, units_1.parseSize)(config.sandboxes.limits.bytes),
|
|
459
|
+
total: (0, units_1.parseSize)(config.sandboxes.total),
|
|
460
|
+
};
|
|
461
|
+
const sandboxes = {
|
|
462
|
+
registry: new sandboxes_1.SandboxRegistry({
|
|
463
|
+
// on a cluster a claim per sandbox in the caller's namespace, else a directory
|
|
464
|
+
storage: config.backend.type === 'kubernetes'
|
|
465
|
+
? new kubernetes_sandbox_storage_1.KubernetesSandboxStorage({ ...config, backend: config.backend }, sandboxLimits, planning.environment.processEnvs, (0, path_1.join)(storage, 'sandboxes', 'staging'))
|
|
466
|
+
: new sandbox_storage_1.HostSandboxStorage((0, path_1.join)(storage, 'sandboxes', 'files'), planning.environment.processEnvs,
|
|
467
|
+
// what containers of runs leave behind is given back and removed with a container
|
|
468
|
+
sandboxRuntimes.includes('docker')
|
|
469
|
+
? new sandbox_docker_1.DockerSandboxContainers(planning.environment.processEnvs.DOCKER_HOST)
|
|
470
|
+
: undefined),
|
|
471
|
+
records: (0, path_1.join)(storage, 'sandboxes', 'records'),
|
|
472
|
+
limits: sandboxLimits,
|
|
342
473
|
repos,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
474
|
+
log,
|
|
475
|
+
}),
|
|
476
|
+
// a sandbox holds code nobody reviewed: only the variables of `sandboxes.env`
|
|
477
|
+
policy: {
|
|
478
|
+
...planning.policy,
|
|
479
|
+
envs: config.sandboxes.env.allow,
|
|
480
|
+
envsFrom: 'sandboxes.env.allow',
|
|
481
|
+
runtimes: sandboxRuntimes,
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
if (sandboxRuntimes.length === 0 && config.entitlements.some((entitlement) => entitlement.sandboxes?.length)) {
|
|
485
|
+
log('warning: entitlements keep sandboxes, but this server runs only local tasks, which it runs on no sandbox; ' +
|
|
486
|
+
'runs on sandboxes are refused until sandboxes.runtimes names a runtime');
|
|
487
|
+
}
|
|
488
|
+
// on a cluster that lists the claims of sandboxes in the namespaces of callers, which a
|
|
489
|
+
// server without sandboxes need not be allowed to
|
|
490
|
+
if (config.backend.type === 'host' || config.entitlements.some((entitlement) => entitlement.sandboxes?.length)) {
|
|
491
|
+
await sandboxes.registry.load();
|
|
492
|
+
}
|
|
493
|
+
const executorOptions = {
|
|
494
|
+
definitions,
|
|
495
|
+
planning,
|
|
496
|
+
repos,
|
|
497
|
+
directory: (0, path_1.join)(storage, 'runs'),
|
|
498
|
+
cache: (0, path_1.join)(storage, 'cache'),
|
|
499
|
+
worker: options.worker,
|
|
500
|
+
sandboxes: { ...sandboxes, network: config.sandboxes.network },
|
|
501
|
+
log,
|
|
502
|
+
};
|
|
503
|
+
const backend = config.backend;
|
|
504
|
+
const executor = options.executor ??
|
|
505
|
+
(backend.type === 'kubernetes'
|
|
506
|
+
? new kubernetes_backend_1.KubernetesExecutor({ ...config, backend }, environment.processEnvs, executorOptions)
|
|
507
|
+
: new executor_1.HostExecutor(executorOptions));
|
|
347
508
|
const sources = new sources_1.SourceStore((0, path_1.join)(storage, 'sources'));
|
|
348
509
|
const registry = new runs_1.RunRegistry({
|
|
349
510
|
executor,
|
|
@@ -358,13 +519,17 @@ async function startServer(config, environment, options = {}) {
|
|
|
358
519
|
log,
|
|
359
520
|
});
|
|
360
521
|
await registry.restore();
|
|
522
|
+
sandboxes.registry.useRuns({
|
|
523
|
+
holder: (id) => registry.holder(id),
|
|
524
|
+
stop: (id) => registry.stopSandbox(id, 'the sandbox was deleted'),
|
|
525
|
+
});
|
|
361
526
|
const proposals = new proposals_1.ProposalRegistry({
|
|
362
527
|
maxWaiting: config.approvals.maxWaiting,
|
|
363
528
|
timeout: (0, units_1.parseDuration)(config.approvals.timeout),
|
|
364
529
|
cooldown: (0, units_1.parseDuration)(config.approvals.cooldown),
|
|
365
530
|
});
|
|
366
531
|
const listener = (req, res) => {
|
|
367
|
-
handle(config, registry, definitions, proposals, sources, planning, repos, verifier, log, req, res).catch((e) => {
|
|
532
|
+
handle(config, registry, definitions, proposals, sources, sandboxes, planning, repos, verifier, log, req, res).catch((e) => {
|
|
368
533
|
if (e instanceof http_2.HttpError) {
|
|
369
534
|
(0, http_2.send)(res, e.status, { error: e.message });
|
|
370
535
|
return;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRunId = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const runId = (0, crypto_1.randomBytes)(4).toString('hex');
|
|
6
|
+
// Tells this process's run apart from a concurrent one of the same items, on
|
|
7
|
+
// this machine or another one using the same daemon or cluster.
|
|
8
|
+
function getRunId() {
|
|
9
|
+
return runId;
|
|
10
|
+
}
|
|
11
|
+
exports.getRunId = getRunId;
|
|
12
|
+
//# sourceMappingURL=run-id.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The hammerkit plugin for opencode 2, and for opencode 1 by its `server`, re-exported by a plugin
|
|
2
|
+
// file of the project: `export { default } from 'hammerkit/harness/opencode'`, see
|
|
3
|
+
// https://hammerkit.dev/docs/remote-compute/agents
|
|
4
|
+
import { hammerkitOpencodeEntry } from '../dist/harness/opencode'
|
|
5
|
+
|
|
6
|
+
const plugin = hammerkitOpencodeEntry((specifier) => import(specifier))
|
|
7
|
+
|
|
8
|
+
export default plugin
|
|
9
|
+
|
|
10
|
+
// the name plugin files of opencode 1 re-exported, the same plugin so opencode 1 loads it once
|
|
11
|
+
export const HammerkitPlugin = plugin
|
package/harness/pi.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The hammerkit extension for pi: `pi install npm:hammerkit`. pi loads this file with its
|
|
2
|
+
// own module, whose tools hammerkit gives a sandbox as their backend, see
|
|
3
|
+
// https://hammerkit.dev/docs/remote-compute/agents
|
|
4
|
+
import * as pi from '@earendil-works/pi-coding-agent'
|
|
5
|
+
import { hammerkitPiExtension } from '../dist/harness/pi'
|
|
6
|
+
|
|
7
|
+
export default hammerkitPiExtension(pi)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hammerkit",
|
|
3
3
|
"description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.27.0-next.2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"build",
|
|
7
7
|
"build-tool",
|
|
@@ -19,8 +19,14 @@
|
|
|
19
19
|
"!dist/**/*.spec.*",
|
|
20
20
|
"!dist/testing",
|
|
21
21
|
"!dist/**/*.map",
|
|
22
|
-
"build.schema.json"
|
|
22
|
+
"build.schema.json",
|
|
23
|
+
"harness"
|
|
23
24
|
],
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./harness/pi.ts"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
24
30
|
"bugs": {
|
|
25
31
|
"url": "https://github.com/no0dles/hammerkit/issues"
|
|
26
32
|
},
|
package/dist/remote/text-diff.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.diffBundles = exports.diffLines = exports.visible = void 0;
|
|
4
|
-
// Text from a proposal is shown to somebody who decides on it, so nothing in it may
|
|
5
|
-
// rewrite the terminal or hide what it says: control and format characters (which
|
|
6
|
-
// include the ones that reorder text) are shown as `?`. Long lines are not cut,
|
|
7
|
-
// what is hidden behind a cut is what a reviewer needs to see.
|
|
8
|
-
function visible(line) {
|
|
9
|
-
return line.replace(/\t/g, ' ').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?');
|
|
10
|
-
}
|
|
11
|
-
exports.visible = visible;
|
|
12
|
-
// Beyond this many comparisons a file is shown whole instead of compared.
|
|
13
|
-
const MAX_CELLS = 4000000;
|
|
14
|
-
// The lines that differ, with `context` lines around each change.
|
|
15
|
-
function diffLines(before, after, context = 2) {
|
|
16
|
-
if ((before.length + 1) * (after.length + 1) > MAX_CELLS) {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
const n = before.length;
|
|
20
|
-
const m = after.length;
|
|
21
|
-
// lengths of the longest common suffixes
|
|
22
|
-
const table = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
23
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
24
|
-
for (let j = m - 1; j >= 0; j--) {
|
|
25
|
-
table[i][j] = before[i] === after[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const all = [];
|
|
29
|
-
let i = 0;
|
|
30
|
-
let j = 0;
|
|
31
|
-
while (i < n && j < m) {
|
|
32
|
-
if (before[i] === after[j]) {
|
|
33
|
-
all.push({ type: ' ', text: before[i] });
|
|
34
|
-
i++;
|
|
35
|
-
j++;
|
|
36
|
-
}
|
|
37
|
-
else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
38
|
-
all.push({ type: '-', text: before[i++] });
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
all.push({ type: '+', text: after[j++] });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
while (i < n)
|
|
45
|
-
all.push({ type: '-', text: before[i++] });
|
|
46
|
-
while (j < m)
|
|
47
|
-
all.push({ type: '+', text: after[j++] });
|
|
48
|
-
const keep = new Array(all.length).fill(false);
|
|
49
|
-
all.forEach((line, index) => {
|
|
50
|
-
if (line.type !== ' ') {
|
|
51
|
-
for (let k = Math.max(0, index - context); k <= Math.min(all.length - 1, index + context); k++) {
|
|
52
|
-
keep[k] = true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return all.filter((_, index) => keep[index]);
|
|
57
|
-
}
|
|
58
|
-
exports.diffLines = diffLines;
|
|
59
|
-
const lines = (text) => (text === '' ? [] : text.replace(/\n$/, '').split('\n'));
|
|
60
|
-
// What a proposal changes against the version it follows, file by file.
|
|
61
|
-
function diffBundles(base, proposed) {
|
|
62
|
-
const out = [];
|
|
63
|
-
const before = base?.files ?? {};
|
|
64
|
-
const paths = [...new Set([...Object.keys(before), ...Object.keys(proposed.files)])].sort();
|
|
65
|
-
for (const path of paths) {
|
|
66
|
-
const old = before[path];
|
|
67
|
-
const next = proposed.files[path];
|
|
68
|
-
if (old === next) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const status = old === undefined ? 'added' : next === undefined ? 'removed' : 'changed';
|
|
72
|
-
out.push(`--- ${visible(path)} (${status})`);
|
|
73
|
-
const compared = old !== undefined && next !== undefined ? diffLines(lines(old), lines(next)) : undefined;
|
|
74
|
-
if (compared) {
|
|
75
|
-
compared.forEach((line) => out.push(`${line.type} ${visible(line.text)}`));
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
// added, removed or too large to compare: all of the old, then all of the new
|
|
79
|
-
lines(old ?? '').forEach((line) => out.push(`- ${visible(line)}`));
|
|
80
|
-
lines(next ?? '').forEach((line) => out.push(`+ ${visible(line)}`));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (base && base.entry !== proposed.entry) {
|
|
84
|
-
out.unshift(`entry file: ${visible(base.entry)} -> ${visible(proposed.entry)}`);
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
exports.diffBundles = diffBundles;
|
|
89
|
-
//# sourceMappingURL=text-diff.js.map
|