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
@@ -4,6 +4,7 @@ exports.getServiceDefinitionHash = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const work_secret_1 = require("./work-secret");
6
6
  const work_service_1 = require("./work-service");
7
+ const work_network_1 = require("./work-network");
7
8
  const work_cache_id_1 = require("./work-cache-id");
8
9
  // Everything a running container of the service was created from, plus the
9
10
  // definitions of the services it needs: a dependent links to the container it
@@ -12,6 +13,7 @@ const work_cache_id_1 = require("./work-cache-id");
12
13
  // service whose definition changed instead of silently reusing it.
13
14
  function getServiceDefinitionHash(item) {
14
15
  const service = item.data;
16
+ const network = (0, work_network_1.getEffectiveNetwork)(service);
15
17
  const definition = {
16
18
  image: service.image,
17
19
  cmd: service.cmd?.cmd ?? null,
@@ -38,6 +40,9 @@ function getServiceDefinitionHash(item) {
38
40
  .map((need) => `${need.name}=${getServiceDefinitionHash(need.service)}`),
39
41
  // only when declared, so a service without keeps its hash
40
42
  ...(service.resources ? { resources: service.resources } : {}),
43
+ // in this run, under its `--network` ceiling: a service started open is
44
+ // recreated for a run that limits it
45
+ ...((0, work_network_1.isLimitedNetwork)(network) ? { network } : {}),
41
46
  };
42
47
  return (0, crypto_1.createHash)('sha1').update(JSON.stringify(definition)).digest('hex');
43
48
  }
@@ -15,6 +15,8 @@ function appendWorkEnvironment(referenced) {
15
15
  kubeConfig: referenced.schema.kubernetes.kubeconfig,
16
16
  httpRoutes: referenced.schema.kubernetes.httpRoutes || [],
17
17
  namespace: referenced.schema.kubernetes.namespace ?? 'default',
18
+ networkPolicyEnforced: referenced.schema.kubernetes.networkPolicy === 'enforced',
19
+ ...(referenced.schema.kubernetes.volumeSize ? { volumeSize: referenced.schema.kubernetes.volumeSize } : {}),
18
20
  };
19
21
  }
20
22
  }
@@ -23,6 +23,7 @@ const resolve_cache_1 = require("../../cache/resolve-cache");
23
23
  const units_1 = require("../../utils/units");
24
24
  const log_1 = require("../../log");
25
25
  const work_resources_1 = require("../work-resources");
26
+ const work_network_1 = require("../work-network");
26
27
  function appendWorkService(workTree, service, environment, context) {
27
28
  const workService = parseService(workTree, service, environment, context);
28
29
  if (!workTree.services[workService.name]) {
@@ -116,6 +117,9 @@ function parseService(workTree, service, environment, context) {
116
117
  init: null,
117
118
  secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
118
119
  resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
120
+ volumeSize: service.schema.volumeSize ?? null,
121
+ network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
122
+ networkCeiling: null,
119
123
  };
120
124
  }
121
125
  }
@@ -19,8 +19,17 @@ const get_work_runtime_1 = require("./get-work-runtime");
19
19
  const resolve_cache_1 = require("../../cache/resolve-cache");
20
20
  const units_1 = require("../../utils/units");
21
21
  const work_resources_1 = require("../work-resources");
22
+ const job_placement_1 = require("../../kubernetes/job-placement");
23
+ const work_network_1 = require("../work-network");
22
24
  function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
23
- const task = parseTask(cwd, referenceTask, environment, context);
25
+ const parsed = parseTask(cwd, referenceTask, environment, context);
26
+ // on a server's cluster a local task runs in a tool image like a container task
27
+ const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
28
+ const task = !server
29
+ ? parsed
30
+ : parsed.type === 'local-task'
31
+ ? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
32
+ : (0, job_placement_1.placeOnPool)(parsed, server);
24
33
  if (!workTree.tasks[task.name]) {
25
34
  const workItem = {
26
35
  // A dependency cycle is rejected before anything runs (checkForLoop), but
@@ -72,6 +81,8 @@ function parseTask(cwd, task, environment, context) {
72
81
  timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
73
82
  secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment, context.secrets, task.schema.account),
74
83
  resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
84
+ network: (0, work_network_1.parseWorkNetwork)(task.schema.network),
85
+ networkCeiling: null,
75
86
  runner: task.schema.runner ?? task.scope.schema.runner ?? null,
76
87
  };
77
88
  if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
@@ -81,6 +92,7 @@ function parseTask(cwd, task, environment, context) {
81
92
  user: (0, get_container_user_1.getContainerUser)(),
82
93
  image: (0, template_value_1.templateValue)(task.schema.image, envs),
83
94
  mounts: (0, parse_work_mounts_1.parseWorkMounts)(cwd, task.schema, envs),
95
+ volumeSize: task.schema.volumeSize ?? null,
84
96
  };
85
97
  }
86
98
  else {
@@ -88,6 +100,10 @@ function parseTask(cwd, task, environment, context) {
88
100
  if (fileSecret) {
89
101
  throw new Error(`task ${task.relativeName}: a local task takes secrets as env only (${(0, work_secret_1.getSecretName)(fileSecret)})`);
90
102
  }
103
+ if (baseWorkNode.network.type !== 'open') {
104
+ throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
105
+ `hammerkit can't limit the network of a local task`);
106
+ }
91
107
  return {
92
108
  ...baseWorkNode,
93
109
  type: 'local-task',
@@ -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
@@ -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
@@ -17,6 +17,10 @@ const work_instance_id_1 = require("./work-instance-id");
17
17
  const stream_1 = require("stream");
18
18
  const service_definition_1 = require("./service-definition");
19
19
  const container_task_state_1 = require("../executer/container-task-state");
20
+ const item_network_1 = require("../docker/item-network");
21
+ const execution_steps_1 = require("../executer/execution-steps");
22
+ const assert_item_paths_1 = require("./utils/assert-item-paths");
23
+ const log_1 = require("../log");
20
24
  function dockerTaskRuntime(task, workEnvironment) {
21
25
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
22
26
  return {
@@ -56,8 +60,18 @@ function dockerTaskRuntime(task, workEnvironment) {
56
60
  for (const container of containers) {
57
61
  await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
58
62
  }
63
+ await (0, item_network_1.removeItemNetworks)(docker, task);
59
64
  },
60
65
  async execute(environment, options) {
66
+ if (environment.hosted) {
67
+ try {
68
+ await (0, assert_item_paths_1.assertItemPathsWithinRoot)(task, environment.cwd);
69
+ }
70
+ catch (e) {
71
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
72
+ return;
73
+ }
74
+ }
61
75
  await (0, docker_task_1.dockerTask)(docker, task, environment, options);
62
76
  },
63
77
  async remove(environment) {
@@ -103,6 +117,18 @@ function isOnHost(generate) {
103
117
  return generate.isFile || generate.export;
104
118
  }
105
119
  async function restoreContainer(docker, environment, item, path) {
120
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
121
+ // on a server the volumes the outputs are restored into carry the run's files, so they
122
+ // are removed with a sandbox
123
+ const root = (0, execution_steps_1.hostedRoot)(environment);
124
+ if (root) {
125
+ const names = item.data.type === 'container-service'
126
+ ? item.data.volumes.filter((v) => !v.inherited).map((v) => v.name)
127
+ : item.data.generates.filter((v) => !v.inherited && !v.isFile).map((v) => v.volumeName);
128
+ for (const name of names) {
129
+ await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, name, (0, get_docker_executor_1.volumeLabels)(root));
130
+ }
131
+ }
106
132
  await (0, using_container_1.usingContainer)(docker, item, {
107
133
  abortSignal: environment.abortCtrl.signal,
108
134
  Image: item.data.image,
@@ -142,6 +168,7 @@ async function restoreContainer(docker, environment, item, path) {
142
168
  }
143
169
  });
144
170
  if (item.data.type === 'container-task') {
171
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
145
172
  const onHost = new Set(item.data.generates.filter((g) => !g.inherited && isOnHost(g)).map((g) => g.path));
146
173
  for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
147
174
  if (onHost.has(generate.path) && (await environment.file.exists(generate.filename))) {
@@ -167,6 +194,8 @@ function dockerServiceRuntime(service, workEnvironment) {
167
194
  if (currentService.Labels['hammerkit-definition'] !== (0, service_definition_1.getServiceDefinitionHash)(service)) {
168
195
  service.status.write('info', `${service.name} changed since it was started, recreating it`);
169
196
  await (0, remove_container_1.removeContainer)(docker.getContainer(currentService.Id));
197
+ // its network, proxy and relay go with it, whatever its new setting
198
+ await (0, item_network_1.removeItemNetworks)(docker, service);
170
199
  return;
171
200
  }
172
201
  const servicePid = 'hammerkit-pid' in currentService.Labels ? parseInt(currentService.Labels['hammerkit-pid']) : undefined;
@@ -190,6 +219,7 @@ function dockerServiceRuntime(service, workEnvironment) {
190
219
  for (const container of containers) {
191
220
  await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
192
221
  }
222
+ await (0, item_network_1.removeItemNetworks)(docker, service);
193
223
  await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(service));
194
224
  },
195
225
  async remove() {
@@ -203,6 +233,16 @@ function dockerServiceRuntime(service, workEnvironment) {
203
233
  }
204
234
  },
205
235
  async execute(environment, options) {
236
+ if (environment.hosted) {
237
+ try {
238
+ await (0, assert_item_paths_1.assertItemPathsWithinRoot)(service, environment.cwd);
239
+ }
240
+ catch (e) {
241
+ service.status.write('error', (0, log_1.getErrorMessage)(e));
242
+ options.state.set({ type: 'end', reason: 'crash', stateKey: options.stateKey });
243
+ return;
244
+ }
245
+ }
206
246
  await (0, docker_service_1.dockerService)(docker, service, options, environment);
207
247
  },
208
248
  async restore(environment, path) {
@@ -231,6 +271,7 @@ function dockerServiceRuntime(service, workEnvironment) {
231
271
  }
232
272
  exports.dockerServiceRuntime = dockerServiceRuntime;
233
273
  async function archiveContainer(docker, environment, item, path) {
274
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
234
275
  await (0, using_container_1.usingContainer)(docker, item, {
235
276
  abortSignal: environment.abortCtrl.signal,
236
277
  Image: item.data.image,