@henols/vice-mcp 0.2.1 → 0.2.2
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 -1
- package/THIRD-PARTY-NOTICES.md +1 -24
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-cli.ts +1465 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/{r2000-d64.ts → anno-d64.ts} +5 -5
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +533 -0
- package/anno-export-asm.ts +1310 -0
- package/anno-index.ts +150 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +240 -0
- package/anno-store.ts +3486 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +2111 -0
- package/anno-types.ts +1636 -0
- package/block-class.ts +201 -0
- package/build.ts +1 -1
- package/capability-registry.ts +3 -1
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/hostpath.ts +1 -1
- package/install-resources.ts +1 -1
- package/package.json +23 -17
- package/prg-image.ts +119 -0
- package/repo-root.ts +20 -5
- package/resources/broker-launch.mjs +8 -4
- package/resources/vice-launcher.sh +3 -3
- package/stock-address.ts +5 -5
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +7 -7
- package/stock-connect.ts +1 -1
- package/stock-dispatch.ts +35 -5
- package/stock-execution.ts +5 -3
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-protocol.ts +16 -11
- package/stock-registers.ts +54 -29
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +9 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +68 -46
- package/r2000-cli.ts +0 -1103
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
package/r2000-tools.ts
DELETED
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// r2000-tools.ts -- the ONE authoritative place in this repo for the curated
|
|
3
|
-
// r2000_* tool surface: which 17 of regenerator2000's 28 MCP tools this
|
|
4
|
-
// project advertises, the allow-list gate (including its D-33 batch
|
|
5
|
-
// recursion), project-path validation, and the runner that drives one
|
|
6
|
-
// r2000-mcp-client.ts session per call.
|
|
7
|
-
//
|
|
8
|
-
// WHY THIS MODULE EXISTS (D-16/D-18): the annotation store is reachable only
|
|
9
|
-
// through a CURATED subset of regenerator2000's own tool surface, not a
|
|
10
|
-
// 28-tool passthrough -- every tool here earns its place by serving one of
|
|
11
|
-
// this phase's four named criteria (see 11-05-PLAN.md's objective table).
|
|
12
|
-
// Excluded, each for a recorded reason: the TUI-shaped tools
|
|
13
|
-
// (`jump_to_address`, `get_disassembly_cursor`, `read_selected`,
|
|
14
|
-
// `toggle_splitter`) have no criterion; `undo`/`redo` are useless under
|
|
15
|
-
// D-17's per-call lifecycle (history dies with the spawned process);
|
|
16
|
-
// `get_address_details` is excluded by D-32 (see below); and
|
|
17
|
-
// `unpack_binary`, `read_region`, `search_memory`, `set_immediate_format`
|
|
18
|
-
// have no criterion in this phase.
|
|
19
|
-
//
|
|
20
|
-
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: the 17 curated
|
|
21
|
-
// `ToolDefinition`s (`R2000_TOOL_DEFINITIONS`), the allow-list
|
|
22
|
-
// (`CURATED_R2000_TOOLS`) and its enforcement (`assertCuratedTool()`,
|
|
23
|
-
// including the batch-recursion gate), the caller-supplied project-path
|
|
24
|
-
// validation (`resolveStorePath()`), and the runner (`runR2000Tool()`) that
|
|
25
|
-
// drives r2000-mcp-client.ts. No other module may hand-list a curated tool
|
|
26
|
-
// name, hand-validate an r2000 project path, or call
|
|
27
|
-
// `r2000-mcp-client.ts` directly -- `vice-proxy.ts` (plan 11-05 Task 2)
|
|
28
|
-
// imports `R2000_TOOL_DEFINITIONS`/`runR2000Tool` from here and nothing
|
|
29
|
-
// else.
|
|
30
|
-
//
|
|
31
|
-
// MEASURED: `r2000_batch_execute`'s partial-failure semantics
|
|
32
|
-
// (`handler.rs:506-542`, read at execution time against the installed
|
|
33
|
-
// regenerator2000-core-0.9.20 crate source). The batch does NOT abort on
|
|
34
|
-
// the first failing inner call -- `handle_tool_call_internal()` is called
|
|
35
|
-
// per entry inside a loop, and each outcome (`Ok`/`Err`) is pushed into a
|
|
36
|
-
// `results` array as `{"status":"success","result":...}` or
|
|
37
|
-
// `{"status":"error","error":...}`; the loop always runs to completion and
|
|
38
|
-
// the whole response is `{"content":[{"type":"text","text":<pretty-JSON
|
|
39
|
-
// results array>}]}`. This is PER-CALL status reporting inside r2000 itself
|
|
40
|
-
// -- orthogonal to (and irrelevant to) D-33's OWN refusal, which happens
|
|
41
|
-
// entirely on our side, before any request reaches the child at all: an
|
|
42
|
-
// uncurated inner name never gets the chance to report a per-call status
|
|
43
|
-
// because the whole batch is refused before the spawn.
|
|
44
|
-
//
|
|
45
|
-
// WHAT NOT TO DO, named concretely:
|
|
46
|
-
// - Never widen CURATED_R2000_TOOLS without a criterion recorded in
|
|
47
|
-
// 11-05-PLAN.md's objective table. A tool earns its place; it is not
|
|
48
|
-
// added because it happens to exist upstream.
|
|
49
|
-
// - Never let runR2000Tool() (or anything it calls) reach the VICE
|
|
50
|
-
// argument-rewriting/host-forwarding/session-establishment seams, or
|
|
51
|
-
// `call()` (vice.ts's transport seam). The r2000_* family never touches
|
|
52
|
-
// the emulator -- it is registered proxy-locally via `buildViceTool()`
|
|
53
|
-
// (plan 11-05 Task 2), which is what makes CLAUDE.md's "derived tools
|
|
54
|
-
// must be intercepted before the host-forwarding seam" constraint moot
|
|
55
|
-
// BY CONSTRUCTION for this family, not by an interception.
|
|
56
|
-
// - Never add a `tools_call`-shaped meta-tool to this surface -- that is
|
|
57
|
-
// exactly the nested-argument smuggling shape `vice.ts`'s `DENY_LIST`
|
|
58
|
-
// exists to close, and `r2000_batch_execute` is already this project's
|
|
59
|
-
// one sanctioned exception, gated by `assertCuratedTool()`'s own batch
|
|
60
|
-
// recursion below.
|
|
61
|
-
// - Never import the VICE host-path/container-path translation modules
|
|
62
|
-
// here. regenerator2000 runs container-side (D-R4, Rule A16); a project
|
|
63
|
-
// path is resolved against `repoRoot()` only. Asserted structurally by
|
|
64
|
-
// the closed host-path consumer-set test.
|
|
65
|
-
// - Never report `r2000_save_project` as persisted on the strength of its
|
|
66
|
-
// own text response -- always route it through
|
|
67
|
-
// `r2000-mcp-client.ts`'s `saveAndVerify()`.
|
|
68
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
69
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
70
|
-
|
|
71
|
-
import { repoRoot } from "./repo-root.ts";
|
|
72
|
-
import { assertLegalAcmeIdentifier } from "./r2000-acme-ident.ts";
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// The wire shapes this module produces/consumes. Deliberately NOT imported
|
|
76
|
-
// from vice-proxy.ts (that file has no exported ToolDefinition/ToolCallResult
|
|
77
|
-
// -- both are file-local types there); these are structurally identical so a
|
|
78
|
-
// value built here is interchangeable wherever vice-proxy.ts combines it with
|
|
79
|
-
// its own manifest-sourced tools.
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
export interface R2000ToolDefinition {
|
|
83
|
-
name: string;
|
|
84
|
-
description: string;
|
|
85
|
-
inputSchema: {
|
|
86
|
-
type: "object";
|
|
87
|
-
properties: Record<string, unknown>;
|
|
88
|
-
required?: string[];
|
|
89
|
-
};
|
|
90
|
-
// Structural compatibility with vice.ts's own ToolInfo (vice-proxy.ts's
|
|
91
|
-
// ToolDefinition alias), which carries this index signature -- lets
|
|
92
|
-
// vice-proxy.ts's buildViceTool() accept an R2000ToolDefinition directly,
|
|
93
|
-
// with no per-call cast at the plan 11-05 Task 2 registration site.
|
|
94
|
-
[key: string]: unknown;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface ToolCallResult {
|
|
98
|
-
content: { type: "text"; text: string }[];
|
|
99
|
-
isError: boolean;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
103
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function okText(text: string): ToolCallResult {
|
|
107
|
-
return { content: [{ type: "text", text }], isError: false };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function errText(text: string): ToolCallResult {
|
|
111
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Converts regenerator2000's own `CallToolResult` (already the same
|
|
115
|
-
* `{content:[{type,text}],isError?}` shape our own `ToolCallResult` uses,
|
|
116
|
-
* since both follow the MCP spec) into a `ToolCallResult` directly, rather
|
|
117
|
-
* than re-wrapping the whole object as a second layer of JSON text. Falls
|
|
118
|
-
* back to `okText(JSON.stringify(...))` only for a result that does not
|
|
119
|
-
* already carry a `content` array -- defensive, since every curated tool's
|
|
120
|
-
* real response has one. */
|
|
121
|
-
function toToolCallResult(result: unknown): ToolCallResult {
|
|
122
|
-
if (isPlainObject(result) && Array.isArray(result.content)) {
|
|
123
|
-
return { content: result.content as ToolCallResult["content"], isError: false };
|
|
124
|
-
}
|
|
125
|
-
return okText(JSON.stringify(result ?? null));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// D-19's shared property: every curated tool's inputSchema starts with this
|
|
130
|
-
// exact `project` property. One constant, spread into every definition below,
|
|
131
|
-
// rather than seventeen hand-typed copies that could drift from each other.
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
const PROJECT_PROPERTY = {
|
|
135
|
-
project: {
|
|
136
|
-
type: "string",
|
|
137
|
-
description:
|
|
138
|
-
"Absolute or workspace-relative path to the .regen2000proj annotation store (D-19: every " +
|
|
139
|
-
"r2000_* tool takes an explicit project path; there is no ambient session state naming the store).",
|
|
140
|
-
},
|
|
141
|
-
} as const;
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// The 64K OutOfRange defect this surface excludes (D-32) -- shared between
|
|
145
|
-
// the outer-name refusal and the batch-inner refusal so both read identically.
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
const ADDRESS_DETAILS_REFUSAL =
|
|
149
|
-
"r2000_get_address_details is not on the curated r2000_* surface (D-32): on a full 64K project " +
|
|
150
|
-
"(exactly what c64-ram-capture produces) it returns {\"type\":\"OutOfRange\"} for EVERY address, " +
|
|
151
|
-
"because handler.rs:1894's `raw_data.len() as u16` wraps 65536 to 0. Filed upstream as " +
|
|
152
|
-
"https://github.com/ricardoquesada/regenerator2000/issues/42. Its answer is a composite of " +
|
|
153
|
-
"instruction semantics, cross-references, labels, comments and block type -- all independently " +
|
|
154
|
-
"reachable through r2000_get_binary_info, r2000_get_cross_references, r2000_get_symbols, " +
|
|
155
|
-
"r2000_get_comments, r2000_get_blocks and r2000_disassemble, every one of which was measured " +
|
|
156
|
-
"working on a 64K project.";
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// The 17 curated tool definitions (D-18's objective table). Each argument
|
|
160
|
-
// shape below was obtained by driving `tools/list` against a real
|
|
161
|
-
// `regenerator2000 --mcp-server-stdio 0.9.20` child and copying its own
|
|
162
|
-
// argument shapes verbatim (never transcribed from a document), with
|
|
163
|
-
// `project` (D-19) prepended to every one.
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
export const R2000_TOOL_DEFINITIONS: readonly R2000ToolDefinition[] = [
|
|
167
|
-
{
|
|
168
|
-
name: "r2000_set_label_name",
|
|
169
|
-
description:
|
|
170
|
-
"Sets a user-defined label at a specific MOS 6502 memory address. Use this to name " +
|
|
171
|
-
"functions, variables, or jump targets to make the disassembly more readable.",
|
|
172
|
-
inputSchema: {
|
|
173
|
-
type: "object",
|
|
174
|
-
properties: {
|
|
175
|
-
...PROJECT_PROPERTY,
|
|
176
|
-
address: {
|
|
177
|
-
type: "integer",
|
|
178
|
-
description: "The memory address where the label should be set (decimal, e.g. 4096 for $1000).",
|
|
179
|
-
},
|
|
180
|
-
name: {
|
|
181
|
-
type: "string",
|
|
182
|
-
description:
|
|
183
|
-
"The label name (e.g. 'init_screen', 'loop_start'). Must be a legal ACME identifier: starts " +
|
|
184
|
-
"with a letter or underscore, followed by letters/digits/underscores only, and must not be a " +
|
|
185
|
-
"6502/6510 mnemonic (e.g. 'LDA'). An illegal name is REJECTED, never sanitized or quoted.",
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
required: ["project", "address", "name"],
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: "r2000_set_comment",
|
|
193
|
-
description:
|
|
194
|
-
"Adds a comment at a specific address. 'line' comments appear on their own line before the " +
|
|
195
|
-
"instruction (supports multi-line). 'side' comments appear inline on the same line as the " +
|
|
196
|
-
"instruction. Carrier for D-25's [confirmed-code]/[probable-code]/[confirmed-data]/" +
|
|
197
|
-
"[probable-data]/[unknown] confidence-prefix convention.",
|
|
198
|
-
inputSchema: {
|
|
199
|
-
type: "object",
|
|
200
|
-
properties: {
|
|
201
|
-
...PROJECT_PROPERTY,
|
|
202
|
-
address: { type: "integer", description: "The memory address for the comment (decimal, e.g. 4096 for $1000)." },
|
|
203
|
-
comment: { type: "string", description: "The comment text. Do not include the ';' prefix." },
|
|
204
|
-
type: {
|
|
205
|
-
type: "string",
|
|
206
|
-
enum: ["line", "side"],
|
|
207
|
-
description: "'line' = comment on its own line before the instruction. 'side' = inline comment on the same line.",
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
required: ["project", "address", "comment", "type"],
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
name: "r2000_set_data_type",
|
|
215
|
-
description:
|
|
216
|
-
"Sets the data type for a memory region. Use this to mark regions as code, bytes, addresses, " +
|
|
217
|
-
"text, split tables, etc.",
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: "object",
|
|
220
|
-
properties: {
|
|
221
|
-
...PROJECT_PROPERTY,
|
|
222
|
-
start_address: { type: "integer", description: "Start of the memory region (inclusive), decimal." },
|
|
223
|
-
end_address: { type: "integer", description: "End of the memory region (inclusive), decimal." },
|
|
224
|
-
data_type: {
|
|
225
|
-
type: "string",
|
|
226
|
-
enum: [
|
|
227
|
-
"code",
|
|
228
|
-
"byte",
|
|
229
|
-
"word",
|
|
230
|
-
"address",
|
|
231
|
-
"petscii",
|
|
232
|
-
"screencode",
|
|
233
|
-
"lo_hi_address",
|
|
234
|
-
"hi_lo_address",
|
|
235
|
-
"lo_hi_word",
|
|
236
|
-
"hi_lo_word",
|
|
237
|
-
"external_file",
|
|
238
|
-
"undefined",
|
|
239
|
-
],
|
|
240
|
-
description:
|
|
241
|
-
"code=MOS 6502 instructions; byte=raw 8-bit data (sprites, charset, tables, unknowns); " +
|
|
242
|
-
"word=16-bit LE values; address=16-bit LE pointers (creates X-Refs, use for jump " +
|
|
243
|
-
"tables/vectors); petscii=PETSCII text; screencode=Screen code text (data written to " +
|
|
244
|
-
"$0400); lo_hi_address=split address table, low bytes first then high bytes (even count " +
|
|
245
|
-
"required); hi_lo_address=split address table, high bytes first (even count required); " +
|
|
246
|
-
"lo_hi_word=split word table, low bytes first (e.g. SID freq tables); " +
|
|
247
|
-
"hi_lo_word=split word table, high bytes first; external_file=large binary blob (SID, " +
|
|
248
|
-
"bitmap, charset) to export as-is; undefined=reset region to unknown state.",
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
required: ["project", "start_address", "end_address", "data_type"],
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: "r2000_add_scope",
|
|
256
|
-
description:
|
|
257
|
-
"Adds a scope covering the specified memory range. Useful for a piece of code that is a " +
|
|
258
|
-
"routine. Starts a lexical level where all new symbols within this range are in the local " +
|
|
259
|
-
"lexical level and are accessible from outside only via explicit scope specification. Nested " +
|
|
260
|
-
"scopes are not supported.",
|
|
261
|
-
inputSchema: {
|
|
262
|
-
type: "object",
|
|
263
|
-
properties: {
|
|
264
|
-
...PROJECT_PROPERTY,
|
|
265
|
-
start_address: { type: "integer", description: "Start address of the scope (inclusive), decimal." },
|
|
266
|
-
end_address: { type: "integer", description: "End address of the scope (inclusive), decimal." },
|
|
267
|
-
},
|
|
268
|
-
required: ["project", "start_address", "end_address"],
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
name: "r2000_get_symbols",
|
|
273
|
-
description:
|
|
274
|
-
"Returns defined labels (user and/or platform) and their addresses. With no arguments " +
|
|
275
|
-
"(besides project) returns ALL symbols. Provide optional filters to narrow results: 'names' " +
|
|
276
|
-
"resolves specific label names to addresses, 'start_address'/'end_address' limits to an " +
|
|
277
|
-
"address range, 'kind' filters by label kind. Filters are combined (AND logic).",
|
|
278
|
-
inputSchema: {
|
|
279
|
-
type: "object",
|
|
280
|
-
properties: {
|
|
281
|
-
...PROJECT_PROPERTY,
|
|
282
|
-
names: {
|
|
283
|
-
type: "array",
|
|
284
|
-
items: { type: "string" },
|
|
285
|
-
description: "Optional list of label names to look up. Only symbols whose name matches one of these strings are returned. Case-sensitive.",
|
|
286
|
-
},
|
|
287
|
-
start_address: { type: "integer", description: "Optional lower bound (inclusive) of the address range to filter by (decimal)." },
|
|
288
|
-
end_address: { type: "integer", description: "Optional upper bound (inclusive) of the address range to filter by (decimal)." },
|
|
289
|
-
kind: {
|
|
290
|
-
type: "string",
|
|
291
|
-
enum: ["user", "system", "auto"],
|
|
292
|
-
description: "Optional filter to return only labels of a given kind. 'user' = user-defined labels, 'system' = predefined system labels (e.g. KERNAL, hardware registers), 'auto' = auto-generated labels (e.g. s_C000).",
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
required: ["project"],
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
name: "r2000_get_comments",
|
|
300
|
-
description:
|
|
301
|
-
"Returns user-defined comments and their addresses. Each entry has 'address' (integer), " +
|
|
302
|
-
"'type' ('line' or 'side'), and 'comment' (string). With no arguments (besides project) " +
|
|
303
|
-
"returns ALL comments. Provide optional filters to narrow results: 'addresses' returns " +
|
|
304
|
-
"comments at specific addresses, 'start_address'/'end_address' limits to an address range, " +
|
|
305
|
-
"'type' filters by comment type. Filters are combined (AND logic). D-25's confidence-prefix " +
|
|
306
|
-
"convention lives in the returned 'comment' text -- filter by prefix on the client side, or " +
|
|
307
|
-
"combine with r2000_search_disassembly.",
|
|
308
|
-
inputSchema: {
|
|
309
|
-
type: "object",
|
|
310
|
-
properties: {
|
|
311
|
-
...PROJECT_PROPERTY,
|
|
312
|
-
addresses: {
|
|
313
|
-
type: "array",
|
|
314
|
-
items: { type: "integer" },
|
|
315
|
-
description: "Optional list of specific addresses (decimal) to retrieve comments from. Only comments at these addresses are returned.",
|
|
316
|
-
},
|
|
317
|
-
start_address: { type: "integer", description: "Optional lower bound (inclusive) of the address range to filter by (decimal)." },
|
|
318
|
-
end_address: { type: "integer", description: "Optional upper bound (inclusive) of the address range to filter by (decimal)." },
|
|
319
|
-
type: { type: "string", enum: ["line", "side"], description: "Optional filter to return only 'line' comments or only 'side' comments." },
|
|
320
|
-
},
|
|
321
|
-
required: ["project"],
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
name: "r2000_get_blocks",
|
|
326
|
-
description:
|
|
327
|
-
"Returns all memory blocks with their address range and type (Code, Byte, Word, Address, " +
|
|
328
|
-
"PETSCII, Screencode, Lo/Hi Address, Hi/Lo Address, Lo/Hi Word, Hi/Lo Word, External File, " +
|
|
329
|
-
"Undefined). Respects splitters.",
|
|
330
|
-
inputSchema: {
|
|
331
|
-
type: "object",
|
|
332
|
-
properties: {
|
|
333
|
-
...PROJECT_PROPERTY,
|
|
334
|
-
block_type: { type: "string", description: "Optional filter to return only blocks of a specific type. Case-insensitive." },
|
|
335
|
-
},
|
|
336
|
-
required: ["project"],
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
name: "r2000_get_cross_references",
|
|
341
|
-
description: "Get a list of addresses that reference the given address (e.g. JSRs, JMPs, loads).",
|
|
342
|
-
inputSchema: {
|
|
343
|
-
type: "object",
|
|
344
|
-
properties: {
|
|
345
|
-
...PROJECT_PROPERTY,
|
|
346
|
-
address: { type: "integer", description: "The target address to find references to (decimal)." },
|
|
347
|
-
},
|
|
348
|
-
required: ["project", "address"],
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
name: "r2000_search_disassembly",
|
|
353
|
-
description:
|
|
354
|
-
"Search the disassembly text for a query string or regular expression. Returns a list of " +
|
|
355
|
-
"matching addresses with context (label, mnemonic, operand, comment). Searches labels, " +
|
|
356
|
-
"comments, and instructions by default; individual fields can be disabled. `max_results` is " +
|
|
357
|
-
"REQUIRED on this surface (no default): regenerator2000's own default is 50, which would " +
|
|
358
|
-
"silently truncate a full-program pass -- pass an explicit ceiling and compare the returned " +
|
|
359
|
-
"count against it to detect truncation.",
|
|
360
|
-
inputSchema: {
|
|
361
|
-
type: "object",
|
|
362
|
-
properties: {
|
|
363
|
-
...PROJECT_PROPERTY,
|
|
364
|
-
query: { type: "string", description: "The search query. Interpreted as a plain case-insensitive substring by default, or as a regex when 'use_regex' is true." },
|
|
365
|
-
max_results: { type: "integer", description: "Maximum number of matching addresses to return. No default on this surface -- always pass an explicit value." },
|
|
366
|
-
use_regex: { type: "boolean", description: "When true the query is compiled as a case-insensitive regular expression ((?i) is prepended automatically). Defaults to false." },
|
|
367
|
-
search_labels: { type: "boolean", description: "Include label names in the search. Defaults to true." },
|
|
368
|
-
search_comments: { type: "boolean", description: "Include side and line comments in the search. Defaults to true." },
|
|
369
|
-
search_instructions: { type: "boolean", description: "Include mnemonic and operand text in the search. Defaults to true." },
|
|
370
|
-
},
|
|
371
|
-
required: ["project", "query", "max_results"],
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
name: "r2000_disassemble",
|
|
376
|
-
description:
|
|
377
|
-
"Performs a control flow disassembly starting at a specific memory address, tracing " +
|
|
378
|
-
"execution paths and automatically converting identified regions to Code blocks.",
|
|
379
|
-
inputSchema: {
|
|
380
|
-
type: "object",
|
|
381
|
-
properties: {
|
|
382
|
-
...PROJECT_PROPERTY,
|
|
383
|
-
address: { type: "integer", description: "The target start address for the disassembly flow analysis (decimal)." },
|
|
384
|
-
},
|
|
385
|
-
required: ["project", "address"],
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
name: "r2000_get_binary_info",
|
|
390
|
-
description:
|
|
391
|
-
"Returns the origin address, size in bytes, target platform (e.g. 'Commodore 64'), filename, " +
|
|
392
|
-
"user-provided description, entropy of the binary (values higher than 7.5 suggest the binary " +
|
|
393
|
-
"might be compressed), and whether the binary may contain undocumented opcodes (a hint, not " +
|
|
394
|
-
"guaranteed).",
|
|
395
|
-
inputSchema: {
|
|
396
|
-
type: "object",
|
|
397
|
-
properties: { ...PROJECT_PROPERTY },
|
|
398
|
-
required: ["project"],
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
name: "r2000_create_project_enum",
|
|
403
|
-
description: "Creates a new project-specific enum definition embedded in the project file (D-21: project-local, never machine-global).",
|
|
404
|
-
inputSchema: {
|
|
405
|
-
type: "object",
|
|
406
|
-
properties: {
|
|
407
|
-
...PROJECT_PROPERTY,
|
|
408
|
-
name: { type: "string", description: "Unique alphanumeric identifier." },
|
|
409
|
-
variants: { type: "object", description: "Variant mapping where keys are numeric strings (decimal, hex 0x/$, bin 0b/%) and values are variant names." },
|
|
410
|
-
description: { type: "string", description: "Optional summary explaining the enum's purpose." },
|
|
411
|
-
},
|
|
412
|
-
required: ["project", "name", "variants"],
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
name: "r2000_update_project_enum",
|
|
417
|
-
description: "Updates or renames an existing project-specific enum.",
|
|
418
|
-
inputSchema: {
|
|
419
|
-
type: "object",
|
|
420
|
-
properties: {
|
|
421
|
-
...PROJECT_PROPERTY,
|
|
422
|
-
name: { type: "string", description: "Existing name of the enum to update." },
|
|
423
|
-
new_name: { type: "string", description: "Optional new name if renaming the enum." },
|
|
424
|
-
variants: { type: "object", description: "Optional complete updated variants mapping." },
|
|
425
|
-
description: { type: "string", description: "Optional updated summary explaining the enum's purpose." },
|
|
426
|
-
},
|
|
427
|
-
required: ["project", "name"],
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
name: "r2000_delete_project_enum",
|
|
432
|
-
description:
|
|
433
|
-
"Deletes a project-specific enum from the project. A regenerated enum set must be able to " +
|
|
434
|
-
"replace an old one (R2000-13's re-runnable generation).",
|
|
435
|
-
inputSchema: {
|
|
436
|
-
type: "object",
|
|
437
|
-
properties: {
|
|
438
|
-
...PROJECT_PROPERTY,
|
|
439
|
-
name: { type: "string", description: "The name of the enum to delete." },
|
|
440
|
-
force: { type: "boolean", description: "If false, fails if the enum has active usages in the disassembly. Set to true to override." },
|
|
441
|
-
},
|
|
442
|
-
required: ["project", "name"],
|
|
443
|
-
},
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
name: "r2000_apply_enum_usage",
|
|
447
|
-
description:
|
|
448
|
-
"Applies an enum definition to format the immediate operand or constant reference at a " +
|
|
449
|
-
"specific address. If name is omitted or empty, clears the enum usage.",
|
|
450
|
-
inputSchema: {
|
|
451
|
-
type: "object",
|
|
452
|
-
properties: {
|
|
453
|
-
...PROJECT_PROPERTY,
|
|
454
|
-
address: { type: "integer", description: "The target instruction address (decimal)." },
|
|
455
|
-
name: { type: "string", description: "The unique name of the enum to apply (e.g., 'vic_registers'). Omit or send empty to clear." },
|
|
456
|
-
},
|
|
457
|
-
required: ["project", "address"],
|
|
458
|
-
},
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
name: "r2000_save_project",
|
|
462
|
-
description:
|
|
463
|
-
"Explicitly saves the current project state to the .regen2000proj file named by 'project'. " +
|
|
464
|
-
"Every OTHER mutating tool on this surface already saves automatically at the end of its own " +
|
|
465
|
-
"call (D-17's per-call lifecycle), so this tool is rarely required standalone -- it exists as " +
|
|
466
|
-
"an explicit flush, and as the natural last entry inside an r2000_batch_execute call. This " +
|
|
467
|
-
"surface takes ONLY 'project' -- the underlying regenerator2000 tool takes no arguments of its " +
|
|
468
|
-
"own and errors when its project_path is unset (handler.rs:350-352,1264-1271), which is " +
|
|
469
|
-
"exactly why the path comes from this session's own process launch. Persistence is verified " +
|
|
470
|
-
"independently by re-reading the project file's content hash from disk (T-11-FALSESUCCESS) -- " +
|
|
471
|
-
"never trusted on the strength of regenerator2000's own success text.",
|
|
472
|
-
inputSchema: {
|
|
473
|
-
type: "object",
|
|
474
|
-
properties: { ...PROJECT_PROPERTY },
|
|
475
|
-
required: ["project"],
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
name: "r2000_batch_execute",
|
|
480
|
-
description:
|
|
481
|
-
"Executes multiple tool calls sequentially in a single r2000 session. Use only when you have " +
|
|
482
|
-
"5+ independent operations to perform at once (e.g. marking many regions, renaming many " +
|
|
483
|
-
"labels). Do not use for operations that depend on each other's results. Every inner " +
|
|
484
|
-
"calls[].name is validated against this surface's own curated set BEFORE any request reaches " +
|
|
485
|
-
"regenerator2000 -- a batch containing even one uncurated inner name is refused WHOLE (D-33).",
|
|
486
|
-
inputSchema: {
|
|
487
|
-
type: "object",
|
|
488
|
-
properties: {
|
|
489
|
-
...PROJECT_PROPERTY,
|
|
490
|
-
calls: {
|
|
491
|
-
type: "array",
|
|
492
|
-
items: {
|
|
493
|
-
type: "object",
|
|
494
|
-
properties: {
|
|
495
|
-
name: { type: "string", description: "Name of the tool to execute -- must be one of this surface's curated r2000_* names." },
|
|
496
|
-
arguments: { type: "object", description: "Arguments for the tool (same shape as that tool's own inputSchema, minus 'project')." },
|
|
497
|
-
},
|
|
498
|
-
required: ["name", "arguments"],
|
|
499
|
-
},
|
|
500
|
-
description: "List of tool calls to execute sequentially, inside one loaded r2000 session.",
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
required: ["project", "calls"],
|
|
504
|
-
},
|
|
505
|
-
},
|
|
506
|
-
] as const;
|
|
507
|
-
|
|
508
|
-
// ---------------------------------------------------------------------------
|
|
509
|
-
// The allow-list gate (D-18/D-33). Derived from R2000_TOOL_DEFINITIONS itself
|
|
510
|
-
// -- one array, not two hand-typed lists that could drift apart (the
|
|
511
|
-
// set-equality test in r2000-tools.test.ts proves this holds, in both
|
|
512
|
-
// directions, rather than assuming it from this derivation alone).
|
|
513
|
-
// ---------------------------------------------------------------------------
|
|
514
|
-
|
|
515
|
-
export const CURATED_R2000_TOOLS: readonly string[] = R2000_TOOL_DEFINITIONS.map((def) => def.name);
|
|
516
|
-
|
|
517
|
-
export interface R2000UncuratedToolErrorOptions {
|
|
518
|
-
toolName: string;
|
|
519
|
-
batchIndex?: number;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** Thrown by `assertCuratedTool()` when a name is outside the curated set --
|
|
523
|
-
* whether at the outer dispatch or nested inside an `r2000_batch_execute`
|
|
524
|
-
* payload. `toolName` names the offending tool (never this class's own
|
|
525
|
-
* `.name`, which stays the class name per this repo's `R2000ViceFlagError`/
|
|
526
|
-
* `StockSymbolsError` convention); `batchIndex` is set only for a refusal
|
|
527
|
-
* discovered while walking a batch's `calls` array. */
|
|
528
|
-
export class R2000UncuratedToolError extends Error {
|
|
529
|
-
toolName: string;
|
|
530
|
-
batchIndex?: number;
|
|
531
|
-
|
|
532
|
-
constructor(message: string, { toolName, batchIndex }: R2000UncuratedToolErrorOptions) {
|
|
533
|
-
super(message);
|
|
534
|
-
this.name = "R2000UncuratedToolError";
|
|
535
|
-
this.toolName = toolName;
|
|
536
|
-
this.batchIndex = batchIndex;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
export interface R2000LabelNameErrorOptions {
|
|
541
|
-
labelName: string;
|
|
542
|
-
batchIndex?: number;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/** Thrown by `assertLegalLabelArg()` (called from `assertCuratedTool()` and
|
|
546
|
-
* `assertCuratedBatch()`) when an `r2000_set_label_name` call's `name`
|
|
547
|
-
* argument is not a legal ACME identifier -- T-11-NAME-INJECT, closed. The
|
|
548
|
-
* policy is REJECT, never sanitize or quote: a malformed name is a bug to
|
|
549
|
-
* surface, so the store's printed name can never diverge from what actually
|
|
550
|
-
* gets exported into ACME source. `labelName` names the offending value
|
|
551
|
-
* (never this class's own `.name`, which stays the class name per this
|
|
552
|
-
* module's `R2000UncuratedToolError` convention); `batchIndex` is set only
|
|
553
|
-
* for a refusal discovered while walking a batch's `calls` array. */
|
|
554
|
-
export class R2000LabelNameError extends Error {
|
|
555
|
-
labelName: string;
|
|
556
|
-
batchIndex?: number;
|
|
557
|
-
|
|
558
|
-
constructor(message: string, { labelName, batchIndex }: R2000LabelNameErrorOptions) {
|
|
559
|
-
super(message);
|
|
560
|
-
this.name = "R2000LabelNameError";
|
|
561
|
-
this.labelName = labelName;
|
|
562
|
-
this.batchIndex = batchIndex;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/** Validates an `r2000_set_label_name` call's `name` argument against the
|
|
567
|
-
* one ACME identifier seam (`r2000-acme-ident.ts`'s `assertLegalAcmeIdentifier()`),
|
|
568
|
-
* re-throwing as `R2000LabelNameError` on failure. A no-op when `args` is
|
|
569
|
-
* not a plain object carrying a string `name` -- that shape is a different
|
|
570
|
-
* concern (a missing/malformed required argument), not this function's.
|
|
571
|
-
* Called from BOTH `assertCuratedTool()` (the outer dispatch) and
|
|
572
|
-
* `assertCuratedBatch()` (the batch-inner call), so the refusal fires
|
|
573
|
-
* identically whether `r2000_set_label_name` is called directly or smuggled
|
|
574
|
-
* inside an `r2000_batch_execute` payload -- before `runR2000Tool()`'s own
|
|
575
|
-
* `try` block either way, which means a refusal REJECTS the returned
|
|
576
|
-
* promise rather than resolving `{isError:true}`. That asymmetry is WR-02
|
|
577
|
-
* (out of scope for this plan, 260821-a86) -- the same posture the existing
|
|
578
|
-
* uncurated-name refusal above already takes, not a new oversight. */
|
|
579
|
-
function assertLegalLabelArg(args: unknown, batchIndex?: number): void {
|
|
580
|
-
if (!isPlainObject(args) || typeof args.name !== "string") return;
|
|
581
|
-
const name = args.name;
|
|
582
|
-
try {
|
|
583
|
-
assertLegalAcmeIdentifier(name, "r2000_set_label_name name");
|
|
584
|
-
} catch (err) {
|
|
585
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
586
|
-
throw new R2000LabelNameError(
|
|
587
|
-
`r2000_set_label_name refused${batchIndex !== undefined ? ` (calls[${batchIndex}])` : ""}: "${name}" is not ` +
|
|
588
|
-
`a legal ACME identifier (${reason}) -- REJECTED, never sanitized or quoted: the store's printed name must ` +
|
|
589
|
-
"never diverge from the exported symbol.",
|
|
590
|
-
{ labelName: name, batchIndex },
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/** Walks a `r2000_batch_execute` payload's `calls` array and refuses the
|
|
596
|
-
* WHOLE batch if any inner call's name is outside `CURATED_R2000_TOOLS`, or
|
|
597
|
-
* if a `calls` entry is malformed (not an object, or missing a string
|
|
598
|
-
* `name`) -- a malformed payload is a REFUSAL, never treated as an empty
|
|
599
|
-
* batch that passes through. Recurses into a nested `r2000_batch_execute`
|
|
600
|
-
* (upstream permits arbitrary tool names inside a batch, including another
|
|
601
|
-
* batch call) so a two-level smuggling attempt is caught the same way a
|
|
602
|
-
* one-level one is. Also refuses WHOLE on an illegal `r2000_set_label_name`
|
|
603
|
-
* name (T-11-NAME-INJECT), naming the offending `calls[i]`. */
|
|
604
|
-
function assertCuratedBatch(args: unknown): void {
|
|
605
|
-
if (!isPlainObject(args) || !Array.isArray(args.calls)) {
|
|
606
|
-
throw new R2000UncuratedToolError(
|
|
607
|
-
"r2000_batch_execute refused: \"calls\" must be an array of {name, arguments} objects -- a " +
|
|
608
|
-
"malformed batch payload is treated as a refusal, never as an empty batch that passes through.",
|
|
609
|
-
{ toolName: "r2000_batch_execute" },
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
const calls = args.calls as unknown[];
|
|
613
|
-
calls.forEach((call, i) => {
|
|
614
|
-
if (!isPlainObject(call) || typeof call.name !== "string") {
|
|
615
|
-
throw new R2000UncuratedToolError(
|
|
616
|
-
`r2000_batch_execute refused WHOLE: calls[${i}] is malformed (missing a string "name") -- ` +
|
|
617
|
-
"treated as a refusal, never as an empty batch that passes through.",
|
|
618
|
-
{ toolName: "r2000_batch_execute", batchIndex: i },
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
if (call.name === "r2000_get_address_details") {
|
|
622
|
-
throw new R2000UncuratedToolError(
|
|
623
|
-
`r2000_batch_execute refused WHOLE: calls[${i}].name is "r2000_get_address_details" -- ${ADDRESS_DETAILS_REFUSAL}`,
|
|
624
|
-
{ toolName: call.name, batchIndex: i },
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
if (!CURATED_R2000_TOOLS.includes(call.name)) {
|
|
628
|
-
throw new R2000UncuratedToolError(
|
|
629
|
-
`r2000_batch_execute refused WHOLE: calls[${i}].name "${call.name}" is outside the curated ` +
|
|
630
|
-
"r2000_* tool surface -- a batch is refused whole if any inner name is outside the curated set (D-33).",
|
|
631
|
-
{ toolName: call.name, batchIndex: i },
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
if (call.name === "r2000_set_label_name") {
|
|
635
|
-
assertLegalLabelArg(call.arguments, i);
|
|
636
|
-
}
|
|
637
|
-
if (call.name === "r2000_batch_execute") {
|
|
638
|
-
assertCuratedBatch(call.arguments);
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* The allow-list gate. Its body's FIRST check is set membership (WHAT NOT TO
|
|
645
|
-
* DO above, and the module header's own discipline mirroring `vice.ts`'s
|
|
646
|
-
* `DENY_LIST` precedent inverted into an allow-list): refuses `name` outright
|
|
647
|
-
* when it is not in `CURATED_R2000_TOOLS`, with a dedicated message for
|
|
648
|
-
* `r2000_get_address_details` naming the 64K defect and the upstream issue
|
|
649
|
-
* (D-32) rather than a generic "unknown tool" refusal. When `name` is
|
|
650
|
-
* `r2000_set_label_name`, additionally validates `args.name` via
|
|
651
|
-
* `assertLegalLabelArg()` (T-11-NAME-INJECT, closed). When `name` is
|
|
652
|
-
* `r2000_batch_execute`, additionally walks `args.calls` via
|
|
653
|
-
* `assertCuratedBatch()` -- refusing the WHOLE batch if any inner name is
|
|
654
|
-
* outside the set, per D-33, or carries an illegal label name.
|
|
655
|
-
*/
|
|
656
|
-
export function assertCuratedTool(name: string, args?: unknown): void {
|
|
657
|
-
if (name === "r2000_get_address_details") {
|
|
658
|
-
throw new R2000UncuratedToolError(ADDRESS_DETAILS_REFUSAL, { toolName: name });
|
|
659
|
-
}
|
|
660
|
-
if (!CURATED_R2000_TOOLS.includes(name)) {
|
|
661
|
-
throw new R2000UncuratedToolError(
|
|
662
|
-
`"${name}" is not part of the curated r2000_* tool surface. Resolution routes: implement it and ` +
|
|
663
|
-
"add it to R2000_TOOL_DEFINITIONS with a named criterion, or remove the caller reference.",
|
|
664
|
-
{ toolName: name },
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
if (name === "r2000_set_label_name") {
|
|
668
|
-
assertLegalLabelArg(args);
|
|
669
|
-
}
|
|
670
|
-
if (name === "r2000_batch_execute") {
|
|
671
|
-
assertCuratedBatch(args);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ---------------------------------------------------------------------------
|
|
676
|
-
// Project-path validation (T-11-PATH-ESCAPE, WR-01 -- closed). Same posture
|
|
677
|
-
// stock-symbols.ts takes for `.lbl` files: an LLM-supplied path reaching a
|
|
678
|
-
// spawned child process. Resolve against repoRoot(), refuse an extension
|
|
679
|
-
// other than .regen2000proj, and refuse anything that escapes the workspace
|
|
680
|
-
// root either directly or via a symlink. WR-01's finding was that a
|
|
681
|
-
// not-yet-existing leaf under a directory symlink bypassed containment
|
|
682
|
-
// entirely (the ENOENT catch fell back to the literal, unresolved path).
|
|
683
|
-
// Containment is now enforced against the DEEPEST EXISTING ancestor's
|
|
684
|
-
// realpath, plus the literal remaining path segments rebuilt on top of it
|
|
685
|
-
// (`resolveViaDeepestExistingAncestor()` below) -- and every one of those
|
|
686
|
-
// remaining segments is itself lstat-guarded against being an unresolved
|
|
687
|
-
// (e.g. dangling) symlink, which would otherwise slip through the
|
|
688
|
-
// ancestor-realpath check the same way the original leaf did.
|
|
689
|
-
// ---------------------------------------------------------------------------
|
|
690
|
-
|
|
691
|
-
export class R2000StorePathError extends Error {
|
|
692
|
-
constructor(message: string) {
|
|
693
|
-
super(message);
|
|
694
|
-
this.name = "R2000StorePathError";
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function isContained(candidate: string, root: string): boolean {
|
|
699
|
-
return candidate === root || candidate.startsWith(root + sep);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/** Walks up from `dirname(resolved)` toward the filesystem root, collecting
|
|
703
|
-
* the literal path segments skipped along the way, until it finds the
|
|
704
|
-
* DEEPEST ancestor for which `realpathSync` succeeds. Rebuilds the
|
|
705
|
-
* candidate as `join(ancestorReal, ...remainingSegments)` -- but first
|
|
706
|
-
* lstat-guards every remaining segment: a symlink whose own target does not
|
|
707
|
-
* exist yet (or otherwise fails to resolve) still redirects a later create
|
|
708
|
-
* outside the workspace, and `realpathSync` on the full path reports plain
|
|
709
|
-
* ENOENT for it, so it would otherwise slip through the ancestor-realpath
|
|
710
|
-
* check entirely. `lstatSync`'s own ENOENT (a genuinely absent component,
|
|
711
|
-
* the tolerated case) is swallowed; any OTHER lstat error propagates as a
|
|
712
|
-
* refusal. Called only from `resolveStorePath()`'s ENOENT branch below. */
|
|
713
|
-
function resolveViaDeepestExistingAncestor(resolved: string, root: string): string {
|
|
714
|
-
const remaining: string[] = [basename(resolved)];
|
|
715
|
-
let current = dirname(resolved);
|
|
716
|
-
let ancestorReal: string;
|
|
717
|
-
for (;;) {
|
|
718
|
-
try {
|
|
719
|
-
ancestorReal = realpathSync(current);
|
|
720
|
-
break;
|
|
721
|
-
} catch (err) {
|
|
722
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
723
|
-
throw new R2000StorePathError(
|
|
724
|
-
`could not resolve ancestor "${current}" while resolving "${resolved}" (${err instanceof Error ? err.message : String(err)})`,
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
const parent = dirname(current);
|
|
728
|
-
if (parent === current) {
|
|
729
|
-
throw new R2000StorePathError(
|
|
730
|
-
`could not find any existing ancestor while resolving "${resolved}" under workspace root "${root}"`,
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
remaining.unshift(basename(current));
|
|
734
|
-
current = parent;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
let accumulated = ancestorReal;
|
|
739
|
-
for (const segment of remaining) {
|
|
740
|
-
accumulated = join(accumulated, segment);
|
|
741
|
-
let st;
|
|
742
|
-
try {
|
|
743
|
-
st = lstatSync(accumulated);
|
|
744
|
-
} catch (err) {
|
|
745
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") continue; // genuinely absent -- tolerated
|
|
746
|
-
throw new R2000StorePathError(
|
|
747
|
-
`could not stat "${accumulated}" while resolving "${resolved}" (${err instanceof Error ? err.message : String(err)})`,
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
if (st.isSymbolicLink()) {
|
|
751
|
-
throw new R2000StorePathError(
|
|
752
|
-
`"${resolved}" contains a symlink component at "${accumulated}" that does not itself resolve (dangling, or ` +
|
|
753
|
-
"otherwise unreadable via realpathSync) -- refusing rather than risk a later create redirecting outside " +
|
|
754
|
-
`the workspace root (${root})`,
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
return accumulated;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/** Resolves `project` against `repoRoot()`, refusing anything that does not
|
|
763
|
-
* end in `.regen2000proj`, or that escapes the workspace either directly or
|
|
764
|
-
* via a symlink. Tolerant of the path not existing yet (ENOENT during the
|
|
765
|
-
* symlink-resolution step) since `r2000_save_project` can create a fresh
|
|
766
|
-
* store -- unlike stock-symbols.ts's `.lbl` reader, this module never reads
|
|
767
|
-
* the file itself, so a missing project is regenerator2000's own concern to
|
|
768
|
-
* report, not this function's. On ENOENT, containment is enforced against
|
|
769
|
-
* the deepest EXISTING ancestor's realpath plus the literal remaining
|
|
770
|
-
* segments (`resolveViaDeepestExistingAncestor()`), closing WR-01 /
|
|
771
|
-
* T-11-PATH-ESCAPE: the previous ENOENT fallback (`real = resolved`, the
|
|
772
|
-
* literal, unresolved path) let a directory symlink one or more levels up
|
|
773
|
-
* from a not-yet-existing leaf bypass containment entirely. */
|
|
774
|
-
export function resolveStorePath(project: unknown): string {
|
|
775
|
-
if (typeof project !== "string" || project.trim() === "") {
|
|
776
|
-
throw new R2000StorePathError(
|
|
777
|
-
`project must be a non-empty string, got ${typeof project === "string" ? "an empty/whitespace-only string" : typeof project}`,
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
const trimmed = project.trim();
|
|
781
|
-
if (!trimmed.toLowerCase().endsWith(".regen2000proj")) {
|
|
782
|
-
throw new R2000StorePathError(
|
|
783
|
-
`"${trimmed}" must end in .regen2000proj -- refusing to hand a non-project path to a spawned regenerator2000 child`,
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const root = repoRoot();
|
|
788
|
-
const resolved = resolve(root, trimmed);
|
|
789
|
-
|
|
790
|
-
if (!isContained(resolved, root)) {
|
|
791
|
-
throw new R2000StorePathError(`"${resolved}" is outside the workspace root (${root}) -- an r2000 project path must live inside the workspace`);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
let realRoot: string;
|
|
795
|
-
try {
|
|
796
|
-
realRoot = realpathSync(root);
|
|
797
|
-
} catch {
|
|
798
|
-
realRoot = root;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
let real: string;
|
|
802
|
-
try {
|
|
803
|
-
real = realpathSync(resolved);
|
|
804
|
-
} catch (err) {
|
|
805
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
806
|
-
throw new R2000StorePathError(`could not resolve "${resolved}" (${err instanceof Error ? err.message : String(err)})`);
|
|
807
|
-
}
|
|
808
|
-
// ENOENT is fine here -- r2000_save_project may create the file fresh.
|
|
809
|
-
// Walk up to the deepest EXISTING ancestor's realpath and rebuild the
|
|
810
|
-
// candidate from it, rather than falling back to the literal
|
|
811
|
-
// (unresolved) `resolved` path -- see resolveViaDeepestExistingAncestor().
|
|
812
|
-
real = resolveViaDeepestExistingAncestor(resolved, root);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
if (!isContained(real, realRoot)) {
|
|
816
|
-
throw new R2000StorePathError(
|
|
817
|
-
`"${resolved}" resolves (via symlink) to "${real}", which is outside the workspace root ` +
|
|
818
|
-
`(${realRoot === root ? realRoot : `${root}, canonically ${realRoot}`}) -- an r2000 project path must live inside the workspace`,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
return real;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// ---------------------------------------------------------------------------
|
|
826
|
-
// Read-only tools never need a save -- calling one leaves project state
|
|
827
|
-
// unchanged. Every OTHER curated tool (besides r2000_save_project itself)
|
|
828
|
-
// mutates state (a label, a comment, a block classification, a scope, an
|
|
829
|
-
// enum, or -- for r2000_disassemble -- the blocks a control-flow trace
|
|
830
|
-
// converts to Code) and MUST be saved before its session exits, or D-17's
|
|
831
|
-
// own per-call lifecycle ("spawn, load, mutate, r2000_save_project, exit")
|
|
832
|
-
// silently loses the mutation: a spawned child that exits without saving
|
|
833
|
-
// discards every in-memory change, so a caller who wrote a label in one
|
|
834
|
-
// runR2000Tool() call and expected a LATER call to see it would be exactly
|
|
835
|
-
// as vulnerable to a lost-write bug as r2000_save_project's own
|
|
836
|
-
// T-11-FALSESUCCESS trap -- just with no error at all. This is why every
|
|
837
|
-
// mutating tool call below saves internally, inside the SAME session, rather
|
|
838
|
-
// than requiring a caller to remember a separate r2000_save_project call.
|
|
839
|
-
//
|
|
840
|
-
// That internal auto-save deliberately calls the PLAIN underlying
|
|
841
|
-
// `r2000_save_project` (no hash verification), never `saveAndVerify()`: an
|
|
842
|
-
// idempotent mutation (e.g. setting a label to the value it already has) is
|
|
843
|
-
// a legitimate no-op whose save correctly produces an UNCHANGED file hash,
|
|
844
|
-
// and `saveAndVerify()`'s whole contract is "throw when the hash does not
|
|
845
|
-
// change" -- applying it to every internal auto-save would misreport that
|
|
846
|
-
// legitimate no-op as T-11-FALSESUCCESS. `saveAndVerify()` is reserved for
|
|
847
|
-
// the ONE case the plan names explicitly: when `r2000_save_project` is the
|
|
848
|
-
// OUTER tool a caller invoked by name.
|
|
849
|
-
// ---------------------------------------------------------------------------
|
|
850
|
-
|
|
851
|
-
const READ_ONLY_R2000_TOOLS: ReadonlySet<string> = new Set([
|
|
852
|
-
"r2000_get_symbols",
|
|
853
|
-
"r2000_get_comments",
|
|
854
|
-
"r2000_get_blocks",
|
|
855
|
-
"r2000_get_cross_references",
|
|
856
|
-
"r2000_search_disassembly",
|
|
857
|
-
"r2000_get_binary_info",
|
|
858
|
-
]);
|
|
859
|
-
|
|
860
|
-
// ---------------------------------------------------------------------------
|
|
861
|
-
// The runner. Drives r2000-mcp-client.ts via a DYNAMIC import so importing
|
|
862
|
-
// R2000_TOOL_DEFINITIONS (registration, at vice-proxy.ts module scope) costs
|
|
863
|
-
// no child process and no socket -- only calling a tool actually spawns one.
|
|
864
|
-
// ---------------------------------------------------------------------------
|
|
865
|
-
|
|
866
|
-
/**
|
|
867
|
-
* Runs one curated `r2000_*` tool call. First statement: `assertCuratedTool`.
|
|
868
|
-
* Second: `resolveStorePath`. Only after both pass does this function reach
|
|
869
|
-
* for `r2000-mcp-client.ts` (dynamically imported, so registering the tool
|
|
870
|
-
* definitions above never pays for it).
|
|
871
|
-
*
|
|
872
|
-
* `r2000_save_project` (called directly by name) is routed through
|
|
873
|
-
* `saveAndVerify()` so a save is never reported on the child's own text
|
|
874
|
-
* response alone (T-11-FALSESUCCESS). Every OTHER mutating tool (everything
|
|
875
|
-
* outside `READ_ONLY_R2000_TOOLS` and not `r2000_save_project` itself) saves
|
|
876
|
-
* internally, inside the same session, immediately after its own call, using
|
|
877
|
-
* a PLAIN save (see the comment above `READ_ONLY_R2000_TOOLS` for why that
|
|
878
|
-
* internal save must not be hash-verified).
|
|
879
|
-
*/
|
|
880
|
-
export async function runR2000Tool(name: string, args: unknown): Promise<ToolCallResult> {
|
|
881
|
-
assertCuratedTool(name, args);
|
|
882
|
-
const projectPath = resolveStorePath(isPlainObject(args) ? args.project : undefined);
|
|
883
|
-
|
|
884
|
-
const { withR2000Session, saveAndVerify } = await import("./r2000-mcp-client.ts");
|
|
885
|
-
|
|
886
|
-
const rest: Record<string, unknown> = isPlainObject(args) ? { ...args } : {};
|
|
887
|
-
delete rest.project;
|
|
888
|
-
|
|
889
|
-
try {
|
|
890
|
-
if (name === "r2000_save_project") {
|
|
891
|
-
const result = await withR2000Session(projectPath, (call) => saveAndVerify(projectPath, call));
|
|
892
|
-
return okText(JSON.stringify(result));
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (READ_ONLY_R2000_TOOLS.has(name)) {
|
|
896
|
-
const result = await withR2000Session(projectPath, (call) => call(name, rest));
|
|
897
|
-
return toToolCallResult(result);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// A mutating tool (including r2000_batch_execute, whose own inner calls
|
|
901
|
-
// all run inside this SAME session per regenerator2000's own
|
|
902
|
-
// batch_execute implementation): call, then save PLAINLY (no hash
|
|
903
|
-
// verification -- see the block comment above), before the session
|
|
904
|
-
// exits.
|
|
905
|
-
const result = await withR2000Session(projectPath, async (call) => {
|
|
906
|
-
const callResult = await call(name, rest);
|
|
907
|
-
await call("r2000_save_project", {});
|
|
908
|
-
return callResult;
|
|
909
|
-
});
|
|
910
|
-
return toToolCallResult(result);
|
|
911
|
-
} catch (err) {
|
|
912
|
-
return errText(`${name} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
913
|
-
}
|
|
914
|
-
}
|