stitchkit 0.91.2 → 0.92.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 (79) hide show
  1. package/CHANGELOG.md +80 -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 +370 -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-7n145nbe.js} +182 -108
  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/node.js +3 -3
  30. package/dist/observability/audit.d.ts +5 -1
  31. package/dist/observability/audit.d.ts.map +1 -1
  32. package/dist/observability/changes.d.ts.map +1 -1
  33. package/dist/observability/context.d.ts +27 -6
  34. package/dist/observability/context.d.ts.map +1 -1
  35. package/dist/observability/event.d.ts +34 -6
  36. package/dist/observability/event.d.ts.map +1 -1
  37. package/dist/observability/index.d.ts +2 -1
  38. package/dist/observability/index.d.ts.map +1 -1
  39. package/dist/observability/index.js +50 -6
  40. package/dist/observability/work.d.ts +55 -0
  41. package/dist/observability/work.d.ts.map +1 -0
  42. package/dist/server/context-contribution.d.ts.map +1 -1
  43. package/dist/server/index.js +4 -4
  44. package/dist/server/types.d.ts +20 -6
  45. package/dist/server/types.d.ts.map +1 -1
  46. package/dist/testing/surface-manifest.d.ts.map +1 -1
  47. package/dist/testing.js +6 -4
  48. package/dist/tool-invoker.js +5 -5
  49. package/dist/tools/cli-checkpoint.d.ts +24 -0
  50. package/dist/tools/cli-checkpoint.d.ts.map +1 -0
  51. package/dist/tools/cli-format.d.ts +10 -0
  52. package/dist/tools/cli-format.d.ts.map +1 -1
  53. package/dist/tools/cli-invoke.d.ts +118 -0
  54. package/dist/tools/cli-invoke.d.ts.map +1 -0
  55. package/dist/tools/cli-manifest.d.ts +6 -9
  56. package/dist/tools/cli-manifest.d.ts.map +1 -1
  57. package/dist/tools/cli-signature.d.ts +81 -0
  58. package/dist/tools/cli-signature.d.ts.map +1 -0
  59. package/dist/tools/cli-stream.d.ts +93 -0
  60. package/dist/tools/cli-stream.d.ts.map +1 -0
  61. package/dist/tools/cli-update.d.ts +62 -0
  62. package/dist/tools/cli-update.d.ts.map +1 -1
  63. package/dist/tools/cli.d.ts +4 -13
  64. package/dist/tools/cli.d.ts.map +1 -1
  65. package/dist/tools/connections/index.js +2 -2
  66. package/dist/tools/execute.d.ts +21 -0
  67. package/dist/tools/execute.d.ts.map +1 -1
  68. package/dist/tools/mcp-progress.d.ts +46 -0
  69. package/dist/tools/mcp-progress.d.ts.map +1 -0
  70. package/dist/tools/mcp-round-policy.d.ts +25 -1
  71. package/dist/tools/mcp-round-policy.d.ts.map +1 -1
  72. package/dist/tools/mcp-round.d.ts +20 -1
  73. package/dist/tools/mcp-round.d.ts.map +1 -1
  74. package/dist/tools/mcp-trace.d.ts +9 -1
  75. package/dist/tools/mcp-trace.d.ts.map +1 -1
  76. package/dist/tools/mount-wait.d.ts.map +1 -1
  77. package/dist/tools.js +159 -50
  78. package/llms-full.txt +358 -8
  79. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  CliArgumentError,
3
3
  DEFAULT_EXIT_CODES,
4
+ cliExitCode,
5
+ cliInvocationResult,
4
6
  createCli,
7
+ createCliInvoker,
5
8
  defineCliCommand,
6
9
  emitResult,
7
10
  extractCliGlobalOptions,
@@ -11,12 +14,18 @@ import {
11
14
  readCapped,
12
15
  renderCliView,
13
16
  routeCliArgv
14
- } from "./index-seyy8dvn.js";
17
+ } from "./index-7n145nbe.js";
18
+ import {
19
+ serializeCanonicalJson
20
+ } from "./index-0j3vcp5m.js";
15
21
  import"./index-sbdmyz75.js";
16
- import"./index-a2nzz56k.js";
17
- import"./index-4x809vqd.js";
18
- import"./index-s3dssqxp.js";
19
- import"./index-vsbzgd7b.js";
22
+ import"./index-2c2bjw3s.js";
23
+ import {
24
+ argumentsDigest
25
+ } from "./index-a916km3r.js";
26
+ import"./index-mzvgxp0n.js";
27
+ import"./index-yn8remb1.js";
28
+ import"./index-tmwcrptx.js";
20
29
  import"./index-f9mb610r.js";
21
30
  import"./index-e0znv5gn.js";
22
31
  import"./index-0a349c33.js";
@@ -26,8 +35,8 @@ import"./index-zcgf3gqf.js";
26
35
  import {
27
36
  coerceJsonArgs
28
37
  } from "./index-7zbps32p.js";
29
- import"./index-8z9we758.js";
30
- import"./index-fenaekmk.js";
38
+ import"./index-vjpvjqpn.js";
39
+ import"./index-h9etkkyk.js";
31
40
  import"./index-cby4ar3v.js";
32
41
  import {
33
42
  AppError
@@ -35,7 +44,61 @@ import {
35
44
  import {
36
45
  safeJsonParse
37
46
  } from "./index-kzfs85xp.js";
38
- import"./index-77fekveh.js";
47
+ import {
48
+ isRecord
49
+ } from "./index-77fekveh.js";
50
+ // src/tools/cli-checkpoint.ts
51
+ import { existsSync, readFileSync } from "node:fs";
52
+
53
+ // src/internal/atomic-file.ts
54
+ import { randomBytes } from "node:crypto";
55
+ import { closeSync, fchmodSync, fsyncSync, openSync, renameSync, writeSync } from "node:fs";
56
+ import { basename, dirname, join } from "node:path";
57
+ function writeFileAtomic(target, bytes, mode) {
58
+ const staged = join(dirname(target), `.${basename(target)}.${randomBytes(12).toString("hex")}.tmp`);
59
+ const handle = openSync(staged, "wx", mode);
60
+ try {
61
+ writeSync(handle, bytes);
62
+ fchmodSync(handle, mode);
63
+ fsyncSync(handle);
64
+ } finally {
65
+ closeSync(handle);
66
+ }
67
+ renameSync(staged, target);
68
+ }
69
+
70
+ // src/tools/cli-checkpoint.ts
71
+ function emptyCheckpoint(entries) {
72
+ return {
73
+ entries,
74
+ digestOf: (line) => argumentsDigest({ command: line.command, args: line.args }),
75
+ record(id, digest, answer) {
76
+ entries[id] = { digest, answer };
77
+ }
78
+ };
79
+ }
80
+ function readCliCheckpoint(path) {
81
+ if (!existsSync(path))
82
+ return emptyCheckpoint({});
83
+ try {
84
+ const parsed = safeJsonParse(readFileSync(path, "utf8"));
85
+ const entries = {};
86
+ if (isRecord(parsed) && isRecord(parsed.entries)) {
87
+ for (const [id, value] of Object.entries(parsed.entries)) {
88
+ if (isRecord(value) && typeof value.digest === "string") {
89
+ entries[id] = { digest: value.digest, answer: value.answer };
90
+ }
91
+ }
92
+ }
93
+ return emptyCheckpoint(entries);
94
+ } catch {
95
+ return emptyCheckpoint({});
96
+ }
97
+ }
98
+ function writeCliCheckpoint(path, checkpoint) {
99
+ writeFileAtomic(path, Buffer.from(`${JSON.stringify({ entries: checkpoint.entries }, null, 2)}
100
+ `, "utf8"), 384);
101
+ }
39
102
  // src/tools/cli-installer.ts
40
103
  function shellQuote(value) {
41
104
  return `'${value.replaceAll("'", `'"'"'`)}'`;
@@ -155,28 +218,98 @@ ${cases}
155
218
  esac`;
156
219
  }
157
220
  // src/tools/cli-manifest.ts
221
+ import { z as z2 } from "zod";
222
+
223
+ // src/tools/cli-signature.ts
224
+ import {
225
+ createPrivateKey,
226
+ createPublicKey,
227
+ sign as cryptoSign,
228
+ verify as cryptoVerify
229
+ } from "node:crypto";
158
230
  import { z } from "zod";
159
- var CliBuildTargetSchema = z.object({
160
- platform: z.string().min(1),
161
- arch: z.string().min(1)
231
+ var CliBuildSignatureSchema = z.object({
232
+ algorithm: z.literal("ed25519"),
233
+ keyId: z.string().min(1),
234
+ signature: z.string().min(1)
235
+ });
236
+ function cliManifestSigningPayload(manifest) {
237
+ return serializeCanonicalJson({
238
+ name: manifest.name,
239
+ version: manifest.version,
240
+ commit: manifest.commit,
241
+ builtAt: manifest.builtAt,
242
+ assets: manifest.assets.map((asset) => ({
243
+ platform: asset.platform,
244
+ arch: asset.arch,
245
+ compression: asset.compression,
246
+ size: asset.size,
247
+ sha256: asset.sha256
248
+ }))
249
+ });
250
+ }
251
+ function publicKeyOf(material) {
252
+ const trimmed = material.trim();
253
+ if (trimmed.includes("BEGIN PUBLIC KEY"))
254
+ return createPublicKey(trimmed);
255
+ const raw = Buffer.from(trimmed, "base64");
256
+ if (raw.length !== 32) {
257
+ throw new Error("[stitchkit] an Ed25519 public key is 32 bytes or SPKI PEM");
258
+ }
259
+ const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
260
+ return createPublicKey({ key: spki, format: "der", type: "spki" });
261
+ }
262
+ function signCliManifest(manifest, options) {
263
+ 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" }));
264
+ return {
265
+ algorithm: "ed25519",
266
+ keyId: options.keyId,
267
+ signature: signature.toString("base64")
268
+ };
269
+ }
270
+ function verifyCliManifest(manifest, signature, trust) {
271
+ const keys = trust?.keys;
272
+ if (!keys || Object.keys(keys).length === 0)
273
+ return "unenforced";
274
+ if (!signature)
275
+ return "missing";
276
+ const material = Object.hasOwn(keys, signature.keyId) ? keys[signature.keyId] : undefined;
277
+ if (material === undefined)
278
+ return "unknown-key";
279
+ try {
280
+ const ok = cryptoVerify(null, Buffer.from(cliManifestSigningPayload(manifest), "utf8"), publicKeyOf(material), Buffer.from(signature.signature, "base64"));
281
+ return ok ? "valid" : "invalid";
282
+ } catch {
283
+ return "invalid";
284
+ }
285
+ }
286
+ function cliSignatureAccepted(verdict) {
287
+ return verdict === "valid" || verdict === "unenforced";
288
+ }
289
+
290
+ // src/tools/cli-manifest.ts
291
+ var CliBuildTargetSchema = z2.object({
292
+ platform: z2.string().min(1),
293
+ arch: z2.string().min(1)
162
294
  });
163
295
  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")
296
+ url: z2.url(),
297
+ compression: z2.enum(["gzip", "none"]).default("none"),
298
+ size: z2.int().positive(),
299
+ sha256: z2.string().regex(/^[0-9a-f]{64}$/, "sha256 must be 64 lowercase hex characters")
168
300
  });
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)
301
+ var CliBuildManifestSchema = z2.object({
302
+ name: z2.string().min(1),
303
+ version: z2.string().min(1),
304
+ commit: z2.string().min(1),
305
+ builtAt: z2.iso.datetime(),
306
+ assets: z2.array(CliBuildAssetSchema),
307
+ signature: CliBuildSignatureSchema.optional()
175
308
  });
176
- var CliBuildStampSchema = z.object({
177
- version: z.string().min(1),
178
- commit: z.string().min(1),
179
- builtAt: z.iso.datetime()
309
+ var CliBuildStampSchema = z2.object({
310
+ version: z2.string().min(1),
311
+ commit: z2.string().min(1),
312
+ builtAt: z2.iso.datetime()
180
313
  });
181
314
  function formatCliBuildStamp(stamp, name) {
182
315
  const prefix = name ? `${name} ` : "";
@@ -188,26 +321,30 @@ function currentCliBuildTarget() {
188
321
  function selectCliBuildAsset(manifest, target = currentCliBuildTarget()) {
189
322
  return manifest.assets.find((asset) => asset.platform === target.platform && asset.arch === target.arch);
190
323
  }
324
+ function isManifestList(value) {
325
+ return Array.isArray(value);
326
+ }
191
327
  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`);
328
+ const published = previous === undefined ? [] : isManifestList(previous) ? previous : [previous];
329
+ for (const candidate of published) {
330
+ if (candidate.version !== next.version)
331
+ continue;
332
+ if (candidate.commit === next.commit)
333
+ continue;
334
+ 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`);
335
+ }
199
336
  }
200
337
  // src/tools/cli-profile.ts
201
338
  import {
202
339
  chmodSync,
203
- existsSync,
340
+ existsSync as existsSync2,
204
341
  mkdirSync,
205
342
  readdirSync,
206
- readFileSync,
343
+ readFileSync as readFileSync2,
207
344
  statSync,
208
345
  writeFileSync
209
346
  } from "node:fs";
210
- import { join } from "node:path";
347
+ import { join as join2 } from "node:path";
211
348
  var PROFILE_SUFFIX = ".json";
212
349
  var FILE_MODE = 384;
213
350
  var DIRECTORY_MODE = 448;
@@ -228,23 +365,23 @@ function createCliProfileStore(config) {
228
365
  `));
229
366
  const pathFor = (name) => {
230
367
  assertProfileName(name);
231
- return join(config.directory, `${name}${PROFILE_SUFFIX}`);
368
+ return join2(config.directory, `${name}${PROFILE_SUFFIX}`);
232
369
  };
233
370
  const list = () => {
234
- if (!existsSync(config.directory))
371
+ if (!existsSync2(config.directory))
235
372
  return [];
236
373
  return readdirSync(config.directory).filter((entry) => entry.endsWith(PROFILE_SUFFIX)).map((entry) => entry.slice(0, -PROFILE_SUFFIX.length)).sort();
237
374
  };
238
375
  const read = (name) => {
239
376
  const path = pathFor(name);
240
- if (!existsSync(path)) {
377
+ if (!existsSync2(path)) {
241
378
  throw new CliProfileError(missingMessage(name, path, config));
242
379
  }
243
380
  const mode = statSync(path).mode & 63;
244
381
  if (mode !== 0) {
245
382
  throw new CliProfileError(`profile "${name}" is readable by other users — run: chmod 600 ${path}`, "FORBIDDEN");
246
383
  }
247
- const parsed = safeJsonParse(readFileSync(path, "utf8"));
384
+ const parsed = safeJsonParse(readFileSync2(path, "utf8"));
248
385
  const result = config.schema.safeParse(parsed);
249
386
  if (!result.success) {
250
387
  const issue = result.error.issues[0];
@@ -279,7 +416,7 @@ function createCliProfileStore(config) {
279
416
  return { name: only, path, value: read(only), substituted: true };
280
417
  }
281
418
  if (configured.length === 0) {
282
- const example = join(config.directory, `<name>${PROFILE_SUFFIX}`);
419
+ const example = join2(config.directory, `<name>${PROFILE_SUFFIX}`);
283
420
  throw new CliProfileError(`no profile is configured in ${config.directory}${config.createHint ? ` — ${config.createHint("<name>", example)}` : ""}`);
284
421
  }
285
422
  throw new CliProfileError(`several profiles are configured (${configured.join(", ")}) — name one`, "BAD_REQUEST");
@@ -290,10 +427,140 @@ function missingMessage(name, path, config) {
290
427
  const hint = config.createHint ? ` — ${config.createHint(name, path)}` : "";
291
428
  return `profile "${name}" is not configured; expected ${path}${hint}`;
292
429
  }
430
+ // src/tools/cli-stream.ts
431
+ import { z as z3 } from "zod";
432
+ var LineSchema = z3.object({
433
+ id: z3.string().min(1),
434
+ command: z3.string().min(1),
435
+ args: z3.record(z3.string(), z3.unknown()).default({})
436
+ });
437
+ async function* readStdinLines(source = process.stdin) {
438
+ const decoder = new TextDecoder;
439
+ let pending = "";
440
+ for await (const chunk of source) {
441
+ pending += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
442
+ let newline = pending.indexOf(`
443
+ `);
444
+ while (newline !== -1) {
445
+ const line = pending.slice(0, newline);
446
+ pending = pending.slice(newline + 1);
447
+ yield line.endsWith("\r") ? line.slice(0, -1) : line;
448
+ newline = pending.indexOf(`
449
+ `);
450
+ }
451
+ }
452
+ if (pending !== "")
453
+ yield pending.endsWith("\r") ? pending.slice(0, -1) : pending;
454
+ }
455
+ function parseLine(line) {
456
+ let parsed;
457
+ try {
458
+ parsed = safeJsonParse(line);
459
+ } catch {
460
+ return { ok: false, answer: malformed("the line is not JSON") };
461
+ }
462
+ const result = LineSchema.safeParse(parsed);
463
+ if (!result.success) {
464
+ const id = isRecord(parsed) && typeof parsed.id === "string" ? parsed.id : undefined;
465
+ return {
466
+ ok: false,
467
+ answer: {
468
+ ...malformed("the line needs `id` and `command`"),
469
+ ...id !== undefined && { id }
470
+ }
471
+ };
472
+ }
473
+ return { ok: true, value: result.data };
474
+ }
475
+ function malformed(message) {
476
+ return {
477
+ id: "",
478
+ ok: false,
479
+ exitCode: 2,
480
+ error: { code: "BAD_REQUEST", message }
481
+ };
482
+ }
483
+ async function runCliStream(invoker, lines, onAnswer, hooks = {}) {
484
+ for await (const line of lines) {
485
+ if (line.trim() === "")
486
+ continue;
487
+ const parsed = parseLine(line);
488
+ if (!parsed.ok) {
489
+ onAnswer(parsed.answer);
490
+ continue;
491
+ }
492
+ const { id, command, args } = parsed.value;
493
+ const answered = hooks.before?.(id, { command, args });
494
+ if (answered) {
495
+ onAnswer(answered);
496
+ continue;
497
+ }
498
+ const outcome = await invoker.invoke(command, args);
499
+ hooks.after?.(id, { command, args }, outcome);
500
+ onAnswer({ id, ...outcome });
501
+ }
502
+ }
503
+ function defineCliStreamCommand(config) {
504
+ return defineCliCommand({
505
+ name: config.name ?? "stream",
506
+ description: config.description ?? "Run one operation per JSON line of stdin, answering each",
507
+ input: z3.object({}),
508
+ handler: async (context) => {
509
+ const invoker = typeof config.invoker === "function" ? await config.invoker() : config.invoker;
510
+ await runCliStream(invoker, (config.readLines ?? readStdinLines)(), (answer) => {
511
+ context.stdout(`${JSON.stringify(answer)}
512
+ `);
513
+ });
514
+ }
515
+ });
516
+ }
517
+ function defineCliBatchCommand(config) {
518
+ return defineCliCommand({
519
+ name: config.name ?? "batch",
520
+ description: config.description ?? "Run one operation per JSON line of stdin, resuming from a checkpoint",
521
+ input: z3.object({
522
+ checkpoint: z3.string().optional()
523
+ }),
524
+ handler: async (context) => {
525
+ const invoker = typeof config.invoker === "function" ? await config.invoker() : config.invoker;
526
+ const path = context.input.checkpoint ?? config.checkpointPath ?? ".stitchkit-batch.json";
527
+ const checkpoint = readCliCheckpoint(path);
528
+ await runCliStream(invoker, (config.readLines ?? readStdinLines)(), (answer) => {
529
+ context.stdout(`${JSON.stringify(answer)}
530
+ `);
531
+ }, {
532
+ before: (id, line) => {
533
+ const digest = checkpoint.digestOf(line);
534
+ const recorded = checkpoint.entries[id];
535
+ if (!recorded)
536
+ return;
537
+ if (recorded.digest !== digest) {
538
+ return {
539
+ id,
540
+ ok: false,
541
+ exitCode: 2,
542
+ error: {
543
+ code: "CONFLICT",
544
+ message: "this id was already run with different content — change the id or start a new checkpoint"
545
+ }
546
+ };
547
+ }
548
+ const replay = isRecord(recorded.answer) ? recorded.answer : {};
549
+ return { ...replay, id, replayed: true };
550
+ },
551
+ after: (id, line, outcome) => {
552
+ if (!outcome.ok)
553
+ return;
554
+ checkpoint.record(id, checkpoint.digestOf(line), outcome);
555
+ writeCliCheckpoint(path, checkpoint);
556
+ }
557
+ });
558
+ }
559
+ });
560
+ }
293
561
  // src/tools/cli-update.ts
294
562
  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";
563
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
297
564
  import { gunzipSync } from "node:zlib";
298
565
  var DEFAULT_CHECK_TIMEOUT_MS = 2000;
299
566
  var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -354,14 +621,19 @@ async function checkCliUpdate(config) {
354
621
  reason: `cannot compare ${config.currentVersion} with ${manifest.version}`
355
622
  };
356
623
  }
624
+ const verdict = verifyCliManifest(manifest, manifest.signature, config.trust);
357
625
  if (order >= 0)
358
- return { status: "current", version: manifest.version };
626
+ return { status: "current", version: manifest.version, signature: verdict };
627
+ if (!cliSignatureAccepted(verdict)) {
628
+ return { status: "unknown", reason: `manifest signature: ${verdict}` };
629
+ }
359
630
  const asset = selectCliBuildAsset(manifest, config.target ?? currentCliBuildTarget());
360
631
  return {
361
632
  status: "outdated",
362
633
  version: manifest.version,
363
634
  manifest,
364
- ...asset && { asset }
635
+ ...asset && { asset },
636
+ signature: verdict
365
637
  };
366
638
  } catch (error) {
367
639
  return {
@@ -372,6 +644,18 @@ async function checkCliUpdate(config) {
372
644
  }
373
645
  async function applyCliUpdate(config) {
374
646
  const target = config.targetPath ?? process.execPath;
647
+ if (config.trust) {
648
+ if (!config.manifest) {
649
+ throw new Error("[stitchkit] update: a trust root needs the manifest the asset came from");
650
+ }
651
+ const verdict = verifyCliManifest(config.manifest, config.manifest.signature, config.trust);
652
+ if (!cliSignatureAccepted(verdict)) {
653
+ throw new Error(`[stitchkit] update: refusing to install — manifest signature ${verdict}`);
654
+ }
655
+ if (!config.manifest.assets.some((asset) => asset.sha256 === config.asset.sha256)) {
656
+ throw new Error("[stitchkit] update: the asset is not one the signed manifest names");
657
+ }
658
+ }
375
659
  const response = await fetchGuarded(new URL(config.asset.url), config.allowPrivateHosts ?? false, {
376
660
  timeoutMs: config.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS,
377
661
  bodyTimeoutMs: config.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS
@@ -382,7 +666,7 @@ async function applyCliUpdate(config) {
382
666
  const transferred = await readCapped(response, config.maxBytes ?? DEFAULT_MAX_ASSET_BYTES);
383
667
  if (!transferred)
384
668
  throw new Error("[stitchkit] update download exceeded the size ceiling");
385
- const bytes = config.asset.compression === "gzip" ? gunzipSync(transferred) : transferred;
669
+ const bytes = config.asset.compression === "gzip" ? gunzipSync(transferred, { maxOutputLength: config.asset.size }) : transferred;
386
670
  if (bytes.length !== config.asset.size) {
387
671
  throw new Error(`[stitchkit] update is ${bytes.length} bytes, manifest says ${config.asset.size} — refusing to install it`);
388
672
  }
@@ -390,28 +674,64 @@ async function applyCliUpdate(config) {
390
674
  if (sha256 !== config.asset.sha256) {
391
675
  throw new Error(`[stitchkit] update digest ${sha256} does not match the manifest — refusing to install it`);
392
676
  }
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);
677
+ const backup = config.backupPath ? keepReplacedBinary(target, config.backupPath) : undefined;
678
+ writeFileAtomic(target, bytes, 493);
679
+ return {
680
+ path: target,
681
+ bytes: bytes.length,
682
+ sha256,
683
+ ...backup && { backupPath: backup.path, backupSha256: backup.sha256 }
684
+ };
685
+ }
686
+ function keepReplacedBinary(target, backupPath) {
687
+ if (!existsSync3(target))
688
+ return;
689
+ const previous = readFileSync3(target);
690
+ writeFileAtomic(backupPath, previous, statSync2(target).mode & 511);
691
+ return { path: backupPath, sha256: createHash("sha256").update(previous).digest("hex") };
692
+ }
693
+ function rollbackCliUpdate(config) {
694
+ const target = config.targetPath ?? process.execPath;
695
+ if (!existsSync3(config.backupPath)) {
696
+ throw new Error(`[stitchkit] rollback: no backup at ${config.backupPath}`);
697
+ }
698
+ const bytes = readFileSync3(config.backupPath);
699
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
700
+ if (sha256 !== config.expectedSha256) {
701
+ throw new Error(`[stitchkit] rollback: backup digest ${sha256} does not match the expected ${config.expectedSha256} — refusing to restore it`);
702
+ }
703
+ writeFileAtomic(target, bytes, statSync2(config.backupPath).mode & 511);
397
704
  return { path: target, bytes: bytes.length, sha256 };
398
705
  }
399
706
  export {
707
+ writeCliCheckpoint,
708
+ verifyCliManifest,
709
+ signCliManifest,
400
710
  selectCliBuildAsset,
711
+ runCliStream,
401
712
  routeCliArgv,
713
+ rollbackCliUpdate,
402
714
  renderCliView,
403
715
  renderCliInstaller,
716
+ readCliCheckpoint,
404
717
  pollUntilDone,
405
718
  parseCliArgs,
406
719
  formatCliBuildStamp,
407
720
  extractCliGlobalOptions,
408
721
  emitResult,
722
+ defineCliStreamCommand,
409
723
  defineCliCommand,
724
+ defineCliBatchCommand,
410
725
  currentCliBuildTarget,
411
726
  createCliProfileStore,
727
+ createCliInvoker,
412
728
  createCli,
413
729
  compareCliVersions,
414
730
  coerceJsonArgs,
731
+ cliSignatureAccepted,
732
+ cliManifestSigningPayload,
733
+ cliInvocationResult,
734
+ cliExitCode,
415
735
  checkCliUpdate,
416
736
  assertCliPublishable,
417
737
  applyCliUpdate,
@@ -419,6 +739,7 @@ export {
419
739
  CliProfileError,
420
740
  CliBuildTargetSchema,
421
741
  CliBuildStampSchema,
742
+ CliBuildSignatureSchema,
422
743
  CliBuildManifestSchema,
423
744
  CliBuildAssetSchema,
424
745
  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;