hammerkit 1.26.0 → 1.27.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 (117) hide show
  1. package/build.schema.json +53 -1
  2. package/dist/cache/backends/local-cache-backend.js +10 -2
  3. package/dist/cache/explain.js +13 -11
  4. package/dist/cli.js +3 -0
  5. package/dist/docker/item-network.js +471 -0
  6. package/dist/docker/pull.js +4 -2
  7. package/dist/docker/remove-orphaned-containers.js +13 -6
  8. package/dist/environment/replace-env-variables.js +8 -6
  9. package/dist/executer/await-completed-dependencies.js +2 -1
  10. package/dist/executer/check-readiness.js +2 -2
  11. package/dist/executer/docker-service.js +24 -13
  12. package/dist/executer/docker-task.js +18 -29
  13. package/dist/executer/event-cache.js +4 -0
  14. package/dist/executer/execute-command.js +18 -5
  15. package/dist/executer/execute-docker.js +4 -1
  16. package/dist/executer/execution-steps.js +33 -7
  17. package/dist/executer/get-docker-executor.js +14 -7
  18. package/dist/executer/scheduler/enqueue-next.js +17 -3
  19. package/dist/executer/watch-loop.js +2 -2
  20. package/dist/executer/watch-state-key.js +1 -1
  21. package/dist/file/within-root.js +42 -0
  22. package/dist/harness/connect.js +99 -0
  23. package/dist/harness/format.js +73 -0
  24. package/dist/harness/mcp-command.js +59 -0
  25. package/dist/harness/mcp.js +432 -0
  26. package/dist/harness/opencode.js +492 -0
  27. package/dist/harness/pi.js +217 -0
  28. package/dist/harness/sandbox-client.js +373 -0
  29. package/dist/index.js +33 -36
  30. package/dist/kubernetes/apply.js +31 -1
  31. package/dist/kubernetes/await-running-state.js +33 -1
  32. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
  33. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
  34. package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
  35. package/dist/kubernetes/ensure-persistent-data.js +48 -7
  36. package/dist/kubernetes/get-service-ip.js +15 -1
  37. package/dist/kubernetes/job-placement.js +99 -0
  38. package/dist/kubernetes/job-secrets.js +306 -0
  39. package/dist/kubernetes/kept-claims.js +191 -0
  40. package/dist/kubernetes/kubernetes-instance.js +10 -2
  41. package/dist/kubernetes/network-gate.js +71 -0
  42. package/dist/kubernetes/network-policy.js +526 -0
  43. package/dist/kubernetes/resources.js +13 -2
  44. package/dist/kubernetes/restore-kubernetes-data.js +19 -4
  45. package/dist/kubernetes/secrets.js +2 -0
  46. package/dist/kubernetes/store-kubernetes-data.js +1 -1
  47. package/dist/kubernetes/volumes.js +100 -11
  48. package/dist/main.js +41 -0
  49. package/dist/network/egress-proxy.js +368 -0
  50. package/dist/network/port-relay.js +35 -0
  51. package/dist/optimizer/get-work-cache-stats.js +16 -0
  52. package/dist/optimizer/work-task-cache-description.js +3 -2
  53. package/dist/planner/service-definition.js +5 -0
  54. package/dist/planner/utils/append-work-environment.js +2 -0
  55. package/dist/planner/utils/append-work-service.js +4 -0
  56. package/dist/planner/utils/append-work-task.js +17 -1
  57. package/dist/planner/utils/assert-item-paths.js +50 -0
  58. package/dist/planner/utils/hosted-volumes.js +35 -0
  59. package/dist/planner/work-network.js +193 -0
  60. package/dist/planner/work-runtime-docker.js +41 -0
  61. package/dist/planner/work-runtime-kubernetes.js +335 -102
  62. package/dist/program.js +21 -0
  63. package/dist/remote/annotations.js +103 -0
  64. package/dist/remote/bundle-text.js +24 -0
  65. package/dist/remote/bundle.js +9 -1
  66. package/dist/remote/remote-commands.js +4 -1
  67. package/dist/remote/remote-work-commands.js +182 -69
  68. package/dist/remote/server-client.js +18 -5
  69. package/dist/remote/source-manifest.js +15 -2
  70. package/dist/remote/token-source.js +67 -0
  71. package/dist/schema/build-file-base-task-schema.js +3 -0
  72. package/dist/schema/build-file-container-service-schema.js +6 -0
  73. package/dist/schema/build-file-container-task-schema.js +3 -0
  74. package/dist/schema/build-file-environment-schema.js +9 -0
  75. package/dist/schema/network-schema.js +78 -0
  76. package/dist/schema/reference-parser.js +1 -0
  77. package/dist/schema/schema-parser.js +16 -3
  78. package/dist/schema/work-scope-parser.js +5 -0
  79. package/dist/secrets/command-secret-provider.js +73 -10
  80. package/dist/secrets/github-app-token-command.js +23 -0
  81. package/dist/secrets/github-app-token.js +191 -0
  82. package/dist/server/accounts-api.js +9 -2
  83. package/dist/server/definitions-api.js +69 -47
  84. package/dist/server/definitions.js +76 -51
  85. package/dist/server/executor.js +156 -41
  86. package/dist/server/git-checkout.js +72 -8
  87. package/dist/server/kubernetes-backend.js +554 -0
  88. package/dist/server/kubernetes-sandbox-storage.js +996 -0
  89. package/dist/server/oauth.js +1 -0
  90. package/dist/server/plan.js +51 -1
  91. package/dist/server/policy.js +104 -15
  92. package/dist/server/proposals-api.js +24 -40
  93. package/dist/server/proposals.js +12 -25
  94. package/dist/server/repo-access.js +37 -6
  95. package/dist/server/run-logs.js +217 -0
  96. package/dist/server/run-plan.js +51 -2
  97. package/dist/server/run-request.js +23 -10
  98. package/dist/server/run-store.js +5 -0
  99. package/dist/server/run-worker-command.js +13 -1
  100. package/dist/server/run-worker.js +121 -24
  101. package/dist/server/runs.js +184 -10
  102. package/dist/server/sandbox-docker.js +62 -0
  103. package/dist/server/sandbox-files-api.js +269 -0
  104. package/dist/server/sandbox-files.js +1054 -0
  105. package/dist/server/sandbox-patch.js +481 -0
  106. package/dist/server/sandbox-pod-command.js +15 -0
  107. package/dist/server/sandbox-pod.js +146 -0
  108. package/dist/server/sandbox-storage.js +462 -0
  109. package/dist/server/sandboxes-api.js +210 -0
  110. package/dist/server/sandboxes.js +519 -0
  111. package/dist/server/server-config.js +412 -22
  112. package/dist/server/server.js +204 -39
  113. package/dist/utils/run-id.js +12 -0
  114. package/harness/opencode.ts +11 -0
  115. package/harness/pi.ts +7 -0
  116. package/package.json +8 -2
  117. package/dist/remote/text-diff.js +0 -89
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertWithinRoot = exports.findRootEscape = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ /**
7
+ * Why a path would carry a read or write out of a root that somebody else wrote (a
8
+ * checkout, a sandbox), or null when it stays inside. A path may not lead out of the
9
+ * root, and none of its parts that exist may be a symbolic link, since docker, tar
10
+ * and the file system would follow it wherever it points. A part that doesn't exist
11
+ * yet is fine, it can't be a link until something makes it.
12
+ */
13
+ async function findRootEscape(root, path) {
14
+ const inside = (0, path_1.relative)((0, path_1.resolve)(root), (0, path_1.resolve)(root, path));
15
+ if (inside === '..' || inside.startsWith(`..${path_1.sep}`) || (0, path_1.isAbsolute)(inside)) {
16
+ return 'is outside of the source';
17
+ }
18
+ if (inside === '') {
19
+ return null;
20
+ }
21
+ let current = (0, path_1.resolve)(root);
22
+ for (const part of inside.split(path_1.sep)) {
23
+ current = (0, path_1.join)(current, part);
24
+ const stats = await (0, promises_1.lstat)(current).catch(() => undefined);
25
+ if (!stats) {
26
+ return null;
27
+ }
28
+ if (stats.isSymbolicLink()) {
29
+ return `goes through the symbolic link ${(0, path_1.relative)(root, current).split(path_1.sep).join('/')}`;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ exports.findRootEscape = findRootEscape;
35
+ async function assertWithinRoot(root, path, describe) {
36
+ const escape = await findRootEscape(root, path);
37
+ if (escape) {
38
+ throw new Error(`${describe} ${escape}`);
39
+ }
40
+ }
41
+ exports.assertWithinRoot = assertWithinRoot;
42
+ //# sourceMappingURL=within-root.js.map
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getHarnessEnvironment = exports.connectSandbox = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
4
+ const get_file_context_1 = require("../file/get-file-context");
5
+ const log_1 = require("../log");
6
+ const work_item_status_1 = require("../planner/work-item-status");
7
+ const host_config_1 = require("../remote/host-config");
8
+ const redact_1 = require("../utils/redact");
9
+ const empty_writable_1 = require("../utils/empty-writable");
10
+ const token_source_1 = require("../remote/token-source");
11
+ const bundle_1 = require("../remote/bundle");
12
+ const sandbox_client_1 = require("./sandbox-client");
13
+ exports.DEFAULT_SHELL_TASK = 'sh';
14
+ // The same settings as variables, for harnesses that load hammerkit as a plugin.
15
+ function harnessOptionsFromEnv(env) {
16
+ const value = (name) => (env[name] && env[name]?.trim()) || undefined;
17
+ return {
18
+ server: value('HAMMERKIT_SERVER'),
19
+ sandbox: value('HAMMERKIT_SANDBOX'),
20
+ repo: value('HAMMERKIT_REPO'),
21
+ ref: value('HAMMERKIT_REF'),
22
+ build: value('HAMMERKIT_BUILD'),
23
+ shell: value('HAMMERKIT_SHELL'),
24
+ };
25
+ }
26
+ exports.harnessOptionsFromEnv = harnessOptionsFromEnv;
27
+ async function connectSandbox(environment, options, log = () => undefined) {
28
+ const config = await (0, host_config_1.readHostConfig)(environment);
29
+ const name = options.server ?? config.run.on;
30
+ if (!name || !(0, host_config_1.hasServer)(config.servers, name)) {
31
+ throw new Error(options.server
32
+ ? `server ${options.server} is not registered, add it with: hammerkit remote add ${options.server} <url> --issuer <url>`
33
+ : 'name a server (HAMMERKIT_SERVER or --server), or set a default with: hammerkit remote use <name>');
34
+ }
35
+ if (!options.build) {
36
+ throw new Error('name the published version of the build file whose tasks run on the sandbox by its hash (HAMMERKIT_BUILD or --build), as hammerkit remote publish printed it');
37
+ }
38
+ // a run names exactly the version it runs, there is no name that follows the latest one
39
+ if (!bundle_1.HASH_PATTERN.test(options.build)) {
40
+ 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
+ }
42
+ const server = config.servers[name];
43
+ const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, name, server) };
44
+ let sandbox;
45
+ if (options.sandbox) {
46
+ sandbox = await sandbox_client_1.SandboxClient.get(connection, options.sandbox);
47
+ }
48
+ else if (options.repo && options.ref) {
49
+ sandbox = await reuseSandbox(connection, options.repo, options.ref);
50
+ if (sandbox) {
51
+ log(`took sandbox ${sandbox.id} of ${sandbox.repo} at ${sandbox.commit} on ${name}, it had no changes`);
52
+ }
53
+ else {
54
+ sandbox = await sandbox_client_1.SandboxClient.create(connection, options.repo, options.ref);
55
+ log(`created sandbox ${sandbox.id} of ${sandbox.repo} at ${sandbox.commit} on ${name}, ` +
56
+ `set HAMMERKIT_SANDBOX=${sandbox.id} to come back to it`);
57
+ }
58
+ }
59
+ else {
60
+ 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
+ }
62
+ return new sandbox_client_1.SandboxClient(connection, sandbox, { hash: options.build, shell: options.shell ?? exports.DEFAULT_SHELL_TASK });
63
+ }
64
+ exports.connectSandbox = connectSandbox;
65
+ // A client that starts with a repository and ref takes a sandbox of the caller's of the same
66
+ // that nobody changed and no run holds, checked out at the ref again, rather than creating
67
+ // one on every start: sandboxes stay until they are deleted, and only a few are allowed.
68
+ async function reuseSandbox(connection, repo, ref) {
69
+ const build = { hash: '', shell: exports.DEFAULT_SHELL_TASK };
70
+ for (const candidate of await sandbox_client_1.SandboxClient.list(connection)) {
71
+ if (candidate.repo !== repo || candidate.ref !== ref || candidate.busy) {
72
+ continue;
73
+ }
74
+ const changes = await new sandbox_client_1.SandboxClient(connection, candidate, build).changes();
75
+ if (changes.files.length === 0) {
76
+ return sandbox_client_1.SandboxClient.fetch(connection, candidate.id, ref);
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ // What hammerkit needs of its environment to read the host config and the sign-in, for a
82
+ // harness that loads it as a plugin rather than through the command line.
83
+ function getHarnessEnvironment(cwd = process.cwd()) {
84
+ const secrets = (0, redact_1.createSecretRegistry)();
85
+ return {
86
+ cwd,
87
+ abortCtrl: new AbortController(),
88
+ processEnvs: process.env,
89
+ file: (0, get_file_context_1.getFileContext)(cwd),
90
+ console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()),
91
+ status: (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets),
92
+ secrets,
93
+ stdout: (0, empty_writable_1.emptyWritable)(),
94
+ stderr: (0, empty_writable_1.emptyWritable)(),
95
+ stdoutColumns: 80,
96
+ };
97
+ }
98
+ exports.getHarnessEnvironment = getHarnessEnvironment;
99
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capOutput = exports.toColonNumbers = exports.formatTree = exports.formatSearch = void 0;
4
+ // Lines as grep tools print them: `path:line: text` for a match, `path-line- text` around
5
+ // one, the file names alone or `path:count`, with a note when the server answered a part.
6
+ function formatSearch(result) {
7
+ let lines;
8
+ if (result.output_mode === 'files_with_matches') {
9
+ lines = result.files ?? [];
10
+ }
11
+ else if (result.output_mode === 'count') {
12
+ lines = (result.counts ?? []).map((count) => `${count.path}:${count.count}`);
13
+ }
14
+ else {
15
+ lines = (result.matches ?? []).map((match) => match.context ? `${match.path}-${match.line}- ${match.text}` : `${match.path}:${match.line}: ${match.text}`);
16
+ }
17
+ if (lines.length === 0) {
18
+ return 'No matches found';
19
+ }
20
+ if (result.truncated) {
21
+ lines.push('', `(${lines.length} of ${result.exact ? '' : 'at least '}${result.total} shown, narrow the search or ask with a higher limit)`);
22
+ }
23
+ return lines.join('\n');
24
+ }
25
+ exports.formatSearch = formatSearch;
26
+ // The paths of a listing, a directory with a trailing `/`.
27
+ function formatTree(result, prefix = '') {
28
+ const lines = result.entries.map((entry) => {
29
+ const path = prefix && entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path;
30
+ return entry.type === 'directory' ? `${path}/` : path;
31
+ });
32
+ if (lines.length === 0) {
33
+ return 'No files found';
34
+ }
35
+ if (result.truncated) {
36
+ lines.push('', `(${result.entries.length} of ${result.total} shown, narrow the pattern)`);
37
+ }
38
+ return lines.join('\n');
39
+ }
40
+ exports.formatTree = formatTree;
41
+ // The server numbers lines like `cat -n` (` 1\tline`); some harnesses print `1: line`.
42
+ function toColonNumbers(content) {
43
+ return content
44
+ .split('\n')
45
+ .map((line) => {
46
+ const match = /^\s*(\d+)\t(.*)$/s.exec(line);
47
+ return match ? `${match[1]}: ${match[2]}` : line;
48
+ })
49
+ .join('\n');
50
+ }
51
+ exports.toColonNumbers = toColonNumbers;
52
+ // The most of a command's output a tool answers: its first and last lines, up to about 30 KB.
53
+ // One `cat` of a big file or a verbose install would otherwise fill the model's context.
54
+ const HEAD_LINES = 50;
55
+ const TAIL_LINES = 200;
56
+ const MAX_CHARS = 30000;
57
+ /**
58
+ * The head and the tail of a long output, with a line in between that says how much is cut
59
+ * and how to read it (`readMore`, for example the run_logs call for the run).
60
+ */
61
+ function capOutput(output, readMore) {
62
+ const lines = output.split('\n');
63
+ if (lines.length <= HEAD_LINES + TAIL_LINES && output.length <= MAX_CHARS) {
64
+ return output;
65
+ }
66
+ const many = lines.length > HEAD_LINES + TAIL_LINES;
67
+ // the end tells most of how a command went: it gets the larger part
68
+ const head = (many ? lines.slice(0, HEAD_LINES).join('\n') : output).slice(0, MAX_CHARS / 4);
69
+ const tail = (many ? lines.slice(-TAIL_LINES).join('\n') : output).slice(-(MAX_CHARS * 3) / 4);
70
+ return `${head}\n… the output is cut here, it has ${lines.length} lines in all: ${readMore}\n${tail}`;
71
+ }
72
+ exports.capOutput = capOutput;
73
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerMcpCommand = void 0;
4
+ const commander_1 = require("commander");
5
+ const log_1 = require("../log");
6
+ const version_1 = require("../version");
7
+ const connect_1 = require("./connect");
8
+ const mcp_1 = require("./mcp");
9
+ // `hammerkit remote mcp`: a Model Context Protocol server on stdio for one sandbox of a
10
+ // server. It lives under `remote`, which already is a command: a top-level `mcp` would hide a
11
+ // task named `mcp`, since `hammerkit <task>` runs a task. Like the other `remote` commands it
12
+ // needs no build file of its own: the tasks are the published ones.
13
+ function registerMcpCommand(remote, environment, input = process.stdin) {
14
+ // errors go through the program, like those of the other remote commands: it carries the exit handling
15
+ const program = remote.parent ?? remote;
16
+ remote
17
+ .command('mcp')
18
+ .description('serve the files, shell and tasks of a sandbox on a server as MCP tools over stdio (experimental)')
19
+ .addOption(new commander_1.Option('--server <name>', 'registered server (default: hammerkit remote use)').env('HAMMERKIT_SERVER'))
20
+ .addOption(new commander_1.Option('--sandbox <id>', 'a sandbox of yours on the server').env('HAMMERKIT_SANDBOX'))
21
+ .addOption(new commander_1.Option('--repo <repo>', 'create a sandbox of this repository').env('HAMMERKIT_REPO'))
22
+ .addOption(new commander_1.Option('--ref <ref>', 'at this branch').env('HAMMERKIT_REF'))
23
+ .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>', 'its task whose last command is a shell, like sh -c')
25
+ .env('HAMMERKIT_SHELL')
26
+ .default(connect_1.DEFAULT_SHELL_TASK))
27
+ .action(async (options) => {
28
+ try {
29
+ // stdout carries the protocol only, everything else goes to stderr
30
+ const log = (line) => environment.stderr.write(`hammerkit remote mcp: ${line}\n`);
31
+ const sandbox = await (0, connect_1.connectSandbox)(environment, options, log);
32
+ log(`sandbox ${sandbox.id} of ${sandbox.sandbox.repo} at ${sandbox.sandbox.commit}`);
33
+ 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. bash runs a command as the ` +
35
+ `task ${sandbox.build.shell} of the published build file ${sandbox.build.hash}, run_task runs its other ` +
36
+ 'tasks; only the files are kept between commands. changes shows what changed since the fetch.';
37
+ const served = (0, mcp_1.serveMcp)(input, environment.stdout, (0, mcp_1.createMcpTools)(async () => sandbox), {
38
+ name: 'hammerkit',
39
+ version: (0, version_1.getVersion)(),
40
+ instructions,
41
+ });
42
+ const stopped = new Promise((resolve) => {
43
+ if (environment.abortCtrl.signal.aborted) {
44
+ resolve();
45
+ }
46
+ environment.abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true });
47
+ });
48
+ await Promise.race([served, stopped]);
49
+ }
50
+ catch (e) {
51
+ if (e instanceof commander_1.CommanderError) {
52
+ throw e;
53
+ }
54
+ program.error(`Mcp was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
55
+ }
56
+ });
57
+ }
58
+ exports.registerMcpCommand = registerMcpCommand;
59
+ //# sourceMappingURL=mcp-command.js.map