stitchkit 0.91.2 → 0.93.0

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 (86) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/dist/agent-runtime-coding-tools.js +3 -3
  3. package/dist/agent-runtime-harness.js +5 -5
  4. package/dist/agent-runtime-sandbox.js +3 -3
  5. package/dist/agent-runtime.js +8 -8
  6. package/dist/cli.d.ts +6 -3
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +377 -49
  9. package/dist/contract/define.d.ts +84 -2
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/index.d.ts +1 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/{index-a2nzz56k.js → index-2c2bjw3s.js} +3 -3
  14. package/dist/{index-seyy8dvn.js → index-3bmnp41g.js} +245 -116
  15. package/dist/{index-axvavfr4.js → index-bx75zwt7.js} +3 -3
  16. package/dist/{index-709nh7z7.js → index-cxqhvq21.js} +2 -1
  17. package/dist/{index-pena034b.js → index-e42wzzz5.js} +3 -3
  18. package/dist/{index-qjeyx5t2.js → index-fq77yfzc.js} +1 -1
  19. package/dist/{index-fenaekmk.js → index-h9etkkyk.js} +37 -27
  20. package/dist/{index-4x809vqd.js → index-mzvgxp0n.js} +2 -2
  21. package/dist/{index-jpfqfg5c.js → index-n0f51dfn.js} +2 -2
  22. package/dist/{index-bv7mytab.js → index-sr93cwf9.js} +1 -1
  23. package/dist/{index-vsbzgd7b.js → index-tmwcrptx.js} +1 -0
  24. package/dist/{index-8z9we758.js → index-vjpvjqpn.js} +1 -1
  25. package/dist/{index-7hz88dy8.js → index-vk3kq9sb.js} +1 -1
  26. package/dist/{index-s3dssqxp.js → index-yn8remb1.js} +60 -56
  27. package/dist/internal/atomic-file.d.ts +21 -0
  28. package/dist/internal/atomic-file.d.ts.map +1 -0
  29. package/dist/internal/secure-fetch.d.ts +12 -0
  30. package/dist/internal/secure-fetch.d.ts.map +1 -1
  31. package/dist/node.js +3 -3
  32. package/dist/observability/audit.d.ts +5 -1
  33. package/dist/observability/audit.d.ts.map +1 -1
  34. package/dist/observability/changes.d.ts.map +1 -1
  35. package/dist/observability/context.d.ts +27 -6
  36. package/dist/observability/context.d.ts.map +1 -1
  37. package/dist/observability/event.d.ts +34 -6
  38. package/dist/observability/event.d.ts.map +1 -1
  39. package/dist/observability/index.d.ts +2 -1
  40. package/dist/observability/index.d.ts.map +1 -1
  41. package/dist/observability/index.js +50 -6
  42. package/dist/observability/work.d.ts +55 -0
  43. package/dist/observability/work.d.ts.map +1 -0
  44. package/dist/server/context-contribution.d.ts.map +1 -1
  45. package/dist/server/index.js +4 -4
  46. package/dist/server/types.d.ts +20 -6
  47. package/dist/server/types.d.ts.map +1 -1
  48. package/dist/testing/surface-manifest.d.ts +34 -1
  49. package/dist/testing/surface-manifest.d.ts.map +1 -1
  50. package/dist/testing.d.ts +1 -1
  51. package/dist/testing.d.ts.map +1 -1
  52. package/dist/testing.js +31 -9
  53. package/dist/tool-invoker.js +5 -5
  54. package/dist/tools/cli-checkpoint.d.ts +24 -0
  55. package/dist/tools/cli-checkpoint.d.ts.map +1 -0
  56. package/dist/tools/cli-command.d.ts +18 -0
  57. package/dist/tools/cli-command.d.ts.map +1 -1
  58. package/dist/tools/cli-format.d.ts +10 -0
  59. package/dist/tools/cli-format.d.ts.map +1 -1
  60. package/dist/tools/cli-invoke.d.ts +143 -0
  61. package/dist/tools/cli-invoke.d.ts.map +1 -0
  62. package/dist/tools/cli-manifest.d.ts +6 -9
  63. package/dist/tools/cli-manifest.d.ts.map +1 -1
  64. package/dist/tools/cli-signature.d.ts +81 -0
  65. package/dist/tools/cli-signature.d.ts.map +1 -0
  66. package/dist/tools/cli-stream.d.ts +93 -0
  67. package/dist/tools/cli-stream.d.ts.map +1 -0
  68. package/dist/tools/cli-update.d.ts +62 -0
  69. package/dist/tools/cli-update.d.ts.map +1 -1
  70. package/dist/tools/cli.d.ts +4 -16
  71. package/dist/tools/cli.d.ts.map +1 -1
  72. package/dist/tools/connections/index.js +2 -2
  73. package/dist/tools/execute.d.ts +21 -0
  74. package/dist/tools/execute.d.ts.map +1 -1
  75. package/dist/tools/mcp-progress.d.ts +46 -0
  76. package/dist/tools/mcp-progress.d.ts.map +1 -0
  77. package/dist/tools/mcp-round-policy.d.ts +25 -1
  78. package/dist/tools/mcp-round-policy.d.ts.map +1 -1
  79. package/dist/tools/mcp-round.d.ts +20 -1
  80. package/dist/tools/mcp-round.d.ts.map +1 -1
  81. package/dist/tools/mcp-trace.d.ts +9 -1
  82. package/dist/tools/mcp-trace.d.ts.map +1 -1
  83. package/dist/tools/mount-wait.d.ts.map +1 -1
  84. package/dist/tools.js +159 -50
  85. package/llms-full.txt +449 -8
  86. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import {
2
2
  CliArgumentError,
3
3
  DEFAULT_EXIT_CODES,
4
+ PrivateAddressRefusal,
5
+ cliExitCode,
6
+ cliInvocationResult,
4
7
  createCli,
8
+ createCliInvoker,
5
9
  defineCliCommand,
6
10
  emitResult,
7
11
  extractCliGlobalOptions,
@@ -11,12 +15,18 @@ import {
11
15
  readCapped,
12
16
  renderCliView,
13
17
  routeCliArgv
14
- } from "./index-seyy8dvn.js";
18
+ } from "./index-3bmnp41g.js";
19
+ import {
20
+ serializeCanonicalJson
21
+ } from "./index-0j3vcp5m.js";
15
22
  import"./index-sbdmyz75.js";
16
- import"./index-a2nzz56k.js";
17
- import"./index-4x809vqd.js";
18
- import"./index-s3dssqxp.js";
19
- import"./index-vsbzgd7b.js";
23
+ import"./index-2c2bjw3s.js";
24
+ import {
25
+ argumentsDigest
26
+ } from "./index-a916km3r.js";
27
+ import"./index-mzvgxp0n.js";
28
+ import"./index-yn8remb1.js";
29
+ import"./index-tmwcrptx.js";
20
30
  import"./index-f9mb610r.js";
21
31
  import"./index-e0znv5gn.js";
22
32
  import"./index-0a349c33.js";
@@ -26,8 +36,8 @@ import"./index-zcgf3gqf.js";
26
36
  import {
27
37
  coerceJsonArgs
28
38
  } from "./index-7zbps32p.js";
29
- import"./index-8z9we758.js";
30
- import"./index-fenaekmk.js";
39
+ import"./index-vjpvjqpn.js";
40
+ import"./index-h9etkkyk.js";
31
41
  import"./index-cby4ar3v.js";
32
42
  import {
33
43
  AppError
@@ -35,7 +45,61 @@ import {
35
45
  import {
36
46
  safeJsonParse
37
47
  } from "./index-kzfs85xp.js";
38
- import"./index-77fekveh.js";
48
+ import {
49
+ isRecord
50
+ } from "./index-77fekveh.js";
51
+ // src/tools/cli-checkpoint.ts
52
+ import { existsSync, readFileSync } from "node:fs";
53
+
54
+ // src/internal/atomic-file.ts
55
+ import { randomBytes } from "node:crypto";
56
+ import { closeSync, fchmodSync, fsyncSync, openSync, renameSync, writeSync } from "node:fs";
57
+ import { basename, dirname, join } from "node:path";
58
+ function writeFileAtomic(target, bytes, mode) {
59
+ const staged = join(dirname(target), `.${basename(target)}.${randomBytes(12).toString("hex")}.tmp`);
60
+ const handle = openSync(staged, "wx", mode);
61
+ try {
62
+ writeSync(handle, bytes);
63
+ fchmodSync(handle, mode);
64
+ fsyncSync(handle);
65
+ } finally {
66
+ closeSync(handle);
67
+ }
68
+ renameSync(staged, target);
69
+ }
70
+
71
+ // src/tools/cli-checkpoint.ts
72
+ function emptyCheckpoint(entries) {
73
+ return {
74
+ entries,
75
+ digestOf: (line) => argumentsDigest({ command: line.command, args: line.args }),
76
+ record(id, digest, answer) {
77
+ entries[id] = { digest, answer };
78
+ }
79
+ };
80
+ }
81
+ function readCliCheckpoint(path) {
82
+ if (!existsSync(path))
83
+ return emptyCheckpoint({});
84
+ try {
85
+ const parsed = safeJsonParse(readFileSync(path, "utf8"));
86
+ const entries = {};
87
+ if (isRecord(parsed) && isRecord(parsed.entries)) {
88
+ for (const [id, value] of Object.entries(parsed.entries)) {
89
+ if (isRecord(value) && typeof value.digest === "string") {
90
+ entries[id] = { digest: value.digest, answer: value.answer };
91
+ }
92
+ }
93
+ }
94
+ return emptyCheckpoint(entries);
95
+ } catch {
96
+ return emptyCheckpoint({});
97
+ }
98
+ }
99
+ function writeCliCheckpoint(path, checkpoint) {
100
+ writeFileAtomic(path, Buffer.from(`${JSON.stringify({ entries: checkpoint.entries }, null, 2)}
101
+ `, "utf8"), 384);
102
+ }
39
103
  // src/tools/cli-installer.ts
40
104
  function shellQuote(value) {
41
105
  return `'${value.replaceAll("'", `'"'"'`)}'`;
@@ -155,28 +219,98 @@ ${cases}
155
219
  esac`;
156
220
  }
157
221
  // src/tools/cli-manifest.ts
222
+ import { z as z2 } from "zod";
223
+
224
+ // src/tools/cli-signature.ts
225
+ import {
226
+ createPrivateKey,
227
+ createPublicKey,
228
+ sign as cryptoSign,
229
+ verify as cryptoVerify
230
+ } from "node:crypto";
158
231
  import { z } from "zod";
159
- var CliBuildTargetSchema = z.object({
160
- platform: z.string().min(1),
161
- arch: z.string().min(1)
232
+ var CliBuildSignatureSchema = z.object({
233
+ algorithm: z.literal("ed25519"),
234
+ keyId: z.string().min(1),
235
+ signature: z.string().min(1)
236
+ });
237
+ function cliManifestSigningPayload(manifest) {
238
+ return serializeCanonicalJson({
239
+ name: manifest.name,
240
+ version: manifest.version,
241
+ commit: manifest.commit,
242
+ builtAt: manifest.builtAt,
243
+ assets: manifest.assets.map((asset) => ({
244
+ platform: asset.platform,
245
+ arch: asset.arch,
246
+ compression: asset.compression,
247
+ size: asset.size,
248
+ sha256: asset.sha256
249
+ }))
250
+ });
251
+ }
252
+ function publicKeyOf(material) {
253
+ const trimmed = material.trim();
254
+ if (trimmed.includes("BEGIN PUBLIC KEY"))
255
+ return createPublicKey(trimmed);
256
+ const raw = Buffer.from(trimmed, "base64");
257
+ if (raw.length !== 32) {
258
+ throw new Error("[stitchkit] an Ed25519 public key is 32 bytes or SPKI PEM");
259
+ }
260
+ const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
261
+ return createPublicKey({ key: spki, format: "der", type: "spki" });
262
+ }
263
+ function signCliManifest(manifest, options) {
264
+ const signature = cryptoSign(null, Buffer.from(cliManifestSigningPayload(manifest), "utf8"), createPrivateKey(typeof options.privateKey === "string" ? options.privateKey : { key: Buffer.from(options.privateKey), format: "der", type: "pkcs8" }));
265
+ return {
266
+ algorithm: "ed25519",
267
+ keyId: options.keyId,
268
+ signature: signature.toString("base64")
269
+ };
270
+ }
271
+ function verifyCliManifest(manifest, signature, trust) {
272
+ const keys = trust?.keys;
273
+ if (!keys || Object.keys(keys).length === 0)
274
+ return "unenforced";
275
+ if (!signature)
276
+ return "missing";
277
+ const material = Object.hasOwn(keys, signature.keyId) ? keys[signature.keyId] : undefined;
278
+ if (material === undefined)
279
+ return "unknown-key";
280
+ try {
281
+ const ok = cryptoVerify(null, Buffer.from(cliManifestSigningPayload(manifest), "utf8"), publicKeyOf(material), Buffer.from(signature.signature, "base64"));
282
+ return ok ? "valid" : "invalid";
283
+ } catch {
284
+ return "invalid";
285
+ }
286
+ }
287
+ function cliSignatureAccepted(verdict) {
288
+ return verdict === "valid" || verdict === "unenforced";
289
+ }
290
+
291
+ // src/tools/cli-manifest.ts
292
+ var CliBuildTargetSchema = z2.object({
293
+ platform: z2.string().min(1),
294
+ arch: z2.string().min(1)
162
295
  });
163
296
  var CliBuildAssetSchema = CliBuildTargetSchema.extend({
164
- url: z.url(),
165
- compression: z.enum(["gzip", "none"]).default("none"),
166
- size: z.int().positive(),
167
- sha256: z.string().regex(/^[0-9a-f]{64}$/, "sha256 must be 64 lowercase hex characters")
297
+ url: z2.url(),
298
+ compression: z2.enum(["gzip", "none"]).default("none"),
299
+ size: z2.int().positive(),
300
+ sha256: z2.string().regex(/^[0-9a-f]{64}$/, "sha256 must be 64 lowercase hex characters")
168
301
  });
169
- var CliBuildManifestSchema = z.object({
170
- name: z.string().min(1),
171
- version: z.string().min(1),
172
- commit: z.string().min(1),
173
- builtAt: z.iso.datetime(),
174
- assets: z.array(CliBuildAssetSchema)
302
+ var CliBuildManifestSchema = z2.object({
303
+ name: z2.string().min(1),
304
+ version: z2.string().min(1),
305
+ commit: z2.string().min(1),
306
+ builtAt: z2.iso.datetime(),
307
+ assets: z2.array(CliBuildAssetSchema),
308
+ signature: CliBuildSignatureSchema.optional()
175
309
  });
176
- var CliBuildStampSchema = z.object({
177
- version: z.string().min(1),
178
- commit: z.string().min(1),
179
- builtAt: z.iso.datetime()
310
+ var CliBuildStampSchema = z2.object({
311
+ version: z2.string().min(1),
312
+ commit: z2.string().min(1),
313
+ builtAt: z2.iso.datetime()
180
314
  });
181
315
  function formatCliBuildStamp(stamp, name) {
182
316
  const prefix = name ? `${name} ` : "";
@@ -188,26 +322,30 @@ function currentCliBuildTarget() {
188
322
  function selectCliBuildAsset(manifest, target = currentCliBuildTarget()) {
189
323
  return manifest.assets.find((asset) => asset.platform === target.platform && asset.arch === target.arch);
190
324
  }
325
+ function isManifestList(value) {
326
+ return Array.isArray(value);
327
+ }
191
328
  function assertCliPublishable(previous, next) {
192
- if (!previous)
193
- return;
194
- if (previous.version !== next.version)
195
- return;
196
- if (previous.commit === next.commit)
197
- return;
198
- throw new Error(`[stitchkit] ${next.name} ${next.version} was already published from commit ${previous.commit}; ` + `refusing to republish it from ${next.commit} — bump the version instead`);
329
+ const published = previous === undefined ? [] : isManifestList(previous) ? previous : [previous];
330
+ for (const candidate of published) {
331
+ if (candidate.version !== next.version)
332
+ continue;
333
+ if (candidate.commit === next.commit)
334
+ continue;
335
+ throw new Error(`[stitchkit] ${next.name} ${next.version} was already published from commit ${candidate.commit}; ` + `refusing to republish it from ${next.commit} — bump the version instead`);
336
+ }
199
337
  }
200
338
  // src/tools/cli-profile.ts
201
339
  import {
202
340
  chmodSync,
203
- existsSync,
341
+ existsSync as existsSync2,
204
342
  mkdirSync,
205
343
  readdirSync,
206
- readFileSync,
344
+ readFileSync as readFileSync2,
207
345
  statSync,
208
346
  writeFileSync
209
347
  } from "node:fs";
210
- import { join } from "node:path";
348
+ import { join as join2 } from "node:path";
211
349
  var PROFILE_SUFFIX = ".json";
212
350
  var FILE_MODE = 384;
213
351
  var DIRECTORY_MODE = 448;
@@ -228,23 +366,23 @@ function createCliProfileStore(config) {
228
366
  `));
229
367
  const pathFor = (name) => {
230
368
  assertProfileName(name);
231
- return join(config.directory, `${name}${PROFILE_SUFFIX}`);
369
+ return join2(config.directory, `${name}${PROFILE_SUFFIX}`);
232
370
  };
233
371
  const list = () => {
234
- if (!existsSync(config.directory))
372
+ if (!existsSync2(config.directory))
235
373
  return [];
236
374
  return readdirSync(config.directory).filter((entry) => entry.endsWith(PROFILE_SUFFIX)).map((entry) => entry.slice(0, -PROFILE_SUFFIX.length)).sort();
237
375
  };
238
376
  const read = (name) => {
239
377
  const path = pathFor(name);
240
- if (!existsSync(path)) {
378
+ if (!existsSync2(path)) {
241
379
  throw new CliProfileError(missingMessage(name, path, config));
242
380
  }
243
381
  const mode = statSync(path).mode & 63;
244
382
  if (mode !== 0) {
245
383
  throw new CliProfileError(`profile "${name}" is readable by other users — run: chmod 600 ${path}`, "FORBIDDEN");
246
384
  }
247
- const parsed = safeJsonParse(readFileSync(path, "utf8"));
385
+ const parsed = safeJsonParse(readFileSync2(path, "utf8"));
248
386
  const result = config.schema.safeParse(parsed);
249
387
  if (!result.success) {
250
388
  const issue = result.error.issues[0];
@@ -279,7 +417,7 @@ function createCliProfileStore(config) {
279
417
  return { name: only, path, value: read(only), substituted: true };
280
418
  }
281
419
  if (configured.length === 0) {
282
- const example = join(config.directory, `<name>${PROFILE_SUFFIX}`);
420
+ const example = join2(config.directory, `<name>${PROFILE_SUFFIX}`);
283
421
  throw new CliProfileError(`no profile is configured in ${config.directory}${config.createHint ? ` — ${config.createHint("<name>", example)}` : ""}`);
284
422
  }
285
423
  throw new CliProfileError(`several profiles are configured (${configured.join(", ")}) — name one`, "BAD_REQUEST");
@@ -290,10 +428,140 @@ function missingMessage(name, path, config) {
290
428
  const hint = config.createHint ? ` — ${config.createHint(name, path)}` : "";
291
429
  return `profile "${name}" is not configured; expected ${path}${hint}`;
292
430
  }
431
+ // src/tools/cli-stream.ts
432
+ import { z as z3 } from "zod";
433
+ var LineSchema = z3.object({
434
+ id: z3.string().min(1),
435
+ command: z3.string().min(1),
436
+ args: z3.record(z3.string(), z3.unknown()).default({})
437
+ });
438
+ async function* readStdinLines(source = process.stdin) {
439
+ const decoder = new TextDecoder;
440
+ let pending = "";
441
+ for await (const chunk of source) {
442
+ pending += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
443
+ let newline = pending.indexOf(`
444
+ `);
445
+ while (newline !== -1) {
446
+ const line = pending.slice(0, newline);
447
+ pending = pending.slice(newline + 1);
448
+ yield line.endsWith("\r") ? line.slice(0, -1) : line;
449
+ newline = pending.indexOf(`
450
+ `);
451
+ }
452
+ }
453
+ if (pending !== "")
454
+ yield pending.endsWith("\r") ? pending.slice(0, -1) : pending;
455
+ }
456
+ function parseLine(line) {
457
+ let parsed;
458
+ try {
459
+ parsed = safeJsonParse(line);
460
+ } catch {
461
+ return { ok: false, answer: malformed("the line is not JSON") };
462
+ }
463
+ const result = LineSchema.safeParse(parsed);
464
+ if (!result.success) {
465
+ const id = isRecord(parsed) && typeof parsed.id === "string" ? parsed.id : undefined;
466
+ return {
467
+ ok: false,
468
+ answer: {
469
+ ...malformed("the line needs `id` and `command`"),
470
+ ...id !== undefined && { id }
471
+ }
472
+ };
473
+ }
474
+ return { ok: true, value: result.data };
475
+ }
476
+ function malformed(message) {
477
+ return {
478
+ id: "",
479
+ ok: false,
480
+ exitCode: 2,
481
+ error: { code: "BAD_REQUEST", message }
482
+ };
483
+ }
484
+ async function runCliStream(invoker, lines, onAnswer, hooks = {}) {
485
+ for await (const line of lines) {
486
+ if (line.trim() === "")
487
+ continue;
488
+ const parsed = parseLine(line);
489
+ if (!parsed.ok) {
490
+ onAnswer(parsed.answer);
491
+ continue;
492
+ }
493
+ const { id, command, args } = parsed.value;
494
+ const answered = hooks.before?.(id, { command, args });
495
+ if (answered) {
496
+ onAnswer(answered);
497
+ continue;
498
+ }
499
+ const outcome = await invoker.invoke(command, args);
500
+ hooks.after?.(id, { command, args }, outcome);
501
+ onAnswer({ id, ...outcome });
502
+ }
503
+ }
504
+ function defineCliStreamCommand(config) {
505
+ return defineCliCommand({
506
+ name: config.name ?? "stream",
507
+ description: config.description ?? "Run one operation per JSON line of stdin, answering each",
508
+ input: z3.object({}),
509
+ handler: async (context) => {
510
+ const invoker = typeof config.invoker === "function" ? await config.invoker() : config.invoker;
511
+ await runCliStream(invoker, (config.readLines ?? readStdinLines)(), (answer) => {
512
+ context.stdout(`${JSON.stringify(answer)}
513
+ `);
514
+ });
515
+ }
516
+ });
517
+ }
518
+ function defineCliBatchCommand(config) {
519
+ return defineCliCommand({
520
+ name: config.name ?? "batch",
521
+ description: config.description ?? "Run one operation per JSON line of stdin, resuming from a checkpoint",
522
+ input: z3.object({
523
+ checkpoint: z3.string().optional()
524
+ }),
525
+ handler: async (context) => {
526
+ const invoker = typeof config.invoker === "function" ? await config.invoker() : config.invoker;
527
+ const path = context.input.checkpoint ?? config.checkpointPath ?? ".stitchkit-batch.json";
528
+ const checkpoint = readCliCheckpoint(path);
529
+ await runCliStream(invoker, (config.readLines ?? readStdinLines)(), (answer) => {
530
+ context.stdout(`${JSON.stringify(answer)}
531
+ `);
532
+ }, {
533
+ before: (id, line) => {
534
+ const digest = checkpoint.digestOf(line);
535
+ const recorded = checkpoint.entries[id];
536
+ if (!recorded)
537
+ return;
538
+ if (recorded.digest !== digest) {
539
+ return {
540
+ id,
541
+ ok: false,
542
+ exitCode: 2,
543
+ error: {
544
+ code: "CONFLICT",
545
+ message: "this id was already run with different content — change the id or start a new checkpoint"
546
+ }
547
+ };
548
+ }
549
+ const replay = isRecord(recorded.answer) ? recorded.answer : {};
550
+ return { ...replay, id, replayed: true };
551
+ },
552
+ after: (id, line, outcome) => {
553
+ if (!outcome.ok)
554
+ return;
555
+ checkpoint.record(id, checkpoint.digestOf(line), outcome);
556
+ writeCliCheckpoint(path, checkpoint);
557
+ }
558
+ });
559
+ }
560
+ });
561
+ }
293
562
  // src/tools/cli-update.ts
294
563
  import { createHash } from "node:crypto";
295
- import { chmodSync as chmodSync2, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
296
- import { basename, dirname, join as join2 } from "node:path";
564
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
297
565
  import { gunzipSync } from "node:zlib";
298
566
  var DEFAULT_CHECK_TIMEOUT_MS = 2000;
299
567
  var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -354,16 +622,27 @@ async function checkCliUpdate(config) {
354
622
  reason: `cannot compare ${config.currentVersion} with ${manifest.version}`
355
623
  };
356
624
  }
625
+ const verdict = verifyCliManifest(manifest, manifest.signature, config.trust);
357
626
  if (order >= 0)
358
- return { status: "current", version: manifest.version };
627
+ return { status: "current", version: manifest.version, signature: verdict };
628
+ if (!cliSignatureAccepted(verdict)) {
629
+ return { status: "unknown", reason: `manifest signature: ${verdict}` };
630
+ }
359
631
  const asset = selectCliBuildAsset(manifest, config.target ?? currentCliBuildTarget());
360
632
  return {
361
633
  status: "outdated",
362
634
  version: manifest.version,
363
635
  manifest,
364
- ...asset && { asset }
636
+ ...asset && { asset },
637
+ signature: verdict
365
638
  };
366
639
  } catch (error) {
640
+ if (error instanceof PrivateAddressRefusal) {
641
+ return {
642
+ status: "unknown",
643
+ reason: `${error.message} — the manifest endpoint is not public. Set ` + "`allowPrivateHosts` if it is your own deployment; the download keeps " + "its own setting, because an asset URL comes from the document."
644
+ };
645
+ }
367
646
  return {
368
647
  status: "unknown",
369
648
  reason: error instanceof Error ? error.message : String(error)
@@ -372,6 +651,18 @@ async function checkCliUpdate(config) {
372
651
  }
373
652
  async function applyCliUpdate(config) {
374
653
  const target = config.targetPath ?? process.execPath;
654
+ if (config.trust) {
655
+ if (!config.manifest) {
656
+ throw new Error("[stitchkit] update: a trust root needs the manifest the asset came from");
657
+ }
658
+ const verdict = verifyCliManifest(config.manifest, config.manifest.signature, config.trust);
659
+ if (!cliSignatureAccepted(verdict)) {
660
+ throw new Error(`[stitchkit] update: refusing to install — manifest signature ${verdict}`);
661
+ }
662
+ if (!config.manifest.assets.some((asset) => asset.sha256 === config.asset.sha256)) {
663
+ throw new Error("[stitchkit] update: the asset is not one the signed manifest names");
664
+ }
665
+ }
375
666
  const response = await fetchGuarded(new URL(config.asset.url), config.allowPrivateHosts ?? false, {
376
667
  timeoutMs: config.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS,
377
668
  bodyTimeoutMs: config.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS
@@ -382,7 +673,7 @@ async function applyCliUpdate(config) {
382
673
  const transferred = await readCapped(response, config.maxBytes ?? DEFAULT_MAX_ASSET_BYTES);
383
674
  if (!transferred)
384
675
  throw new Error("[stitchkit] update download exceeded the size ceiling");
385
- const bytes = config.asset.compression === "gzip" ? gunzipSync(transferred) : transferred;
676
+ const bytes = config.asset.compression === "gzip" ? gunzipSync(transferred, { maxOutputLength: config.asset.size }) : transferred;
386
677
  if (bytes.length !== config.asset.size) {
387
678
  throw new Error(`[stitchkit] update is ${bytes.length} bytes, manifest says ${config.asset.size} — refusing to install it`);
388
679
  }
@@ -390,28 +681,64 @@ async function applyCliUpdate(config) {
390
681
  if (sha256 !== config.asset.sha256) {
391
682
  throw new Error(`[stitchkit] update digest ${sha256} does not match the manifest — refusing to install it`);
392
683
  }
393
- const staged = join2(dirname(target), `.${basename(target)}.${process.pid}.${Date.now()}`);
394
- writeFileSync2(staged, bytes, { mode: 493 });
395
- chmodSync2(staged, 493);
396
- renameSync(staged, target);
684
+ const backup = config.backupPath ? keepReplacedBinary(target, config.backupPath) : undefined;
685
+ writeFileAtomic(target, bytes, 493);
686
+ return {
687
+ path: target,
688
+ bytes: bytes.length,
689
+ sha256,
690
+ ...backup && { backupPath: backup.path, backupSha256: backup.sha256 }
691
+ };
692
+ }
693
+ function keepReplacedBinary(target, backupPath) {
694
+ if (!existsSync3(target))
695
+ return;
696
+ const previous = readFileSync3(target);
697
+ writeFileAtomic(backupPath, previous, statSync2(target).mode & 511);
698
+ return { path: backupPath, sha256: createHash("sha256").update(previous).digest("hex") };
699
+ }
700
+ function rollbackCliUpdate(config) {
701
+ const target = config.targetPath ?? process.execPath;
702
+ if (!existsSync3(config.backupPath)) {
703
+ throw new Error(`[stitchkit] rollback: no backup at ${config.backupPath}`);
704
+ }
705
+ const bytes = readFileSync3(config.backupPath);
706
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
707
+ if (sha256 !== config.expectedSha256) {
708
+ throw new Error(`[stitchkit] rollback: backup digest ${sha256} does not match the expected ${config.expectedSha256} — refusing to restore it`);
709
+ }
710
+ writeFileAtomic(target, bytes, statSync2(config.backupPath).mode & 511);
397
711
  return { path: target, bytes: bytes.length, sha256 };
398
712
  }
399
713
  export {
714
+ writeCliCheckpoint,
715
+ verifyCliManifest,
716
+ signCliManifest,
400
717
  selectCliBuildAsset,
718
+ runCliStream,
401
719
  routeCliArgv,
720
+ rollbackCliUpdate,
402
721
  renderCliView,
403
722
  renderCliInstaller,
723
+ readCliCheckpoint,
404
724
  pollUntilDone,
405
725
  parseCliArgs,
406
726
  formatCliBuildStamp,
407
727
  extractCliGlobalOptions,
408
728
  emitResult,
729
+ defineCliStreamCommand,
409
730
  defineCliCommand,
731
+ defineCliBatchCommand,
410
732
  currentCliBuildTarget,
411
733
  createCliProfileStore,
734
+ createCliInvoker,
412
735
  createCli,
413
736
  compareCliVersions,
414
737
  coerceJsonArgs,
738
+ cliSignatureAccepted,
739
+ cliManifestSigningPayload,
740
+ cliInvocationResult,
741
+ cliExitCode,
415
742
  checkCliUpdate,
416
743
  assertCliPublishable,
417
744
  applyCliUpdate,
@@ -419,6 +746,7 @@ export {
419
746
  CliProfileError,
420
747
  CliBuildTargetSchema,
421
748
  CliBuildStampSchema,
749
+ CliBuildSignatureSchema,
422
750
  CliBuildManifestSchema,
423
751
  CliBuildAssetSchema,
424
752
  CliArgumentError
@@ -183,10 +183,42 @@ export interface EndpointMcpInputRequired<TKey extends string = string, TSchema
183
183
  /** Flat primitive object schema accepted from the host. */
184
184
  schema: TSchema;
185
185
  }
186
+ /**
187
+ * What the tool call looks like when the questions are chosen — the same
188
+ * `params` / `input` the handler would receive, parsed by the contract's own
189
+ * schemas.
190
+ */
191
+ export interface McpInputRequiredCall {
192
+ params: unknown;
193
+ input: unknown;
194
+ }
195
+ /**
196
+ * Choose this call's elicitation rounds from its arguments.
197
+ *
198
+ * The static list is declared before any call exists, which is enough when the
199
+ * questions are a property of the operation. It is not enough when they are a
200
+ * property of the ARGUMENTS — one model takes `aspect_ratio`, another takes
201
+ * `duration`, a third needs an input image — and there the declared list can
202
+ * only be empty, so the tool description ends up teaching the model to work
203
+ * around a mechanism the protocol already has.
204
+ *
205
+ * Returning an empty list is a legitimate answer: this call needs nothing, run
206
+ * it. Everything else about the mechanism is unchanged — the state is still
207
+ * signed, still bound to the principal, the operation and the argument digest,
208
+ * and still counted against `maxRounds`.
209
+ *
210
+ * The resolved list is fingerprinted into the signed state and re-checked on
211
+ * every round, so a resolver that answers differently mid-conversation is
212
+ * refused rather than silently asking round 2's question under round 1's key.
213
+ */
214
+ export type McpInputRequiredResolver<TRequests extends readonly EndpointMcpInputRequired[] = readonly EndpointMcpInputRequired[]> = (call: McpInputRequiredCall) => TRequests | Promise<TRequests>;
186
215
  /** MCP-only execution policy attached to a contract or runtime tool. */
187
216
  export interface EndpointMcpPolicy<TRequests extends readonly EndpointMcpInputRequired[] = readonly EndpointMcpInputRequired[]> {
188
- /** Ordered elicitation rounds. Every key must be unique. */
189
- inputRequired: TRequests;
217
+ /**
218
+ * Ordered elicitation rounds — a fixed list, or a function of the parsed
219
+ * arguments. Every key must be unique, in either form.
220
+ */
221
+ inputRequired: TRequests | McpInputRequiredResolver<TRequests>;
190
222
  }
191
223
  interface HttpOnlyEndpointDef extends EndpointDefBase {
192
224
  expose: readonly ['HTTP'];
@@ -417,7 +449,45 @@ export interface McpCallContext {
417
449
  clientInfo?: McpClientInfo;
418
450
  outcome?: McpRoundOutcome;
419
451
  round?: number;
452
+ /**
453
+ * The host's progress token for this call, when it asked for progress.
454
+ *
455
+ * Declarative, like everything else on this type: it says the host is
456
+ * listening. Reporting is `ctx.reportProgress`, which is a function and
457
+ * therefore deliberately NOT here — this type is also the type of
458
+ * `RequestEvent.mcp`, and an audit row's shape must not claim to carry
459
+ * behaviour.
460
+ */
461
+ progressToken?: string | number;
462
+ }
463
+ /**
464
+ * One progress update a tool handler sends to the host mid-call.
465
+ *
466
+ * `progress` is a number the protocol requires. It is optional here because a
467
+ * handler often has a stage to name and no scale to name it on — "uploading",
468
+ * "rendering frame 12" — and the honest answer to "how far along" is then the
469
+ * ordinal of the update itself, which is what gets sent. That is a fact about
470
+ * what happened, not a percentage nobody measured; `total` stays absent, so a
471
+ * host renders it as an unbounded counter rather than a bar at 3%.
472
+ */
473
+ export interface McpProgressUpdate {
474
+ /** Human-facing stage, shown by the host while the call is still running. */
475
+ message?: string;
476
+ /** How far along, on whatever scale `total` implies. */
477
+ progress?: number;
478
+ /** The scale, when the operation knows it. */
479
+ total?: number;
420
480
  }
481
+ /**
482
+ * Send one progress update for the call in flight.
483
+ *
484
+ * Present on every tool call and a no-op unless the host asked for progress by
485
+ * sending a token, so a handler never branches on transport. It never throws
486
+ * and never rejects: a message about work must not be able to kill the work it
487
+ * describes. Fire-and-forget — the returned promise settles when the
488
+ * notification has been handed to the transport, and ignoring it is correct.
489
+ */
490
+ export type McpReportProgress = (update: McpProgressUpdate) => Promise<void>;
421
491
  export interface RuntimeContext {
422
492
  params: unknown;
423
493
  input: unknown;
@@ -444,6 +514,16 @@ export interface RuntimeContext {
444
514
  signal?: AbortSignal;
445
515
  /** Validated metadata for an MCP call; absent on every other transport. */
446
516
  mcp?: McpCallContext;
517
+ /**
518
+ * Report progress on the call in flight.
519
+ *
520
+ * Present on every TOOL call — MCP, agent and CLI — and a no-op wherever
521
+ * nobody is listening, so a handler never branches on transport to say what
522
+ * it is doing. Absent on the HTTP transport, which has no channel for it;
523
+ * that is why the field is optional and a handler that also serves HTTP
524
+ * writes `ctx.reportProgress?.(…)`.
525
+ */
526
+ reportProgress?: McpReportProgress;
447
527
  [key: string]: unknown;
448
528
  }
449
529
  export interface HandlerContext<TParams = undefined, TInput = undefined> {
@@ -464,6 +544,8 @@ export interface HandlerContext<TParams = undefined, TInput = undefined> {
464
544
  userAgent?: string;
465
545
  /** Validated metadata for an MCP call; absent on every other transport. */
466
546
  mcp?: McpCallContext;
547
+ /** Report progress on the call in flight — a no-op when none was asked for. */
548
+ reportProgress?: McpReportProgress;
467
549
  [key: string]: unknown;
468
550
  }
469
551
  type Prop<T, K extends string> = K extends keyof T ? T[K] : undefined;