hammerkit 1.27.0-next.8 → 2.0.0-next.1

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.
Files changed (129) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +76 -17
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-http-route.js +1 -1
  36. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  38. package/dist/kubernetes/ensure-persistent-data.js +212 -10
  39. package/dist/kubernetes/follow-pod-logs.js +182 -0
  40. package/dist/kubernetes/get-service-ip.js +2 -2
  41. package/dist/kubernetes/job-placement.js +36 -12
  42. package/dist/kubernetes/job-secrets.js +9 -6
  43. package/dist/kubernetes/kept-claims.js +22 -10
  44. package/dist/kubernetes/kubernetes-instance.js +1 -0
  45. package/dist/kubernetes/network-policy.js +16 -8
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/remove-stale-items.js +46 -0
  48. package/dist/kubernetes/resources.js +25 -3
  49. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  50. package/dist/kubernetes/sandbox-runner.js +628 -0
  51. package/dist/kubernetes/secrets.js +7 -11
  52. package/dist/kubernetes/service-rollouts.js +74 -0
  53. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  54. package/dist/kubernetes/volumes.js +96 -11
  55. package/dist/main.js +2 -0
  56. package/dist/optimizer/calculate-checksum.js +3 -2
  57. package/dist/optimizer/check-key.js +94 -0
  58. package/dist/optimizer/check-keys.js +64 -0
  59. package/dist/optimizer/get-work-cache-stats.js +17 -3
  60. package/dist/optimizer/work-task-cache-description.js +49 -1
  61. package/dist/planner/service-definition.js +9 -4
  62. package/dist/planner/utils/append-work-dependencies.js +5 -3
  63. package/dist/planner/utils/append-work-service.js +18 -1
  64. package/dist/planner/utils/append-work-task.js +13 -3
  65. package/dist/planner/utils/get-work-runtime.js +6 -1
  66. package/dist/planner/utils/normalize-path.js +19 -1
  67. package/dist/planner/utils/parse-work-mount.js +7 -1
  68. package/dist/planner/utils/parse-work-port.js +4 -1
  69. package/dist/planner/validate.js +22 -0
  70. package/dist/planner/work-network.js +3 -1
  71. package/dist/planner/work-runtime-aggregate.js +32 -0
  72. package/dist/planner/work-runtime-kubernetes.js +153 -26
  73. package/dist/planner/work-task.js +6 -1
  74. package/dist/program.js +27 -9
  75. package/dist/remote/device-login.js +7 -2
  76. package/dist/remote/host-config.js +69 -3
  77. package/dist/remote/remote-commands.js +33 -7
  78. package/dist/remote/remote-status.js +112 -0
  79. package/dist/remote/remote-work-commands.js +562 -45
  80. package/dist/remote/server-client.js +15 -2
  81. package/dist/remote/token-source.js +64 -26
  82. package/dist/run-program.js +9 -0
  83. package/dist/schema/build-file-base-task-schema.js +3 -3
  84. package/dist/schema/build-file-container-service-schema.js +10 -0
  85. package/dist/schema/build-file-container-task-schema.js +16 -1
  86. package/dist/schema/build-file-include-schema.js +18 -4
  87. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  88. package/dist/schema/build-file-task-schema.js +5 -1
  89. package/dist/schema/capabilities-schema.js +56 -0
  90. package/dist/schema/reference-parser.js +119 -11
  91. package/dist/schema/removed-properties.js +20 -0
  92. package/dist/schema/schema-parser.js +94 -41
  93. package/dist/schema/work-scope-parser.js +33 -10
  94. package/dist/secrets/command-secret-provider.js +3 -1
  95. package/dist/secrets/github-app-token-command.js +3 -1
  96. package/dist/secrets/secret-catalog.js +43 -19
  97. package/dist/server/accounts-api.js +4 -1
  98. package/dist/server/approvals-api.js +1 -1
  99. package/dist/server/capacity-format.js +36 -0
  100. package/dist/server/capacity.js +300 -0
  101. package/dist/server/checks-api.js +83 -0
  102. package/dist/server/checks.js +116 -0
  103. package/dist/server/dashboard.js +196 -0
  104. package/dist/server/definitions-api.js +66 -3
  105. package/dist/server/endpoints.js +794 -0
  106. package/dist/server/executor.js +164 -9
  107. package/dist/server/kubernetes-backend.js +478 -70
  108. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  109. package/dist/server/overview.js +271 -0
  110. package/dist/server/plan.js +170 -121
  111. package/dist/server/policy.js +34 -2
  112. package/dist/server/proposals-api.js +17 -8
  113. package/dist/server/repo-access.js +5 -0
  114. package/dist/server/run-logs.js +4 -0
  115. package/dist/server/run-plan.js +38 -4
  116. package/dist/server/run-request.js +58 -3
  117. package/dist/server/run-worker.js +262 -13
  118. package/dist/server/runs.js +969 -36
  119. package/dist/server/sandbox-pod.js +28 -0
  120. package/dist/server/sandbox-storage.js +23 -0
  121. package/dist/server/sandboxes.js +8 -0
  122. package/dist/server/server-config.js +246 -10
  123. package/dist/server/server.js +429 -32
  124. package/dist/server/service-runs.js +82 -0
  125. package/dist/server/timings.js +132 -0
  126. package/dist/server/wait-reason.js +21 -0
  127. package/dist/utils/tar.js +28 -1
  128. package/dist/utils/terminal-prompt.js +53 -0
  129. package/package.json +8 -2
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HostExecutor = exports.volumeScope = exports.cacheNamespace = exports.defaultWorker = void 0;
3
+ exports.HostExecutor = exports.removeServiceRunVolumes = exports.serviceRunScopes = exports.volumeScope = exports.cacheNamespace = exports.defaultWorker = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const crypto_1 = require("crypto");
6
+ const checks_1 = require("./checks");
6
7
  const fs_1 = require("fs");
7
8
  const promises_1 = require("fs/promises");
8
9
  const path_1 = require("path");
@@ -11,6 +12,7 @@ const resolve_git_source_1 = require("../includes/resolve-git-source");
11
12
  const git_checkout_1 = require("./git-checkout");
12
13
  const run_plan_1 = require("./run-plan");
13
14
  const work_network_1 = require("../planner/work-network");
15
+ const execute_docker_1 = require("../executer/execute-docker");
14
16
  // `.git` as a file system may read it: any case, and a name Windows reads without its
15
17
  // trailing dots and spaces, or by its short name
16
18
  function isGitDirectory(segment) {
@@ -35,19 +37,65 @@ exports.defaultWorker = defaultWorker;
35
37
  // share one, those of an upload or a sandbox only their owner's. Somebody who can only
36
38
  // upload, or an agent on a sandbox, can not leave an entry for a run from a reviewed
37
39
  // repository to find.
40
+ //
41
+ // A check on a sandbox runs on a copy of the files a commit of them would hold, so it is a
42
+ // run of the repository like a ref run of that commit, and shares its cache: what its
43
+ // owner may write in the sandbox is not part of it. It leaves entries a run from main can
44
+ // find, keyed by the files it ran on, as a ref run of a branch does: so only a caller who
45
+ // may push a branch of the repository and run from it may check (server.ts).
38
46
  function cacheNamespace(run) {
39
- const key = run.source.type === 'ref' ? `ref\0${run.source.repo}` : `${run.source.type}\0${run.owner.iss}\0${run.owner.sub}`;
40
- return `${run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
47
+ const repository = run.source.type === 'ref' || (run.source.type === 'sandbox' && run.source.check);
48
+ const key = repository
49
+ ? `ref\0${run.source.repo}`
50
+ : `${run.source.type}\0${run.owner.iss}\0${run.owner.sub}`;
51
+ return `${repository ? 'ref' : run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
41
52
  }
42
53
  exports.cacheNamespace = cacheNamespace;
43
54
  // The docker volumes of a run's services are its caller's, within the cache's source trust:
44
55
  // two callers of one repository share a cache, a task is keyed by what it reads, but not the
45
- // data their services keep.
46
- function volumeScope(run) {
56
+ // data their services keep. A service run's are its own: they stay while it sleeps and go
57
+ // when it ends (removeServiceRunVolumes). They carry the server's key (see serviceRunScopes),
58
+ // so a server sweeps its own only, next to another one on the same docker.
59
+ function volumeScope(run, server = '') {
60
+ if (run.up) {
61
+ return `${serviceRunScopes(server)}${(0, crypto_1.createHash)('sha256').update(run.id).digest('hex').slice(0, 24)}`;
62
+ }
47
63
  const key = `${cacheNamespace(run)}\0${run.owner.iss}\0${run.owner.sub}`;
48
64
  return `${run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
49
65
  }
50
66
  exports.volumeScope = volumeScope;
67
+ // the start of the volume scopes of the service runs of a server, by the directory its runs
68
+ // work in
69
+ function serviceRunScopes(server) {
70
+ return `up-${(0, crypto_1.createHash)('sha256').update(server).digest('hex').slice(0, 8)}-`;
71
+ }
72
+ exports.serviceRunScopes = serviceRunScopes;
73
+ // the volume of a service of a service run, with the scope of its run (see scopeServiceVolumes)
74
+ const SERVICE_RUN_VOLUME = /^hammerkit-(up-[0-9a-f]{8}-[0-9a-f]{24})-/;
75
+ /**
76
+ * Removes the docker volumes of the services of service runs whose scope `remove` names: of
77
+ * one run that ended, or at a start, of every run but those that sleep with their volumes.
78
+ * One a container still uses stays, and is said.
79
+ */
80
+ async function removeServiceRunVolumes(docker, remove, log) {
81
+ const listed = await docker.listVolumes({ filters: { name: ['hammerkit-up-'] } });
82
+ const removed = [];
83
+ for (const volume of listed.Volumes ?? []) {
84
+ const scope = SERVICE_RUN_VOLUME.exec(volume.Name)?.[1];
85
+ if (!scope || !remove(scope)) {
86
+ continue;
87
+ }
88
+ try {
89
+ await docker.getVolume(volume.Name).remove();
90
+ removed.push(volume.Name);
91
+ }
92
+ catch (e) {
93
+ log(`the volume ${volume.Name} could not be removed: ${e instanceof Error ? e.message : String(e)}`);
94
+ }
95
+ }
96
+ return removed;
97
+ }
98
+ exports.removeServiceRunVolumes = removeServiceRunVolumes;
51
99
  /**
52
100
  * Runs the tasks of a run on this machine with the engine of `hammerkit run`, in a
53
101
  * process of its own.
@@ -65,6 +113,56 @@ exports.volumeScope = volumeScope;
65
113
  class HostExecutor {
66
114
  constructor(options) {
67
115
  this.options = options;
116
+ // the directories of the executions that run now
117
+ this.directories = new Set();
118
+ }
119
+ // the docker daemon the runs' services are on, as their processes reach it
120
+ docker() {
121
+ return (0, execute_docker_1.createDockerClient)(this.options.planning.environment.processEnvs.DOCKER_HOST);
122
+ }
123
+ // the volumes of a service run's services, which its end removes
124
+ async removeVolumes(run, context) {
125
+ const scope = volumeScope(run, this.options.directory);
126
+ const say = (text) => {
127
+ this.options.log?.(`run=${run.id} ${text}`);
128
+ context?.emit({ type: 'log', task: 'hammerkit', stream: 'stderr', text });
129
+ };
130
+ try {
131
+ await removeServiceRunVolumes(this.docker(), (candidate) => candidate === scope, say);
132
+ }
133
+ catch (e) {
134
+ say(`the volumes of the services could not be removed: ${e instanceof Error ? e.message : String(e)}`);
135
+ }
136
+ }
137
+ /** A service run that slept with its volumes ended without waking: they go. */
138
+ async forget(run) {
139
+ if (run.up && !this.options.placement) {
140
+ await this.removeVolumes(run);
141
+ }
142
+ }
143
+ /**
144
+ * At the start of the server: the volumes of service runs go, but of those that sleep with
145
+ * them. A server without docker has none.
146
+ */
147
+ restored(runs) {
148
+ if (this.options.placement) {
149
+ return;
150
+ }
151
+ const own = serviceRunScopes(this.options.directory);
152
+ const kept = new Set(runs.filter((run) => run.up).map((run) => volumeScope(run, this.options.directory)));
153
+ const log = (line) => this.options.log?.(line);
154
+ removeServiceRunVolumes(this.docker(), (scope) => scope.startsWith(own) && !kept.has(scope), log)
155
+ .then((removed) => {
156
+ if (removed.length > 0) {
157
+ log(`removed ${removed.length} volumes of service runs from before the server started`);
158
+ }
159
+ })
160
+ .catch((e) => {
161
+ const code = e.code;
162
+ if (code !== 'ENOENT' && code !== 'ECONNREFUSED') {
163
+ log(`the volumes of earlier service runs could not be removed: ${e instanceof Error ? e.message : e}`);
164
+ }
165
+ });
68
166
  }
69
167
  async execute(run, context) {
70
168
  const { definitions, planning, repos } = this.options;
@@ -110,7 +208,8 @@ class HostExecutor {
110
208
  abort();
111
209
  }
112
210
  context.signal.addEventListener('abort', abort, { once: true });
113
- const root = (0, path_1.join)(this.options.directory, run.id);
211
+ // a service run that moves executes beside itself, each in a directory of its own
212
+ const root = this.directoryOf(run);
114
213
  let attachment;
115
214
  try {
116
215
  // where the process of the run starts: the checkout of a ref, or for a sandbox the
@@ -121,13 +220,42 @@ class HostExecutor {
121
220
  let claim;
122
221
  // the files of the sandbox the tasks work on
123
222
  let work;
124
- if (source.type === 'ref') {
223
+ if (source.type === 'sandbox' && source.check) {
224
+ // a check works on the files a push of the sandbox commits, copied out of it: never
225
+ // on the sandbox, whose ignored files (node_modules, outputs) its owner writes
226
+ const { registry, network } = sandboxes;
227
+ cwd = (0, path_1.join)(root, 'source');
228
+ try {
229
+ await registry.exportTree(source.id, source.tree, cwd, abortCtrl.signal);
230
+ }
231
+ catch (e) {
232
+ throw new Error(`the files of the sandbox ${source.id} could not be copied for the check, submit it again: ${e instanceof Error ? e.message : String(e)}`);
233
+ }
234
+ // a repository of its own, as a checkout of the commit is: the root of the project,
235
+ // which the paths of the tasks' keys are relative to, is the one of git
236
+ await (0, git_checkout_1.git)(['init', '--quiet', cwd], {}, abortCtrl.signal, `the check of sandbox ${source.id}`, 'init');
237
+ context.emit({
238
+ type: 'log',
239
+ task: 'hammerkit',
240
+ stream: 'stdout',
241
+ text: `check of sandbox ${source.id} of ${source.repo}: files ${source.tree}, fetched at ${source.commit}`,
242
+ });
243
+ await this.writeBundle(cwd, bundle.files);
244
+ input = {
245
+ entry: (0, path_1.join)(cwd, version.entry),
246
+ ...(planning.policy.network ? { network: (0, work_network_1.toNetworkSchema)(planning.policy.network) } : {}),
247
+ sandboxNetwork: network,
248
+ };
249
+ }
250
+ else if (source.type === 'ref') {
125
251
  cwd = (0, path_1.join)(root, 'source');
126
252
  const { repo, ref, commit: pinned } = source;
127
253
  const credentials = await repos.credentials(repo);
128
254
  let commit;
129
255
  try {
130
- commit = await (0, git_checkout_1.checkoutRef)(repos.url(repo), ref, cwd, credentials.env, abortCtrl.signal, pinned);
256
+ // a woken service run starts the commit it started first, where the ref is now or not
257
+ const fetched = run.wakes ? pinned : ref;
258
+ commit = await (0, git_checkout_1.checkoutRef)(repos.url(repo), fetched, cwd, credentials.env, abortCtrl.signal, pinned);
131
259
  }
132
260
  catch (e) {
133
261
  throw repos.scrub(e, credentials.values);
@@ -187,27 +315,51 @@ class HostExecutor {
187
315
  tasks: run.tasks,
188
316
  options: run.options,
189
317
  accounts: run.accounts,
190
- volumeScope: volumeScope(run),
318
+ volumeScope: volumeScope(run, this.options.directory),
319
+ ...((0, checks_1.isCheckRun)(run) ? { check: true } : {}),
191
320
  catalog: planning.environment.hosted?.catalog,
321
+ ...(run.up
322
+ ? {
323
+ up: {
324
+ services: run.up.services,
325
+ endpoints: run.up.endpoints.map(({ service, port }) => ({ service, port })),
326
+ },
327
+ }
328
+ : {}),
192
329
  ...(placed ? { kubernetes: placed.kubernetes } : {}),
330
+ ...(!placed && this.options.capabilityRuntime ? { capabilityRuntime: this.options.capabilityRuntime } : {}),
193
331
  }, cwd, { ...this.processEnvironment(home, cache), ...placed?.env }, context, abortCtrl.signal);
194
332
  }
195
333
  finally {
196
334
  if (placement && placed) {
197
335
  await placement.release(run, context);
198
336
  }
337
+ else if (run.up && !context.keep?.()) {
338
+ // the services are gone with the process, their volumes stay only while the run sleeps
339
+ await this.removeVolumes(run, context);
340
+ }
199
341
  }
200
342
  }
201
343
  finally {
202
344
  context.signal.removeEventListener('abort', abort);
203
345
  attachment?.release?.();
204
346
  await this.removeRunDirectory(run, root, context);
347
+ this.directories.delete(root);
205
348
  if (source.type === 'sandbox') {
206
349
  // what the run wrote counts against the owner's limits, from their next run or create
207
350
  sandboxes?.registry.measure(source.id).catch(() => undefined);
208
351
  }
209
352
  }
210
353
  }
354
+ directoryOf(run) {
355
+ for (let execution = 1;; execution++) {
356
+ const root = (0, path_1.join)(this.options.directory, execution === 1 ? run.id : `${run.id}-${execution}`);
357
+ if (!this.directories.has(root)) {
358
+ this.directories.add(root);
359
+ return root;
360
+ }
361
+ }
362
+ }
211
363
  // A task of the run may still hold a file of its directory for a moment after the run
212
364
  // ended, which Windows refuses to remove. A directory that stays is reported, to the run
213
365
  // by its error code and to the operator with its path; it does not change how the run ended.
@@ -282,6 +434,9 @@ class HostExecutor {
282
434
  if (message.kind === 'event') {
283
435
  context.emit(message.event);
284
436
  }
437
+ else if (message.kind === 'up') {
438
+ context.up?.(message.targets);
439
+ }
285
440
  else if (message.kind === 'result') {
286
441
  result = message;
287
442
  // nothing left to tell the process