hammerkit 1.27.0-next.2 → 1.27.0-next.21

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 (103) hide show
  1. package/build.schema.json +27 -0
  2. package/dist/cache/backends/registry-cache-backend.js +2 -2
  3. package/dist/cache/explain.js +17 -0
  4. package/dist/cli.js +1 -0
  5. package/dist/docker/item-network.js +13 -5
  6. package/dist/executer/deadline.js +17 -3
  7. package/dist/executer/docker-service.js +30 -8
  8. package/dist/executer/event-cache.js +20 -2
  9. package/dist/executer/execute-work-task.js +5 -3
  10. package/dist/executer/execution-steps.js +25 -4
  11. package/dist/executer/reset-work-tree.js +4 -0
  12. package/dist/executer/scheduler/enqueue-next.js +7 -0
  13. package/dist/executer/scheduler/state-key.js +42 -5
  14. package/dist/harness/connect.js +20 -6
  15. package/dist/harness/mcp-command.js +22 -6
  16. package/dist/harness/mcp.js +63 -114
  17. package/dist/harness/opencode.js +158 -50
  18. package/dist/harness/pi.js +29 -0
  19. package/dist/harness/sandbox-client.js +207 -65
  20. package/dist/harness/service-tools.js +141 -0
  21. package/dist/harness/task-tools.js +221 -0
  22. package/dist/kubernetes/apply.js +27 -16
  23. package/dist/kubernetes/claim-layout.js +71 -0
  24. package/dist/kubernetes/ensure-http-route.js +1 -1
  25. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
  26. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
  27. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  28. package/dist/kubernetes/ensure-persistent-data.js +30 -20
  29. package/dist/kubernetes/get-service-ip.js +2 -2
  30. package/dist/kubernetes/job-secrets.js +33 -19
  31. package/dist/kubernetes/kept-claims.js +22 -10
  32. package/dist/kubernetes/network-policy.js +85 -27
  33. package/dist/kubernetes/remove-persistent-data.js +3 -1
  34. package/dist/kubernetes/resources.js +25 -3
  35. package/dist/kubernetes/restore-kubernetes-data.js +9 -3
  36. package/dist/kubernetes/sandbox-runner.js +626 -0
  37. package/dist/kubernetes/secrets.js +7 -11
  38. package/dist/kubernetes/volumes.js +6 -3
  39. package/dist/network/egress-proxy.js +45 -6
  40. package/dist/optimizer/calculate-checksum.js +3 -2
  41. package/dist/optimizer/check-key.js +94 -0
  42. package/dist/optimizer/check-keys.js +64 -0
  43. package/dist/optimizer/get-work-cache-stats.js +17 -3
  44. package/dist/optimizer/work-task-cache-description.js +51 -1
  45. package/dist/planner/utils/append-work-dependencies.js +10 -5
  46. package/dist/planner/utils/append-work-service.js +1 -0
  47. package/dist/planner/utils/append-work-task.js +15 -2
  48. package/dist/planner/utils/get-work-runtime.js +6 -1
  49. package/dist/planner/utils/normalize-path.js +19 -1
  50. package/dist/planner/utils/parse-work-mount.js +7 -1
  51. package/dist/planner/utils/parse-work-port.js +4 -1
  52. package/dist/planner/utils/task-arguments.js +3 -0
  53. package/dist/planner/validate.js +22 -0
  54. package/dist/planner/work-network.js +3 -1
  55. package/dist/planner/work-runtime-aggregate.js +32 -0
  56. package/dist/planner/work-runtime-docker.js +2 -1
  57. package/dist/planner/work-runtime-kubernetes.js +97 -48
  58. package/dist/planner/work-runtime-local.js +2 -1
  59. package/dist/planner/work-task.js +6 -1
  60. package/dist/program.js +35 -13
  61. package/dist/remote/annotations.js +13 -1
  62. package/dist/remote/device-login.js +7 -2
  63. package/dist/remote/host-config.js +64 -3
  64. package/dist/remote/remote-commands.js +41 -9
  65. package/dist/remote/remote-work-commands.js +246 -1
  66. package/dist/remote/server-client.js +15 -2
  67. package/dist/remote/token-source.js +64 -26
  68. package/dist/run-program.js +9 -0
  69. package/dist/schema/build-file-base-task-schema.js +5 -1
  70. package/dist/schema/build-file-container-service-schema.js +2 -0
  71. package/dist/schema/build-file-include-schema.js +10 -4
  72. package/dist/schema/reference-parser.js +9 -1
  73. package/dist/schema/schema-parser.js +36 -4
  74. package/dist/schema/work-scope-parser.js +33 -10
  75. package/dist/secrets/github-app-token-command.js +3 -1
  76. package/dist/server/accounts-api.js +3 -1
  77. package/dist/server/approvals-api.js +1 -1
  78. package/dist/server/checks-api.js +83 -0
  79. package/dist/server/checks.js +116 -0
  80. package/dist/server/definitions-api.js +4 -2
  81. package/dist/server/endpoints.js +612 -0
  82. package/dist/server/executor.js +56 -5
  83. package/dist/server/kubernetes-backend.js +303 -64
  84. package/dist/server/kubernetes-sandbox-storage.js +48 -3
  85. package/dist/server/oauth.js +12 -1
  86. package/dist/server/plan.js +126 -62
  87. package/dist/server/policy.js +32 -2
  88. package/dist/server/repo-access.js +5 -0
  89. package/dist/server/run-plan.js +50 -19
  90. package/dist/server/run-request.js +64 -3
  91. package/dist/server/run-worker.js +146 -10
  92. package/dist/server/runs.js +347 -27
  93. package/dist/server/sandbox-pod.js +28 -0
  94. package/dist/server/sandbox-storage.js +23 -0
  95. package/dist/server/sandboxes-api.js +10 -4
  96. package/dist/server/sandboxes.js +8 -0
  97. package/dist/server/server-command.js +39 -20
  98. package/dist/server/server-config.js +213 -14
  99. package/dist/server/server.js +268 -15
  100. package/dist/server/service-runs.js +82 -0
  101. package/dist/utils/abort-on-signals.js +46 -6
  102. package/dist/utils/tar.js +36 -0
  103. package/package.json +1 -1
@@ -412,6 +412,14 @@ class SandboxRegistry {
412
412
  async state(id, signal) {
413
413
  return this.options.storage.state(keyOf(this.existing(id)), signal);
414
414
  }
415
+ // The files of `tree`, which the sandbox's files must be now, written into `into`: what a
416
+ // check runs on.
417
+ async exportTree(id, tree, into, signal) {
418
+ const sandbox = this.existing(id);
419
+ sandbox.lastUsedAt = this.timestamp();
420
+ await this.save(sandbox);
421
+ await this.options.storage.exportTree(keyOf(sandbox), tree, into, signal);
422
+ }
415
423
  // What a run works on, `into` a directory of the run when the storage copies the files;
416
424
  // the run is the sandbox's use.
417
425
  async attach(id, into) {
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
5
5
  const log_1 = require("../log");
6
6
  const server_config_1 = require("./server-config");
7
7
  const server_1 = require("./server");
8
+ const abort_on_signals_1 = require("../utils/abort-on-signals");
8
9
  // Runs this machine as a hammerkit server until interrupted. Like `remote`, it
9
10
  // only needs its own config file, not a build file.
10
11
  function registerServerCommand(program, environment) {
@@ -13,31 +14,46 @@ function registerServerCommand(program, environment) {
13
14
  .description('serve this machine to remote runs (experimental)')
14
15
  .addOption(new commander_1.Option('-c, --config <file>', 'server config file').env('HAMMERKIT_SERVER_CONFIG'))
15
16
  .action(async (options) => {
17
+ // SIGHUP reads the entitlements again, so who may do what changes without a
18
+ // restart; the rest of the config needs one. The server takes it over from the
19
+ // abort of the CLI, which stops other commands on it, before it starts: a SIGHUP
20
+ // while it reads its config and starts waits for it. Windows sends SIGHUP when the
21
+ // console closes, which stops the server there too.
22
+ let server;
23
+ let reloadWhenStarted = false;
24
+ const reload = async (running) => {
25
+ try {
26
+ const result = running.reload(await (0, server_config_1.readServerConfig)(environment, options.config));
27
+ if (result.restart.length > 0) {
28
+ environment.stdout.write(`changed but not reloaded, restart for: ${result.restart.join(', ')}\n`);
29
+ }
30
+ }
31
+ catch (e) {
32
+ environment.stdout.write(`entitlements were not reloaded: ${(0, log_1.getErrorMessage)(e)}\n`);
33
+ }
34
+ };
35
+ const releaseReload = process.platform !== 'win32'
36
+ ? (0, abort_on_signals_1.takeOverSignal)(process, 'SIGHUP', () => {
37
+ if (server) {
38
+ void reload(server);
39
+ }
40
+ else {
41
+ reloadWhenStarted = true;
42
+ }
43
+ })
44
+ : () => undefined;
16
45
  try {
17
46
  if (!options.config) {
18
47
  throw new Error('pass the server config with --config or HAMMERKIT_SERVER_CONFIG');
19
48
  }
20
49
  const config = await (0, server_config_1.readServerConfig)(environment, options.config);
21
- const server = await (0, server_1.startServer)(config, environment, {
50
+ const running = await (0, server_1.startServer)(config, environment, {
22
51
  log: (line) => environment.stdout.write(`${line}\n`),
23
52
  });
24
- environment.stdout.write(`hammerkit server listening on ${server.url}\n`);
25
- // SIGHUP reads the entitlements again, so who may do what changes without
26
- // a restart; the rest of the config needs one
27
- const reload = async () => {
28
- try {
29
- const result = server.reload(await (0, server_config_1.readServerConfig)(environment, options.config));
30
- if (result.restart.length > 0) {
31
- environment.stdout.write(`changed but not reloaded, restart for: ${result.restart.join(', ')}\n`);
32
- }
33
- }
34
- catch (e) {
35
- environment.stdout.write(`entitlements were not reloaded: ${(0, log_1.getErrorMessage)(e)}\n`);
36
- }
37
- };
38
- const onReload = () => void reload();
39
- if (process.platform !== 'win32') {
40
- process.on('SIGHUP', onReload);
53
+ server = running;
54
+ environment.stdout.write(`hammerkit server listening on ${running.url}\n`);
55
+ if (reloadWhenStarted) {
56
+ void reload(running);
41
57
  }
42
58
  await new Promise((resolve) => {
43
59
  if (environment.abortCtrl.signal.aborted) {
@@ -45,8 +61,8 @@ function registerServerCommand(program, environment) {
45
61
  }
46
62
  environment.abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true });
47
63
  });
48
- process.off('SIGHUP', onReload);
49
- await server.close();
64
+ releaseReload();
65
+ await running.close();
50
66
  environment.stdout.write('hammerkit server stopped\n');
51
67
  }
52
68
  catch (e) {
@@ -55,6 +71,9 @@ function registerServerCommand(program, environment) {
55
71
  }
56
72
  program.error(`Server was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
57
73
  }
74
+ finally {
75
+ releaseReload();
76
+ }
58
77
  });
59
78
  }
60
79
  exports.registerServerCommand = registerServerCommand;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectPlatform = exports.getSecretCatalog = exports.readServerConfig = exports.getSourceAccounts = exports.getSandboxRuntimes = exports.serverConfigSchema = exports.sourceSchema = exports.entitlementSchema = exports.sandboxGrantSchema = exports.sandboxBranchTypeSchema = exports.buildsLevelSchema = exports.accountLevelSchema = exports.getMaxVolumeSize = exports.DEFAULT_MAX_VOLUME_SIZE = exports.DEFAULT_DENIED_EGRESS = exports.kubernetesPoolSchema = exports.accountSchema = exports.issuerSchema = exports.serverPlatformSchema = void 0;
3
+ exports.detectPlatform = exports.getSecretCatalog = exports.readServerConfig = exports.getServerAccounts = exports.getSandboxRuntimes = exports.serverConfigSchema = exports.sourceSchema = exports.entitlementSchema = exports.endpointGrantSchema = exports.endpointLevelSchema = exports.sandboxGrantSchema = exports.sandboxBranchTypeSchema = exports.buildsLevelSchema = exports.accountLevelSchema = exports.getMaxVolumeSize = exports.DEFAULT_MAX_VOLUME_SIZE = exports.DEFAULT_DENIED_EGRESS = exports.kubernetesPoolSchema = exports.accountSchema = exports.issuerSchema = exports.serverPlatformSchema = void 0;
4
4
  const net_1 = require("net");
5
5
  const zod_1 = require("zod");
6
6
  const yaml_1 = require("yaml");
@@ -188,8 +188,12 @@ const kubernetesSandboxesSchema = zod_1.z
188
188
  // when left out. With a class of their own, a ResourceQuota on that class in each
189
189
  // caller's namespace holds `sandboxes.limits`.
190
190
  storageClass: zod_1.z.string().min(1).optional(),
191
- // how long the pod of a sandbox's files stays after the last request
191
+ // how long the pod of a sandbox's files stays after the last request; the runner of the
192
+ // sandbox goes with it
192
193
  idle: duration.default('1m'),
194
+ // exec the commands of a task shaped like a shell (it mounts the sandbox only, has no
195
+ // needs, outputs or secrets) into one pod per sandbox, in place of a Job each
196
+ runner: zod_1.z.boolean().default(true),
193
197
  // of the pod of a sandbox's files, which runs git and ripgrep over all of them
194
198
  resources: zod_1.z
195
199
  .object({ requests: quantitiesSchema.optional(), limits: quantitiesSchema.optional() })
@@ -238,8 +242,22 @@ const kubernetesBackendSchema = zod_1.z
238
242
  .min(1)
239
243
  .default([{ name: 'default', platform: { os: 'linux', arch: 'amd64' }, nodeSelector: {}, tolerations: [] }]),
240
244
  // per secret provider, the image its command runs in: secrets are read in the
241
- // Job of the task, not by the server
242
- providers: zod_1.z.record(zod_1.z.object({ image: zod_1.z.string().min(1) }).strict()).default({}),
245
+ // Job of the task, not by the server. `allow` are the hosts its command reaches, such
246
+ // as secretmanager.googleapis.com: the egress proxy of a task with a limited network
247
+ // lets the init containers that read its secrets reach them, and nothing else does
248
+ providers: zod_1.z
249
+ .record(zod_1.z
250
+ .object({
251
+ image: zod_1.z.string().min(1),
252
+ allow: zod_1.z
253
+ .array(zod_1.z
254
+ .string()
255
+ .refine((host) => (0, network_schema_1.isAllowEntry)(host) && !host.includes(':'), 'expected a host name such as secretmanager.googleapis.com or *.example.com'))
256
+ .min(1)
257
+ .optional(),
258
+ })
259
+ .strict())
260
+ .default({}),
243
261
  // the ResourceQuota of every caller's namespace, such as `requests.cpu: '8'`
244
262
  quota: zod_1.z.record(zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number()]).transform(String)).optional(),
245
263
  // what a container without `resources` requests (a LimitRange), so it is
@@ -309,6 +327,11 @@ exports.sandboxGrantSchema = zod_1.z
309
327
  .refine((branches) => Object.keys(branches).length > 0, 'name the branches a sandbox may start from'),
310
328
  })
311
329
  .strict();
330
+ // `open`: open the endpoints of the service runs of a repository, whoever started them, and
331
+ // extend those runs; `publish`: also name endpoints in one's own service runs.
332
+ exports.endpointLevelSchema = zod_1.z.enum(['open', 'publish']);
333
+ // One repository (`host/path` pattern) whose endpoints an entitlement opens or publishes.
334
+ exports.endpointGrantSchema = zod_1.z.object({ repo: repoPatternSchema, level: exports.endpointLevelSchema }).strict();
312
335
  exports.entitlementSchema = zod_1.z
313
336
  .object({
314
337
  ...whoShape,
@@ -318,19 +341,33 @@ exports.entitlementSchema = zod_1.z
318
341
  // the repositories (`host/path` patterns) the caller may run from a git ref
319
342
  repos: zod_1.z.array(repoPatternSchema).default([]),
320
343
  builds: exports.buildsLevelSchema.optional(),
344
+ // `read`: may look up which tasks runs passed for files of the `repos` (remote verify),
345
+ // and nothing more; a builds level includes it
346
+ checks: zod_1.z.literal('read').optional(),
321
347
  // the repositories the caller may keep sandboxes of, checkouts on the server that runs
322
348
  // work on: a list of its own, the `repos` above are for runs from a ref
323
349
  sandboxes: zod_1.z.array(exports.sandboxGrantSchema).optional(),
350
+ // the repositories whose service runs' endpoints the caller may open, or publish
351
+ endpoints: zod_1.z.array(exports.endpointGrantSchema).optional(),
324
352
  })
325
353
  .strict()
326
354
  .superRefine((entitlement, ctx) => {
327
355
  checkWho(entitlement, ctx);
328
356
  if (Object.keys(entitlement.accounts).length === 0 &&
329
357
  entitlement.builds === undefined &&
330
- !entitlement.sandboxes?.length) {
358
+ entitlement.checks === undefined &&
359
+ !entitlement.sandboxes?.length &&
360
+ !entitlement.endpoints?.length) {
361
+ ctx.addIssue({
362
+ code: zod_1.z.ZodIssueCode.custom,
363
+ message: 'name accounts to use or request, a builds level, checks, sandboxes or endpoints',
364
+ });
365
+ }
366
+ if (entitlement.checks && entitlement.repos.length === 0) {
331
367
  ctx.addIssue({
332
368
  code: zod_1.z.ZodIssueCode.custom,
333
- message: 'name accounts to use or request, a builds level or sandboxes',
369
+ path: ['repos'],
370
+ message: 'checks are read for repositories: name them in repos',
334
371
  });
335
372
  }
336
373
  });
@@ -451,6 +488,40 @@ exports.serverConfigSchema = zod_1.z
451
488
  // the most any task or service of a run reaches besides its needs: none, open
452
489
  // or { allow: [hosts] }. A version that asks for more is refused at publish.
453
490
  network: network_schema_1.networkSchema.default('open'),
491
+ // the most a task or service that reads a secret, or a variable of the server
492
+ // other than PATH, HOME and CI, reaches, under `network`: so a tool it runs can't
493
+ // send the value anywhere. Checked like `network`.
494
+ secretNetwork: network_schema_1.networkSchema.default('open'),
495
+ // which tasks take arguments: `any` but those that say `args: false`, or
496
+ // `declared`, only those that say `args: true`
497
+ args: zod_1.z.enum(['any', 'declared']).default('any'),
498
+ // runs of services (`services` in a run), kept up until their ttl ends
499
+ up: zod_1.z
500
+ .object({
501
+ // how long the services stay once ready, when the run names no ttl; then they are removed
502
+ ttl: duration.default('8h'),
503
+ // how long the services stay up without a request to an endpoint before they sleep,
504
+ // when the run names none: the next request wakes them. Unset, they never sleep.
505
+ sleepTtl: duration.optional(),
506
+ // the furthest a run, or an extend, sets the end from now
507
+ maxTtl: duration.default('72h'),
508
+ limits: zod_1.z
509
+ .object({
510
+ // service runs one caller has starting or up at a time; one more is 429
511
+ up: zod_1.z.number().int().min(1).default(3),
512
+ })
513
+ .strict()
514
+ .default({}),
515
+ // service runs starting or up on the whole server; one more is 503
516
+ total: zod_1.z.number().int().min(1).default(20),
517
+ // on a Kubernetes backend, the ResourceQuota of the namespace of each service
518
+ // run; backend.quota when left out
519
+ quota: zod_1.z.record(zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number()]).transform(String)).optional(),
520
+ })
521
+ .strict()
522
+ .default({}),
523
+ // how long a passed check stays for `remote verify` to find (see checks.ts)
524
+ checksKeep: duration.default('30d'),
454
525
  })
455
526
  .strict()
456
527
  .default({}),
@@ -488,6 +559,45 @@ exports.serverConfigSchema = zod_1.z
488
559
  })
489
560
  .strict()
490
561
  .default({}),
562
+ // ports of service runs published at hosts of their own below `domain`, behind the
563
+ // server's sign-in. Absent, a run naming endpoints is refused.
564
+ endpoints: zod_1.z
565
+ .object({
566
+ // `run.example.com`: an endpoint is https://<label>.run.example.com, all of them
567
+ // pointing at this server with one wildcard DNS record
568
+ domain: zod_1.z
569
+ .string()
570
+ .regex(/^(?=.{1,200}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, 'expected a domain name in lower case, such as run.example.com'),
571
+ // a wildcard certificate for *.<domain>, served for the hosts of endpoints; left out
572
+ // behind a TLS proxy, which holds it
573
+ tls: zod_1.z
574
+ .object({ cert: zod_1.z.string().min(1), key: zod_1.z.string().min(1) })
575
+ .strict()
576
+ .optional(),
577
+ // how a person signs in with a browser: the authorization code flow of an issuer of
578
+ // `issuers`, with a client of its own; without it only tokens open endpoints
579
+ login: zod_1.z
580
+ .object({
581
+ issuer: zod_1.z.string().min(1),
582
+ clientId: zod_1.z.string().min(1),
583
+ // for providers whose web clients need one (Google): read as an account no
584
+ // entitlement may name, as the secret of a source is
585
+ clientSecret: zod_1.z
586
+ .object({
587
+ from: zod_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_-]*:.+$/, 'expected provider:reference'),
588
+ account: accountNameSchema,
589
+ })
590
+ .strict()
591
+ .optional(),
592
+ scope: zod_1.z.string().min(1).default('openid'),
593
+ })
594
+ .strict()
595
+ .optional(),
596
+ // how long a sign-in at one endpoint lasts, at most until its run ends
597
+ session: duration.default('8h'),
598
+ })
599
+ .strict()
600
+ .optional(),
491
601
  backend: zod_1.z.discriminatedUnion('type', [hostBackendSchema, kubernetesBackendSchema]).default({ type: 'host' }),
492
602
  })
493
603
  .strict()
@@ -555,7 +665,7 @@ exports.serverConfigSchema = zod_1.z
555
665
  });
556
666
  }
557
667
  });
558
- const readers = getSourceAccounts(config);
668
+ const readers = getServerAccounts(config);
559
669
  config.entitlements.forEach((entitlement, index) => {
560
670
  for (const account of Object.keys(entitlement.accounts)) {
561
671
  if (readers.includes(account)) {
@@ -586,7 +696,10 @@ exports.serverConfigSchema = zod_1.z
586
696
  message: `under runs.network, ${shared}`,
587
697
  });
588
698
  }
589
- if (config.sandboxes.runtimes?.includes('local') && config.sandboxes.network !== 'open') {
699
+ // on a cluster a local task runs in a pod of a pool's tool image, which can be limited
700
+ if (config.sandboxes.runtimes?.includes('local') &&
701
+ config.sandboxes.network !== 'open' &&
702
+ config.backend.type === 'host') {
590
703
  ctx.addIssue({
591
704
  code: zod_1.z.ZodIssueCode.custom,
592
705
  path: ['sandboxes', 'network'],
@@ -635,7 +748,7 @@ exports.serverConfigSchema = zod_1.z
635
748
  });
636
749
  // the secrets of tasks are read in their Job, with the image of the provider; the
637
750
  // ones of the sources are read by the server, which checks the code out itself
638
- const readers = getSourceAccounts(config);
751
+ const readers = getServerAccounts(config);
639
752
  for (const [name, account] of Object.entries(config.accounts)) {
640
753
  if (readers.includes(name)) {
641
754
  continue;
@@ -697,6 +810,13 @@ exports.serverConfigSchema = zod_1.z
697
810
  message: 'runs.network limits every task with a NetworkPolicy, set networkPolicy: enforced once the network plugin of the cluster enforces them',
698
811
  });
699
812
  }
813
+ if (config.runs.secretNetwork !== 'open' && backend.networkPolicy !== 'enforced') {
814
+ ctx.addIssue({
815
+ code: zod_1.z.ZodIssueCode.custom,
816
+ path: ['backend', 'networkPolicy'],
817
+ message: 'runs.secretNetwork limits the tasks that read secrets with a NetworkPolicy, set networkPolicy: enforced once the network plugin of the cluster enforces them',
818
+ });
819
+ }
700
820
  if (config.sandboxes.network !== 'open' &&
701
821
  backend.networkPolicy !== 'enforced' &&
702
822
  config.entitlements.some((entitlement) => entitlement.sandboxes?.length)) {
@@ -707,13 +827,85 @@ exports.serverConfigSchema = zod_1.z
707
827
  });
708
828
  }
709
829
  }
710
- if (config.runs.network !== 'open' && config.runtimes.includes('local')) {
830
+ if (config.runs.network !== 'open' && config.runtimes.includes('local') && backend.type === 'host') {
711
831
  ctx.addIssue({
712
832
  code: zod_1.z.ZodIssueCode.custom,
713
833
  path: ['runs', 'network'],
714
834
  message: 'the network of a local task can not be limited, allow only the docker runtime with runs.network',
715
835
  });
716
836
  }
837
+ if ((0, units_1.parseDuration)(config.runs.up.ttl) > (0, units_1.parseDuration)(config.runs.up.maxTtl)) {
838
+ ctx.addIssue({
839
+ code: zod_1.z.ZodIssueCode.custom,
840
+ path: ['runs', 'up', 'ttl'],
841
+ message: `a ttl of ${config.runs.up.ttl} is more than maxTtl of ${config.runs.up.maxTtl}`,
842
+ });
843
+ }
844
+ const endpoints = config.endpoints;
845
+ if (endpoints) {
846
+ if (endpoints.tls && config.behindTlsProxy) {
847
+ ctx.addIssue({
848
+ code: zod_1.z.ZodIssueCode.custom,
849
+ path: ['endpoints', 'tls'],
850
+ message: 'behind a TLS proxy the proxy holds the certificate of the endpoints, leave out endpoints.tls',
851
+ });
852
+ }
853
+ if (endpoints.tls && !config.tls) {
854
+ ctx.addIssue({
855
+ code: zod_1.z.ZodIssueCode.custom,
856
+ path: ['endpoints', 'tls'],
857
+ message: 'the certificate of the endpoints is served next to the one of the server, set tls as well',
858
+ });
859
+ }
860
+ if (!endpoints.tls && !config.behindTlsProxy && (config.tls || !LOOPBACK_HOSTS.includes(config.listen.host))) {
861
+ ctx.addIssue({
862
+ code: zod_1.z.ZodIssueCode.custom,
863
+ path: ['endpoints', 'tls'],
864
+ message: `name the certificate of *.${endpoints.domain} in endpoints.tls, or set behindTlsProxy: true when a proxy in front holds it`,
865
+ });
866
+ }
867
+ const login = endpoints.login;
868
+ if (login && !config.issuers.some((issuer) => issuer.issuer === login.issuer)) {
869
+ ctx.addIssue({
870
+ code: zod_1.z.ZodIssueCode.custom,
871
+ path: ['endpoints', 'login', 'issuer'],
872
+ message: `issuer ${login.issuer} is not one of the accepted issuers`,
873
+ });
874
+ }
875
+ if (login?.clientSecret) {
876
+ const { from, account } = login.clientSecret;
877
+ const kind = from.substring(0, from.indexOf(':'));
878
+ const where = ['endpoints', 'login', 'clientSecret'];
879
+ if (!config.accounts[account]) {
880
+ ctx.addIssue({
881
+ code: zod_1.z.ZodIssueCode.custom,
882
+ path: [...where, 'account'],
883
+ message: `unknown account ${account}`,
884
+ });
885
+ }
886
+ else if (!config.accounts[account].providers[kind]) {
887
+ ctx.addIssue({
888
+ code: zod_1.z.ZodIssueCode.custom,
889
+ path: [...where, 'from'],
890
+ message: `account ${account} has no credentials for the provider ${kind}`,
891
+ });
892
+ }
893
+ if (!config.secretProviders[kind]) {
894
+ ctx.addIssue({
895
+ code: zod_1.z.ZodIssueCode.custom,
896
+ path: [...where, 'from'],
897
+ message: `secret provider ${kind} is not declared in secretProviders`,
898
+ });
899
+ }
900
+ }
901
+ if (backend.type === 'kubernetes' && backend.kubeconfig) {
902
+ ctx.addIssue({
903
+ code: zod_1.z.ZodIssueCode.custom,
904
+ path: ['endpoints'],
905
+ message: 'endpoints reach the services of runs at their Service in the cluster, run the server in the cluster rather than with a kubeconfig',
906
+ });
907
+ }
908
+ }
717
909
  if (!config.tls && !config.behindTlsProxy && !LOOPBACK_HOSTS.includes(config.listen.host)) {
718
910
  ctx.addIssue({
719
911
  code: zod_1.z.ZodIssueCode.custom,
@@ -729,11 +921,18 @@ function getSandboxRuntimes(config) {
729
921
  return config.sandboxes.runtimes ?? config.runtimes.filter((runtime) => runtime === 'docker');
730
922
  }
731
923
  exports.getSandboxRuntimes = getSandboxRuntimes;
732
- // The accounts that read repositories: no caller uses them and no build file names them.
733
- function getSourceAccounts(config) {
734
- return [...new Set(config.sources.flatMap((source) => (source.secret ? [source.secret.account] : [])))];
924
+ // The accounts only the server reads secrets as: those that read the repositories of
925
+ // `sources`, and the one of the client secret of `endpoints.login`. No caller uses them and
926
+ // no build file names them.
927
+ function getServerAccounts(config) {
928
+ return [
929
+ ...new Set([
930
+ ...config.sources.flatMap((source) => (source.secret ? [source.secret.account] : [])),
931
+ ...(config.endpoints?.login?.clientSecret ? [config.endpoints.login.clientSecret.account] : []),
932
+ ]),
933
+ ];
735
934
  }
736
- exports.getSourceAccounts = getSourceAccounts;
935
+ exports.getServerAccounts = getServerAccounts;
737
936
  async function readServerConfig(environment, path) {
738
937
  if (!(await environment.file.exists(path))) {
739
938
  throw new Error(`unable to read ${path}: no such file`);