hammerkit 1.27.0-next.2 → 1.27.0-next.20

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 (100) hide show
  1. package/build.schema.json +9 -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 +5 -2
  11. package/dist/executer/scheduler/enqueue-next.js +7 -0
  12. package/dist/executer/scheduler/state-key.js +42 -5
  13. package/dist/harness/connect.js +20 -6
  14. package/dist/harness/mcp-command.js +22 -6
  15. package/dist/harness/mcp.js +63 -114
  16. package/dist/harness/opencode.js +158 -50
  17. package/dist/harness/pi.js +29 -0
  18. package/dist/harness/sandbox-client.js +207 -65
  19. package/dist/harness/service-tools.js +141 -0
  20. package/dist/harness/task-tools.js +221 -0
  21. package/dist/kubernetes/apply.js +27 -16
  22. package/dist/kubernetes/claim-layout.js +71 -0
  23. package/dist/kubernetes/ensure-http-route.js +1 -1
  24. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
  25. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
  26. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  27. package/dist/kubernetes/ensure-persistent-data.js +30 -20
  28. package/dist/kubernetes/get-service-ip.js +2 -2
  29. package/dist/kubernetes/job-secrets.js +33 -19
  30. package/dist/kubernetes/kept-claims.js +22 -10
  31. package/dist/kubernetes/network-policy.js +85 -27
  32. package/dist/kubernetes/remove-persistent-data.js +3 -1
  33. package/dist/kubernetes/resources.js +25 -3
  34. package/dist/kubernetes/restore-kubernetes-data.js +9 -3
  35. package/dist/kubernetes/sandbox-runner.js +626 -0
  36. package/dist/kubernetes/secrets.js +7 -11
  37. package/dist/kubernetes/volumes.js +6 -3
  38. package/dist/network/egress-proxy.js +45 -6
  39. package/dist/optimizer/calculate-checksum.js +3 -2
  40. package/dist/optimizer/check-key.js +94 -0
  41. package/dist/optimizer/check-keys.js +64 -0
  42. package/dist/optimizer/get-work-cache-stats.js +17 -3
  43. package/dist/optimizer/work-task-cache-description.js +51 -1
  44. package/dist/planner/utils/append-work-dependencies.js +10 -5
  45. package/dist/planner/utils/append-work-service.js +1 -0
  46. package/dist/planner/utils/append-work-task.js +15 -2
  47. package/dist/planner/utils/get-work-runtime.js +6 -1
  48. package/dist/planner/utils/normalize-path.js +19 -1
  49. package/dist/planner/utils/parse-work-mount.js +7 -1
  50. package/dist/planner/utils/parse-work-port.js +4 -1
  51. package/dist/planner/utils/task-arguments.js +3 -0
  52. package/dist/planner/validate.js +22 -0
  53. package/dist/planner/work-network.js +3 -1
  54. package/dist/planner/work-runtime-aggregate.js +32 -0
  55. package/dist/planner/work-runtime-docker.js +2 -1
  56. package/dist/planner/work-runtime-kubernetes.js +97 -48
  57. package/dist/planner/work-runtime-local.js +2 -1
  58. package/dist/planner/work-task.js +6 -1
  59. package/dist/program.js +14 -4
  60. package/dist/remote/annotations.js +13 -1
  61. package/dist/remote/device-login.js +7 -2
  62. package/dist/remote/host-config.js +64 -3
  63. package/dist/remote/remote-commands.js +41 -9
  64. package/dist/remote/remote-work-commands.js +246 -1
  65. package/dist/remote/server-client.js +15 -2
  66. package/dist/remote/token-source.js +64 -26
  67. package/dist/run-program.js +9 -0
  68. package/dist/schema/build-file-base-task-schema.js +5 -1
  69. package/dist/schema/build-file-container-service-schema.js +2 -0
  70. package/dist/schema/reference-parser.js +9 -1
  71. package/dist/schema/work-scope-parser.js +6 -0
  72. package/dist/secrets/github-app-token-command.js +3 -1
  73. package/dist/server/accounts-api.js +3 -1
  74. package/dist/server/approvals-api.js +1 -1
  75. package/dist/server/checks-api.js +83 -0
  76. package/dist/server/checks.js +116 -0
  77. package/dist/server/definitions-api.js +4 -2
  78. package/dist/server/endpoints.js +612 -0
  79. package/dist/server/executor.js +56 -5
  80. package/dist/server/kubernetes-backend.js +303 -64
  81. package/dist/server/kubernetes-sandbox-storage.js +48 -3
  82. package/dist/server/oauth.js +12 -1
  83. package/dist/server/plan.js +126 -62
  84. package/dist/server/policy.js +32 -2
  85. package/dist/server/repo-access.js +5 -0
  86. package/dist/server/run-plan.js +50 -19
  87. package/dist/server/run-request.js +64 -3
  88. package/dist/server/run-worker.js +146 -10
  89. package/dist/server/runs.js +347 -27
  90. package/dist/server/sandbox-pod.js +28 -0
  91. package/dist/server/sandbox-storage.js +23 -0
  92. package/dist/server/sandboxes-api.js +10 -4
  93. package/dist/server/sandboxes.js +8 -0
  94. package/dist/server/server-command.js +39 -20
  95. package/dist/server/server-config.js +213 -14
  96. package/dist/server/server.js +268 -15
  97. package/dist/server/service-runs.js +82 -0
  98. package/dist/utils/abort-on-signals.js +46 -6
  99. package/dist/utils/tar.js +36 -0
  100. package/package.json +1 -1
@@ -7,6 +7,8 @@ exports.runProgram = void 0;
7
7
  const program_1 = require("./program");
8
8
  const parse_error_1 = require("./schema/parse-error");
9
9
  const colors_1 = __importDefault(require("colors"));
10
+ const commander_1 = require("commander");
11
+ const log_1 = require("./log");
10
12
  function runProgram(env, argv, exitOverride) {
11
13
  return (0, program_1.getProgram)(env, argv)
12
14
  .then(({ program, args }) => {
@@ -18,8 +20,15 @@ function runProgram(env, argv, exitOverride) {
18
20
  for (const error of err.zod.errors) {
19
21
  env.stderr.write(` ${colors_1.default.underline(colors_1.default.blue(`error`))} at ${colors_1.default.gray(error.path.join('.'))} ${error.message} ${colors_1.default.gray(error.code)}\n`);
20
22
  }
23
+ // printed, but still a failure: CI must not pass on a build file that doesn't parse
24
+ throw err;
21
25
  }
22
26
  else {
27
+ // what commander reports (program.error, a wrong option) is printed already; anything
28
+ // else, such as a build file that is no YAML, would otherwise end the process silently
29
+ if (!(err instanceof commander_1.CommanderError)) {
30
+ env.stderr.write(`${colors_1.default.red('error')}: ${(0, log_1.getErrorMessage)(err)}\n`);
31
+ }
23
32
  throw err;
24
33
  }
25
34
  });
@@ -40,8 +40,12 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
40
40
  account: (0, zod_1.string)().optional(),
41
41
  // CPU and memory limits of the container; a local task is not limited
42
42
  resources: resources_schema_1.resourcesSchema.optional(),
43
- // what the container reaches besides its needs; a local task can't be limited
43
+ // what the container reaches besides its needs; a local task can be limited only where
44
+ // it runs in a container, on a server's cluster
44
45
  network: network_schema_1.networkSchema.optional(),
46
+ // whether its last command takes the arguments after `--`: false refuses them, true
47
+ // says it is meant to, which a server with `runs.args: declared` requires
48
+ args: (0, zod_1.boolean)().optional(),
45
49
  // Name of the server (see `hammerkit remote`) the task has to run on; defaults
46
50
  // to the build file's `runner`. Never a local fallback when it is unknown.
47
51
  runner: runner_schema_1.runnerSchema.optional(),
@@ -45,6 +45,8 @@ exports.buildFileContainerServiceSchema = (0, zod_1.object)({
45
45
  volumeSize: size_schema_1.sizeSchema.describe('size of its claim on Kubernetes, e.g. 10Gi').optional(),
46
46
  // what the service reaches besides its needs; tasks needing it always reach it
47
47
  network: network_schema_1.networkSchema.optional(),
48
+ // whether its `cmd` takes the arguments after `--` of `up`: false refuses them
49
+ args: (0, zod_1.boolean)().optional(),
48
50
  })
49
51
  .strict()
50
52
  .describe('container service');
@@ -129,6 +129,7 @@ function applySchemaExtension(reference, task) {
129
129
  task.schema.timeout = task.schema.timeout ?? extend.schema.timeout;
130
130
  task.schema.runner = task.schema.runner ?? extend.schema.runner;
131
131
  task.schema.network = task.schema.network ?? extend.schema.network;
132
+ task.schema.args = task.schema.args ?? extend.schema.args;
132
133
  if (!reset.has('cmds'))
133
134
  task.schema.cmds = extendArray(task.schema.cmds, extend.schema.cmds);
134
135
  if (!reset.has('src'))
@@ -209,7 +210,9 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
209
210
  for (const needName of schema.needs || []) {
210
211
  if (typeof needName === 'string') {
211
212
  taskOrService.needs.push({
212
- relativeName: needName,
213
+ // the host name it is reached by: a service of an include, `services:postgres`, as
214
+ // `services-postgres`, since Docker and DNS take no colon
215
+ relativeName: needName.replace(/:/g, '-'),
213
216
  service: findService(reference, scope, needName),
214
217
  });
215
218
  }
@@ -220,6 +223,11 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
220
223
  });
221
224
  }
222
225
  }
226
+ const names = taskOrService.needs.map((need) => need.relativeName);
227
+ const twice = names.find((name, index) => names.indexOf(name) !== index);
228
+ if (twice) {
229
+ throw new Error(`${taskOrService.relativeName} needs two services as ${twice}, give one another name with name:`);
230
+ }
223
231
  }
224
232
  function findTask(ctx, scope, name, type) {
225
233
  const ref = (0, split_name_1.splitName)(name);
@@ -117,12 +117,18 @@ function matchesTaskWorkScope(task, scope) {
117
117
  if ((0, work_scope_1.isContextTaskFilter)(scope)) {
118
118
  return task.name === scope.taskName;
119
119
  }
120
+ if (scope.serviceNames) {
121
+ return false;
122
+ }
120
123
  return (0, label_values_1.appliesToLabels)(task.data.labels, scope);
121
124
  }
122
125
  function matchesServiceWorkScope(service, scope) {
123
126
  if ((0, work_scope_1.isContextTaskFilter)(scope)) {
124
127
  return false;
125
128
  }
129
+ if (scope.serviceNames) {
130
+ return scope.serviceNames.includes(service.name);
131
+ }
126
132
  return (0, label_values_1.appliesToLabels)(service.labels, scope);
127
133
  }
128
134
  //# sourceMappingURL=work-scope-parser.js.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerGithubAppTokenCommand = void 0;
4
+ const commander_1 = require("commander");
4
5
  const log_1 = require("../log");
5
6
  const github_app_token_1 = require("./github-app-token");
6
7
  // `hammerkit github-app-token <owner/repo>`: a secret provider command for the token a
@@ -18,7 +19,8 @@ function registerGithubAppTokenCommand(program, environment) {
18
19
  }
19
20
  catch (e) {
20
21
  environment.stderr.write(`github-app-token: ${(0, log_1.getErrorMessage)(e)}\n`);
21
- throw e;
22
+ // said already: the CLI does not print it again
23
+ throw new commander_1.CommanderError(1, 'hammerkit.githubAppToken', (0, log_1.getErrorMessage)(e));
22
24
  }
23
25
  });
24
26
  }
@@ -6,7 +6,7 @@ const policy_1 = require("./policy");
6
6
  /**
7
7
  * Who the caller is and what they may do:
8
8
  *
9
- * GET /v1/me identity, accounts with their level, repositories, the builds level and the sandbox grants
9
+ * GET /v1/me identity, accounts with their level, repositories, the builds level the sandbox and endpoint grants
10
10
  * GET /v1/accounts the accounts the caller has a level on, with what the operator says about them
11
11
  */
12
12
  async function handleAccounts(config, log, principal, req, res, url) {
@@ -31,7 +31,9 @@ async function handleAccounts(config, log, principal, req, res, url) {
31
31
  accounts,
32
32
  repos: grant.repos,
33
33
  builds: grant.builds,
34
+ checks: grant.checks || grant.builds !== null ? 'read' : null,
34
35
  sandboxes: grant.sandboxes,
36
+ endpoints: grant.endpoints,
35
37
  });
36
38
  return true;
37
39
  }
@@ -13,7 +13,7 @@ function toApprovalView(run) {
13
13
  return {
14
14
  id: run.id,
15
15
  state: run.state,
16
- requestedBy: run.owner,
16
+ requestedBy: { iss: run.owner.iss, sub: run.owner.sub },
17
17
  createdAt: run.createdAt,
18
18
  definition: run.definition,
19
19
  tasks: run.tasks,
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleChecks = exports.MAX_LOOKUP_KEYS = void 0;
4
+ const zod_1 = require("zod");
5
+ const http_1 = require("./http");
6
+ const policy_1 = require("./policy");
7
+ const checks_1 = require("./checks");
8
+ const repo_1 = require("./repo");
9
+ // the tasks one lookup may ask about
10
+ exports.MAX_LOOKUP_KEYS = 100;
11
+ const lookupSchema = zod_1.z
12
+ .object({
13
+ repo: zod_1.z.string().transform((value, ctx) => {
14
+ const repo = (0, repo_1.normalizeRepo)(value);
15
+ if (!repo) {
16
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: 'expected a repository like github.com/corp/payments' });
17
+ return zod_1.z.NEVER;
18
+ }
19
+ return repo;
20
+ }),
21
+ keys: zod_1.z
22
+ .array(zod_1.z
23
+ .object({
24
+ task: zod_1.z.string().min(1).max(200),
25
+ key: zod_1.z.string().regex(checks_1.CHECK_KEY_PATTERN, 'expected a check key, 64 hex digits'),
26
+ })
27
+ .strict())
28
+ .min(1)
29
+ .max(exports.MAX_LOOKUP_KEYS),
30
+ })
31
+ .strict();
32
+ /**
33
+ * What runs passed, for CI that verifies instead of running (`hammerkit remote verify`):
34
+ *
35
+ * POST /v1/checks/lookup { repo, keys: [{ task, key }] } the newest passed run per key
36
+ *
37
+ * The caller computed the keys from its own checkout (check-key.ts); a key nothing passed
38
+ * for, or whose version of the build file was yanked since, is answered with none.
39
+ */
40
+ async function handleChecks(config, checks, definitions, log, principal, req, res, url) {
41
+ if (url.pathname !== '/v1/checks/lookup') {
42
+ return false;
43
+ }
44
+ if (req.method !== 'POST') {
45
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
46
+ return true;
47
+ }
48
+ const parsed = lookupSchema.safeParse(await (0, http_1.readJson)(req));
49
+ if (!parsed.success) {
50
+ const issue = parsed.error.issues[0];
51
+ (0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
52
+ return true;
53
+ }
54
+ const { repo, keys } = parsed.data;
55
+ const grant = (0, policy_1.getGrant)(config.entitlements, principal);
56
+ if (!(0, policy_1.mayReadChecks)(grant, repo)) {
57
+ log(`refuse checks iss=${principal.iss} sub=${principal.sub} repo=${repo}`);
58
+ (0, http_1.send)(res, 403, { error: `you may not read the checks of ${repo}` });
59
+ return true;
60
+ }
61
+ const answers = await Promise.all(keys.map(async ({ task, key }) => {
62
+ const record = await checks.find(repo, key);
63
+ const yanked = record ? definitions.find(record.definition)?.yanked : undefined;
64
+ if (!record || yanked) {
65
+ return {
66
+ task,
67
+ key,
68
+ passed: null,
69
+ ...(yanked ? { reason: `it passed with ${record?.definition}, which was yanked since` } : {}),
70
+ };
71
+ }
72
+ const { repo: _repo, key: _key, accounts: _accounts, ...view } = record;
73
+ void _repo;
74
+ void _key;
75
+ void _accounts;
76
+ return { task, key, passed: view };
77
+ }));
78
+ log(`checks iss=${principal.iss} sub=${principal.sub} repo=${repo} passed=${answers.filter((a) => a.passed).length}/${answers.length}`);
79
+ (0, http_1.send)(res, 200, { checks: answers });
80
+ return true;
81
+ }
82
+ exports.handleChecks = handleChecks;
83
+ //# sourceMappingURL=checks-api.js.map
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CheckStore = exports.getCheckRecords = exports.isCheckRun = exports.CHECK_KEY_PATTERN = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const promises_1 = require("fs/promises");
6
+ const path_1 = require("path");
7
+ exports.CHECK_KEY_PATTERN = /^[0-9a-f]{64}$/;
8
+ // The runs whose results are checks: those of a commit the server checked out, and those
9
+ // on a copy of the files of a sandbox, which hold nothing a commit of them would not. A run
10
+ // with arguments ran something other than the task, and is no check of it.
11
+ function isCheckRun(run) {
12
+ // a run of services keeps them up, it passes nothing
13
+ if ((run.options.args ?? []).length > 0 || run.up) {
14
+ return false;
15
+ }
16
+ return run.source.type === 'ref' || (run.source.type === 'sandbox' && run.source.check === true);
17
+ }
18
+ exports.isCheckRun = isCheckRun;
19
+ // What a run that ended passed, with the keys its executor reported before the tasks ran.
20
+ function getCheckRecords(run, keys) {
21
+ if (!isCheckRun(run) || run.source.type === 'upload') {
22
+ return [];
23
+ }
24
+ const source = run.source.type === 'ref'
25
+ ? { type: 'ref', ref: run.source.ref, commit: run.source.commit }
26
+ : { type: 'sandbox', sandbox: run.source.id, commit: run.source.commit, tree: run.source.tree };
27
+ const records = [];
28
+ for (const [task, result] of Object.entries(run.results ?? {})) {
29
+ const key = keys[task];
30
+ if (!key || !exports.CHECK_KEY_PATTERN.test(key) || (result.status !== 'succeeded' && result.status !== 'cached')) {
31
+ continue;
32
+ }
33
+ records.push({
34
+ repo: run.source.repo,
35
+ key,
36
+ task,
37
+ status: result.status,
38
+ run: run.id,
39
+ by: { iss: run.owner.iss, sub: run.owner.sub },
40
+ source,
41
+ definition: run.definition,
42
+ accounts: [...run.accounts],
43
+ finishedAt: result.finishedAt ?? run.finishedAt ?? new Date().toISOString(),
44
+ ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
45
+ });
46
+ }
47
+ return records;
48
+ }
49
+ exports.getCheckRecords = getCheckRecords;
50
+ /**
51
+ * One file per repository and key, the newest run's: `<directory>/<repo hash>/<key>.json`,
52
+ * written to a temporary file and renamed. A record older than `keep` is not found and is
53
+ * removed by `sweep`.
54
+ */
55
+ class CheckStore {
56
+ constructor(directory, options) {
57
+ this.directory = directory;
58
+ this.options = options;
59
+ this.now = options.now ?? Date.now;
60
+ }
61
+ async save(records) {
62
+ for (const record of records) {
63
+ const folder = (0, path_1.join)(this.directory, repoFolder(record.repo));
64
+ await (0, promises_1.mkdir)(folder, { recursive: true, mode: 0o700 });
65
+ const file = (0, path_1.join)(folder, `${record.key}.json`);
66
+ const temporary = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
67
+ await (0, promises_1.writeFile)(temporary, JSON.stringify(record), { mode: 0o600 });
68
+ await (0, promises_1.rename)(temporary, file);
69
+ }
70
+ }
71
+ async find(repo, key) {
72
+ if (!exports.CHECK_KEY_PATTERN.test(key)) {
73
+ return undefined;
74
+ }
75
+ let record;
76
+ try {
77
+ record = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(this.directory, repoFolder(repo), `${key}.json`), 'utf8'));
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ if (record.repo !== repo || record.key !== key || this.expired(record)) {
83
+ return undefined;
84
+ }
85
+ return record;
86
+ }
87
+ // Removes the records that are older than `keep`.
88
+ async sweep() {
89
+ let folders;
90
+ try {
91
+ folders = await (0, promises_1.readdir)(this.directory);
92
+ }
93
+ catch {
94
+ return;
95
+ }
96
+ for (const folder of folders) {
97
+ const path = (0, path_1.join)(this.directory, folder);
98
+ for (const name of await (0, promises_1.readdir)(path).catch(() => [])) {
99
+ const file = (0, path_1.join)(path, name);
100
+ const stats = await (0, promises_1.stat)(file).catch(() => undefined);
101
+ if (stats && this.now() - stats.mtimeMs > this.options.keep) {
102
+ await (0, promises_1.rm)(file, { force: true });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ expired(record) {
108
+ const at = Date.parse(record.finishedAt);
109
+ return !Number.isFinite(at) || this.now() - at > this.options.keep;
110
+ }
111
+ }
112
+ exports.CheckStore = CheckStore;
113
+ function repoFolder(repo) {
114
+ return (0, crypto_1.createHash)('sha256').update(repo).digest('hex').slice(0, 32);
115
+ }
116
+ //# sourceMappingURL=checks.js.map
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
5
5
  const bundle_1 = require("../remote/bundle");
6
6
  const definitions_1 = require("./definitions");
7
7
  const http_1 = require("./http");
8
+ const run_plan_1 = require("./run-plan");
8
9
  const oauth_1 = require("./oauth");
9
10
  const policy_1 = require("./policy");
10
11
  const proposals_1 = require("./proposals");
@@ -19,7 +20,8 @@ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
19
20
  * `annotations`, which are not part of the hash
20
21
  * GET /v1/definitions the versions, newest first; `?<key>=<value>`
21
22
  * keeps the ones with those annotations
22
- * GET /v1/definitions/<hash> one version
23
+ * GET /v1/definitions/<hash> one version, with `arguments`: per task null when
24
+ * a run of it may pass arguments, else why not
23
25
  * GET /v1/definitions/<hash>/files its files
24
26
  * POST /v1/definitions/<hash>/yank pull it back (body: optional {reason})
25
27
  * POST /v1/definitions/<hash>/unyank let it go on
@@ -72,7 +74,7 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
72
74
  bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
73
75
  }
74
76
  else {
75
- (0, http_1.send)(res, 200, found);
77
+ (0, http_1.send)(res, 200, found.plan ? { ...found, arguments: (0, run_plan_1.describeArguments)(found.plan, config.runs) } : found);
76
78
  }
77
79
  return true;
78
80
  }