@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.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- 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
|
+
}
|