@henols/vice-mcp 0.1.12 → 0.2.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/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/package.json +12 -1
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +431 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +49 -6
package/stock-connect.ts
CHANGED
|
@@ -30,7 +30,17 @@
|
|
|
30
30
|
// a connect() that silently sits unserviced in the backlog (PROTO-08,
|
|
31
31
|
// D-13, vice-broker-client.ts's own MonitorOwnershipError header
|
|
32
32
|
// comment).
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
ViceMonitorClient,
|
|
35
|
+
CommandType,
|
|
36
|
+
ErrorCode,
|
|
37
|
+
StockProtocolError,
|
|
38
|
+
StockFramingError,
|
|
39
|
+
StockDesyncError,
|
|
40
|
+
StockResponseMismatchError,
|
|
41
|
+
StockConnectionClosedError,
|
|
42
|
+
StockRequestTimeoutError,
|
|
43
|
+
} from "./stock-protocol.ts";
|
|
34
44
|
import { readCapabilityRecord, writeCapabilityRecord, type CapabilityDeps } from "./backend-detect.mts";
|
|
35
45
|
import { MachineRestartedError, ViceError, readEpoch, type EpochResult } from "./vice.ts";
|
|
36
46
|
import {
|
|
@@ -74,9 +84,12 @@ export interface StockCapabilities {
|
|
|
74
84
|
* request body but VICE stores it internally in a uint16_t
|
|
75
85
|
* (monitor_binary.c:1492) -- any count >= 65536 wraps silently server-side.
|
|
76
86
|
* Clamp client-side rather than ever sending an unclamped value. This
|
|
77
|
-
* handshake
|
|
78
|
-
*
|
|
79
|
-
*
|
|
87
|
+
* handshake now probes with count 1, never 0 -- real VICE
|
|
88
|
+
* (monitor_binary.c:1491-1497) rejects `requested_count < 1` with
|
|
89
|
+
* InvalidParameter (0x81), so 0 is a malformed request, not a valid "give me
|
|
90
|
+
* the newest entry" request (confirmed live, see 07-RESEARCH.md Pitfall 8).
|
|
91
|
+
* The clamp remains a general guard for any future caller of this same
|
|
92
|
+
* request shape. */
|
|
80
93
|
const CPU_HISTORY_MAX_COUNT = 65535;
|
|
81
94
|
|
|
82
95
|
/** WR-02/WR-12: bounded at BOTH ends, and against non-finite input. A bare
|
|
@@ -93,26 +106,87 @@ export function clampCpuHistoryCount(count: number): number {
|
|
|
93
106
|
return Math.min(Math.max(Math.trunc(count), 0), CPU_HISTORY_MAX_COUNT);
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
/** Sends CPUHISTORY_GET (0x86) with memspace=main and a
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
/** Sends CPUHISTORY_GET (0x86) with memspace=main and a clamped count of 1
|
|
110
|
+
* (the minimum real VICE accepts -- monitor_binary.c:1491-1497 rejects
|
|
111
|
+
* `requested_count < 1` with InvalidParameter, confirmed live in
|
|
112
|
+
* 07-RESEARCH.md Pitfall 8; count=1 is also probe-binmon.mjs's own
|
|
113
|
+
* already-verified value), and maps the wire outcome to
|
|
114
|
+
* CpuHistoryCapability's three-way answer -- 0x00 OK -> "available", 0x83
|
|
115
|
+
* INVALID_TYPE -> "absent" (the pre-3.10 case), 0x8f CMD_FAILURE ->
|
|
116
|
+
* "not_compiled_in" (the distinct compiled-without-support case), 0x81
|
|
117
|
+
* INVALID_PARAMETER -> "absent" (with a well-formed count=1 request, this
|
|
118
|
+
* code can no longer originate from this client's own malformed count -- it
|
|
119
|
+
* means the connected build's CPUHISTORY_GET rejected a minimal well-formed
|
|
120
|
+
* request, so the route is unusable on this build; collapsing to "absent"
|
|
121
|
+
* rather than a fourth capability value matches resolveCapabilities()'s own
|
|
122
|
+
* cache comment, which already documents "absent" and "not_compiled_in" as
|
|
123
|
+
* both meaning "never attempt CPUHISTORY_GET again", differing only in
|
|
124
|
+
* why).
|
|
125
|
+
*
|
|
126
|
+
* CR-01 (07-VERIFICATION.md gap 1, live-reproduced against a genuine VICE
|
|
127
|
+
* >= 3.10 build): a build that DOES support the opcode answers with a real,
|
|
128
|
+
* well-formed CPUHISTORY_GET frame -- `StockFramingError | response type
|
|
129
|
+
* 0x86 body is 52 byte(s), needs at least 65` -- that this client's parser
|
|
130
|
+
* cannot yet decode (the layout fix is a separate plan, 07-12). A decode
|
|
131
|
+
* failure is not a wire error code, so it is NOT a StockProtocolError, and
|
|
132
|
+
* before this fix it fell through to the final `throw err` below and killed
|
|
133
|
+
* the entire handshake. The rule this function now enforces: a *decode*
|
|
134
|
+
* failure (StockFramingError, StockDesyncError, StockResponseMismatchError
|
|
135
|
+
* -- the monitor answered with SOME frame, so the opcode demonstrably
|
|
136
|
+
* exists, but this client could not read the answer) resolves to "absent",
|
|
137
|
+
* exactly like the pre-3.10 case above, because "absent" already means
|
|
138
|
+
* "never attempt CPUHISTORY_GET again" to every consumer. A *transport*
|
|
139
|
+
* failure (StockConnectionClosedError, StockRequestTimeoutError, or any
|
|
140
|
+
* other unrecognized error) is a different thing entirely -- the connection
|
|
141
|
+
* itself is unusable, not just this one opcode -- and must still propagate
|
|
142
|
+
* unchanged so stockConnect()'s own retry/restart posture can see it. Do
|
|
143
|
+
* NOT widen this into a bare `catch { return "absent"; }`: that would also
|
|
144
|
+
* swallow a dead socket and hand back a capability answer for a session
|
|
145
|
+
* nobody could actually establish.
|
|
146
|
+
*
|
|
147
|
+
* CR-01 (07-REVIEW.md re-review): the three decode classes above answer a
|
|
148
|
+
* capability VALUE but they are NOT an observation of the build's
|
|
149
|
+
* capability -- the server answered, this client could not read the answer.
|
|
150
|
+
* So the probe now reports its PROVENANCE alongside the value, and
|
|
151
|
+
* resolveCapabilities() persists only `"wire"`-sourced answers. Persisting a
|
|
152
|
+
* decode failure wrote `cpuHistoryAvailable: false` to a record whose only
|
|
153
|
+
* invalidation key was the VICE version quad, so a single transient desync
|
|
154
|
+
* -- or any parser bug -- permanently disabled Route A for that binary and
|
|
155
|
+
* no shipped parser fix could ever invalidate it. Keep the two concepts
|
|
156
|
+
* separate: `capability` is what this process should do next, `source` is
|
|
157
|
+
* whether anyone actually established it. */
|
|
158
|
+
interface CpuHistoryProbe {
|
|
159
|
+
/** What this process should do about Route A, right now. */
|
|
160
|
+
capability: CpuHistoryCapability;
|
|
161
|
+
/** `"wire"` -- the monitor's own answer, decoded: a fact about this build,
|
|
162
|
+
* safe to persist. `"decode_failure"` -- the monitor answered with a frame
|
|
163
|
+
* this client could not decode: usable in-process, NEVER persistable. */
|
|
164
|
+
source: "wire" | "decode_failure";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function probeCpuHistory(client: ViceMonitorClient): Promise<CpuHistoryProbe> {
|
|
168
|
+
const count = clampCpuHistoryCount(1);
|
|
105
169
|
const body = Buffer.alloc(5);
|
|
106
170
|
body[0] = 0x00; // memspace: main
|
|
107
171
|
body.writeUInt32LE(count, 1);
|
|
108
172
|
try {
|
|
109
173
|
await client.send(CommandType.CpuHistoryGet, body);
|
|
110
|
-
return "available";
|
|
174
|
+
return { capability: "available", source: "wire" };
|
|
111
175
|
} catch (err) {
|
|
112
176
|
if (err instanceof StockProtocolError) {
|
|
113
|
-
if (err.errorCode === ErrorCode.InvalidType) return "absent"; // 0x83 -- opcode absent, pre-3.10
|
|
114
|
-
if (err.errorCode === ErrorCode.CmdFailure) return "not_compiled_in"; // 0x8f -- compiled without support
|
|
177
|
+
if (err.errorCode === ErrorCode.InvalidType) return { capability: "absent", source: "wire" }; // 0x83 -- opcode absent, pre-3.10
|
|
178
|
+
if (err.errorCode === ErrorCode.CmdFailure) return { capability: "not_compiled_in", source: "wire" }; // 0x8f -- compiled without support
|
|
179
|
+
if (err.errorCode === ErrorCode.InvalidParameter) return { capability: "absent", source: "wire" }; // 0x81 -- minimal well-formed request rejected by this build
|
|
115
180
|
}
|
|
181
|
+
// CR-01: the opcode answered with a real frame this client could not
|
|
182
|
+
// decode -- the opcode EXISTS, Route A is merely unusable by THIS
|
|
183
|
+
// client. An in-process "absent" is right; a persisted one is not.
|
|
184
|
+
if (err instanceof StockFramingError) return { capability: "absent", source: "decode_failure" };
|
|
185
|
+
if (err instanceof StockDesyncError) return { capability: "absent", source: "decode_failure" };
|
|
186
|
+
if (err instanceof StockResponseMismatchError) return { capability: "absent", source: "decode_failure" };
|
|
187
|
+
// Anything else -- notably StockConnectionClosedError and
|
|
188
|
+
// StockRequestTimeoutError -- is a transport failure, not a capability
|
|
189
|
+
// answer, and must still fail the handshake.
|
|
116
190
|
throw err;
|
|
117
191
|
}
|
|
118
192
|
}
|
|
@@ -145,11 +219,54 @@ async function resolveCapabilities(client: ViceMonitorClient, versionQuad: strin
|
|
|
145
219
|
}
|
|
146
220
|
}
|
|
147
221
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
222
|
+
// CR-01: probeCpuHistory() is guarded against decode failures, but this
|
|
223
|
+
// call site guards against anything ELSE this function is not prepared to
|
|
224
|
+
// interpret -- so no uninterpreted error can reach stockConnect()'s fatal
|
|
225
|
+
// catch. Real transport/instance conditions (a closed socket, a timed-out
|
|
226
|
+
// request, or a proven machine restart) still rethrow, because those mean
|
|
227
|
+
// the handshake itself must fail, not just this one capability. Anything
|
|
228
|
+
// unclassifiable degrades to "absent" WITHOUT writing a capability cache
|
|
229
|
+
// record -- a capability answer nobody could actually establish must
|
|
230
|
+
// never be persisted for the next connect.
|
|
231
|
+
//
|
|
232
|
+
// CR-01 (07-REVIEW.md re-review) narrows this catch in one direction and
|
|
233
|
+
// fixes the write below. A StockProtocolError this module does not
|
|
234
|
+
// classify is rethrown rather than laundered into a capability answer:
|
|
235
|
+
// the set that reaches here includes InvalidApiVersion (0x82), and an
|
|
236
|
+
// api-version rejection is the one condition step 3 of the handshake
|
|
237
|
+
// exists to make FATAL -- reporting it as "Route A is absent" hides a
|
|
238
|
+
// connection that is not actually usable. The residual catch-all below
|
|
239
|
+
// therefore covers only genuinely untyped failures (a bug in this client,
|
|
240
|
+
// not a wire answer), which still degrade without persisting.
|
|
241
|
+
let probe: CpuHistoryProbe;
|
|
242
|
+
try {
|
|
243
|
+
probe = await probeCpuHistory(client);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (err instanceof StockConnectionClosedError || err instanceof StockRequestTimeoutError || err instanceof MachineRestartedError) {
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
if (err instanceof StockProtocolError) {
|
|
249
|
+
// An unclassified WIRE error code. probeCpuHistory() already maps
|
|
250
|
+
// every code that means "Route A is unusable"; anything else is a
|
|
251
|
+
// condition this client does not understand, and a handshake that
|
|
252
|
+
// cannot understand the server's refusal must fail, not guess.
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
console.error(`resolveCapabilities: unclassified error resolving CPUHISTORY_GET capability, falling back to "absent": ${String(err)}`);
|
|
256
|
+
return { cpuHistory: "absent" };
|
|
257
|
+
}
|
|
258
|
+
// CR-01: persist ONLY a wire-sourced answer. A decode failure means the
|
|
259
|
+
// opcode exists and this client could not read the reply -- writing
|
|
260
|
+
// `cpuHistoryAvailable: false` for it would pin Route A off for this
|
|
261
|
+
// binary until VICE itself is upgraded, because the record's only other
|
|
262
|
+
// invalidation key is the version quad. writeCapabilityRecord() stamps
|
|
263
|
+
// CAPABILITY_SCHEMA_VERSION so a shipped parser change invalidates
|
|
264
|
+
// records written by the older parser; that is the second half of the
|
|
265
|
+
// same fix and cannot substitute for this one.
|
|
266
|
+
if (deps.binPath && probe.source === "wire") {
|
|
267
|
+
writeCap(deps.binPath, { versionQuad, cpuHistoryAvailable: probe.capability === "available" }, { supervisorDir: deps.supervisorDir });
|
|
151
268
|
}
|
|
152
|
-
return { cpuHistory };
|
|
269
|
+
return { cpuHistory: probe.capability };
|
|
153
270
|
}
|
|
154
271
|
|
|
155
272
|
// ---------------------------------------------------------------------------
|
package/stock-derived.ts
CHANGED
|
@@ -18,21 +18,42 @@
|
|
|
18
18
|
// hazard. There, NOT translating an emulator-side path is the bug -- four
|
|
19
19
|
// tools carry a filename stock VICE opens on the HOST, and stock-paths.ts's
|
|
20
20
|
// whole job is making sure that translation happens. HERE, translating a
|
|
21
|
-
// CLIENT-SIDE-DERIVED path is the bug:
|
|
22
|
-
//
|
|
23
|
-
// tool sitting behind call() would receive
|
|
24
|
-
// them INSIDE THE CONTAINER (ROADMAP Phase 4
|
|
25
|
-
// derived-tool seam this file anchors exists so a
|
|
26
|
-
//
|
|
27
|
-
// rewriteArguments() at all.
|
|
21
|
+
// CLIENT-SIDE-DERIVED path is the bug: `forwardToVice()` calls
|
|
22
|
+
// `rewriteArguments(args, name)` itself, before it ever delegates to
|
|
23
|
+
// `call()` -- so a derived tool sitting behind `call()` would receive
|
|
24
|
+
// HOST-translated paths and act on them INSIDE THE CONTAINER (ROADMAP Phase 4
|
|
25
|
+
// Notes, CLAUDE.md). The derived-tool seam this file anchors exists so a
|
|
26
|
+
// derived tool's handler is reached BEFORE `forwardToVice()` runs
|
|
27
|
+
// `rewriteArguments()` at all.
|
|
28
28
|
//
|
|
29
|
-
// SECOND CONSUMER
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
29
|
+
// SECOND CONSUMER: `gatherWedgeEvidence()` in vice-proxy.ts calls
|
|
30
|
+
// `rewriteArguments()` itself, for `vice_display_screenshot`. On the stock
|
|
31
|
+
// backend, PERFORMING that translation becomes the bug -- its own comment
|
|
32
|
+
// inverts. Phase 5 criterion 5 owns that fix; it is deliberately NOT
|
|
33
|
+
// repointed here.
|
|
34
|
+
//
|
|
35
|
+
// CITATION STYLE, deliberate (07-REVIEW.md WR-12): the two call sites above
|
|
36
|
+
// are named by SYMBOL (`forwardToVice()`'s own `rewriteArguments(args, name)`
|
|
37
|
+
// call; `gatherWedgeEvidence()`'s own call), never by line number. This header
|
|
38
|
+
// used to cite `vice-proxy.ts:2773`, `:1343` and `:1367`; by the time WR-12 was
|
|
39
|
+
// filed the real lines were 2889, 1344 and 1368 -- 07-16 edited vice-proxy.ts
|
|
40
|
+
// without re-verifying them, and this header is the in-tree statement of
|
|
41
|
+
// CLAUDE.md's derived-tool constraint, so its citations are load-bearing. Line
|
|
42
|
+
// numbers in this file drift every phase; grep for the symbol instead.
|
|
43
|
+
//
|
|
44
|
+
// WHY THE HAZARD IS UNREACHABLE ON STOCK TODAY -- restated in terms of what
|
|
45
|
+
// actually enforces it (WR-12 again; the previous reason had gone stale):
|
|
46
|
+
// `buildBackendAwareTool()` routes EVERY stock tool call to
|
|
47
|
+
// `dispatchStock()`, so `forwardToVice()`, `handleRecycle()` and
|
|
48
|
+
// `gatherWedgeEvidence()` are reachable only on the fork arm. That is a
|
|
49
|
+
// structural property of the registration, not of any tool's name. The reason
|
|
50
|
+
// this header used to give -- "handleRecycle() is backend-aware and refused by
|
|
51
|
+
// name after CR-07, and vice_display_screenshot does not exist on stock until
|
|
52
|
+
// Phase 5" -- is now WRONG on its first clause: Phase 7 implemented
|
|
53
|
+
// `vice_recycle` on stock (`handleRecycleStock`) and registered it in
|
|
54
|
+
// STOCK_DERIVED_TOOLS below. The conclusion held; the stated reason did not,
|
|
55
|
+
// which is worse than no reason for a constraint a future reader will
|
|
56
|
+
// re-derive.
|
|
36
57
|
//
|
|
37
58
|
// WHAT NOT TO DO:
|
|
38
59
|
// - Never `import` hostpath.ts from this file, or from any module listed
|
|
@@ -50,6 +71,11 @@
|
|
|
50
71
|
// - Never re-implement session acquisition here -- withDerivedTool() in
|
|
51
72
|
// stock-dispatch.ts delegates to the one ensureStockSession() the 25
|
|
52
73
|
// direct tools use.
|
|
74
|
+
// - Never add a name to STOCK_DERIVED_TOOLS without adding its module to
|
|
75
|
+
// package.json's files[] in the SAME commit (Phase 3 Rule 2) --
|
|
76
|
+
// withDerivedTool() refuses on this set, and a declared-but-unshipped
|
|
77
|
+
// module fails at module load in the published tarball rather than at
|
|
78
|
+
// dispatch.
|
|
53
79
|
import { ViceError, type ViceErrorOptions } from "./vice.ts";
|
|
54
80
|
import type { StockToolResult } from "./stock-handler.ts";
|
|
55
81
|
import type { StockDispatchDeps } from "./stock-dispatch.ts";
|
|
@@ -76,6 +102,18 @@ export class DerivedToolError extends ViceError {
|
|
|
76
102
|
|
|
77
103
|
export const STOCK_DERIVED_TOOLS: ReadonlySet<string> = new Set([
|
|
78
104
|
"vice_disassemble", // Phase 4, DERIV-07's first consumer (04-05) -- client-side 6510 disassembler
|
|
105
|
+
"vice_memory_search", // Phase 5, DERIV-01
|
|
106
|
+
"vice_memory_compare", // Phase 5, DERIV-01
|
|
107
|
+
"vice_symbols_load", // Phase 5, DERIV-04
|
|
108
|
+
"vice_symbols_lookup", // Phase 5, DERIV-04
|
|
109
|
+
"vice_vicii_get_state", // Phase 5, DERIV-05
|
|
110
|
+
"vice_cia_get_state", // Phase 5, DERIV-05
|
|
111
|
+
"vice_sprite_get", // Phase 5, DERIV-06
|
|
112
|
+
"vice_sprite_inspect", // Phase 5, DERIV-06
|
|
113
|
+
"vice_cycles_stopwatch", // Phase 7, TIME-01
|
|
114
|
+
"vice_run_until", // Phase 7, TIME-02
|
|
115
|
+
"vice_diagnose", // Phase 7, TIME-04
|
|
116
|
+
"vice_recycle", // Phase 7, TIME-04
|
|
79
117
|
]);
|
|
80
118
|
|
|
81
119
|
/**
|
|
@@ -85,6 +123,21 @@ export const STOCK_DERIVED_TOOLS: ReadonlySet<string> = new Set([
|
|
|
85
123
|
* threaded down for anything the handler needs beyond the session (matching
|
|
86
124
|
* StockSessionHandler's own `deps` parameter).
|
|
87
125
|
*
|
|
126
|
+
* THE ONE DECLARED EXCEPTION (Phase 7, plan 07-09): `vice_diagnose`
|
|
127
|
+
* (`handleDiagnoseStock`, stock-diagnose.ts) is registered with
|
|
128
|
+
* `needsSession: false` yet DOES reach the wire -- it calls
|
|
129
|
+
* `ensureStockSession(deps)` itself, inside its own try/catch, rather than
|
|
130
|
+
* through withDerivedTool()'s session preamble. This is deliberate, not a
|
|
131
|
+
* contradiction of the rule above: a thrown MonitorOwnershipError or
|
|
132
|
+
* MachineRestartedError during acquisition must become one of
|
|
133
|
+
* vice_diagnose's own five VERDICTS (`monitor_held_elsewhere`, `restarted`)
|
|
134
|
+
* rather than withDerivedTool()'s generic refusal text -- the exact string
|
|
135
|
+
* those verdicts exist to replace. Every other `needsSession: false` handler
|
|
136
|
+
* in STOCK_DERIVED_TOOLS still structurally cannot reach the wire; this is
|
|
137
|
+
* the one named handler that opts out of that guarantee, and it does so by
|
|
138
|
+
* calling the SAME ensureStockSession() every session-needing handler
|
|
139
|
+
* calls, never a lighter-weight variant of it.
|
|
140
|
+
*
|
|
88
141
|
* `StockDispatchDeps` is imported `type`-only from stock-dispatch.ts -- under
|
|
89
142
|
* verbatimModuleSyntax an `import type` erases completely at compile time,
|
|
90
143
|
* so it creates no runtime cycle even though stock-dispatch.ts imports THIS
|