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
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertWorkTreePathsWithinRoot = exports.assertItemPathsWithinRoot = void 0;
4
+ const within_root_1 = require("../../file/within-root");
5
+ const work_service_1 = require("../work-service");
6
+ const work_task_1 = require("../work-task");
7
+ const hosted_volumes_1 = require("./hosted-volumes");
8
+ /**
9
+ * Refuses an item whose sources, outputs or mounts leave the root of a run, or go
10
+ * through a symbolic link in it. The server runs build files it did not write, in a
11
+ * checkout it did not write either: a `src: ../..` or a link in the checkout would
12
+ * have the server mount or upload its own files (its credentials, other callers'
13
+ * caches) into a task. Checked when the run is planned and again before each item
14
+ * runs, since an earlier task may have left a link behind.
15
+ */
16
+ async function assertItemPathsWithinRoot(item, root) {
17
+ const data = item.data;
18
+ for (const source of 'src' in data ? data.src : []) {
19
+ if (!source.inherited) {
20
+ await (0, within_root_1.assertWithinRoot)(root, source.absolutePath, `${item.name}: src ${source.source}`);
21
+ }
22
+ }
23
+ if ((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data)) {
24
+ for (const mount of data.mounts) {
25
+ await (0, within_root_1.assertWithinRoot)(root, mount.localPath, `${item.name}: mount ${mount.localPath}`);
26
+ }
27
+ }
28
+ if ((0, work_service_1.isContainerWorkService)(data)) {
29
+ for (const volume of data.volumes) {
30
+ if (!(0, hosted_volumes_1.isVolumeName)(volume.name)) {
31
+ throw new Error(`${item.name}: volume ${volume.name} is not a volume name, a path of the server can't be mounted`);
32
+ }
33
+ }
34
+ }
35
+ if ((0, work_task_1.isWorkTask)(data)) {
36
+ for (const generate of data.generates) {
37
+ if (!generate.inherited) {
38
+ await (0, within_root_1.assertWithinRoot)(root, generate.path, `${item.name}: generates ${generate.path}`);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ exports.assertItemPathsWithinRoot = assertItemPathsWithinRoot;
44
+ async function assertWorkTreePathsWithinRoot(workTree, root) {
45
+ for (const item of [...Object.values(workTree.tasks), ...Object.values(workTree.services)]) {
46
+ await assertItemPathsWithinRoot(item, root);
47
+ }
48
+ }
49
+ exports.assertWorkTreePathsWithinRoot = assertWorkTreePathsWithinRoot;
50
+ //# sourceMappingURL=assert-item-paths.js.map
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scopeServiceVolumes = exports.isVolumeName = void 0;
4
+ const work_service_1 = require("../work-service");
5
+ // What Docker takes as the name of a volume. Anything else in a service's `volumes` is a
6
+ // path of the host to Docker (`/var/run/docker.sock:/run/docker.sock` binds the socket).
7
+ const VOLUME_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]+$/;
8
+ function isVolumeName(name) {
9
+ return VOLUME_NAME.test(name);
10
+ }
11
+ exports.isVolumeName = isVolumeName;
12
+ /**
13
+ * On a server, the docker volumes of a run's services are its caller's: each name gets the
14
+ * run's scope (its caller and source trust) in front, so a build file can neither open nor,
15
+ * with `resetOnChange`, remove a volume of another caller, of a sandbox or of the host.
16
+ * The outputs a service inherits from its deps keep their names: those are made from paths
17
+ * in the run's own files, and the task that writes them uses the same name. A Kubernetes
18
+ * backend keeps claims in the caller's namespace and is left as it is.
19
+ */
20
+ function scopeServiceVolumes(workTree, scope) {
21
+ const scoped = new Set();
22
+ for (const service of Object.values(workTree.services)) {
23
+ if (!(0, work_service_1.isContainerWorkService)(service.data)) {
24
+ continue;
25
+ }
26
+ for (const volume of service.data.volumes) {
27
+ if (!volume.inherited && !scoped.has(volume)) {
28
+ scoped.add(volume);
29
+ volume.name = `hammerkit-${scope}-${volume.name}`;
30
+ }
31
+ }
32
+ }
33
+ }
34
+ exports.scopeServiceVolumes = scopeServiceVolumes;
35
+ //# sourceMappingURL=hosted-volumes.js.map
@@ -101,6 +101,9 @@ function applyArguments(selection, args, keepCache = false) {
101
101
  if (item.type === 'kubernetes-service') {
102
102
  throw new Error(`service ${item.name} is a kubernetes service and has no cmd for the arguments after --`);
103
103
  }
104
+ if (item.args === false) {
105
+ throw new Error(`${item.type === 'container-service' ? 'service' : 'task'} ${item.name} takes no arguments after -- (args: false)`);
106
+ }
104
107
  const target = getArgumentsTarget(item);
105
108
  if (!target) {
106
109
  throw new Error(item.type === 'container-service'
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyNetworkCeiling = exports.formatNetwork = exports.getTcpForwards = exports.matchesHost = exports.isLimitedNetwork = exports.toNetworkSchema = exports.exceedsNetwork = exports.getEffectiveNetwork = exports.limitNetwork = exports.parseNetworkOption = exports.parseWorkNetwork = exports.OPEN_NETWORK = void 0;
4
+ const network_schema_1 = require("../schema/network-schema");
5
+ exports.OPEN_NETWORK = { type: 'open' };
6
+ function parseWorkNetwork(schema) {
7
+ if (!schema || schema === 'open') {
8
+ return exports.OPEN_NETWORK;
9
+ }
10
+ if (schema === 'none') {
11
+ return { type: 'none' };
12
+ }
13
+ return { type: 'allow', hosts: normalizeHosts(schema.allow) };
14
+ }
15
+ exports.parseWorkNetwork = parseWorkNetwork;
16
+ // `--network none`, `--network open` or `--network allow:<host>,<host:port>`
17
+ function parseNetworkOption(value) {
18
+ if (value === 'open' || value === 'none') {
19
+ return { type: value };
20
+ }
21
+ if (value.startsWith('allow:')) {
22
+ const hosts = value
23
+ .substring('allow:'.length)
24
+ .split(',')
25
+ .map((host) => host.trim())
26
+ .filter((host) => host.length > 0);
27
+ const invalid = hosts.find((host) => !(0, network_schema_1.isAllowEntry)(host));
28
+ if (invalid) {
29
+ throw new Error(`invalid host "${invalid}", expected a name such as registry.npmjs.org or *.example.com, ` +
30
+ 'or a name and port such as db.example.com:5432');
31
+ }
32
+ const error = (0, network_schema_1.getAllowListError)(hosts);
33
+ if (error) {
34
+ throw new Error(error);
35
+ }
36
+ return { type: 'allow', hosts: normalizeHosts(hosts) };
37
+ }
38
+ throw new Error(`invalid network "${value}", expected none, open or allow:<host>,<host>`);
39
+ }
40
+ exports.parseNetworkOption = parseNetworkOption;
41
+ // The setting under a ceiling: a more open setting is clamped down to it, and
42
+ // an allow list under an allow ceiling keeps the hosts both lists allow. A
43
+ // name one list allows only through a pattern reaches no private address.
44
+ function limitNetwork(network, ceiling) {
45
+ if (!ceiling || ceiling.type === 'open' || network.type === 'none') {
46
+ return network;
47
+ }
48
+ if (ceiling.type === 'none') {
49
+ return ceiling;
50
+ }
51
+ if (network.type === 'open') {
52
+ return ceiling;
53
+ }
54
+ const hosts = [
55
+ ...network.hosts.filter((host) => ceiling.hosts.some((limit) => coversHost(limit, host))),
56
+ ...ceiling.hosts.filter((limit) => network.hosts.some((host) => coversHost(host, limit))),
57
+ ];
58
+ const normalized = normalizeHosts(hosts);
59
+ const publicOnly = normalized.filter((host) => !host.startsWith('*.') && !(coversByName(network, host) && coversByName(ceiling, host)));
60
+ return publicOnly.length > 0 ? { type: 'allow', hosts: normalized, publicOnly } : { type: 'allow', hosts: normalized };
61
+ }
62
+ exports.limitNetwork = limitNetwork;
63
+ // Whether a name (not a pattern) of the list covers `host` and may reach
64
+ // private addresses.
65
+ function coversByName(network, host) {
66
+ return network.hosts.some((entry) => !entry.startsWith('*.') && !network.publicOnly?.includes(entry) && coversHost(entry, host));
67
+ }
68
+ // The network of a task or service in this run: its own setting under the
69
+ // run's ceiling (`--network`).
70
+ function getEffectiveNetwork(item) {
71
+ return limitNetwork(item.network, item.networkCeiling);
72
+ }
73
+ exports.getEffectiveNetwork = getEffectiveNetwork;
74
+ // Whether `network` reaches more than `ceiling` allows: a host it allows that no
75
+ // host of the ceiling covers. Redundant hosts of the ceiling don't count.
76
+ function exceedsNetwork(network, ceiling) {
77
+ if (ceiling.type === 'open' || network.type === 'none') {
78
+ return false;
79
+ }
80
+ if (network.type === 'open') {
81
+ return true;
82
+ }
83
+ if (ceiling.type === 'none') {
84
+ return network.hosts.length > 0;
85
+ }
86
+ return network.hosts.some((host) => !ceiling.hosts.some((limit) => coversHost(limit, host)));
87
+ }
88
+ exports.exceedsNetwork = exceedsNetwork;
89
+ // The setting as a build file writes it.
90
+ function toNetworkSchema(network) {
91
+ return network.type === 'allow' ? { allow: [...network.hosts] } : network.type;
92
+ }
93
+ exports.toNetworkSchema = toNetworkSchema;
94
+ function isLimitedNetwork(network) {
95
+ return network.type !== 'open';
96
+ }
97
+ exports.isLimitedNetwork = isLimitedNetwork;
98
+ // Whether a request to `host` passes `pattern`: the same name, or a name under
99
+ // the domain of a `*.` pattern (the domain itself is not included).
100
+ function matchesHost(pattern, host) {
101
+ const name = host.toLowerCase().replace(/\.$/, '');
102
+ const rule = pattern.toLowerCase();
103
+ return rule.startsWith('*.') ? name.endsWith(rule.substring(1)) : name === rule;
104
+ }
105
+ exports.matchesHost = matchesHost;
106
+ // Whether everything `host` (a name, a `*.` pattern or a name and port)
107
+ // allows, `limit` allows too. A host without a port reaches every port of it
108
+ // through the proxy, so it covers a forward to one of them; a forward covers
109
+ // only itself.
110
+ function coversHost(limit, host) {
111
+ const limitEntry = (0, network_schema_1.parseAllowEntry)(limit);
112
+ const hostEntry = (0, network_schema_1.parseAllowEntry)(host);
113
+ if (!limitEntry || !hostEntry) {
114
+ return false;
115
+ }
116
+ if (limitEntry.port !== null) {
117
+ return hostEntry.port === limitEntry.port && matchesHost(limitEntry.host, hostEntry.host);
118
+ }
119
+ if (hostEntry.host.startsWith('*.')) {
120
+ // `*.b.example.com` is within `*.example.com`
121
+ return (limitEntry.host.startsWith('*.') &&
122
+ hostEntry.host.toLowerCase().endsWith(limitEntry.host.toLowerCase().substring(1)));
123
+ }
124
+ return matchesHost(limitEntry.host, hostEntry.host);
125
+ }
126
+ // The raw TCP forwards of a network: the entries of `allow` with a port.
127
+ function getTcpForwards(network) {
128
+ if (network.type !== 'allow') {
129
+ return [];
130
+ }
131
+ return network.hosts
132
+ .map(network_schema_1.parseAllowEntry)
133
+ .filter((entry) => entry !== null && entry.port !== null);
134
+ }
135
+ exports.getTcpForwards = getTcpForwards;
136
+ function normalizeHosts(hosts) {
137
+ return [...new Set(hosts.map((host) => host.toLowerCase()))].sort();
138
+ }
139
+ function formatNetwork(network) {
140
+ if (network.type === 'allow') {
141
+ return network.hosts.length > 0 ? `allow ${network.hosts.join(', ')}` : 'allow (no hosts)';
142
+ }
143
+ return network.type;
144
+ }
145
+ exports.formatNetwork = formatNetwork;
146
+ // Sets the run's ceiling on every task and service of the work tree. A local
147
+ // task can't be limited, so a ceiling below open with one in the run is an
148
+ // error before anything runs. So is a limited item that needs a service under
149
+ // a name that is no host name: the proxy passes the names of the needed
150
+ // services, and `*.com` would pass every .com host. A TCP forward under the
151
+ // name of a needed service would hide that service, so it is an error too, as
152
+ // are two forwards on one port, which the ceiling can leave from two lists.
153
+ function applyNetworkCeiling(workTree, ceiling) {
154
+ const limit = ceiling && isLimitedNetwork(ceiling) ? ceiling : null;
155
+ if (limit) {
156
+ const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task');
157
+ if (local) {
158
+ throw new Error(`--network ${formatNetwork(limit)} needs every task in a container, ` +
159
+ `hammerkit can't limit the network of the local task ${local.name}`);
160
+ }
161
+ }
162
+ for (const task of Object.values(workTree.tasks)) {
163
+ task.data.networkCeiling = limit;
164
+ }
165
+ for (const service of Object.values(workTree.services)) {
166
+ if (service.data.type === 'container-service') {
167
+ service.data.networkCeiling = limit;
168
+ }
169
+ }
170
+ for (const item of [...Object.values(workTree.tasks), ...Object.values(workTree.services)]) {
171
+ if (item.data.type === 'local-task' || item.data.type === 'kubernetes-service') {
172
+ continue;
173
+ }
174
+ const network = getEffectiveNetwork(item.data);
175
+ const invalid = item.needs.find((need) => !(0, network_schema_1.isNeedName)(need.name));
176
+ if (isLimitedNetwork(network) && invalid) {
177
+ throw new Error(`${item.name}: network ${formatNetwork(network)} reaches a needed service by a host name, ` +
178
+ `"${invalid.name}" is none (name it like db or api-v2)`);
179
+ }
180
+ const shared = network.type === 'allow' ? (0, network_schema_1.getAllowListError)(network.hosts) : null;
181
+ if (shared) {
182
+ const under = limit ? ` under --network ${formatNetwork(limit)}` : '';
183
+ throw new Error(`${item.name}: network ${formatNetwork(network)}${under}, ${shared}`);
184
+ }
185
+ const hidden = getTcpForwards(network).find((forward) => item.needs.some((need) => need.name.toLowerCase() === forward.host.toLowerCase()));
186
+ if (hidden) {
187
+ throw new Error(`${item.name}: network ${formatNetwork(network)} forwards ${hidden.host}:${hidden.port}, ` +
188
+ `which is the name of a service it needs`);
189
+ }
190
+ }
191
+ }
192
+ exports.applyNetworkCeiling = applyNetworkCeiling;
193
+ //# sourceMappingURL=work-network.js.map
@@ -12,11 +12,16 @@ const event_cache_1 = require("../executer/event-cache");
12
12
  const get_docker_executor_1 = require("../executer/get-docker-executor");
13
13
  const path_1 = require("path");
14
14
  const tar_1 = require("tar");
15
+ const tar_2 = require("../utils/tar");
15
16
  const plan_work_volume_1 = require("./utils/plan-work-volume");
16
17
  const work_instance_id_1 = require("./work-instance-id");
17
18
  const stream_1 = require("stream");
18
19
  const service_definition_1 = require("./service-definition");
19
20
  const container_task_state_1 = require("../executer/container-task-state");
21
+ const item_network_1 = require("../docker/item-network");
22
+ const execution_steps_1 = require("../executer/execution-steps");
23
+ const assert_item_paths_1 = require("./utils/assert-item-paths");
24
+ const log_1 = require("../log");
20
25
  function dockerTaskRuntime(task, workEnvironment) {
21
26
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
22
27
  return {
@@ -56,8 +61,18 @@ function dockerTaskRuntime(task, workEnvironment) {
56
61
  for (const container of containers) {
57
62
  await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
58
63
  }
64
+ await (0, item_network_1.removeItemNetworks)(docker, task);
59
65
  },
60
66
  async execute(environment, options) {
67
+ if (environment.hosted) {
68
+ try {
69
+ await (0, assert_item_paths_1.assertItemPathsWithinRoot)(task, environment.cwd);
70
+ }
71
+ catch (e) {
72
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
73
+ return;
74
+ }
75
+ }
61
76
  await (0, docker_task_1.dockerTask)(docker, task, environment, options);
62
77
  },
63
78
  async remove(environment) {
@@ -103,6 +118,18 @@ function isOnHost(generate) {
103
118
  return generate.isFile || generate.export;
104
119
  }
105
120
  async function restoreContainer(docker, environment, item, path) {
121
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
122
+ // on a server the volumes the outputs are restored into carry the run's files, so they
123
+ // are removed with a sandbox
124
+ const root = (0, execution_steps_1.hostedRoot)(environment);
125
+ if (root) {
126
+ const names = item.data.type === 'container-service'
127
+ ? item.data.volumes.filter((v) => !v.inherited).map((v) => v.name)
128
+ : item.data.generates.filter((v) => !v.inherited && !v.isFile).map((v) => v.volumeName);
129
+ for (const name of names) {
130
+ await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, name, (0, get_docker_executor_1.volumeLabels)(root));
131
+ }
132
+ }
106
133
  await (0, using_container_1.usingContainer)(docker, item, {
107
134
  abortSignal: environment.abortCtrl.signal,
108
135
  Image: item.data.image,
@@ -142,6 +169,7 @@ async function restoreContainer(docker, environment, item, path) {
142
169
  }
143
170
  });
144
171
  if (item.data.type === 'container-task') {
172
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
145
173
  const onHost = new Set(item.data.generates.filter((g) => !g.inherited && isOnHost(g)).map((g) => g.path));
146
174
  for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
147
175
  if (onHost.has(generate.path) && (await environment.file.exists(generate.filename))) {
@@ -167,6 +195,8 @@ function dockerServiceRuntime(service, workEnvironment) {
167
195
  if (currentService.Labels['hammerkit-definition'] !== (0, service_definition_1.getServiceDefinitionHash)(service)) {
168
196
  service.status.write('info', `${service.name} changed since it was started, recreating it`);
169
197
  await (0, remove_container_1.removeContainer)(docker.getContainer(currentService.Id));
198
+ // its network, proxy and relay go with it, whatever its new setting
199
+ await (0, item_network_1.removeItemNetworks)(docker, service);
170
200
  return;
171
201
  }
172
202
  const servicePid = 'hammerkit-pid' in currentService.Labels ? parseInt(currentService.Labels['hammerkit-pid']) : undefined;
@@ -190,6 +220,7 @@ function dockerServiceRuntime(service, workEnvironment) {
190
220
  for (const container of containers) {
191
221
  await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
192
222
  }
223
+ await (0, item_network_1.removeItemNetworks)(docker, service);
193
224
  await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(service));
194
225
  },
195
226
  async remove() {
@@ -203,6 +234,16 @@ function dockerServiceRuntime(service, workEnvironment) {
203
234
  }
204
235
  },
205
236
  async execute(environment, options) {
237
+ if (environment.hosted) {
238
+ try {
239
+ await (0, assert_item_paths_1.assertItemPathsWithinRoot)(service, environment.cwd);
240
+ }
241
+ catch (e) {
242
+ service.status.write('error', (0, log_1.getErrorMessage)(e));
243
+ options.state.set({ type: 'end', reason: 'crash', stateKey: options.stateKey });
244
+ return;
245
+ }
246
+ }
206
247
  await (0, docker_service_1.dockerService)(docker, service, options, environment);
207
248
  },
208
249
  async restore(environment, path) {
@@ -231,6 +272,7 @@ function dockerServiceRuntime(service, workEnvironment) {
231
272
  }
232
273
  exports.dockerServiceRuntime = dockerServiceRuntime;
233
274
  async function archiveContainer(docker, environment, item, path) {
275
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
234
276
  await (0, using_container_1.usingContainer)(docker, item, {
235
277
  abortSignal: environment.abortCtrl.signal,
236
278
  Image: item.data.image,
@@ -261,7 +303,7 @@ async function archiveContainer(docker, environment, item, path) {
261
303
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(item.data, path)) {
262
304
  // a file output is a bind mount of a host file, so archive it from the host
263
305
  const readable = hostFiles.has(generatedArchive.path)
264
- ? stream_1.Readable.from((0, tar_1.create)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)]))
306
+ ? stream_1.Readable.from((0, tar_2.packArchive)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)]))
265
307
  : await container.getArchive({
266
308
  path: generatedArchive.path,
267
309
  });