hammerkit 1.27.0-next.21 → 1.27.0-next.23

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.awaitRunObjectsGone = exports.removeRunObjects = exports.CallerNamespaces = exports.getVolumeScope = exports.KubernetesExecutor = exports.getRunRoleBinding = exports.removeServiceRunNamespace = exports.getServerIngressPolicy = exports.getNamespacePolicies = exports.getClusterAccess = exports.isCallerNamespace = exports.SERVICE_RUN_LABEL = exports.getServiceRunNamespace = exports.getCallerNamespace = void 0;
3
+ exports.awaitRunObjectsGone = exports.removeRunObjects = exports.CallerNamespaces = exports.getVolumeScope = exports.getPrivilegedNamespaceProblem = exports.getCapabilitiesProblem = exports.getPodSecurityLabels = exports.KubernetesExecutor = exports.getRunRoleBinding = exports.removeServiceRunNamespace = exports.getServerIngressPolicy = exports.getNamespacePolicies = exports.getClusterAccess = exports.isCallerNamespace = exports.SERVICE_RUN_LABEL = exports.getServiceRunNamespace = exports.getCallerNamespace = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const fs_1 = require("fs");
6
6
  const os_1 = require("os");
@@ -368,11 +368,12 @@ class KubernetesExecutor {
368
368
  return { kubernetes: this.environmentOf(run, namespace), env: this.access.env };
369
369
  }
370
370
  const label = (0, job_placement_1.getRunLabelValue)(run.id);
371
+ let capabilitiesProblem;
371
372
  await this.enqueue(namespace, async () => {
372
373
  if (context.signal.aborted) {
373
374
  throw new Error('the run was cancelled');
374
375
  }
375
- await this.namespaces.prepare(namespace);
376
+ capabilitiesProblem = await this.namespaces.prepare(namespace);
376
377
  // what an earlier run left when the server stopped before its end, or when its
377
378
  // release failed: the objects of a run that does not execute. One that starts
378
379
  // after this waits for it here before it makes any.
@@ -394,7 +395,10 @@ class KubernetesExecutor {
394
395
  });
395
396
  this.sweepLeftClaims();
396
397
  context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `runs in the namespace ${namespace}` });
397
- return { kubernetes: this.environmentOf(run, namespace, work), env: this.access.env };
398
+ return {
399
+ kubernetes: this.environmentOf(run, namespace, work, capabilitiesProblem),
400
+ env: this.access.env,
401
+ };
398
402
  },
399
403
  release: async (run, context) => {
400
404
  const namespace = this.namespaceOf(run);
@@ -462,7 +466,7 @@ class KubernetesExecutor {
462
466
  };
463
467
  }
464
468
  // The cluster as the process of a run sees it: the namespace its tasks and services go in.
465
- environmentOf(run, namespace, work) {
469
+ environmentOf(run, namespace, work, capabilitiesProblem) {
466
470
  const backend = this.config.backend;
467
471
  return {
468
472
  type: 'kubernetes',
@@ -483,6 +487,14 @@ class KubernetesExecutor {
483
487
  ...(work?.claim ? { workspace: getWorkspace(work.root, work.claim, namespace, backend.sandboxes.runner) } : {}),
484
488
  // the claims of a service run go with its namespace
485
489
  ...(this.keep > 0 && !run.up ? { volumeScope: getVolumeScope(run) } : {}),
490
+ ...(backend.capabilities
491
+ ? {
492
+ capabilities: {
493
+ runtimeClass: backend.capabilities.runtimeClass,
494
+ ...(capabilitiesProblem ? { unavailable: capabilitiesProblem } : {}),
495
+ },
496
+ }
497
+ : {}),
486
498
  },
487
499
  };
488
500
  }
@@ -565,6 +577,50 @@ class KubernetesExecutor {
565
577
  }
566
578
  }
567
579
  exports.KubernetesExecutor = KubernetesExecutor;
580
+ const ENFORCE_LABEL = 'pod-security.kubernetes.io/enforce';
581
+ const AUDIT_LABEL = 'pod-security.kubernetes.io/audit';
582
+ /**
583
+ * The Pod Security level a caller's namespace is made with: `baseline`, no privileged pods,
584
+ * host paths or host network, whatever a build asks. `baseline` also refuses the capabilities
585
+ * of `backend.capabilities`, sandboxed or not: with them the namespace enforces `privileged`
586
+ * and audits `baseline`, and the operator's admission policy (see the docs on capabilities)
587
+ * refuses what `baseline` would to a pod outside the sandbox's RuntimeClass.
588
+ */
589
+ function getPodSecurityLabels(backend) {
590
+ return backend.capabilities
591
+ ? { [ENFORCE_LABEL]: 'privileged', [AUDIT_LABEL]: 'baseline' }
592
+ : { [ENFORCE_LABEL]: 'baseline' };
593
+ }
594
+ exports.getPodSecurityLabels = getPodSecurityLabels;
595
+ /**
596
+ * The server never changes the level of a namespace it made (that would take `patch` on every
597
+ * namespace of the cluster): one made before `backend.capabilities` is relabelled by the
598
+ * operator. Until then its tasks that add capabilities fail with this, the others run.
599
+ */
600
+ function getCapabilitiesProblem(namespace, labels, backend) {
601
+ const enforce = labels?.[ENFORCE_LABEL];
602
+ if (!backend.capabilities || enforce === 'privileged') {
603
+ return undefined;
604
+ }
605
+ return (`the namespace ${namespace} was made before backend.capabilities and enforces ` +
606
+ `${enforce ? `Pod Security ${enforce}` : 'no Pod Security level of its own'}, not privileged: kubectl label namespace ${namespace} ` +
607
+ `${ENFORCE_LABEL}=privileged ${AUDIT_LABEL}=baseline --overwrite`);
608
+ }
609
+ exports.getCapabilitiesProblem = getCapabilitiesProblem;
610
+ /**
611
+ * A namespace made while the server had `backend.capabilities` admits privileged pods but for
612
+ * the operator's admission policy. Without capabilities the server runs nothing in it until the
613
+ * operator sets it back.
614
+ */
615
+ function getPrivilegedNamespaceProblem(namespace, labels, backend) {
616
+ if (backend.capabilities || labels?.[ENFORCE_LABEL] !== 'privileged') {
617
+ return undefined;
618
+ }
619
+ return (`the namespace ${namespace} enforces Pod Security privileged, made while the server had ` +
620
+ `backend.capabilities, and the server runs nothing in it without them: kubectl label namespace ` +
621
+ `${namespace} ${ENFORCE_LABEL}=baseline ${AUDIT_LABEL}- --overwrite`);
622
+ }
623
+ exports.getPrivilegedNamespaceProblem = getPrivilegedNamespaceProblem;
568
624
  // What may mount a claim a run kept: the runs of the same repository, as they share the
569
625
  // cache, and on a sandbox the runs of that sandbox only, whose claim is on the same node or
570
626
  // zone as the kept ones. The owner is the namespace's.
@@ -595,7 +651,8 @@ class CallerNamespaces {
595
651
  // are admitted under a quota on compute. All of it is replaced when the server sets up
596
652
  // a namespace it did not set up in the last `NAMESPACE_SETUP_REUSE_MS` (it reads the
597
653
  // config once, at its start), so the namespace follows the config; a namespace made anew
598
- // since (deleted and made again) is set up again at once.
654
+ // since (deleted and made again) is set up again at once. Returns what keeps the tasks that
655
+ // add capabilities from running in it (getCapabilitiesProblem), if anything.
599
656
  async prepare(namespace,
600
657
  // a service run's own namespace: its label, the namespace of the server that reaches its
601
658
  // endpoints, and its quota
@@ -613,6 +670,12 @@ class CallerNamespaces {
613
670
  if (existing && existing.metadata?.labels?.[MANAGED_LABEL] !== 'true') {
614
671
  throw new Error(`the namespace ${namespace} was not made by hammerkit, the server does not run tasks in it`);
615
672
  }
673
+ const privileged = getPrivilegedNamespaceProblem(namespace, existing?.metadata?.labels, backend);
674
+ if (privileged) {
675
+ throw new Error(privileged);
676
+ }
677
+ // one made by another run of this server at the same time has the labels of its config
678
+ const problem = () => getCapabilitiesProblem(namespace, existing?.metadata?.labels ?? getPodSecurityLabels(backend), backend);
616
679
  const started = Date.now();
617
680
  const applied = this.applied.get(namespace);
618
681
  if (existing &&
@@ -620,7 +683,7 @@ class CallerNamespaces {
620
683
  applied &&
621
684
  applied.uid === existing.metadata?.uid &&
622
685
  started - applied.at < NAMESPACE_SETUP_REUSE_MS) {
623
- return;
686
+ return problem();
624
687
  }
625
688
  this.applied.delete(namespace);
626
689
  if (!existing) {
@@ -633,8 +696,7 @@ class CallerNamespaces {
633
696
  [MANAGED_LABEL]: 'true',
634
697
  'hammerkit.dev/caller': namespace,
635
698
  ...(serviceRun ? { [exports.SERVICE_RUN_LABEL]: serviceRun.serviceRun } : {}),
636
- // no privileged pods, host paths or host network, whatever a build asks
637
- 'pod-security.kubernetes.io/enforce': 'baseline',
699
+ ...getPodSecurityLabels(backend),
638
700
  },
639
701
  },
640
702
  },
@@ -703,6 +765,7 @@ class CallerNamespaces {
703
765
  if (uid) {
704
766
  this.applied.set(namespace, { uid, at: started });
705
767
  }
768
+ return problem();
706
769
  }
707
770
  whoAmI() {
708
771
  if (!this.subject) {
@@ -40,50 +40,33 @@ const COMMIT_PATTERN = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
40
40
  // A hash alone can only name what is inside the bundle, so everything a build
41
41
  // file pulls in from elsewhere has to be pinned: a git include needs a commit
42
42
  // and a remote that is not a path on the server.
43
- function findLooseIncludes(bundle, noRemote = false) {
43
+ function checkGitSource(path, where, source, noRemote) {
44
+ if (noRemote === 'proposal') {
45
+ throw new DefinitionError(`${path}: ${where}: a proposal may not include from git, the server would fetch it before anybody approved it`);
46
+ }
47
+ if (noRemote === 'check') {
48
+ throw new DefinitionError(`${path}: ${where}: checking a build file that includes from git needs builds: publish, the server would fetch it`);
49
+ }
44
50
  const problems = [];
45
- for (const [path, content] of Object.entries(bundle.files)) {
46
- if (!/\.ya?ml$/i.test(path)) {
47
- continue;
48
- }
49
- let document;
50
- try {
51
- document = (0, yaml_1.parse)(content);
52
- }
53
- catch {
54
- continue; // a file that isn't valid yaml is reported when the bundle is parsed
55
- }
56
- for (const block of ['includes', 'references']) {
57
- for (const [name, include] of Object.entries(document?.[block] ?? {})) {
58
- const source = include;
59
- if (!source || typeof source !== 'object' || source.git === undefined) {
60
- continue;
61
- }
62
- if (noRemote) {
63
- problems.push(`${path}: ${block}.${name}: a proposal may not include from git, the server would fetch it before anybody approved it`);
64
- continue;
65
- }
66
- if (typeof source.git !== 'string' || !/^(https:\/\/|ssh:\/\/|git@[\w.-]+:)/.test(source.git)) {
67
- problems.push(`${path}: ${block}.${name}: git has to be an https:// or ssh:// address`);
68
- }
69
- if (typeof source.ref !== 'string' || !COMMIT_PATTERN.test(source.ref)) {
70
- problems.push(`${path}: ${block}.${name}: pin ref to a commit, a branch or tag can move`);
71
- }
72
- }
73
- }
51
+ if (!/^(https:\/\/|ssh:\/\/|git@[\w.-]+:)/.test(source.git)) {
52
+ problems.push(`${path}: ${where}: git has to be an https:// or ssh:// address`);
53
+ }
54
+ if (typeof source.ref !== 'string' || !COMMIT_PATTERN.test(source.ref)) {
55
+ problems.push(`${path}: ${where}: pin ref to a commit, a branch or tag can move`);
56
+ }
57
+ if (problems.length > 0) {
58
+ throw new DefinitionError(problems.join('\n'));
74
59
  }
75
- return problems;
76
60
  }
77
61
  // What a build file may not name on a server: the places it would run or store things
78
62
  // are the operator's. The docker host, a Kubernetes cluster and a remote cache are
79
63
  // credentials of the machine, not of the build file, and a project file that could carry
80
64
  // them could send runs, or the cache, anywhere.
81
- function findHostProblems(bundle) {
65
+ // A file a server leaves out (`publish: false`) is not checked, and neither is a service
66
+ // with `publish: false`: what is left out is never run there.
67
+ function findHostProblems(files) {
82
68
  const problems = [];
83
- for (const [path, content] of Object.entries(bundle.files)) {
84
- if (!/\.ya?ml$/i.test(path)) {
85
- continue;
86
- }
69
+ for (const [path, content] of Object.entries(files)) {
87
70
  let document;
88
71
  try {
89
72
  document = (0, yaml_1.parse)(content);
@@ -105,6 +88,7 @@ function findHostProblems(bundle) {
105
88
  for (const [name, service] of Object.entries(document.services ?? {})) {
106
89
  if (service &&
107
90
  typeof service === 'object' &&
91
+ service.publish !== false &&
108
92
  ('selector' in service || 'kubeconfig' in service || 'context' in service)) {
109
93
  problems.push(`${path}: services.${name}: Kubernetes services are not supported on a server`);
110
94
  }
@@ -112,6 +96,17 @@ function findHostProblems(bundle) {
112
96
  }
113
97
  return problems;
114
98
  }
99
+ // The files of the bundle a server publishes, see getPublishedFiles
100
+ function publishedBundleFiles(bundle, directory, entry) {
101
+ const files = {};
102
+ for (const scope of (0, reference_parser_1.getPublishedFiles)(entry)) {
103
+ const path = (0, path_1.relative)(directory, scope.fileName).split(path_1.sep).join('/');
104
+ if (!scope.remote && path in bundle.files) {
105
+ files[path] = bundle.files[path];
106
+ }
107
+ }
108
+ return files;
109
+ }
115
110
  // What a build file may read while it is inspected: its own files. The rest of
116
111
  // the server is not its business (a missing file is "not there", not an error).
117
112
  function confined(files, root) {
@@ -193,6 +188,18 @@ function findItemPolicyProblems(item, policy, root) {
193
188
  problems.push(`${item.name}: runs on the ${runtime} runtime, which this server does not allow (runtimes: ${policy.runtimes.join(', ')})`);
194
189
  }
195
190
  const data = item.data;
191
+ // a capability holds in the sandboxed runtime the operator named for it, so only those
192
+ // the operator allows are added
193
+ const refused = policy.capabilities
194
+ ? ('capAdd' in data ? data.capAdd : []).filter((capability) => !policy.capabilities?.includes(capability))
195
+ : [];
196
+ if (refused.length > 0) {
197
+ const from = policy.capabilitiesFrom ?? 'backend.capabilities.allow';
198
+ problems.push(`${item.name}: adds the ${refused.length > 1 ? 'capabilities' : 'capability'} ${refused.join(', ')}, ` +
199
+ (policy.capabilities && policy.capabilities.length > 0
200
+ ? `which this server does not allow (${from}: ${policy.capabilities.join(', ')})`
201
+ : `and this server allows none (${from})`));
202
+ }
196
203
  // what a task reads and writes is the bundle's: `src: ../..` would have the server
197
204
  // copy its own files (its credentials, other callers' caches) into the task
198
205
  for (const source of 'src' in data ? data.src : []) {
@@ -308,6 +315,10 @@ function toPlan(root, item) {
308
315
  if (data.scope.remote) {
309
316
  flags.push('remote-include');
310
317
  }
318
+ const capAdd = (0, work_task_1.isContainerWorkTask)(data) ? data.capAdd : [];
319
+ if (capAdd.length > 0) {
320
+ flags.push('capabilities');
321
+ }
311
322
  const isTask = data.type === 'local-task' || data.type === 'container-task';
312
323
  return {
313
324
  kind: isTask ? 'task' : 'service',
@@ -326,6 +337,7 @@ function toPlan(root, item) {
326
337
  mounts,
327
338
  ports,
328
339
  generates: 'generates' in data ? data.generates.map((generate) => generate.path) : [],
340
+ ...(capAdd.length > 0 ? { capAdd: [...capAdd] } : {}),
329
341
  flags,
330
342
  };
331
343
  }
@@ -342,10 +354,6 @@ function describeFailure(e) {
342
354
  // when somebody wants to run it. The same function answers what a run will do,
343
355
  // with the scope of the run: which tasks, services and accounts it involves.
344
356
  async function planBundle(bundle, environment, options = {}) {
345
- const problems = [...findLooseIncludes(bundle, options.noRemote), ...(options.policy ? findHostProblems(bundle) : [])];
346
- if (problems.length > 0) {
347
- throw new DefinitionError(problems.join('\n'));
348
- }
349
357
  const directory = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-definition-'));
350
358
  try {
351
359
  for (const [path, content] of Object.entries(bundle.files)) {
@@ -358,7 +366,20 @@ async function planBundle(bundle, environment, options = {}) {
358
366
  file: confined((0, get_file_context_1.getFileContext)(directory), directory),
359
367
  };
360
368
  try {
361
- const { ctx, scope } = await (0, schema_parser_1.createParseContext)((0, path_1.join)(directory, bundle.entry), inspected);
369
+ const { ctx, scope } = await (0, schema_parser_1.createParseContext)((0, path_1.join)(directory, bundle.entry), inspected, {
370
+ // a hash alone can only name what is inside the bundle, see checkGitSource
371
+ checkGitSource: (source, from, block, name) => checkGitSource(
372
+ // a file of a git include is named by its repository
373
+ from.remote
374
+ ? `${from.remote.git} ${(0, path_1.relative)(from.remote.root, from.fileName).split(path_1.sep).join('/')}`
375
+ : (0, path_1.relative)(directory, from.fileName), `${block}.${name}`, source, options.noRemote),
376
+ });
377
+ if (options.policy) {
378
+ const problems = findHostProblems(publishedBundleFiles(bundle, directory, scope));
379
+ if (problems.length > 0) {
380
+ throw new DefinitionError(problems.join('\n'));
381
+ }
382
+ }
362
383
  const context = await (0, reference_parser_1.parseReferences)(ctx, scope, inspected);
363
384
  const workTree = (0, work_scope_parser_1.getWorkContext)(context, options.scope ?? { environmentName: null }, inspected);
364
385
  const items = itemsOf(workTree);
@@ -51,6 +51,9 @@ exports.workerInputSchema = zod_1.z
51
51
  kubernetes: zod_1.z
52
52
  .custom((value) => typeof value === 'object' && value !== null && value.type === 'kubernetes')
53
53
  .optional(),
54
+ // on the server's docker host, the runtime a container that adds capabilities runs in
55
+ // (`backend.capabilities.runtime`)
56
+ capabilityRuntime: zod_1.z.string().min(1).optional(),
54
57
  // the most any task or service reaches besides its needs, as `--network`: `runs.network`
55
58
  network: network_schema_1.networkSchema.optional(),
56
59
  // for a run on a sandbox, `sandboxes.network`: the run gets the lower of the two
@@ -153,7 +156,12 @@ up) {
153
156
  // is what is streamed: it is redacted with the secrets of this run
154
157
  const environment = {
155
158
  processEnvs,
156
- hosted: { catalog: input.catalog, accounts: input.accounts, kubernetes: input.kubernetes },
159
+ hosted: {
160
+ catalog: input.catalog,
161
+ accounts: input.accounts,
162
+ kubernetes: input.kubernetes,
163
+ capabilityRuntime: input.capabilityRuntime,
164
+ },
157
165
  abortCtrl,
158
166
  cwd: work,
159
167
  file: (0, get_file_context_1.getFileContext)(work),
@@ -8,6 +8,7 @@ const log_1 = require("../log");
8
8
  const units_1 = require("../utils/units");
9
9
  const version_1 = require("../version");
10
10
  const network_schema_1 = require("../schema/network-schema");
11
+ const capabilities_schema_1 = require("../schema/capabilities-schema");
11
12
  const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
12
13
  const repo_1 = require("./repo");
13
14
  const work_network_1 = require("../planner/work-network");
@@ -117,12 +118,24 @@ exports.accountSchema = zod_1.z
117
118
  });
118
119
  }
119
120
  });
121
+ // The capabilities (`capAdd`) a task of a run may add, and the sandboxed runtime its
122
+ // container then runs in, so they hold in the sandbox's own kernel and never in the host's
123
+ const capabilitiesAllowSchema = zod_1.z
124
+ .array(zod_1.z.enum(capabilities_schema_1.LINUX_CAPABILITIES))
125
+ .min(1)
126
+ .refine((names) => new Set(names).size === names.length, 'a capability is listed twice');
120
127
  // The tasks of runs execute on this machine, with its docker daemon and local tools.
121
128
  const hostBackendSchema = zod_1.z
122
129
  .object({
123
130
  type: zod_1.z.literal('host'),
124
131
  // what this machine offers; detected when left out
125
132
  platform: exports.serverPlatformSchema.optional(),
133
+ // `runtime` is a runtime of the docker daemon (`runtimes` in its daemon.json), such as
134
+ // runsc of gVisor
135
+ capabilities: zod_1.z
136
+ .object({ allow: capabilitiesAllowSchema, runtime: zod_1.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/) })
137
+ .strict()
138
+ .optional(),
126
139
  })
127
140
  .strict();
128
141
  const tolerationSchema = zod_1.z
@@ -282,6 +295,9 @@ const kubernetesBackendSchema = zod_1.z
282
295
  // build file's environment does: hammerkit can't tell from the API, and without it a
283
296
  // task with a limited network fails rather than runs open
284
297
  networkPolicy: zod_1.z.literal('enforced').optional(),
298
+ // `runtimeClass` is a RuntimeClass of a sandbox, such as gvisor; the pods of tasks that
299
+ // add capabilities run in it
300
+ capabilities: zod_1.z.object({ allow: capabilitiesAllowSchema, runtimeClass: kubernetesNameSchema }).strict().optional(),
285
301
  })
286
302
  .strict();
287
303
  // the most a task's claim may request on a server whose operator states no maxVolumeSize
@@ -546,6 +562,9 @@ exports.serverConfigSchema = zod_1.z
546
562
  .object({ allow: zod_1.z.array(zod_1.z.string().regex(/^[A-Za-z_][A-Za-z0-9_()]*$/)).default([]) })
547
563
  .strict()
548
564
  .default({}),
565
+ // the capabilities of `backend.capabilities.allow` a task of a run on a sandbox may
566
+ // add: none by default, a sandbox holds code nobody reviewed
567
+ capabilities: zod_1.z.array(zod_1.z.enum(capabilities_schema_1.LINUX_CAPABILITIES)).default([]),
549
568
  // what one caller may keep; a sandbox over it can't be created
550
569
  limits: zod_1.z
551
570
  .object({
@@ -677,6 +696,16 @@ exports.serverConfigSchema = zod_1.z
677
696
  }
678
697
  }
679
698
  });
699
+ const allowed = config.backend.capabilities?.allow ?? [];
700
+ config.sandboxes.capabilities.forEach((capability, index) => {
701
+ if (!allowed.includes(capability)) {
702
+ ctx.addIssue({
703
+ code: zod_1.z.ZodIssueCode.custom,
704
+ path: ['sandboxes', 'capabilities', index],
705
+ message: `the capability ${capability} is not in backend.capabilities.allow, no run may add it`,
706
+ });
707
+ }
708
+ });
680
709
  const sandboxNetwork = (0, work_network_1.parseWorkNetwork)(config.sandboxes.network);
681
710
  const runsNetwork = (0, work_network_1.parseWorkNetwork)(config.runs.network);
682
711
  if ((0, work_network_1.exceedsNetwork)(sandboxNetwork, runsNetwork)) {
@@ -617,6 +617,7 @@ async function startServer(config, environment, options = {}) {
617
617
  network: (0, work_network_1.parseWorkNetwork)(config.runs.network),
618
618
  secretNetwork: (0, work_network_1.parseWorkNetwork)(config.runs.secretNetwork),
619
619
  args: config.runs.args,
620
+ capabilities: config.backend.capabilities?.allow ?? [],
620
621
  proxiedProviders: config.backend.type === 'kubernetes'
621
622
  ? Object.entries(config.backend.providers)
622
623
  .filter(([, provider]) => provider.allow)
@@ -657,6 +658,8 @@ async function startServer(config, environment, options = {}) {
657
658
  envs: config.sandboxes.env.allow,
658
659
  envsFrom: 'sandboxes.env.allow',
659
660
  runtimes: sandboxRuntimes,
661
+ capabilities: config.sandboxes.capabilities,
662
+ capabilitiesFrom: 'sandboxes.capabilities',
660
663
  },
661
664
  };
662
665
  if (sandboxRuntimes.length === 0 && config.entitlements.some((entitlement) => entitlement.sandboxes?.length)) {
@@ -676,6 +679,9 @@ async function startServer(config, environment, options = {}) {
676
679
  cache: (0, path_1.join)(storage, 'cache'),
677
680
  worker: options.worker,
678
681
  sandboxes: { ...sandboxes, network: config.sandboxes.network },
682
+ ...(config.backend.type === 'host' && config.backend.capabilities
683
+ ? { capabilityRuntime: config.backend.capabilities.runtime }
684
+ : {}),
679
685
  log,
680
686
  };
681
687
  const backend = config.backend;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "1.27.0-next.21",
4
+ "version": "1.27.0-next.23",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",