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.
- package/CHANGELOG.md +137 -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 +377 -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-3bmnp41g.js} +245 -116
- 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/internal/secure-fetch.d.ts +12 -0
- package/dist/internal/secure-fetch.d.ts.map +1 -1
- 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 +34 -1
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +31 -9
- 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-command.d.ts +18 -0
- package/dist/tools/cli-command.d.ts.map +1 -1
- 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 +143 -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 -16
- 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 +449 -8
- 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-
|
|
18
|
+
} from "./index-3bmnp41g.js";
|
|
19
|
+
import {
|
|
20
|
+
serializeCanonicalJson
|
|
21
|
+
} from "./index-0j3vcp5m.js";
|
|
15
22
|
import"./index-sbdmyz75.js";
|
|
16
|
-
import"./index-
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
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-
|
|
30
|
-
import"./index-
|
|
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
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
165
|
-
compression:
|
|
166
|
-
size:
|
|
167
|
-
sha256:
|
|
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 =
|
|
170
|
-
name:
|
|
171
|
-
version:
|
|
172
|
-
commit:
|
|
173
|
-
builtAt:
|
|
174
|
-
assets:
|
|
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 =
|
|
177
|
-
version:
|
|
178
|
-
commit:
|
|
179
|
-
builtAt:
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
369
|
+
return join2(config.directory, `${name}${PROFILE_SUFFIX}`);
|
|
232
370
|
};
|
|
233
371
|
const list = () => {
|
|
234
|
-
if (!
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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 {
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
/**
|
|
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;
|