@henols/vice-mcp 0.2.2 → 0.2.4

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 (84) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
@@ -1,390 +0,0 @@
1
- // capability-registry.ts
2
- //
3
- // WHY THIS FILE EXISTS (BACK-05): today, a tool name the ACTIVE backend does
4
- // not advertise falls straight through vice-proxy.ts's CallToolRequestSchema
5
- // override to its generic `Unknown tool: ${name}` fallback -- the exact same
6
- // message a genuine typo gets. That is indistinguishable and unhelpful: a
7
- // caller cannot tell "you misspelled this" from "this tool exists, but only
8
- // on the other backend, for this specific reason." This module is the ONE
9
- // authoritative place holding that per-backend capability data -- names,
10
- // reason categories, reason text, and which backend actually provides each
11
- // one -- so a future call site (the runtime refusal wired in plan 08-02, the
12
- // generated support table in plan 08-03, the skill-text lint in plan 08-04)
13
- // reads exactly one source rather than re-deriving or hand-copying it.
14
- //
15
- // WHAT NOT TO DO: do not hand-maintain a second copy of this data anywhere
16
- // else in the repo (D-E; see CLAUDE.md's "re-deriving a cross-cutting seam
17
- // locally" anti-pattern). If a consumer needs this data in a different
18
- // shape, import CAPABILITY_REGISTRY and reshape it there -- never re-type
19
- // the 26 entries or their reasons from memory.
20
- //
21
- // SECURITY POSTURE: this module is a READ-ONLY MESSAGE-TEXT LOOKUP and is
22
- // NEVER an authorization boundary. DENY_LIST (vice.ts) remains the only
23
- // refusal in this tree that is a security control; that check runs first, at
24
- // every call site, and this module never overrides or duplicates it. Every
25
- // string held here is already public: it names only information already
26
- // published in docs/stock-vice-parity.md and this public repository -- no
27
- // credential, no secret, no host, no port, no path, and no tool name that is
28
- // not already present in one of the two shipped manifests
29
- // (tools-manifest.json, tools-manifest.stock.json).
30
- //
31
- // This is the exact shape vice.ts's DENY_LIST / denyListRefusalMessage()
32
- // already established: one exported readonly array, one exported
33
- // message-rendering function, keyed by hazard/reason shape rather than one
34
- // wording reused for every entry -- because telling a caller the wrong
35
- // reason shape for what is otherwise the same permanent refusal invites a
36
- // pointless retry (that function's own doc comment makes the identical
37
- // point).
38
- //
39
- // EXCLUDED, DELIBERATELY (see docs/stock-vice-parity.md and
40
- // 08-RESEARCH.md's "Capability Delta Registry"):
41
- // - "vice_diagnose" and "vice_recycle" are NOT capability gaps: they are
42
- // synthetic, proxy-local tools registered on BOTH backends by
43
- // vice-proxy.ts's buildBackendAwareTool()/resolveAdvertisedToolDefinition()
44
- // synthetic-registration call sites, never listed in either raw manifest
45
- // JSON file. A naive set-difference over the two manifests misclassifies
46
- // them as a divergence; they are not one, and including them here would
47
- // be a factual error, not merely an omission.
48
- // - "initialize", "notifications_initialized", "tools_call", "tools_list"
49
- // are already refused by vice.ts's DENY_LIST, which runs strictly BEFORE
50
- // any capability-registry lookup and owns a different hazard shape
51
- // entirely (confused-deputy bypass, not a missing capability). They must
52
- // keep being owned there, not duplicated here.
53
- //
54
- // This module imports nothing at runtime -- the only import is a type-only
55
- // import of ViceBackend, which is erased by Node's type-stripping -- so it
56
- // has no transport, no filesystem, and no process dependency of its own.
57
- import type { ViceBackend } from "./backend-detect.mts";
58
-
59
- /**
60
- * Three reason categories, matching the distinctions the project's own docs
61
- * already draw (docs/stock-vice-parity.md SS A/B):
62
- * - "hardware": no 1:1 opcode can ever exist because of a hardware or
63
- * firmware property (a write-only register, per-read recomputation, no
64
- * monitor command for a physical line). Permanent; nothing to build.
65
- * - "descoped": theoretically buildable client-side (an opcode or
66
- * equivalent already exists) but cut from v0.2.0 scope because no
67
- * shipped skill calls it.
68
- * - "stock-only-gain": the reverse direction -- stock has a native opcode
69
- * the fork's custom HTTP API never exposed an equivalent RPC for.
70
- */
71
- export type CapabilityCategory = "hardware" | "descoped" | "stock-only-gain";
72
-
73
- /**
74
- * One row of capability data. `providedBy` names the backend that DOES have
75
- * the capability (never the backend refusing it). `reason` is one sentence
76
- * of user-facing prose ending in a full stop -- no planning identifier
77
- * (BACK-05, SKILL-01, DERIV-*, SHOT-*, GAIN-*, "Phase N") belongs in this
78
- * field; those are internal routing annotations, not something a caller
79
- * calling a tool by name should ever see. `alternative`, when present, names
80
- * a concrete route that exists on the OTHER backend today -- omit it rather
81
- * than inventing one where none exists.
82
- */
83
- export interface CapabilityEntry {
84
- name: string;
85
- category: CapabilityCategory;
86
- providedBy: ViceBackend;
87
- reason: string;
88
- alternative?: string;
89
- }
90
-
91
- const KEYBOARD_ALTERNATIVE =
92
- "vice_keyboard_type / vice_keyboard_petscii inject text through the KERNAL keyboard buffer, " +
93
- "and vice_joystick_set covers most in-game input -- but a program polling $DC00/$DC01 directly " +
94
- "will not see buffer injection.";
95
-
96
- /**
97
- * The 26-entry capability delta: every tool one backend advertises that the
98
- * other genuinely does not, after excluding registration artifacts
99
- * (vice_diagnose/vice_recycle) and DENY_LIST's own four meta-tools -- see
100
- * the header comment above and 08-RESEARCH.md's "Capability Delta Registry"
101
- * for the full accounting this array is derived from.
102
- */
103
- export const CAPABILITY_REGISTRY: readonly CapabilityEntry[] = [
104
- // --- hardware (6), providedBy: fork -------------------------------------
105
- {
106
- name: "vice_sid_get_state",
107
- category: "hardware",
108
- providedBy: "fork",
109
- reason:
110
- "SID's $D400-$D418 registers are write-only in hardware, and the binary monitor exposes " +
111
- "no SID read command.",
112
- },
113
- {
114
- name: "vice_keyboard_matrix",
115
- category: "hardware",
116
- providedBy: "fork",
117
- reason:
118
- "The binary monitor's KEYBOARD_FEED (0x72) only injects PETSCII buffer text; the emulator " +
119
- "recomputes CIA port B from its own keyboard array on every read, so there is no wire " +
120
- "command that can drive the raw matrix.",
121
- alternative: KEYBOARD_ALTERNATIVE,
122
- },
123
- {
124
- name: "vice_keyboard_restore",
125
- category: "hardware",
126
- providedBy: "fork",
127
- reason:
128
- "RESTORE pulses the NMI line directly; it is not part of the keyboard matrix, and " +
129
- "KEYBOARD_FEED has no way to produce it.",
130
- alternative: KEYBOARD_ALTERNATIVE,
131
- },
132
- {
133
- name: "vice_keyboard_chord",
134
- category: "hardware",
135
- providedBy: "fork",
136
- reason:
137
- "KEYBOARD_FEED injects a whole string at a time; it has no primitive for holding multiple " +
138
- "keys down together for a span of frames.",
139
- alternative: KEYBOARD_ALTERNATIVE,
140
- },
141
- {
142
- name: "vice_keyboard_key_press",
143
- category: "hardware",
144
- providedBy: "fork",
145
- reason:
146
- "KEYBOARD_FEED has no hold/release primitive -- it injects a complete string, not an " +
147
- "individual key-down event.",
148
- alternative: KEYBOARD_ALTERNATIVE,
149
- },
150
- {
151
- name: "vice_keyboard_key_release",
152
- category: "hardware",
153
- providedBy: "fork",
154
- reason:
155
- "KEYBOARD_FEED has no hold/release primitive -- it injects a complete string, not an " +
156
- "individual key-up event.",
157
- alternative: KEYBOARD_ALTERNATIVE,
158
- },
159
-
160
- // --- descoped (18), providedBy: fork -------------------------------------
161
- {
162
- name: "vice_disk_detach",
163
- category: "descoped",
164
- providedBy: "fork",
165
- reason:
166
- "No detach opcode exists on the stock binary monitor; attaching a different disk image " +
167
- "covers the same workflow.",
168
- },
169
- {
170
- name: "vice_disk_read_sector",
171
- category: "descoped",
172
- providedBy: "fork",
173
- reason:
174
- "Reading a sector would require parsing the .d64 file client-side rather than calling a " +
175
- "live-drive opcode, and no shipped skill calls it.",
176
- },
177
- {
178
- name: "vice_display_screenshot",
179
- category: "descoped",
180
- providedBy: "fork",
181
- reason:
182
- "The client-side PNG encoder for the INDEXED8 framebuffer DISPLAY_GET returns was descoped " +
183
- "because no shipped skill calls it.",
184
- },
185
- {
186
- name: "vice_display_get_dimensions",
187
- category: "descoped",
188
- providedBy: "fork",
189
- reason: "Descoped alongside vice_display_screenshot -- no shipped skill calls it.",
190
- },
191
- {
192
- name: "vice_backtrace",
193
- category: "descoped",
194
- providedBy: "fork",
195
- reason: "No shipped skill calls it.",
196
- },
197
- {
198
- name: "vice_checkpoint_group_add",
199
- category: "descoped",
200
- providedBy: "fork",
201
- reason: "No shipped skill calls any checkpoint-group tool.",
202
- },
203
- {
204
- name: "vice_checkpoint_group_create",
205
- category: "descoped",
206
- providedBy: "fork",
207
- reason: "No shipped skill calls any checkpoint-group tool.",
208
- },
209
- {
210
- name: "vice_checkpoint_group_list",
211
- category: "descoped",
212
- providedBy: "fork",
213
- reason: "No shipped skill calls any checkpoint-group tool.",
214
- },
215
- {
216
- name: "vice_checkpoint_group_toggle",
217
- category: "descoped",
218
- providedBy: "fork",
219
- reason: "No shipped skill calls any checkpoint-group tool.",
220
- },
221
- {
222
- name: "vice_checkpoint_set_ignore_count",
223
- category: "descoped",
224
- providedBy: "fork",
225
- reason:
226
- "No native wire ignore-count exists; the only implementation would require resuming the " +
227
- "machine on every ignored hit, which the no-unrequested-resume policy forbids. " +
228
- "CHECKPOINT_INFO's reply still reports an existing ignore count read-only.",
229
- },
230
- {
231
- name: "vice_cia_set_state",
232
- category: "descoped",
233
- providedBy: "fork",
234
- reason:
235
- "The write half of a tool whose read half already ships on stock; no shipped skill calls " +
236
- "the write half.",
237
- },
238
- {
239
- name: "vice_vicii_set_state",
240
- category: "descoped",
241
- providedBy: "fork",
242
- reason:
243
- "The write half of a tool whose read half already ships on stock; no shipped skill calls " +
244
- "the write half.",
245
- },
246
- {
247
- name: "vice_sprite_set",
248
- category: "descoped",
249
- providedBy: "fork",
250
- reason:
251
- "The write half of a tool whose read half already ships on stock; no shipped skill calls " +
252
- "the write half.",
253
- },
254
- {
255
- name: "vice_memory_fill",
256
- category: "descoped",
257
- providedBy: "fork",
258
- reason: "No shipped skill calls it.",
259
- },
260
- {
261
- name: "vice_sid_set_state",
262
- category: "descoped",
263
- providedBy: "fork",
264
- reason:
265
- "SID writes work fine over MEM_SET at $D400-$D418 -- this is not a hardware loss, only " +
266
- "reads are write-only in hardware. It is simply not implemented because no shipped skill " +
267
- "calls it.",
268
- },
269
- {
270
- name: "vice_machine_config_get",
271
- category: "descoped",
272
- providedBy: "fork",
273
- reason:
274
- "Full resource get/set access was descoped; the fork's tool is a hand-curated whitelist " +
275
- "subset that never shipped on stock.",
276
- },
277
- {
278
- name: "vice_machine_config_set",
279
- category: "descoped",
280
- providedBy: "fork",
281
- reason:
282
- "Full resource get/set access was descoped; the fork's tool is a hand-curated whitelist " +
283
- "subset that never shipped on stock. Its advertised WarpMode resource is fork-only: stock " +
284
- "has no runtime warp resource at all, and warp on stock is a launch-time flag, not a " +
285
- "resource that can be toggled while running.",
286
- },
287
- {
288
- name: "vice_joystick_tap",
289
- category: "descoped",
290
- providedBy: "fork",
291
- reason:
292
- "Requires running the machine for a measured hold-then-release interval, which depends on " +
293
- "timing infrastructure; not yet built, and no shipped skill calls it.",
294
- },
295
-
296
- // --- stock-only-gain (2), providedBy: stock ------------------------------
297
- {
298
- name: "vice_execution_until_return",
299
- category: "stock-only-gain",
300
- providedBy: "stock",
301
- reason: "The fork's custom HTTP API has no equivalent RPC; this is the native EXECUTE_UNTIL_RETURN opcode.",
302
- },
303
- {
304
- name: "vice_registers_available",
305
- category: "stock-only-gain",
306
- providedBy: "stock",
307
- reason: "The fork has no equivalent enumeration call; this is the native REGISTERS_AVAILABLE opcode.",
308
- },
309
- ];
310
-
311
- /**
312
- * Plain name-keyed lookup over CAPABILITY_REGISTRY. `name` is untrusted
313
- * `request.params.name` from the wire: it is used ONLY as an equality
314
- * comparison value below. Never interpolate it into a path, a command, or
315
- * anything executed.
316
- */
317
- export function capabilityEntryFor(name: string): CapabilityEntry | undefined {
318
- return CAPABILITY_REGISTRY.find((entry) => entry.name === name);
319
- }
320
-
321
- /**
322
- * Renders the BACK-05 refusal for `name` on `activeBackend`, or `undefined`
323
- * when there is nothing to refuse -- mirroring denyListRefusalMessage()'s
324
- * keyed-by-hazard-shape contract (vice.ts).
325
- *
326
- * Returns `undefined` when:
327
- * - no registry entry exists for `name` (a genuinely unknown tool name --
328
- * a typo -- must still fall through to the generic "Unknown tool"
329
- * message at the call site, not this function's wording); or
330
- * - `entry.providedBy === activeBackend` (defensive: the active backend
331
- * already advertises this tool, so a miss here is a genuine
332
- * unknown-tool case, not a capability gap, and must not be
333
- * misclassified as one).
334
- *
335
- * Otherwise renders one of three wordings, selected by `entry.category`:
336
- * - "hardware": names the tool, states it is unrecoverable on
337
- * `activeBackend`, gives `entry.reason`, then names `entry.providedBy`
338
- * with the actionable `Set VICE_BACKEND=...`, then `entry.alternative`
339
- * when present. No "wait for a later phase" framing -- none is coming
340
- * for a hardware loss, but a stock route may still exist and must be
341
- * named here, since this is where the caller actually reads it.
342
- * - "descoped": names the tool, states it is not implemented on
343
- * `activeBackend`, gives `entry.reason`, then `entry.providedBy` and
344
- * `Set VICE_BACKEND=...`, then `entry.alternative` when present. The
345
- * literal token "unrecoverable" must NEVER appear in this wording: a
346
- * reader told "not supported" for both a hardware loss and a merely
347
- * unbuilt tool cannot tell which one is worth filing an issue about.
348
- * - "stock-only-gain": names the tool, states it is not implemented on the
349
- * fork backend specifically, gives `entry.reason`, then
350
- * `Set VICE_BACKEND=stock`.
351
- */
352
- export function capabilityRefusalMessage(
353
- name: string,
354
- activeBackend: ViceBackend,
355
- ): string | undefined {
356
- const entry = capabilityEntryFor(name);
357
- if (!entry) return undefined;
358
- if (entry.providedBy === activeBackend) return undefined;
359
-
360
- // `alternative` is rendered in EVERY branch that has one. It used to be
361
- // read only inside the "descoped" branch, which made the field dead at
362
- // runtime: all five entries that carry an alternative are category
363
- // "hardware", and no "descoped" entry has one. The generated support
364
- // table, the skill playbooks and README all printed the stock route while
365
- // the runtime refusal -- the one surface BACK-05 exists for -- dropped it.
366
- // Do not re-scope this back into a single branch.
367
- const alt = entry.alternative ? ` ${entry.alternative}` : "";
368
-
369
- if (entry.category === "hardware") {
370
- return (
371
- `${entry.name} is unrecoverable on the ${activeBackend} backend: ${entry.reason} ` +
372
- `Use the ${entry.providedBy} backend instead (Set VICE_BACKEND=${entry.providedBy}).${alt}`
373
- );
374
- }
375
-
376
- if (entry.category === "descoped") {
377
- return (
378
- `${entry.name} is not implemented on the ${activeBackend} backend: ${entry.reason} ` +
379
- `Use the ${entry.providedBy} backend instead (Set VICE_BACKEND=${entry.providedBy}).${alt}`
380
- );
381
- }
382
-
383
- // category === "stock-only-gain": only reachable with activeBackend ===
384
- // "fork" and entry.providedBy === "stock", since the same-backend guard
385
- // above already excluded the activeBackend === "stock" case.
386
- return (
387
- `${entry.name} is not implemented on the fork backend: ${entry.reason} ` +
388
- `Use the stock backend instead (Set VICE_BACKEND=stock).${alt}`
389
- );
390
- }
@@ -1,124 +0,0 @@
1
- #!/usr/bin/env node
2
- // The ONLY writer of tools-manifest.json (see vice-proxy.mjs's tools/list
3
- // handler doc comment -- that handler is a pure, offline READ of the file
4
- // this CLI produces). An operator runs this by hand against a running host
5
- // to refresh the snapshot; vice-proxy.mjs never imports this file, so this
6
- // file's stdout is a normal CLI stream, not the MCP channel.
7
- //
8
- // Sibling import: the transport module lives in this skill's own scripts/
9
- // directory (plan 01.1-04 relocated it from the now-retired `vice-session`
10
- // skill).
11
- import { serverInfo, activeInstance, type ServerInfoPayload, type ToolInfo } from "./vice.ts";
12
- import { writeFileSync, chmodSync, renameSync } from "node:fs";
13
- import { fileURLToPath } from "node:url";
14
- import { basename, dirname, join, resolve } from "node:path";
15
-
16
- const HERE = dirname(fileURLToPath(import.meta.url));
17
- const DEFAULT_MANIFEST_PATH = join(HERE, "tools-manifest.json");
18
-
19
- // The hand-authored stock surface's own filename (stock-dispatch.ts's
20
- // tools-manifest.stock.json, plan 02-09) -- named here ONLY so
21
- // writeManifestAtomic() below can refuse to ever target it, never as
22
- // something this file writes.
23
- const STOCK_MANIFEST_BASENAME = "tools-manifest.stock.json";
24
-
25
- function manifestPath(): string {
26
- return process.env.VICE_TOOLS_MANIFEST
27
- ? resolve(process.env.VICE_TOOLS_MANIFEST)
28
- : DEFAULT_MANIFEST_PATH;
29
- }
30
-
31
- export interface ToolsManifest {
32
- generated_at: string;
33
- endpoint: string;
34
- tools: ToolInfo[];
35
- }
36
-
37
- /** Write `manifest` to `path` via the tmp sibling -> restrict-mode -> content
38
- * -> rename sequence (01.6.1-06 Decision 1), copied verbatim in shape from
39
- * vice-broker.mts's writeBrokerRecord(). The tmp file is created EMPTY and
40
- * mode-tightened to 0o600 BEFORE any content reaches it, then renamed into
41
- * place -- so a crash or a full disk mid-write can only ever leave a stray
42
- * tmp sibling behind, never a partial or empty file at the real path. This
43
- * is what makes this module's own long-standing promise ("a partial or
44
- * empty manifest is never written over a good one on failure") true against
45
- * a crash, not only against a rejected handshake -- the rejected-handshake
46
- * half of that guarantee already held (the early return below, unchanged
47
- * from the original), but a crash between "open the file" and "write the
48
- * content" could previously still truncate a good manifest to nothing.
49
- *
50
- * T-02-31 (02-09): this function regenerates the manifest from a LIVE fork
51
- * host's tools/list, so its output path must NEVER be
52
- * tools-manifest.stock.json -- that file is the hand-authored, separately
53
- * committed stock surface (D-07/D-09), and this refresh path overwriting it
54
- * with the fork's full tool list would silently destroy the trimming that
55
- * surface exists to enforce. This assertion makes that impossible by
56
- * accident; a future `--stock` flag pointing this generator at the stock
57
- * file has to edit this line deliberately, not merely change a default. */
58
- function writeManifestAtomic(path: string, manifest: ToolsManifest): void {
59
- if (basename(path) === STOCK_MANIFEST_BASENAME) {
60
- throw new Error(
61
- `refresh-manifest: refusing to write ${STOCK_MANIFEST_BASENAME} -- this generator regenerates the fork's ` +
62
- `manifest from a live host and must never overwrite the hand-authored stock surface (D-07/D-09)`
63
- );
64
- }
65
- const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
66
- writeFileSync(tmpPath, "");
67
- chmodSync(tmpPath, 0o600);
68
- writeFileSync(tmpPath, JSON.stringify(manifest, null, 2) + "\n");
69
- renameSync(tmpPath, path);
70
- }
71
-
72
- /** Parses no arguments (there are none), performs the host handshake, and
73
- * writes the manifest -- or leaves the existing one untouched and sets a
74
- * non-zero exit code if the handshake fails. Exported (01.6.1-06 Decision 2)
75
- * so this can be driven directly in a test without spawning a process; the
76
- * bottom-of-module entry-point guard below is what makes that safe -- import
77
- * alone must never perform a handshake or write anything. */
78
- export async function main(): Promise<void> {
79
- const path = manifestPath();
80
-
81
- // serverInfo() performs the host handshake (initialize + tools/list) and
82
- // already strips DENY_LIST names before returning (vice.ts's own
83
- // documented choke point) -- vice-proxy.mjs's tools/list handler applies
84
- // the SAME filter again at read time, so a snapshot generated by any other
85
- // means is still safe, but this refresh path inherits the omission for
86
- // free.
87
- let info: ServerInfoPayload;
88
- try {
89
- info = (await serverInfo()) as ServerInfoPayload;
90
- } catch (e) {
91
- const message = e instanceof Error ? e.message : String(e);
92
- console.error(
93
- `refresh-manifest: could not reach the host VICE MCP server (${message}) -- ` +
94
- `manifest at ${path} left UNCHANGED. A partial or empty manifest is never written over a good one on ` +
95
- `failure -- whether the failure is a rejected handshake (this path) or a crash mid-write, the latter now ` +
96
- `also covered by the tmp-sibling -> restrict-mode -> content -> rename sequence below.`
97
- );
98
- process.exitCode = 1;
99
- return;
100
- }
101
-
102
- const tools = Array.isArray(info?.tools) ? info.tools : [];
103
- const manifest: ToolsManifest = {
104
- generated_at: new Date().toISOString(),
105
- endpoint: activeInstance().url,
106
- tools,
107
- };
108
-
109
- writeManifestAtomic(path, manifest);
110
- console.log(`refresh-manifest: wrote ${tools.length} tool${tools.length === 1 ? "" : "s"} to ${path}`);
111
- }
112
-
113
- // -------------------------------------------------------------------- CLI
114
- // Guarded exactly like vice-broker.mts's own entry point: importing this
115
- // module (e.g. from a test) must never itself perform a handshake or write
116
- // anything. process.exitCode, never process.exit(), so any pending I/O
117
- // flushes first.
118
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
119
- main().catch((e: unknown) => {
120
- const detail = e instanceof Error && e.stack ? e.stack : String(e);
121
- console.error(`refresh-manifest: unexpected error -- ${detail}`);
122
- process.exitCode = 1;
123
- });
124
- }