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.
- package/CHANGELOG.md +80 -0
- package/dist/agent-runtime-coding-tools.js +3 -3
- package/dist/agent-runtime-harness.js +5 -5
- package/dist/agent-runtime-sandbox.js +3 -3
- package/dist/agent-runtime.js +8 -8
- package/dist/cli.d.ts +6 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +370 -49
- package/dist/contract/define.d.ts +84 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/{index-a2nzz56k.js → index-2c2bjw3s.js} +3 -3
- package/dist/{index-seyy8dvn.js → index-7n145nbe.js} +182 -108
- package/dist/{index-axvavfr4.js → index-bx75zwt7.js} +3 -3
- package/dist/{index-709nh7z7.js → index-cxqhvq21.js} +2 -1
- package/dist/{index-pena034b.js → index-e42wzzz5.js} +3 -3
- package/dist/{index-qjeyx5t2.js → index-fq77yfzc.js} +1 -1
- package/dist/{index-fenaekmk.js → index-h9etkkyk.js} +37 -27
- package/dist/{index-4x809vqd.js → index-mzvgxp0n.js} +2 -2
- package/dist/{index-jpfqfg5c.js → index-n0f51dfn.js} +2 -2
- package/dist/{index-bv7mytab.js → index-sr93cwf9.js} +1 -1
- package/dist/{index-vsbzgd7b.js → index-tmwcrptx.js} +1 -0
- package/dist/{index-8z9we758.js → index-vjpvjqpn.js} +1 -1
- package/dist/{index-7hz88dy8.js → index-vk3kq9sb.js} +1 -1
- package/dist/{index-s3dssqxp.js → index-yn8remb1.js} +60 -56
- package/dist/internal/atomic-file.d.ts +21 -0
- package/dist/internal/atomic-file.d.ts.map +1 -0
- package/dist/node.js +3 -3
- package/dist/observability/audit.d.ts +5 -1
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/changes.d.ts.map +1 -1
- package/dist/observability/context.d.ts +27 -6
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/event.d.ts +34 -6
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.d.ts +2 -1
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +50 -6
- package/dist/observability/work.d.ts +55 -0
- package/dist/observability/work.d.ts.map +1 -0
- package/dist/server/context-contribution.d.ts.map +1 -1
- package/dist/server/index.js +4 -4
- package/dist/server/types.d.ts +20 -6
- package/dist/server/types.d.ts.map +1 -1
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.js +6 -4
- package/dist/tool-invoker.js +5 -5
- package/dist/tools/cli-checkpoint.d.ts +24 -0
- package/dist/tools/cli-checkpoint.d.ts.map +1 -0
- package/dist/tools/cli-format.d.ts +10 -0
- package/dist/tools/cli-format.d.ts.map +1 -1
- package/dist/tools/cli-invoke.d.ts +118 -0
- package/dist/tools/cli-invoke.d.ts.map +1 -0
- package/dist/tools/cli-manifest.d.ts +6 -9
- package/dist/tools/cli-manifest.d.ts.map +1 -1
- package/dist/tools/cli-signature.d.ts +81 -0
- package/dist/tools/cli-signature.d.ts.map +1 -0
- package/dist/tools/cli-stream.d.ts +93 -0
- package/dist/tools/cli-stream.d.ts.map +1 -0
- package/dist/tools/cli-update.d.ts +62 -0
- package/dist/tools/cli-update.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +4 -13
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/connections/index.js +2 -2
- package/dist/tools/execute.d.ts +21 -0
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/mcp-progress.d.ts +46 -0
- package/dist/tools/mcp-progress.d.ts.map +1 -0
- package/dist/tools/mcp-round-policy.d.ts +25 -1
- package/dist/tools/mcp-round-policy.d.ts.map +1 -1
- package/dist/tools/mcp-round.d.ts +20 -1
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-trace.d.ts +9 -1
- package/dist/tools/mcp-trace.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools.js +159 -50
- package/llms-full.txt +358 -8
- 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-
|
|
17
|
+
} from "./index-7n145nbe.js";
|
|
18
|
+
import {
|
|
19
|
+
serializeCanonicalJson
|
|
20
|
+
} from "./index-0j3vcp5m.js";
|
|
15
21
|
import"./index-sbdmyz75.js";
|
|
16
|
-
import"./index-
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
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-
|
|
30
|
-
import"./index-
|
|
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
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
165
|
-
compression:
|
|
166
|
-
size:
|
|
167
|
-
sha256:
|
|
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 =
|
|
170
|
-
name:
|
|
171
|
-
version:
|
|
172
|
-
commit:
|
|
173
|
-
builtAt:
|
|
174
|
-
assets:
|
|
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 =
|
|
177
|
-
version:
|
|
178
|
-
commit:
|
|
179
|
-
builtAt:
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
368
|
+
return join2(config.directory, `${name}${PROFILE_SUFFIX}`);
|
|
232
369
|
};
|
|
233
370
|
const list = () => {
|
|
234
|
-
if (!
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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 {
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
/**
|
|
189
|
-
|
|
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;
|