hammerkit 1.27.0-next.13 → 1.27.0-next.15

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.
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskToolArgs = exports.taskToolSchema = exports.createTaskTools = exports.describeTasks = exports.describeRun = exports.readLogHint = exports.readTaskListing = exports.LIST_TASKS_MS = void 0;
4
+ // the most tasks a description lists, and of a task's description or command it shows
5
+ const MAX_LISTED = 40;
6
+ const MAX_DESCRIPTION_CHARS = 200;
7
+ const MAX_COMMAND_CHARS = 100;
8
+ // how long a client waits for the tasks of the build file when it starts, it goes on without them
9
+ exports.LIST_TASKS_MS = 15000;
10
+ // The tasks of the published version, or why they could not be read within `ms`.
11
+ async function readTaskListing(list, ms = exports.LIST_TASKS_MS) {
12
+ let timer;
13
+ const timeout = new Promise((_, reject) => {
14
+ timer = setTimeout(() => reject(new Error(`no answer within ${ms / 1000} s`)), ms);
15
+ timer.unref?.();
16
+ });
17
+ try {
18
+ return await Promise.race([list(), timeout]);
19
+ }
20
+ catch (e) {
21
+ return { error: e instanceof Error ? e.message : String(e) };
22
+ }
23
+ finally {
24
+ clearTimeout(timer);
25
+ }
26
+ }
27
+ exports.readTaskListing = readTaskListing;
28
+ // the hint at the end of a cut output or a result, for the model
29
+ function readLogHint(run, task) {
30
+ return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
31
+ }
32
+ exports.readLogHint = readLogHint;
33
+ // A run of a task in a few lines: how it went, why it ran, its last lines.
34
+ function describeRun(task, ran) {
35
+ const result = ran.result;
36
+ const lines = [`run ${ran.run.id}: ${ran.run.state}`];
37
+ if (result) {
38
+ lines.push(`task ${task}: ${result.status}` +
39
+ (result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
40
+ (result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
41
+ (result.cache?.hit ? ', from the cache' : ''));
42
+ if (result.error) {
43
+ lines.push(`error: ${result.error}`);
44
+ }
45
+ if (result.cache?.causes?.length) {
46
+ lines.push(`ran because: ${result.cache.causes.join('; ')}`);
47
+ }
48
+ }
49
+ else if (ran.run.error) {
50
+ lines.push(`error: ${ran.run.error}`);
51
+ }
52
+ if (result && result.lastLog.length > 0) {
53
+ lines.push('last lines:', ...result.lastLog);
54
+ }
55
+ lines.push(readLogHint(ran.run.id, task));
56
+ return lines.join('\n');
57
+ }
58
+ exports.describeRun = describeRun;
59
+ /**
60
+ * The tasks for the model, one line each: what it is for (or its command, when the build
61
+ * file says nothing) and whether it takes arguments. `shell` is left out, the shell tool runs it.
62
+ */
63
+ function describeTasks(listing, shell) {
64
+ if (listing === undefined) {
65
+ return '';
66
+ }
67
+ if (!Array.isArray(listing)) {
68
+ return `The tasks of the build file could not be listed (${listing.error}); name one as the build file does.`;
69
+ }
70
+ const tasks = listing.filter((task) => !shell || task.name !== shell);
71
+ if (tasks.length === 0) {
72
+ return 'The published build file has no tasks to run.';
73
+ }
74
+ const lines = tasks.slice(0, MAX_LISTED).map((task) => {
75
+ const parts = [`- ${task.name}`];
76
+ if (task.description) {
77
+ parts.push(`: ${cut(oneLine(task.description), MAX_DESCRIPTION_CHARS)}`);
78
+ }
79
+ else if (task.command) {
80
+ parts.push(`: runs \`${cut(oneLine(task.command), MAX_COMMAND_CHARS)}\``);
81
+ }
82
+ if (task.args === null) {
83
+ parts.push(' (takes args)');
84
+ }
85
+ else if (task.args !== undefined || task.command === null) {
86
+ parts.push(' (takes no args)');
87
+ }
88
+ return parts.join('');
89
+ });
90
+ if (tasks.length > MAX_LISTED) {
91
+ lines.push(`- and ${tasks.length - MAX_LISTED} more`);
92
+ }
93
+ return ['The tasks of the published build file:', ...lines].join('\n');
94
+ }
95
+ exports.describeTasks = describeTasks;
96
+ function oneLine(text) {
97
+ return text.replace(/\s+/g, ' ').trim();
98
+ }
99
+ function cut(text, max) {
100
+ return text.length > max ? `${text.slice(0, max)}…` : text;
101
+ }
102
+ /**
103
+ * `run_task` and `run_logs`, with the tasks the description of `run_task` lists. Without a
104
+ * listing it names none, as the MCP tools before a version could be read.
105
+ */
106
+ function createTaskTools(listing, shell) {
107
+ const tasks = describeTasks(listing, shell);
108
+ return [
109
+ {
110
+ name: 'run_task',
111
+ title: 'Run a task',
112
+ description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
113
+ 'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
114
+ 'its last command, for example a test file. It runs in the container, with the network and the ' +
115
+ 'options the build file gives the task.' +
116
+ (tasks ? `\n\n${tasks}` : ''),
117
+ parameters: {
118
+ task: { type: 'string', description: 'The name of the task' },
119
+ args: { type: 'string[]', description: 'Arguments for its last command' },
120
+ },
121
+ required: ['task'],
122
+ async call(client, args, signal) {
123
+ const ran = await client.runTask(args.task, { args: args.args?.length ? args.args : undefined, signal });
124
+ return describeRun(args.task, ran);
125
+ },
126
+ },
127
+ {
128
+ name: 'run_logs',
129
+ title: 'Read the log of a run',
130
+ description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
131
+ 'the lines that contain a text (grep, a fixed string) with lines of context around them.',
132
+ parameters: {
133
+ run: { type: 'string', description: 'The id of the run' },
134
+ task: { type: 'string', description: 'Only the lines of this task' },
135
+ tail: {
136
+ type: 'integer',
137
+ description: 'The last lines, or with grep the last matches',
138
+ minimum: 1,
139
+ maximum: 1000,
140
+ },
141
+ from: { type: 'integer', description: 'The first line to read', minimum: 1 },
142
+ limit: { type: 'integer', description: 'How many lines to read', minimum: 1, maximum: 1000 },
143
+ grep: { type: 'string', description: 'Only lines that contain this text' },
144
+ ignore_case: { type: 'boolean', description: 'Ignore case in grep' },
145
+ context: { type: 'integer', description: 'Lines before and after each match', minimum: 0, maximum: 20 },
146
+ },
147
+ required: ['run'],
148
+ async call(client, args) {
149
+ const page = await client.logs(args.run, {
150
+ task: args.task || undefined,
151
+ tail: args.tail || undefined,
152
+ from: args.from || undefined,
153
+ limit: args.limit || undefined,
154
+ grep: args.grep || undefined,
155
+ i: args.ignore_case || undefined,
156
+ context: args.context || undefined,
157
+ });
158
+ const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
159
+ lines.push('', `(${page.total} lines${args.grep ? ` searched, ${page.lines.filter((line) => line.match !== false).length} match here` : ''}${page.next ? `, read on with from ${page.next}` : ''}${page.truncated ? ', the server cut the log off at its limit' : ''})`);
160
+ return lines.join('\n');
161
+ },
162
+ },
163
+ ];
164
+ }
165
+ exports.createTaskTools = createTaskTools;
166
+ // The parameters as a JSON schema, for MCP and opencode 2.
167
+ function taskToolSchema(tool) {
168
+ return {
169
+ type: 'object',
170
+ properties: Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
171
+ if (parameter.type === 'string[]') {
172
+ return [name, { type: 'array', items: { type: 'string' }, description: parameter.description }];
173
+ }
174
+ if (parameter.type === 'integer') {
175
+ const { maximum } = parameter;
176
+ return [
177
+ name,
178
+ {
179
+ type: 'integer',
180
+ description: parameter.description,
181
+ minimum: parameter.minimum,
182
+ ...(maximum !== undefined ? { maximum } : {}),
183
+ },
184
+ ];
185
+ }
186
+ return [name, { type: parameter.type, description: parameter.description }];
187
+ })),
188
+ required: tool.required,
189
+ };
190
+ }
191
+ exports.taskToolSchema = taskToolSchema;
192
+ // The parameters with the zod of opencode 1.
193
+ function taskToolArgs(tool, z) {
194
+ return Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
195
+ let type;
196
+ if (parameter.type === 'string[]') {
197
+ type = z.array(z.string());
198
+ }
199
+ else if (parameter.type === 'integer') {
200
+ type = z.number().int().min(parameter.minimum);
201
+ type = parameter.maximum !== undefined ? type.max(parameter.maximum) : type;
202
+ }
203
+ else {
204
+ type = parameter.type === 'boolean' ? z.boolean() : z.string();
205
+ }
206
+ return [name, (tool.required.includes(name) ? type : type.optional()).describe(parameter.description)];
207
+ }));
208
+ }
209
+ exports.taskToolArgs = taskToolArgs;
210
+ //# sourceMappingURL=task-tools.js.map
@@ -28,7 +28,9 @@ async function ensureKubernetesServiceExists(instance, env, service) {
28
28
  ...(headless
29
29
  ? {}
30
30
  : {
31
+ // a Service of more than one port needs a name for each
31
32
  ports: service.data.ports.map((port) => ({
33
+ name: `tcp-${port.hostPort ?? port.containerPort}`,
32
34
  port: port.hostPort ?? port.containerPort,
33
35
  targetPort: port.containerPort,
34
36
  })),
@@ -8,7 +8,10 @@ function parseWorkPort(port) {
8
8
  return { containerPort: port, hostPort: port };
9
9
  }
10
10
  else if (parts.length === 2) {
11
- return { containerPort: parsePort(parts[1], false), hostPort: parsePort(parts[0], true) };
11
+ // `0:80` is not published, as an empty host port: 0 (a free port) is for the endpoints
12
+ // of a server only
13
+ const hostPort = parsePort(parts[0], true);
14
+ return { containerPort: parsePort(parts[1], false), hostPort: hostPort === 0 ? null : hostPort };
12
15
  }
13
16
  else {
14
17
  throw new Error(`invalid port ${port}`);
@@ -13,6 +13,8 @@ const annotations_1 = require("./annotations");
13
13
  const server_client_1 = require("./server-client");
14
14
  const bundle_text_1 = require("./bundle-text");
15
15
  const sandbox_client_1 = require("../harness/sandbox-client");
16
+ const units_1 = require("../utils/units");
17
+ const service_tools_1 = require("../harness/service-tools");
16
18
  const path_1 = require("path");
17
19
  const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
18
20
  // what the publisher said about where a version comes from, one line
@@ -97,6 +99,21 @@ function describePlan(plan) {
97
99
  exports.describePlan = describePlan;
98
100
  function registerWorkCommands(remote, context) {
99
101
  const { environment, buildFile, getServer, guarded, passthrough, fail } = context;
102
+ // the ref a run starts from: what the options name, else the checkout here
103
+ const refSource = async (options) => {
104
+ const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
105
+ const repo = options.repo ?? checkout.repo;
106
+ const ref = options.ref ?? checkout.branch;
107
+ // the commit checked out here is the code meant, when the ref is the branch it is on
108
+ const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
109
+ if (!repo) {
110
+ throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
111
+ }
112
+ if (!ref) {
113
+ throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
114
+ }
115
+ return { type: 'ref', repo, ref, ...(commit ? { commit } : {}) };
116
+ };
100
117
  const connect = async (name) => {
101
118
  const chosen = await getServer(name);
102
119
  // a CI job or an agent brings its own token, a person signs in
@@ -147,17 +164,8 @@ function registerWorkCommands(remote, context) {
147
164
  if (!bundle_1.HASH_PATTERN.test(definition)) {
148
165
  throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
149
166
  }
150
- const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
151
- const repo = options.repo ?? checkout.repo;
152
- const ref = options.ref ?? checkout.branch;
153
- // the commit checked out here is the code meant, when the ref is the branch it is on
154
- const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
155
- if (!repo) {
156
- throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
157
- }
158
- if (!ref) {
159
- throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
160
- }
167
+ const source = await refSource(options);
168
+ const ref = source.ref;
161
169
  const { name: serverName, server } = await getServer(options.server);
162
170
  const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
163
171
  const runOptions = {};
@@ -174,7 +182,7 @@ function registerWorkCommands(remote, context) {
174
182
  submitted = await (0, sandbox_client_1.submitRun)(connection, {
175
183
  definition,
176
184
  tasks,
177
- source: { type: 'ref', repo, ref, ...(commit ? { commit } : {}) },
185
+ source,
178
186
  options: runOptions,
179
187
  });
180
188
  }
@@ -211,6 +219,84 @@ function registerWorkCommands(remote, context) {
211
219
  const exitCode = failed?.exitCode && failed.exitCode > 0 ? failed.exitCode : 1;
212
220
  fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, exitCode);
213
221
  }));
222
+ remote
223
+ .command('up <services...>')
224
+ .description('start services of a published build file on a server from a git ref and keep them up until their ttl ends, ' +
225
+ 'with their endpoints at addresses of their own behind the sign-in of the server')
226
+ .addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
227
+ .addOption(new commander_1.Option('--endpoint <service:port>', 'publish this port of a service, such as web:8080; may be given more than once').argParser(collectOption))
228
+ .addOption(new commander_1.Option('--ttl <duration>', 'how long they stay once ready, such as 2h, then they are removed; the server default when left out'))
229
+ .addOption(new commander_1.Option('--sleep-ttl <duration>', 'how long they stay up without a request, such as 30m, then they sleep until the next request wakes them; ' +
230
+ 'the server default when left out'))
231
+ .addOption(new commander_1.Option('--name <name>', 'a later run of yours with the same name replaces this one and keeps its addresses; the branch when left out'))
232
+ .addOption(new commander_1.Option('--build <hash>', 'the published version to run (sha256:...), or HAMMERKIT_BUILD; the hash of the build file here when left out'))
233
+ .addOption(new commander_1.Option('--repo <host/path>', 'the repository; origin of the checkout here when left out'))
234
+ .addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to start; the branch checked out here when left out'))
235
+ .addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at; the commit checked out here when --ref is left out'))
236
+ .addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
237
+ .action(guarded('Remote up was not successful', async (services, options) => {
238
+ const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
239
+ const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
240
+ if (!bundle_1.HASH_PATTERN.test(definition)) {
241
+ throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
242
+ }
243
+ const endpoints = (options.endpoint ?? []).map(service_tools_1.parseEndpoint);
244
+ const source = await refSource(options);
245
+ const { name: serverName, server } = await getServer(options.server);
246
+ const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
247
+ const submitted = await (0, sandbox_client_1.submitRun)(connection, {
248
+ definition,
249
+ services,
250
+ endpoints,
251
+ ...(options.ttl ? { ttl: options.ttl } : {}),
252
+ ...(options.sleepTtl ? { sleepTtl: options.sleepTtl } : {}),
253
+ ...(options.name ? { name: options.name } : {}),
254
+ source,
255
+ options: options.env ? { env: options.env } : {},
256
+ });
257
+ environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${services.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(source.ref)}${submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : ''}\n`);
258
+ const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
259
+ signal: environment.abortCtrl.signal,
260
+ untilUp: true,
261
+ onLine: (line) => environment.stdout.write(`[${(0, server_client_1.printable)(line.task)}] ${printableLine(line.text)}\n`),
262
+ onWaiting: (run) => {
263
+ for (const approval of run.approvals ?? []) {
264
+ environment.stdout.write(`waiting until somebody who uses ${(0, server_client_1.printable)(approval.account)} approves it, before ${(0, server_client_1.printable)(approval.expiresAt)}: hammerkit remote approve ${(0, server_client_1.printable)(run.id)}\n`);
265
+ }
266
+ },
267
+ });
268
+ const run = followed.run;
269
+ if (run.state !== 'up') {
270
+ fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, 1);
271
+ }
272
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} is up until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}${run.up?.sleepTtl ? `, asleep after ${(0, units_1.formatDuration)(run.up.sleepTtl)} without a request` : ''}\n`);
273
+ for (const endpoint of run.up?.endpoints ?? []) {
274
+ environment.stdout.write(` ${(0, server_client_1.printable)(endpoint.service)}:${endpoint.port} ${(0, server_client_1.printable)(endpoint.url)}\n`);
275
+ }
276
+ environment.stdout.write(`extend it with: hammerkit remote extend ${(0, server_client_1.printable)(run.id)} 8h, stop it with: hammerkit remote stop ${(0, server_client_1.printable)(run.id)}\n`);
277
+ }));
278
+ remote
279
+ .command('extend <run> <ttl> [server]')
280
+ .description('keep a service run that is up for this long from now, such as 8h, at most the maxTtl of the server')
281
+ .action(guarded('Remote extend was not successful', async (id, ttl, name) => {
282
+ if (!RUN_ID.test(id)) {
283
+ throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...)`);
284
+ }
285
+ const { name: serverName, server, token } = await connect(name);
286
+ const run = (await (0, server_client_1.callServer)(server, token, 'POST', `/v1/runs/${id}/extend`, { ttl })).body;
287
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} on ${serverName} stays up until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}\n`);
288
+ }));
289
+ remote
290
+ .command('stop <run> [server]')
291
+ .description('stop a run: a service run takes its services and endpoints down')
292
+ .action(guarded('Remote stop was not successful', async (id, name) => {
293
+ if (!RUN_ID.test(id)) {
294
+ throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...)`);
295
+ }
296
+ const { name: serverName, server, token } = await connect(name);
297
+ const run = (await (0, server_client_1.callServer)(server, token, 'POST', `/v1/runs/${id}/cancel`)).body;
298
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} on ${serverName}: ${run.state === 'up' || run.state === 'running' ? 'stopping' : (0, server_client_1.printable)(run.state)}\n`);
299
+ }));
214
300
  remote
215
301
  .command('versions [server]')
216
302
  .description('list the published versions of build files on a server, newest first, with where they come from')
@@ -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
@@ -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) {
@@ -32,6 +32,7 @@ async function handleAccounts(config, log, principal, req, res, url) {
32
32
  repos: grant.repos,
33
33
  builds: grant.builds,
34
34
  sandboxes: grant.sandboxes,
35
+ endpoints: grant.endpoints,
35
36
  });
36
37
  return true;
37
38
  }
@@ -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,
@@ -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
  }