@henols/vice-mcp 0.2.2 → 0.2.3

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 (77) 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 +1674 -99
  5. package/anno-enum-gen.ts +416 -30
  6. package/anno-export-asm.ts +1175 -89
  7. package/anno-graphics.ts +338 -0
  8. package/anno-hazard-report.ts +1367 -0
  9. package/anno-import.ts +495 -0
  10. package/anno-join.ts +480 -0
  11. package/anno-provenance-ledger.ts +472 -0
  12. package/anno-register.ts +159 -0
  13. package/anno-store-export.ts +661 -0
  14. package/anno-store.ts +518 -2
  15. package/anno-tools.ts +1169 -16
  16. package/anno-types.ts +275 -2
  17. package/backend-detect.mts +124 -312
  18. package/build.ts +3 -1
  19. package/capture-predicate.ts +597 -0
  20. package/channel-lock.ts +349 -0
  21. package/evid-ingest.ts +217 -0
  22. package/evid-reconcile.ts +316 -0
  23. package/host-tool-client.ts +430 -0
  24. package/incident-record.ts +23 -12
  25. package/install-resources.ts +29 -13
  26. package/memmap-lookup.ts +285 -0
  27. package/package.json +27 -8
  28. package/prg-image.ts +1 -2
  29. package/repo-root.ts +87 -3
  30. package/resources/backend-detect.mjs +98 -236
  31. package/resources/broker-control.mjs +189 -16
  32. package/resources/broker-epoch.mjs +1 -1
  33. package/resources/broker-kill.mjs +8 -2
  34. package/resources/broker-launch.mjs +365 -210
  35. package/resources/broker-state.mjs +64 -18
  36. package/resources/container-guard.mjs +1 -1
  37. package/resources/ghidra-project.mjs +790 -0
  38. package/resources/host-tool.mjs +2561 -0
  39. package/resources/vice-broker.mjs +330 -184
  40. package/resources/vice-launcher.sh +127 -9
  41. package/stock-address.ts +1 -1
  42. package/stock-condition.ts +1 -1
  43. package/stock-connect.ts +9 -5
  44. package/stock-derived.ts +29 -37
  45. package/stock-diagnose.ts +200 -36
  46. package/stock-dispatch.ts +179 -77
  47. package/stock-handler.ts +1 -1
  48. package/stock-paths.ts +18 -14
  49. package/stock-petscii.ts +1 -1
  50. package/stock-protocol.ts +1 -1
  51. package/stock-recycle.ts +83 -2
  52. package/stock-reproducible-run.ts +811 -0
  53. package/stock-run-until.ts +100 -1
  54. package/stock-symbols.ts +4 -4
  55. package/stock-timing.ts +1 -1
  56. package/stop-oracle.ts +167 -0
  57. package/text-capability-probe.ts +660 -0
  58. package/text-connect.ts +157 -0
  59. package/text-protocol.ts +810 -0
  60. package/text-tools.ts +778 -0
  61. package/textmon-backtrace.ts +385 -0
  62. package/textmon-cpuhistory.ts +335 -0
  63. package/textmon-memmap.ts +494 -0
  64. package/textmon-profile.ts +458 -0
  65. package/textmon-registers.ts +748 -0
  66. package/tools-manifest.stock.json +864 -3
  67. package/vice-broker-client.ts +189 -42
  68. package/vice-errors.ts +268 -0
  69. package/vice-proxy.ts +339 -2144
  70. package/vsf-slice.ts +640 -0
  71. package/anno-d64.ts +0 -310
  72. package/capability-registry.ts +0 -390
  73. package/refresh-manifest.ts +0 -124
  74. package/tools-manifest.json +0 -1223
  75. package/vice-probe.ts +0 -278
  76. package/vice-sync.ts +0 -336
  77. package/vice.ts +0 -772
package/vice-errors.ts ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ // vice-errors.ts
3
+ //
4
+ // WHY THIS FILE EXISTS: the shared error hierarchy and lease-state
5
+ // accessors, used by BOTH the fork transport's former callers and the
6
+ // surviving stock lease-acquisition path. Split out of vice.ts (removing
7
+ // the forked VICE MCP backend) because buildHeldLease() in vice-proxy.ts
8
+ // reads activeInstance() on EVERY tool call, on BOTH backends, through
9
+ // ensureBrokerLease() -- deleting vice.ts wholesale would have broken stock
10
+ // lease acquisition, not just the fork's own transport. See
11
+ // docs/stock-hard-losses.md for the stock backend's own accepted hardware
12
+ // limitations, a related but separate concern.
13
+ //
14
+ // WHAT NOT TO DO:
15
+ // - The fork transport (call(), rpc(), ensureInitialized(), withReconnect(),
16
+ // the outer-name refusal array and its refusal-message builder, and the
17
+ // session-identity apparatus: SessionInfo, beginSession(),
18
+ // sessionReconnects(), lastToolCall(), assertSameMachine()) was deleted
19
+ // along with vice.ts once it had no remaining stock-path consumer. Never
20
+ // reintroduce any of it here -- this module is backend-agnostic by
21
+ // design, and none of that apparatus has a place to come back to.
22
+ import { resolve, join } from "node:path";
23
+ import { readFileSync } from "node:fs";
24
+
25
+ import { supervisorDir } from "./repo-root.ts";
26
+ import { isInsideContainer, type ContainerGuardDeps } from "./container-guard.mts";
27
+
28
+ // The address of the host machine -- the ONE definition every consumer that
29
+ // needs to build a host-facing URL from a bare port reads, instead of each
30
+ // inlining its own `process.env.VICE_MCP_HOST || "host.docker.internal"`
31
+ // copy (there were three such copies before vice.ts's own predecessor
32
+ // consolidated them). A FUNCTION, not a module-level constant, so it stays
33
+ // sensitive to a runtime env override -- some tests mutate
34
+ // process.env.VICE_MCP_HOST across cases within the SAME process, which a
35
+ // constant captured once at import time would silently stop honouring.
36
+ //
37
+ // CONTAINER-AWARE (2026-08-05, developer instruction). The default was
38
+ // previously the bare literal "host.docker.internal", which is correct in
39
+ // exactly ONE of the two environments this code runs in: it is a
40
+ // Docker-provided alias, published into the container by
41
+ // .devcontainer/devcontainer.json's `--add-host=host.docker.internal:host-gateway`,
42
+ // and it does not resolve on the host at all. Host-bound modules genuinely
43
+ // do consume this tree (vice-broker.mts references vice-broker-client), so
44
+ // a single unconditional answer was wrong for one side by construction.
45
+ //
46
+ // Detection is delegated to container-guard.mts's isInsideContainer() rather
47
+ // than re-derived -- see that function's own comment for why a second
48
+ // detector is a bug waiting to happen here.
49
+ //
50
+ // Non-container branch is 127.0.0.1 rather than "localhost" DELIBERATELY:
51
+ // "localhost" may resolve to ::1 first, and the broker binds 0.0.0.0 --
52
+ // IPv4-only (broker-control.mts's documented bind), so an IPv6 loopback
53
+ // connect would be refused by a listener that is in fact running. An
54
+ // explicit IPv4 literal cannot pick the wrong family. It also classifies as
55
+ // `loopback` under vice-broker-client.ts's classifyConnectHost(), which
56
+ // that resolver deliberately does NOT refuse, and is not `wildcard_bind`,
57
+ // so it does not trip the pre-connect refusal.
58
+ export function mcpHost(deps?: ContainerGuardDeps): string {
59
+ return process.env.VICE_MCP_HOST || (isInsideContainer(deps) ? "host.docker.internal" : "127.0.0.1");
60
+ }
61
+
62
+ // Where tools/vice-supervisor.sh (host-only) writes its restart epoch --
63
+ // resolved via repo-root.ts's supervisorDir() (never a fixed hop count off
64
+ // this file's own location), so the path is correct regardless of the
65
+ // caller's cwd AND regardless of how deep this file sits under the repo
66
+ // root. Overridable for tests and for anyone running the supervisor with a
67
+ // non-default VICE_SUPERVISOR_DIR. This remains the default that
68
+ // activeEpochFile below starts from.
69
+ export const EPOCH_FILE: string = process.env.VICE_EPOCH_FILE
70
+ ? resolve(process.env.VICE_EPOCH_FILE)
71
+ : join(supervisorDir(), "epoch.json");
72
+
73
+ export interface ActiveInstance {
74
+ port: number;
75
+ url: string;
76
+ epochFile: string;
77
+ pooled: boolean;
78
+ }
79
+
80
+ // -------------------------------------------------------- active instance
81
+ //
82
+ // Mutable module-level state, deliberately NOT frozen at module load:
83
+ // restart detection has to stay correct PER INSTANCE, which is impossible if
84
+ // the epoch path is fixed at import time. useInstance() below is the only
85
+ // writer; every other read goes through the functions in this file so a
86
+ // lease redirect takes effect everywhere at once -- vice.ts's rpc() reads
87
+ // its POST target through activeInstance().url, readEpoch()'s own default
88
+ // path reads activeEpochFile directly (same module), and vice.ts's
89
+ // beginSession() reads its default epoch path through
90
+ // activeInstance().epochFile. None of those three read this module's
91
+ // private state directly except readEpoch() itself, which lives here.
92
+
93
+ // LEGACY PORT DEFAULT: the pre-split fallback derived its default port by
94
+ // parsing vice.ts's DEFAULT_ENDPOINT, an HTTP-URL-shaped constant that only
95
+ // ever made sense for the fork's HTTP transport. That derivation is NOT
96
+ // carried across this split: every stock caller of activeInstance() --
97
+ // starting with buildHeldLease() in vice-proxy.ts, reached through
98
+ // ensureBrokerLease() on EVERY tool call, on every backend -- runs strictly
99
+ // AFTER a useInstance() write has already replaced this fallback (traced
100
+ // during the split: ensureBrokerLease()'s only path to buildHeldLease()
101
+ // without a prior write returns early via the VICE_MCP_URL override branch,
102
+ // which never calls activeInstance() at all). LEGACY_DEFAULT_PORT is
103
+ // therefore a plain, self-contained numeric default, kept only so this
104
+ // module never hands back an ill-typed ActiveInstance before any lease
105
+ // exists: the 6510-6599 band reserved by convention for an x64sc a human
106
+ // launches on the host for their OWN work, never the broker's own
107
+ // allocated band (6600+, DEFAULT_BASE_PORT in broker-state.mts).
108
+ const LEGACY_DEFAULT_PORT = 6510;
109
+
110
+ let activeUrl: string = `http://${mcpHost()}:${LEGACY_DEFAULT_PORT}/mcp`;
111
+ let activeEpochFile: string = EPOCH_FILE;
112
+ let activePort: number = LEGACY_DEFAULT_PORT;
113
+ // Not part of the seam redirect itself (nothing in this file reads this to
114
+ // decide behaviour) -- carried purely as identity metadata so a caller like
115
+ // an incident record can note whether an instance came from a pooled grant
116
+ // or the unpooled default, without needing its own separate channel back to
117
+ // whatever acquired the lease. Extra, optional field on useInstance()'s
118
+ // object arg -- a caller passing only {port,url,epochFile} (the documented
119
+ // minimum) still works exactly as before, defaulting to false.
120
+ let activePooled = false;
121
+
122
+ export interface UseInstanceOptions {
123
+ port: number;
124
+ url: string;
125
+ epochFile: string;
126
+ pooled?: boolean;
127
+ }
128
+
129
+ /**
130
+ * Redirect the lease seam to a specific pooled (or fallback) instance.
131
+ *
132
+ * This function is deliberately backend-agnostic: it only updates the
133
+ * lease-state accessors above. It does NOT reset the fork transport's own
134
+ * MCP handshake flag (vice.ts's `initialized`) -- that variable is owned by
135
+ * vice.ts, not this module, and the fork transport now derives its own
136
+ * "is this handshake still valid" answer by comparing
137
+ * its last-initialized URL against activeInstance().url on every call (see
138
+ * vice.ts's ensureInitialized()), rather than this function reaching across
139
+ * a module boundary to flip a flag it does not own.
140
+ */
141
+ export function useInstance({ port, url, epochFile, pooled = false }: UseInstanceOptions): void {
142
+ activeUrl = url;
143
+ activeEpochFile = epochFile;
144
+ activePort = port;
145
+ activePooled = pooled;
146
+ }
147
+
148
+ /** Read-only accessor: the instance the seam is currently pointed at. */
149
+ export function activeInstance(): ActiveInstance {
150
+ return { port: activePort, url: activeUrl, epochFile: activeEpochFile, pooled: activePooled };
151
+ }
152
+
153
+ export interface ViceErrorOptions {
154
+ code?: number | string;
155
+ data?: unknown;
156
+ }
157
+
158
+ export class ViceError extends Error {
159
+ code?: number | string;
160
+ data?: unknown;
161
+
162
+ constructor(message: string, { code, data }: ViceErrorOptions = {}) {
163
+ super(message);
164
+ this.name = "ViceError";
165
+ this.code = code;
166
+ this.data = data;
167
+ }
168
+ }
169
+
170
+ export interface MachineRestartedErrorOptions {
171
+ baselineEpoch?: number | null;
172
+ currentEpoch?: number | null;
173
+ where?: string;
174
+ lastToolCall?: string | null;
175
+ }
176
+
177
+ /**
178
+ * Thrown when a reconnect happened and the emulator's identity across that
179
+ * reconnect could not be proven -- either the epoch file shows it changed,
180
+ * or nothing (no epoch file, no surviving armed checkpoint) could prove it
181
+ * didn't. Carries the evidence a caller needs to write a void note: the
182
+ * epochs compared, where in the pipeline the check ran, and the last tool
183
+ * call attempted before detection.
184
+ */
185
+ export class MachineRestartedError extends ViceError {
186
+ baselineEpoch?: number | null;
187
+ currentEpoch?: number | null;
188
+ where?: string;
189
+ lastToolCall?: string | null;
190
+
191
+ constructor(message: string, { baselineEpoch, currentEpoch, where, lastToolCall }: MachineRestartedErrorOptions = {}) {
192
+ super(message);
193
+ this.name = "MachineRestartedError";
194
+ this.baselineEpoch = baselineEpoch;
195
+ this.currentEpoch = currentEpoch;
196
+ this.where = where;
197
+ this.lastToolCall = lastToolCall;
198
+ }
199
+ }
200
+
201
+ /** True iff `value` is a well-formed, generic JSON object -- not null, not
202
+ * an array. Matches vice.ts's own (and vice-broker.mts's / vice-broker-
203
+ * client.ts's) isPlainObject() predicate exactly -- the same narrowing
204
+ * discipline this module tree uses everywhere a parsed JSON value's fields
205
+ * are touched. Kept as a private, un-exported duplicate rather than an
206
+ * import from vice.ts, per this file's own "never import from vice.ts"
207
+ * rule above -- the same duplication-over-cross-import choice this tree
208
+ * already makes for its other small, single-purpose narrowing helpers. */
209
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
210
+ return typeof value === "object" && value !== null && !Array.isArray(value);
211
+ }
212
+
213
+ export interface EpochResult {
214
+ present: boolean;
215
+ epoch: number | null;
216
+ spawned_at: string | null;
217
+ pid: number | null;
218
+ path: string;
219
+ reason?: string;
220
+ }
221
+
222
+ /**
223
+ * Read the supervisor's epoch file. Synchronous -- this is a plain, cheap
224
+ * file read; the whole point of the epoch check is that it costs zero MCP
225
+ * traffic, unlike the checkpoint-fallback probe. NEVER throws: absence is
226
+ * normal (no supervisor running at all) and must not be an error -- the
227
+ * harness has to keep working exactly as it does today with no supervisor.
228
+ *
229
+ * Treats the file's contents as untrusted, host-written input: JSON.parse
230
+ * in try/catch, `epoch` must decode to a finite integer, unknown fields are
231
+ * ignored, and no path derived from the file's contents is ever opened.
232
+ */
233
+ export function readEpoch(path: string = activeEpochFile): EpochResult {
234
+ const absent: EpochResult = { present: false, epoch: null, spawned_at: null, pid: null, path };
235
+ let raw: string;
236
+ try {
237
+ raw = readFileSync(path, "utf8");
238
+ } catch {
239
+ return { ...absent, reason: "epoch file absent" };
240
+ }
241
+ let parsed: unknown;
242
+ try {
243
+ parsed = JSON.parse(raw);
244
+ } catch {
245
+ return { ...absent, reason: "epoch file present but not valid JSON" };
246
+ }
247
+ if (!isPlainObject(parsed) || !Number.isInteger(parsed.epoch)) {
248
+ return { ...absent, reason: 'epoch file present but its "epoch" field is not a finite integer' };
249
+ }
250
+ return {
251
+ present: true,
252
+ epoch: parsed.epoch as number,
253
+ spawned_at: typeof parsed.spawned_at === "string" ? parsed.spawned_at : null,
254
+ pid: typeof parsed.pid === "number" && Number.isFinite(parsed.pid) ? parsed.pid : null,
255
+ path,
256
+ };
257
+ }
258
+
259
+ // A single tool's discovery metadata, as returned by the fork's tools/list
260
+ // RPC. Moved here (rather than left in vice.ts) because stock-dispatch.ts
261
+ // needs the TYPE ONLY, with no other dependency on the fork transport that
262
+ // declares it.
263
+ export interface ToolInfo {
264
+ name: string;
265
+ description?: string;
266
+ inputSchema?: unknown;
267
+ [key: string]: unknown;
268
+ }