hammerkit 1.26.0 → 1.27.0-next.10

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 (128) hide show
  1. package/build.schema.json +62 -1
  2. package/dist/cache/backends/local-cache-backend.js +10 -2
  3. package/dist/cache/backends/registry-cache-backend.js +2 -2
  4. package/dist/cache/explain.js +13 -11
  5. package/dist/cli.js +3 -0
  6. package/dist/docker/item-network.js +471 -0
  7. package/dist/docker/pull.js +4 -2
  8. package/dist/docker/remove-orphaned-containers.js +13 -6
  9. package/dist/environment/replace-env-variables.js +8 -6
  10. package/dist/executer/await-completed-dependencies.js +2 -1
  11. package/dist/executer/check-readiness.js +2 -2
  12. package/dist/executer/docker-service.js +24 -13
  13. package/dist/executer/docker-task.js +18 -29
  14. package/dist/executer/event-cache.js +24 -2
  15. package/dist/executer/execute-command.js +18 -5
  16. package/dist/executer/execute-docker.js +4 -1
  17. package/dist/executer/execution-steps.js +33 -7
  18. package/dist/executer/get-docker-executor.js +14 -7
  19. package/dist/executer/scheduler/enqueue-next.js +17 -3
  20. package/dist/executer/watch-loop.js +2 -2
  21. package/dist/executer/watch-state-key.js +1 -1
  22. package/dist/file/within-root.js +42 -0
  23. package/dist/harness/connect.js +99 -0
  24. package/dist/harness/format.js +73 -0
  25. package/dist/harness/mcp-command.js +59 -0
  26. package/dist/harness/mcp.js +432 -0
  27. package/dist/harness/opencode.js +492 -0
  28. package/dist/harness/pi.js +217 -0
  29. package/dist/harness/sandbox-client.js +411 -0
  30. package/dist/index.js +33 -36
  31. package/dist/kubernetes/apply.js +42 -1
  32. package/dist/kubernetes/await-running-state.js +33 -1
  33. package/dist/kubernetes/claim-layout.js +71 -0
  34. package/dist/kubernetes/ensure-http-route.js +1 -1
  35. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +46 -28
  36. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +132 -5
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +27 -11
  38. package/dist/kubernetes/ensure-persistent-data.js +75 -24
  39. package/dist/kubernetes/get-service-ip.js +16 -2
  40. package/dist/kubernetes/job-placement.js +99 -0
  41. package/dist/kubernetes/job-secrets.js +320 -0
  42. package/dist/kubernetes/kept-claims.js +203 -0
  43. package/dist/kubernetes/kubernetes-instance.js +10 -2
  44. package/dist/kubernetes/network-gate.js +71 -0
  45. package/dist/kubernetes/network-policy.js +583 -0
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/resources.js +37 -4
  48. package/dist/kubernetes/restore-kubernetes-data.js +28 -7
  49. package/dist/kubernetes/secrets.js +7 -9
  50. package/dist/kubernetes/store-kubernetes-data.js +1 -1
  51. package/dist/kubernetes/volumes.js +104 -12
  52. package/dist/log.js +1 -1
  53. package/dist/main.js +41 -0
  54. package/dist/network/egress-proxy.js +407 -0
  55. package/dist/network/port-relay.js +35 -0
  56. package/dist/optimizer/get-work-cache-stats.js +16 -0
  57. package/dist/optimizer/work-task-cache-description.js +5 -2
  58. package/dist/planner/service-definition.js +5 -0
  59. package/dist/planner/utils/append-work-dependencies.js +5 -2
  60. package/dist/planner/utils/append-work-environment.js +2 -0
  61. package/dist/planner/utils/append-work-service.js +5 -0
  62. package/dist/planner/utils/append-work-task.js +20 -1
  63. package/dist/planner/utils/assert-item-paths.js +50 -0
  64. package/dist/planner/utils/hosted-volumes.js +35 -0
  65. package/dist/planner/utils/task-arguments.js +3 -0
  66. package/dist/planner/work-network.js +193 -0
  67. package/dist/planner/work-runtime-docker.js +43 -1
  68. package/dist/planner/work-runtime-kubernetes.js +370 -111
  69. package/dist/planner/work-runtime-local.js +2 -1
  70. package/dist/program.js +33 -5
  71. package/dist/remote/annotations.js +115 -0
  72. package/dist/remote/bundle-text.js +24 -0
  73. package/dist/remote/bundle.js +9 -1
  74. package/dist/remote/remote-commands.js +15 -3
  75. package/dist/remote/remote-work-commands.js +272 -71
  76. package/dist/remote/server-client.js +18 -5
  77. package/dist/remote/source-manifest.js +15 -2
  78. package/dist/remote/token-source.js +67 -0
  79. package/dist/schema/build-file-base-task-schema.js +7 -0
  80. package/dist/schema/build-file-container-service-schema.js +8 -0
  81. package/dist/schema/build-file-container-task-schema.js +3 -0
  82. package/dist/schema/build-file-environment-schema.js +9 -0
  83. package/dist/schema/network-schema.js +78 -0
  84. package/dist/schema/reference-parser.js +2 -0
  85. package/dist/schema/schema-parser.js +16 -3
  86. package/dist/schema/work-scope-parser.js +5 -0
  87. package/dist/secrets/command-secret-provider.js +73 -10
  88. package/dist/secrets/github-app-token-command.js +26 -0
  89. package/dist/secrets/github-app-token.js +191 -0
  90. package/dist/server/accounts-api.js +9 -2
  91. package/dist/server/definitions-api.js +69 -47
  92. package/dist/server/definitions.js +76 -51
  93. package/dist/server/executor.js +156 -41
  94. package/dist/server/git-checkout.js +72 -8
  95. package/dist/server/kubernetes-backend.js +603 -0
  96. package/dist/server/kubernetes-sandbox-storage.js +997 -0
  97. package/dist/server/oauth.js +12 -0
  98. package/dist/server/plan.js +88 -2
  99. package/dist/server/policy.js +104 -15
  100. package/dist/server/proposals-api.js +24 -40
  101. package/dist/server/proposals.js +12 -25
  102. package/dist/server/repo-access.js +37 -6
  103. package/dist/server/run-logs.js +217 -0
  104. package/dist/server/run-plan.js +48 -2
  105. package/dist/server/run-request.js +29 -10
  106. package/dist/server/run-store.js +5 -0
  107. package/dist/server/run-worker-command.js +13 -1
  108. package/dist/server/run-worker.js +121 -24
  109. package/dist/server/runs.js +191 -10
  110. package/dist/server/sandbox-docker.js +62 -0
  111. package/dist/server/sandbox-files-api.js +269 -0
  112. package/dist/server/sandbox-files.js +1054 -0
  113. package/dist/server/sandbox-patch.js +481 -0
  114. package/dist/server/sandbox-pod-command.js +15 -0
  115. package/dist/server/sandbox-pod.js +146 -0
  116. package/dist/server/sandbox-storage.js +462 -0
  117. package/dist/server/sandboxes-api.js +216 -0
  118. package/dist/server/sandboxes.js +519 -0
  119. package/dist/server/server-command.js +39 -20
  120. package/dist/server/server-config.js +443 -22
  121. package/dist/server/server.js +219 -40
  122. package/dist/utils/abort-on-signals.js +46 -6
  123. package/dist/utils/run-id.js +12 -0
  124. package/dist/utils/tar.js +25 -0
  125. package/harness/opencode.ts +11 -0
  126. package/harness/pi.ts +7 -0
  127. package/package.json +8 -2
  128. package/dist/remote/text-diff.js +0 -89
@@ -3,32 +3,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.registerWorkCommands = void 0;
6
+ exports.registerWorkCommands = exports.describePlan = exports.describeApproval = void 0;
7
7
  const commander_1 = require("commander");
8
8
  const colors_1 = __importDefault(require("colors"));
9
- const credentials_1 = require("./credentials");
9
+ const token_source_1 = require("./token-source");
10
10
  const collect_bundle_1 = require("./collect-bundle");
11
11
  const bundle_1 = require("./bundle");
12
+ const annotations_1 = require("./annotations");
12
13
  const server_client_1 = require("./server-client");
13
- const text_diff_1 = require("./text-diff");
14
- const PROPOSAL_NAME = /^([a-z0-9][a-z0-9._-]{0,63})@(sha256:[0-9a-f]{64})$/i;
15
- // A proposed version is named like `payments@sha256:...`
16
- function parseProposal(value) {
17
- const match = PROPOSAL_NAME.exec(value);
18
- return match && bundle_1.HASH_PATTERN.test(match[2]) ? { name: match[1], hash: match[2] } : undefined;
19
- }
14
+ const bundle_text_1 = require("./bundle-text");
15
+ const sandbox_client_1 = require("../harness/sandbox-client");
16
+ const path_1 = require("path");
20
17
  const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
18
+ // what the publisher said about where a version comes from, one line
19
+ // What a publisher said about where a version comes from: the server takes it as said,
20
+ // so anybody else is told it is not checked.
21
+ function annotationLines(annotations, label = 'from (unverified)') {
22
+ const entries = Object.entries(annotations ?? {});
23
+ return entries.length > 0
24
+ ? [` ${label}: ${entries.map(([key, value]) => `${(0, server_client_1.printable)(key)}=${(0, server_client_1.printable)(value)}`).join(' ')}`]
25
+ : [];
26
+ }
27
+ function yankLines(version) {
28
+ return version.yanked
29
+ ? [
30
+ ` yanked by ${(0, server_client_1.printable)(version.yanked.by.sub)}${version.yanked.reason ? `: ${(0, server_client_1.printable)(version.yanked.reason)}` : ''}`,
31
+ ]
32
+ : [];
33
+ }
34
+ // A line a task printed, shown as it is but for control characters, so it can't rewrite
35
+ // the terminal; tabs stay.
36
+ function printableLine(text) {
37
+ return text.split('\t').map(server_client_1.printableWhole).join('\t');
38
+ }
39
+ const collectOption = (value, previous = []) => [...previous, value];
21
40
  // What a person who approves is shown of a run: everything the decision rests on.
22
- function describe(run) {
23
- const source = run.source.type === 'ref' ? `ref ${run.source.ref}` : `snapshot ${run.source.snapshot}`;
24
- const options = Object.entries(run.options)
41
+ function describeApproval(run) {
42
+ const source = run.source.type === 'ref'
43
+ ? `ref ${run.source.ref}`
44
+ : run.source.type === 'sandbox'
45
+ ? `sandbox of ${run.source.repo} at ${run.source.commit}, ${run.source.clean ? 'unchanged' : `with changes, files ${run.source.tree}`} (files .gitignore leaves out are not shown)`
46
+ : `snapshot ${run.source.snapshot}`;
47
+ // the arguments are shown whole on a line of their own, each as a JSON string,
48
+ // so commas, spaces and quotes inside one stay visible
49
+ const { args, ...settings } = run.options;
50
+ const options = Object.entries(settings)
25
51
  .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
26
52
  .join(' ');
27
53
  return [
28
54
  `${colors_1.default.bold((0, server_client_1.printable)(run.id))} requested by ${(0, server_client_1.printable)(run.requestedBy.sub)} (${(0, server_client_1.printable)(run.requestedBy.iss)})`,
29
- ` ${(0, server_client_1.printable)(run.definition.name)} ${(0, server_client_1.printable)(run.definition.hash)}`,
55
+ ` build file ${(0, server_client_1.printable)(run.definition)}`,
30
56
  ` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
31
57
  ` accounts: ${run.accounts.length > 0 ? run.accounts.map(server_client_1.printable).join(', ') : 'none'}, source ${(0, server_client_1.printable)(source)}${options ? `, ${(0, server_client_1.printable)(options)}` : ''}`,
58
+ ...(Array.isArray(args) && args.length > 0
59
+ ? [` arguments: ${args.map((arg) => (0, server_client_1.printableWhole)(JSON.stringify(arg))).join(' ')}`]
60
+ : []),
32
61
  ...(run.approvals ?? []).map((approval) => ` ${(0, server_client_1.printable)(approval.account)}: ${approval.approvedBy
33
62
  ? `approved by ${(0, server_client_1.printable)(approval.approvedBy.sub)}`
34
63
  : 'waits for somebody who uses it to approve'}`),
@@ -36,6 +65,7 @@ function describe(run) {
36
65
  ` decide before ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`,
37
66
  ];
38
67
  }
68
+ exports.describeApproval = describeApproval;
39
69
  // What to look at first, one line.
40
70
  function flagLines(run) {
41
71
  const flagged = Object.entries(run.plan ?? {}).filter(([, entry]) => entry.flags.length > 0);
@@ -51,7 +81,11 @@ function flagLines(run) {
51
81
  function describePlan(plan) {
52
82
  return Object.entries(plan).flatMap(([name, entry]) => [
53
83
  ` ${colors_1.default.bold((0, server_client_1.printable)(name))} ${entry.kind}, ${(0, server_client_1.printable)(entry.runtime)}${entry.image ? ` ${(0, server_client_1.printable)(entry.image)}` : ''}${entry.flags.length > 0 ? ` [${entry.flags.map(server_client_1.printable).join(', ')}]` : ''}`,
54
- ...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printable)(cmd)}`),
84
+ // a command is what is decided on: shown whole, however long
85
+ ...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printableWhole)(cmd)}`),
86
+ ...(entry.network !== undefined && entry.network !== 'open'
87
+ ? [` network: ${(0, server_client_1.printable)(typeof entry.network === 'string' ? entry.network : JSON.stringify(entry.network))}`]
88
+ : []),
55
89
  ...(entry.deps.length > 0 ? [` after: ${entry.deps.map(server_client_1.printable).join(', ')}`] : []),
56
90
  ...(entry.needs.length > 0 ? [` needs: ${entry.needs.map(server_client_1.printable).join(', ')}`] : []),
57
91
  ...entry.secrets.map((secret) => ` secret ${(0, server_client_1.printable)(secret.from)}${secret.account ? ` as ${(0, server_client_1.printable)(secret.account)}` : ''} -> ${(0, server_client_1.printable)(secret.target)}`),
@@ -60,29 +94,159 @@ function describePlan(plan) {
60
94
  ...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
61
95
  ]);
62
96
  }
97
+ exports.describePlan = describePlan;
63
98
  function registerWorkCommands(remote, context) {
64
- const { environment, buildFile, getServer, guarded } = context;
99
+ const { environment, buildFile, getServer, guarded, passthrough, fail } = context;
65
100
  const connect = async (name) => {
66
101
  const chosen = await getServer(name);
67
- const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
102
+ // a CI job or an agent brings its own token, a person signs in
103
+ const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
68
104
  return { ...chosen, token };
69
105
  };
70
106
  remote
71
- .command('publish <definition> [server]')
72
- .description('publish the build file and the files it includes to a server, under a name runs can refer to')
73
- .action(guarded('Remote publish was not successful', async (definition, name) => {
107
+ .command('publish [server]')
108
+ .description('publish the build file and the files it includes to a server, runs refer to it by its hash')
109
+ .addOption(new commander_1.Option('--annotation <key=value>', 'say more about where it comes from, or leave one out with key=; path, repo, commit, machine and user are filled in').argParser(collectOption))
110
+ .action(guarded('Remote publish was not successful', async (name, options) => {
74
111
  const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
112
+ const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
75
113
  const { name: serverName, server, token } = await connect(name);
76
- const answer = await (0, server_client_1.callServer)(server, token, 'PUT', `/v1/definitions/${encodeURIComponent(definition)}`, bundle);
114
+ const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
115
+ ...bundle,
116
+ annotations,
117
+ });
77
118
  const version = answer.body;
78
119
  const files = Object.keys(bundle.files).length;
79
120
  const proposed = answer.status === 202;
80
121
  const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
81
- environment.stdout.write(`${verb} ${definition} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
122
+ environment.stdout.write(`${verb} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
82
123
  const names = [...(version.tasks ?? []), ...(version.services ?? [])];
83
124
  environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
125
+ for (const line of annotationLines(version.annotations, 'from')) {
126
+ environment.stdout.write(`${line.trimStart()}\n`);
127
+ }
84
128
  if (proposed) {
85
- environment.stdout.write(`it can not run before somebody who may publish ${(0, server_client_1.printable)(definition)} approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
129
+ environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
130
+ }
131
+ }));
132
+ remote
133
+ .command('run <tasks...>')
134
+ .description('run tasks of a published build file on a server from a git ref, print their output and exit as they did')
135
+ .addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
136
+ .addOption(new commander_1.Option('--build <hash>', 'the published version to run (sha256:...), or HAMMERKIT_BUILD; the hash of the build file here when left out'))
137
+ .addOption(new commander_1.Option('--repo <host/path>', 'the repository, such as github.com/corp/payments; origin of the checkout here when left out'))
138
+ .addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to run; the branch checked out here when left out'))
139
+ .addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at, else the run is refused; the commit checked out here when --ref is left out'))
140
+ .addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
141
+ .addOption(new commander_1.Option('--cache <method>', 'as --cache of run').choices(['checksum', 'modify-date', 'none']))
142
+ .addOption(new commander_1.Option('--timeout <duration>', 'stop the run after this long, such as 30m'))
143
+ .action(guarded('Remote run was not successful', async (tasks, options) => {
144
+ const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
145
+ const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
146
+ if (!bundle_1.HASH_PATTERN.test(definition)) {
147
+ throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
148
+ }
149
+ const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
150
+ const repo = options.repo ?? checkout.repo;
151
+ const ref = options.ref ?? checkout.branch;
152
+ // the commit checked out here is the code meant, when the ref is the branch it is on
153
+ const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
154
+ if (!repo) {
155
+ throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
156
+ }
157
+ if (!ref) {
158
+ throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
159
+ }
160
+ const { name: serverName, server } = await getServer(options.server);
161
+ const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
162
+ const runOptions = {};
163
+ for (const key of ['env', 'cache', 'timeout']) {
164
+ if (options[key] !== undefined) {
165
+ runOptions[key] = options[key];
166
+ }
167
+ }
168
+ if (passthrough.length > 0) {
169
+ runOptions.args = passthrough;
170
+ }
171
+ let submitted;
172
+ try {
173
+ submitted = await (0, sandbox_client_1.submitRun)(connection, {
174
+ definition,
175
+ tasks,
176
+ source: { type: 'ref', repo, ref, ...(commit ? { commit } : {}) },
177
+ options: runOptions,
178
+ });
179
+ }
180
+ catch (e) {
181
+ if (e.status === 400 && !named && /no published build file/.test(String(e))) {
182
+ throw new Error(`the build file here is not published on ${serverName} (${definition}), publish it first`);
183
+ }
184
+ throw e;
185
+ }
186
+ const at = submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : '';
187
+ environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${tasks.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(ref)}${at}\n`);
188
+ const prefixed = tasks.length > 1;
189
+ const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
190
+ signal: environment.abortCtrl.signal,
191
+ onLine: (line) => {
192
+ environment.stdout.write(`${prefixed || line.task !== tasks[0] ? `[${(0, server_client_1.printable)(line.task)}] ` : ''}${printableLine(line.text)}\n`);
193
+ },
194
+ onWaiting: (run) => {
195
+ for (const approval of run.approvals ?? []) {
196
+ environment.stdout.write(`waiting until somebody who uses ${(0, server_client_1.printable)(approval.account)} approves it, before ${(0, server_client_1.printable)(approval.expiresAt)}: hammerkit remote approve ${(0, server_client_1.printable)(run.id)}\n`);
197
+ }
198
+ },
199
+ });
200
+ const run = followed.run;
201
+ if (run.state === 'succeeded') {
202
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} succeeded\n`);
203
+ return;
204
+ }
205
+ // the exit code of the first task that failed, as `hammerkit run` would end
206
+ const failed = tasks
207
+ .map((task) => run.results?.[task])
208
+ .concat(Object.values(run.results ?? {}))
209
+ .find((result) => result?.status === 'failed');
210
+ const exitCode = failed?.exitCode && failed.exitCode > 0 ? failed.exitCode : 1;
211
+ fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, exitCode);
212
+ }));
213
+ remote
214
+ .command('versions [server]')
215
+ .description('list the published versions of build files on a server, newest first, with where they come from')
216
+ .addOption(new commander_1.Option('--filter <key=value>', 'only the versions with this annotation, such as repo=github.com/corp/payments').argParser(collectOption))
217
+ .action(guarded('Remote versions was not successful', async (name, options) => {
218
+ const query = new URLSearchParams();
219
+ for (const filter of (options.filter ?? [])) {
220
+ const index = filter.indexOf('=');
221
+ if (index <= 0) {
222
+ throw new Error(`${(0, server_client_1.printable)(filter)} is not a filter, write it as key=value`);
223
+ }
224
+ if (index === filter.length - 1) {
225
+ throw new Error(`${(0, server_client_1.printable)(filter)} matches nothing, an annotation always has a value`);
226
+ }
227
+ query.append(filter.slice(0, index), filter.slice(index + 1));
228
+ }
229
+ const { name: serverName, server, token } = await connect(name);
230
+ const path = `/v1/definitions${query.toString() !== '' ? `?${query}` : ''}`;
231
+ const { body } = await (0, server_client_1.callServer)(server, token, 'GET', path);
232
+ if (body.definitions.length === 0) {
233
+ environment.stdout.write(`${serverName}: no published versions${query.toString() !== '' ? ' match' : ''}\n`);
234
+ return;
235
+ }
236
+ environment.stdout.write(`${serverName}: ${body.definitions.length} published\n`);
237
+ for (const version of body.definitions) {
238
+ environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(version.hash))} published by ${(0, server_client_1.printable)(version.publishedBy.sub)} at ${(0, server_client_1.printable)(version.publishedAt)}${version.approvedBy ? `, approved by ${(0, server_client_1.printable)(version.approvedBy.sub)}` : ''}\n`);
239
+ for (const line of annotationLines(version.annotations)) {
240
+ environment.stdout.write(`${line}\n`);
241
+ }
242
+ const names = [...version.tasks, ...version.services];
243
+ environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
244
+ if (version.accounts.length > 0) {
245
+ environment.stdout.write(` accounts: ${version.accounts.map(server_client_1.printable).join(', ')}\n`);
246
+ }
247
+ for (const line of yankLines(version)) {
248
+ environment.stdout.write(`${line}\n`);
249
+ }
86
250
  }
87
251
  }));
88
252
  remote
@@ -97,21 +261,84 @@ function registerWorkCommands(remote, context) {
97
261
  }
98
262
  environment.stdout.write(`${serverName}: ${body.approvals.length} waiting for your approval\n`);
99
263
  for (const run of body.approvals) {
100
- for (const line of describe(run)) {
264
+ for (const line of describeApproval(run)) {
101
265
  environment.stdout.write(`${line}\n`);
102
266
  }
103
267
  }
104
268
  }));
269
+ // A version that is published: who published it, what its tasks do and every file.
270
+ const showPublished = async (hash, serverName, server, token) => {
271
+ const path = `/v1/definitions/${hash}`;
272
+ let version;
273
+ try {
274
+ version = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
275
+ }
276
+ catch (e) {
277
+ if (e instanceof server_client_1.ServerError && e.status === 404) {
278
+ throw new Error(`${serverName} has no proposal or published version ${(0, server_client_1.printable)(hash)} you may see`);
279
+ }
280
+ throw e;
281
+ }
282
+ const files = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
283
+ environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(version.hash))} on ${serverName}, published by ${(0, server_client_1.printable)(version.publishedBy.sub)} (${(0, server_client_1.printable)(version.publishedBy.iss)}) at ${(0, server_client_1.printable)(version.publishedAt)}${version.approvedBy ? `, approved by ${(0, server_client_1.printable)(version.approvedBy.sub)}` : ''}\n`);
284
+ for (const line of [
285
+ ...yankLines(version),
286
+ ...annotationLines(version.annotations),
287
+ ...flagLines(version),
288
+ ...(version.plan ? describePlan(version.plan) : [' the server did not record what its tasks do']),
289
+ ...(0, bundle_text_1.showBundle)(files),
290
+ ]) {
291
+ environment.stdout.write(`${line}\n`);
292
+ }
293
+ };
105
294
  remote
106
- .command('show <run> [server]')
107
- .description('show a run that waits for your approval in full: the commands, secrets and accounts of its tasks')
108
- .action(guarded('Remote show was not successful', async (run, name) => {
109
- if (!RUN_ID.test(run)) {
110
- throw new Error(`${(0, server_client_1.printable)(run)} is not a run id, list them with: hammerkit remote approvals`);
111
- }
112
- const { server, token } = await connect(name);
113
- const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${run}`)).body;
114
- for (const line of describe(view)) {
295
+ .command('show <target> [server]')
296
+ .description('show a run (run_...) or a proposed or published version (sha256:...) in full: the commands, secrets and accounts of its tasks, and the files of a version')
297
+ .action(guarded('Remote show was not successful', async (target, name) => {
298
+ if (!RUN_ID.test(target) && !bundle_1.HASH_PATTERN.test(target)) {
299
+ throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a version, list them with: hammerkit remote approvals, remote proposals or remote versions`);
300
+ }
301
+ const { name: serverName, server, token } = await connect(name);
302
+ if (bundle_1.HASH_PATTERN.test(target)) {
303
+ const path = `/v1/proposals/${target}`;
304
+ let proposal;
305
+ try {
306
+ proposal = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
307
+ }
308
+ catch (e) {
309
+ if (e instanceof server_client_1.ServerError && e.status === 404) {
310
+ await showPublished(target, serverName, server, token);
311
+ return;
312
+ }
313
+ throw e;
314
+ }
315
+ if (proposal.state === 'approved') {
316
+ await showPublished(target, serverName, server, token);
317
+ return;
318
+ }
319
+ if (proposal.state !== 'waiting') {
320
+ // a decided proposal keeps no files
321
+ environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} on ${serverName}, proposed by ${(0, server_client_1.printable)(proposal.proposedBy.sub)}, ${(0, server_client_1.printable)(proposal.state)}${proposal.decidedBy ? ` by ${(0, server_client_1.printable)(proposal.decidedBy.sub)}` : ''}${proposal.reason ? `: ${(0, server_client_1.printable)(proposal.reason)}` : ''}\n`);
322
+ for (const line of annotationLines(proposal.annotations)) {
323
+ environment.stdout.write(`${line}\n`);
324
+ }
325
+ environment.stdout.write(' its files are not kept once it is decided\n');
326
+ return;
327
+ }
328
+ const files = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
329
+ environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} on ${serverName}, proposed by ${(0, server_client_1.printable)(proposal.proposedBy.sub)} (${(0, server_client_1.printable)(proposal.proposedBy.iss)}), ${(0, server_client_1.printable)(proposal.state)}\n`);
330
+ for (const line of [
331
+ ...annotationLines(proposal.annotations),
332
+ ...flagLines(proposal),
333
+ ...describePlan(proposal.plan ?? {}),
334
+ ...(0, bundle_text_1.showBundle)(files),
335
+ ]) {
336
+ environment.stdout.write(`${line}\n`);
337
+ }
338
+ return;
339
+ }
340
+ const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${target}`)).body;
341
+ for (const line of describeApproval(view)) {
115
342
  environment.stdout.write(`${line}\n`);
116
343
  }
117
344
  environment.stdout.write(view.plan ? `${describePlan(view.plan).join('\n')}\n` : 'the server did not record what its tasks do\n');
@@ -130,7 +357,10 @@ function registerWorkCommands(remote, context) {
130
357
  for (const proposal of body.proposals) {
131
358
  const who = proposal.mine ? 'you' : (0, server_client_1.printable)(proposal.proposedBy.sub);
132
359
  const state = proposal.canDecide ? colors_1.default.bold('waits for your approval') : (0, server_client_1.printable)(proposal.state);
133
- environment.stdout.write(`${colors_1.default.bold(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)}`)} proposed by ${who}, ${state}\n`);
360
+ environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} proposed by ${who}, ${state}\n`);
361
+ for (const line of annotationLines(proposal.annotations)) {
362
+ environment.stdout.write(`${line}\n`);
363
+ }
134
364
  const names = [...proposal.tasks, ...proposal.services];
135
365
  environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
136
366
  if (proposal.accounts.length > 0) {
@@ -141,9 +371,6 @@ function registerWorkCommands(remote, context) {
141
371
  for (const line of flagLines(proposal)) {
142
372
  environment.stdout.write(`${line}\n`);
143
373
  }
144
- if (proposal.stale) {
145
- environment.stdout.write(` stale: ${(0, server_client_1.printable)(proposal.name)} has a newer version, reject it and propose again on top of it\n`);
146
- }
147
374
  if (proposal.state === 'waiting') {
148
375
  environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
149
376
  }
@@ -152,43 +379,19 @@ function registerWorkCommands(remote, context) {
152
379
  }
153
380
  }
154
381
  }));
155
- remote
156
- .command('diff <proposal> [server]')
157
- .description('show what a proposed version (name@hash) changes against the version it follows')
158
- .action(guarded('Remote diff was not successful', async (target, name) => {
159
- const proposal = parseProposal(target);
160
- if (!proposal) {
161
- throw new Error(`${(0, server_client_1.printable)(target)} is not a proposal, list them with: hammerkit remote proposals`);
162
- }
163
- const { name: serverName, server, token } = await connect(name);
164
- const path = `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}`;
165
- const view = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
166
- const proposed = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
167
- const base = view.base
168
- ? (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/definitions/${encodeURIComponent(proposal.name)}/${view.base}/files`)).body
169
- : undefined;
170
- environment.stdout.write(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)} on ${serverName}, proposed by ${(0, server_client_1.printable)(view.proposedBy.sub)} (${(0, server_client_1.printable)(view.proposedBy.iss)})\n`);
171
- environment.stdout.write(base ? `compared with ${(0, server_client_1.printable)(view.base)}\n` : 'no version is published yet, everything is new\n');
172
- if (view.stale) {
173
- environment.stdout.write(`warning: ${(0, server_client_1.printable)(proposal.name)} has a newer version (${(0, server_client_1.printable)(view.latest)}), this can only be rejected\n`);
174
- }
175
- const lines = (0, text_diff_1.diffBundles)(base, proposed);
176
- environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
177
- }));
178
382
  for (const action of ['yank', 'unyank']) {
179
383
  remote
180
384
  .command(`${action} <version> [server]`)
181
385
  .description(action === 'yank'
182
- ? 'pull a published version (name@hash) back: no run starts from it and it is not latest'
183
- : 'let a yanked version (name@hash) go on')
386
+ ? 'pull a published version (sha256:...) back: no run starts from it'
387
+ : 'let a yanked version (sha256:...) go on')
184
388
  .addOption(new commander_1.Option('--reason <text>', 'why, shown to whoever tries to run it').argParser((value) => value))
185
389
  .action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
186
- const version = parseProposal(target);
187
- if (!version) {
188
- throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it like payments@sha256:...`);
390
+ if (!bundle_1.HASH_PATTERN.test(target)) {
391
+ throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it by its hash: sha256:...`);
189
392
  }
190
393
  const { name: serverName, server, token } = await connect(name);
191
- await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${encodeURIComponent(version.name)}/${version.hash}/${action}`, options.reason ? { reason: options.reason } : undefined);
394
+ await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${target}/${action}`, options.reason ? { reason: options.reason } : undefined);
192
395
  environment.stdout.write(`${action === 'yank' ? 'yanked' : 'let go on'} ${(0, server_client_1.printable)(target)} on ${serverName}\n`);
193
396
  }));
194
397
  }
@@ -196,18 +399,16 @@ function registerWorkCommands(remote, context) {
196
399
  remote
197
400
  .command(`${action} <target> [server]`)
198
401
  .description(action === 'approve'
199
- ? 'let a run (run_...) or a proposed version (name@hash) that waits for your approval go ahead'
200
- : 'end a run (run_...) or drop a proposed version (name@hash) that waits for your approval')
402
+ ? 'let a run (run_...) or a proposed version (sha256:...) that waits for your approval go ahead'
403
+ : 'end a run (run_...) or drop a proposed version (sha256:...) that waits for your approval')
201
404
  .addOption(new commander_1.Option('--reason <text>', 'why, shown to who asked').argParser((value) => value))
202
405
  .action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
203
- const proposal = parseProposal(target);
406
+ const proposal = bundle_1.HASH_PATTERN.test(target);
204
407
  if (!proposal && !RUN_ID.test(target)) {
205
408
  throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a proposed version, list them with: hammerkit remote approvals or remote proposals`);
206
409
  }
207
410
  const { name: serverName, server, token } = await connect(name);
208
- const path = proposal
209
- ? `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}/${action}`
210
- : `/v1/approvals/${target}/${action}`;
411
+ const path = proposal ? `/v1/proposals/${target}/${action}` : `/v1/approvals/${target}/${action}`;
211
412
  const { body } = await (0, server_client_1.callServer)(server, token, 'POST', path, options.reason ? { reason: options.reason } : undefined);
212
413
  const rejected = body.state === 'rejected';
213
414
  environment.stdout.write(proposal
@@ -1,20 +1,33 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.callServer = exports.getMe = exports.getDiscovery = exports.printable = exports.ServerError = void 0;
3
+ exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printable = exports.ServerError = void 0;
4
4
  const discovery_1 = require("../server/discovery");
5
5
  const TIMEOUT_MS = 10000;
6
6
  const WRITE_TIMEOUT_MS = 30000;
7
7
  class ServerError extends Error {
8
+ // what the server answered, when it did
9
+ constructor(message, status) {
10
+ super(message);
11
+ this.status = status;
12
+ }
8
13
  }
9
14
  exports.ServerError = ServerError;
10
15
  // Text from the server or the identity provider is shown to the user: no
11
- // control characters, so it can't rewrite the terminal.
16
+ // control characters, so it can't rewrite the terminal, and format characters
17
+ // (bidi overrides, zero-width) written out, so text reads in the order it runs.
18
+ // Long text is cut, and says how much was.
12
19
  function printable(value) {
20
+ const text = printableWhole(value);
21
+ return text.length > 300 ? `${text.slice(0, 300)}… (+${text.length - 300} chars)` : text;
22
+ }
23
+ exports.printable = printable;
24
+ // The same, never cut: for what somebody decides on, such as a command.
25
+ function printableWhole(value) {
13
26
  return String(value)
14
27
  .replace(/\p{Cc}/gu, '?')
15
- .slice(0, 300);
28
+ .replace(/\p{Cf}/gu, (char) => `\\u{${char.codePointAt(0)?.toString(16)}}`);
16
29
  }
17
- exports.printable = printable;
30
+ exports.printableWhole = printableWhole;
18
31
  async function getJson(url, token) {
19
32
  let response;
20
33
  try {
@@ -80,7 +93,7 @@ async function callServer(server, token, method, path, body) {
80
93
  }
81
94
  if (!response.ok) {
82
95
  const said = answer && typeof answer.error === 'string' ? `: ${printable(answer.error)}` : '';
83
- throw new ServerError(`${server.url} answered ${response.status}${said}`);
96
+ throw new ServerError(`${server.url} answered ${response.status}${said}`, response.status);
84
97
  }
85
98
  return { status: response.status, body: answer };
86
99
  }
@@ -53,6 +53,16 @@ function hashSource(manifest) {
53
53
  }
54
54
  exports.hashSource = hashSource;
55
55
  const DENIED_DIRECTORIES = ['.git', '.ssh', '.aws', '.gnupg', '.kube'];
56
+ // the short names Windows gives these directories, `GIT~1` for `.git`
57
+ const DENIED_SHORT_NAMES = /^(git|ssh|aws|gnupg|kube)~\d+$/;
58
+ // A name as a case-insensitive disk opens it: Windows and macOS open `.SSH` as `.ssh`, and
59
+ // Windows drops trailing dots and spaces (`.git.`) and reads `name:stream` as `name`.
60
+ function canonicalName(name) {
61
+ return name
62
+ .replace(/:.*$/, '')
63
+ .replace(/[. ]+$/, '')
64
+ .toLowerCase();
65
+ }
56
66
  const DENIED_NAMES = [
57
67
  /^\.env$/i,
58
68
  /^\.env\.(?!example$|sample$|template$|dist$)/i,
@@ -65,12 +75,15 @@ const DENIED_NAMES = [
65
75
  // must not be what decides that a key leaves the machine.
66
76
  function deniedSourcePath(path) {
67
77
  const parts = path.split('/');
68
- const directory = parts.slice(0, -1).find((part) => DENIED_DIRECTORIES.includes(part));
78
+ const directory = parts.slice(0, -1).find((part) => {
79
+ const name = canonicalName(part);
80
+ return DENIED_DIRECTORIES.includes(name) || DENIED_SHORT_NAMES.test(name);
81
+ });
69
82
  if (directory !== undefined) {
70
83
  return `${directory}/ holds credentials or history`;
71
84
  }
72
85
  const name = parts[parts.length - 1];
73
- if (DENIED_NAMES.some((pattern) => pattern.test(name))) {
86
+ if (DENIED_NAMES.some((pattern) => pattern.test(name) || pattern.test(canonicalName(name)))) {
74
87
  return `${name} looks like a credential file`;
75
88
  }
76
89
  return null;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTokenSource = exports.isToken = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const credentials_1 = require("./credentials");
6
+ // A bearer token is one word of visible ASCII; anything else is output of a token command
7
+ // that holds more than the token, which must never reach an error message.
8
+ function isToken(value) {
9
+ return /^[\x21-\x7e]+$/.test(value);
10
+ }
11
+ exports.isToken = isToken;
12
+ /**
13
+ * The token a client of a server sends: a harness, and the `remote` commands. In this order:
14
+ *
15
+ * - `HAMMERKIT_TOKEN_COMMAND`: a command that prints a short-lived token of the agent's own
16
+ * identity, such as a workload identity or a service account of the identity provider. It
17
+ * runs again when the server no longer accepts the token.
18
+ * - `HAMMERKIT_TOKEN`: such a token, as the agent's platform hands it over.
19
+ * - the sign-in of `hammerkit remote login <server>`.
20
+ *
21
+ * The token only goes to the URL the server is registered with in the host config.
22
+ */
23
+ function getTokenSource(environment, name, server) {
24
+ const command = environment.processEnvs.HAMMERKIT_TOKEN_COMMAND;
25
+ if (command) {
26
+ let current;
27
+ const run = () => new Promise((resolve, reject) => {
28
+ (0, child_process_1.exec)(command, { env: environment.processEnvs, timeout: 60000 }, (error, stdout) => {
29
+ const token = String(stdout).trim();
30
+ if (error || !token) {
31
+ reject(new Error('HAMMERKIT_TOKEN_COMMAND printed no token'));
32
+ }
33
+ else if (!isToken(token)) {
34
+ // never the output itself: it can hold more secrets than the token
35
+ reject(new Error('HAMMERKIT_TOKEN_COMMAND printed more than a token, it has to print the token alone'));
36
+ }
37
+ else {
38
+ resolve(token);
39
+ }
40
+ });
41
+ });
42
+ return {
43
+ token: () => {
44
+ current = current ?? run();
45
+ current.catch(() => (current = undefined));
46
+ return current;
47
+ },
48
+ renew: async () => {
49
+ current = undefined;
50
+ },
51
+ };
52
+ }
53
+ const fixed = environment.processEnvs.HAMMERKIT_TOKEN?.trim();
54
+ if (fixed) {
55
+ return {
56
+ token: async () => {
57
+ if (!isToken(fixed)) {
58
+ throw new Error('HAMMERKIT_TOKEN holds more than a token, it has to be the token alone');
59
+ }
60
+ return fixed;
61
+ },
62
+ };
63
+ }
64
+ return { token: () => (0, credentials_1.getAccessToken)(environment, name, server) };
65
+ }
66
+ exports.getTokenSource = getTokenSource;
67
+ //# sourceMappingURL=token-source.js.map
@@ -12,6 +12,7 @@ const duration_schema_1 = require("./duration-schema");
12
12
  const build_file_secret_schema_1 = require("./build-file-secret-schema");
13
13
  const resources_schema_1 = require("./resources-schema");
14
14
  const runner_schema_1 = require("./runner-schema");
15
+ const network_schema_1 = require("./network-schema");
15
16
  // what `extend` adds to (the rest, e.g. `image` or `shell`, the task just overrides)
16
17
  exports.RESET_PROPERTIES = ['deps', 'needs', 'src', 'generates', 'cmds', 'mounts', 'envs', 'labels'];
17
18
  exports.buildFileBaseTaskSchema = (0, zod_1.object)({
@@ -39,6 +40,12 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
39
40
  account: (0, zod_1.string)().optional(),
40
41
  // CPU and memory limits of the container; a local task is not limited
41
42
  resources: resources_schema_1.resourcesSchema.optional(),
43
+ // what the container reaches besides its needs; a local task can be limited only where
44
+ // it runs in a container, on a server's cluster
45
+ network: network_schema_1.networkSchema.optional(),
46
+ // whether its last command takes the arguments after `--`: false refuses them, true
47
+ // says it is meant to, which a server with `runs.args: declared` requires
48
+ args: (0, zod_1.boolean)().optional(),
42
49
  // Name of the server (see `hammerkit remote`) the task has to run on; defaults
43
50
  // to the build file's `runner`. Never a local fallback when it is unknown.
44
51
  runner: runner_schema_1.runnerSchema.optional(),