@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/text-tools.ts
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// text-tools.ts
|
|
3
|
+
//
|
|
4
|
+
// THE ONE place a text-channel tool handler lives (plan 41-06, CHAN-03).
|
|
5
|
+
// Every handler here takes the text channel's own halt authority through
|
|
6
|
+
// withTextChannelLock() (text-protocol.ts, plan 41-02) around its whole
|
|
7
|
+
// logical operation and never issues a bare command() outside it --
|
|
8
|
+
// command() itself refuses when channel-lock.ts's mutex is not held by the
|
|
9
|
+
// text channel, so a handler that forgot to acquire would be refused, not
|
|
10
|
+
// silently allowed through.
|
|
11
|
+
//
|
|
12
|
+
// No handler here accepts a caller-supplied command string, now or later.
|
|
13
|
+
// The generic remote-execution seam this project rejected on the record for
|
|
14
|
+
// host_tool (a single per-binary run-arbitrary-command op) would be WORSE
|
|
15
|
+
// here, over a channel that is unauthenticated and can read and write host
|
|
16
|
+
// files -- broker-launch.mts's own bind-widening warning already states
|
|
17
|
+
// this for this same channel. Every outbound command below is a fixed
|
|
18
|
+
// literal drawn from TEXT_COMMAND_ALLOWLIST; text-protocol.ts's command()
|
|
19
|
+
// itself refuses anything else BY NAME (D-01), and neither handler below
|
|
20
|
+
// ever builds a command string from an argument.
|
|
21
|
+
//
|
|
22
|
+
// SESSION LIFECYCLE -- MEASURED, not this plan's assumed default (Rule 1
|
|
23
|
+
// deviation, see SUMMARY): StockConnectSession (stock-connect.ts) carries no
|
|
24
|
+
// text session at all, and ensureStockSession()/withStockSession() never
|
|
25
|
+
// call textConnect() -- as of this plan, textConnect()/textDisconnect() are
|
|
26
|
+
// invoked ONLY from test code. So there is no session-lifetime-held text
|
|
27
|
+
// session anywhere in production code to reuse. Each call below is its OWN
|
|
28
|
+
// textConnect()/textDisconnect() pair -- the ONLY acquisition path these two
|
|
29
|
+
// tools have, not a second one competing with a first.
|
|
30
|
+
//
|
|
31
|
+
// ADAPTER CHOICE -- MEASURED, deviates from this plan's literal instruction
|
|
32
|
+
// (Rule 1 deviation, see SUMMARY): withStockSession() and
|
|
33
|
+
// withDerivedTool(..., { needsSession: true }, ...) (stock-dispatch.ts) both
|
|
34
|
+
// wrap the ENTIRE delegated handler call in withChannelLockHeld(), which
|
|
35
|
+
// acquires channel-lock.ts's SINGLE, cross-channel mutex for `channel:
|
|
36
|
+
// "binary"` for the whole call. A handler reached through either adapter
|
|
37
|
+
// that then called withTextChannelLock() internally would be a SECOND
|
|
38
|
+
// acquireChannelLock() call while the first (binary) is still held by the
|
|
39
|
+
// very same call stack -- channel-lock.ts is not reentrant and does not
|
|
40
|
+
// distinguish "the same logical caller" from "a different one" -- so the
|
|
41
|
+
// inner acquire would queue behind itself and could only ever resolve by
|
|
42
|
+
// expiring CHANNEL_LOCK_ACQUIRE_TIMEOUT_MS (630s by default) and erroring: a
|
|
43
|
+
// de facto deadlock, not a working call. Neither handler below needs a
|
|
44
|
+
// binary session or the binary channel's authority at all, so both are
|
|
45
|
+
// registered in stock-dispatch.ts with
|
|
46
|
+
// `withDerivedTool(toolName, { needsSession: false }, handler)` instead --
|
|
47
|
+
// the SAME existing adapter configuration `vice_diagnose`/
|
|
48
|
+
// `vice_symbols_load` already use, never a third adapter. Each handler
|
|
49
|
+
// resolves the lease through `deps.ensureLease()` itself (free to call
|
|
50
|
+
// repeatedly -- ensureStockSession()'s own header comment) and takes ONLY
|
|
51
|
+
// the text channel's own lock, via withTextChannelLock(), around its one
|
|
52
|
+
// command().
|
|
53
|
+
//
|
|
54
|
+
// WHAT NOT TO DO:
|
|
55
|
+
// - Never accept a caller-supplied, free-text command string anywhere in
|
|
56
|
+
// this module's public surface (D-01).
|
|
57
|
+
// - Never call TextMonitorClient.command() outside withTextChannelLock().
|
|
58
|
+
// - Never register either handler through withStockSession() or
|
|
59
|
+
// withDerivedTool(..., { needsSession: true }, ...) -- see the ADAPTER
|
|
60
|
+
// CHOICE comment above for the self-deadlock this would cause.
|
|
61
|
+
// - Never dial a raw host/port or open a second broker lease -- both
|
|
62
|
+
// handlers obtain lease coordinates through deps.ensureLease() (the SAME
|
|
63
|
+
// provider ensureStockSession() itself calls) and dial only through
|
|
64
|
+
// textConnect().
|
|
65
|
+
// - Never write a third error converter -- reuse convertHandshakeError()
|
|
66
|
+
// and convertWireError() (stock-handler.ts) exactly as every other stock
|
|
67
|
+
// handler does; a ChannelLockTimeoutError is passed through verbatim
|
|
68
|
+
// (its own `.message` IS channelLockRefusalMessage()'s output), matching
|
|
69
|
+
// withChannelLockHeld()'s own discipline in stock-dispatch.ts.
|
|
70
|
+
// - Never embed a phase number in any string or template literal here.
|
|
71
|
+
import { textConnect, textDisconnect } from "./text-connect.ts";
|
|
72
|
+
import { withTextChannelLock, buildTextCommand, type TextMonitorClient } from "./text-protocol.ts";
|
|
73
|
+
import { MonitorOwnershipError } from "./vice-broker-client.ts";
|
|
74
|
+
import { ChannelLockTimeoutError } from "./channel-lock.ts";
|
|
75
|
+
import { isErrorText, derivedAnswer, convertHandshakeError, convertWireError, type StockToolResult } from "./stock-handler.ts";
|
|
76
|
+
import { parseAccessMap, accessMapRanges, type AccessMap, type AccessMapRangesOptions } from "./textmon-memmap.ts";
|
|
77
|
+
import { parseCpuHistory } from "./textmon-cpuhistory.ts";
|
|
78
|
+
import { parseBacktrace } from "./textmon-backtrace.ts";
|
|
79
|
+
import { parseFlatProfile } from "./textmon-profile.ts";
|
|
80
|
+
import { parseIoRegisters } from "./textmon-registers.ts";
|
|
81
|
+
import {
|
|
82
|
+
classifyTextCapabilityResponse,
|
|
83
|
+
probeTextCapability,
|
|
84
|
+
textCapabilityVerdictFor,
|
|
85
|
+
textCapabilityRefusalMessage,
|
|
86
|
+
textCapabilityIdentityWarning,
|
|
87
|
+
type TextCapabilityIdentity,
|
|
88
|
+
type TextCapabilityBrokerIdentity,
|
|
89
|
+
} from "./text-capability-probe.ts";
|
|
90
|
+
import type { StockDispatchDeps } from "./stock-dispatch.ts";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Shared preamble every handler below runs: resolve the lease
|
|
94
|
+
* (`deps.ensureLease()` -- the SAME provider ensureStockSession() itself
|
|
95
|
+
* calls, never a second acquisition), open a fresh text-monitor session
|
|
96
|
+
* through textConnect(), hold channel-lock.ts's mutex for `channel: "text"`
|
|
97
|
+
* around exactly one command via withTextChannelLock(), and always tear the
|
|
98
|
+
* session down again (textDisconnect()) whether `fn` succeeded or threw.
|
|
99
|
+
*/
|
|
100
|
+
async function withTextTool(
|
|
101
|
+
toolName: string,
|
|
102
|
+
deps: StockDispatchDeps,
|
|
103
|
+
fn: (client: TextMonitorClient) => Promise<StockToolResult>,
|
|
104
|
+
): Promise<StockToolResult> {
|
|
105
|
+
const leaseOutcome = await deps.ensureLease();
|
|
106
|
+
if (!leaseOutcome.ok) {
|
|
107
|
+
return isErrorText(leaseOutcome.message);
|
|
108
|
+
}
|
|
109
|
+
const lease = leaseOutcome.lease;
|
|
110
|
+
if (lease === null) {
|
|
111
|
+
return isErrorText(
|
|
112
|
+
`${toolName}: VICE_MCP_URL is set, so there is no broker-managed instance and no broker control session to ` +
|
|
113
|
+
`claim the text-monitor socket through -- unset VICE_MCP_URL to use the on-demand broker, or connect to a ` +
|
|
114
|
+
`broker-managed instance directly.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let session;
|
|
119
|
+
try {
|
|
120
|
+
session = await textConnect({
|
|
121
|
+
host: lease.host,
|
|
122
|
+
remoteMonitorPort: lease.remoteMonitorPort,
|
|
123
|
+
targetId: lease.targetId,
|
|
124
|
+
brokerControl: lease.brokerControl,
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof MonitorOwnershipError) {
|
|
128
|
+
return convertHandshakeError(toolName, err);
|
|
129
|
+
}
|
|
130
|
+
return convertWireError(toolName, err);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return await withTextChannelLock(toolName, () => fn(session.client), { timeoutMs: deps.channelLockTimeoutMs });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// ChannelLockTimeoutError's own `.message` IS
|
|
137
|
+
// channelLockRefusalMessage()'s output -- passed through verbatim below,
|
|
138
|
+
// never routed through convertWireError(), matching
|
|
139
|
+
// withChannelLockHeld()'s (stock-dispatch.ts) own discipline for the
|
|
140
|
+
// binary side.
|
|
141
|
+
if (err instanceof ChannelLockTimeoutError) {
|
|
142
|
+
return isErrorText(err.message);
|
|
143
|
+
}
|
|
144
|
+
return convertWireError(toolName, err);
|
|
145
|
+
} finally {
|
|
146
|
+
try {
|
|
147
|
+
await textDisconnect(session);
|
|
148
|
+
} catch (releaseErr) {
|
|
149
|
+
console.error(`${toolName}: textDisconnect after use did not complete: ${String(releaseErr)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `vice_device_console` -- takes NO arguments at all. Issues the single
|
|
156
|
+
* allowlisted verb `device c:` (the colon is required; the spelling without
|
|
157
|
+
* it is a syntax error the monitor rejects) inside withTextChannelLock(),
|
|
158
|
+
* and answers with the framed response plus a statement that the default
|
|
159
|
+
* device (memspace) was reset to the main CPU. This is an explicit tool, not
|
|
160
|
+
* auto-healing on the stepping path (D-03) -- nothing here is called from
|
|
161
|
+
* `ADVANCE_INSTRUCTIONS` or `EXECUTE_UNTIL_RETURN`; no shipped tool can
|
|
162
|
+
* contaminate `default_memspace` today (drive checkpoints are deferred past
|
|
163
|
+
* this milestone), so auto-healing would add a text round trip and a mutex
|
|
164
|
+
* acquisition to the hottest binary-side path to defend a route nothing
|
|
165
|
+
* currently opens.
|
|
166
|
+
*/
|
|
167
|
+
export async function handleDeviceConsole(_args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
168
|
+
return withTextTool("vice_device_console", deps, async (client) => {
|
|
169
|
+
const response = await client.command("device c:");
|
|
170
|
+
return derivedAnswer({
|
|
171
|
+
response,
|
|
172
|
+
note: "default device (memspace) reset to the main CPU",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `vice_warp_set` -- takes exactly one parameter, `enabled: boolean`,
|
|
179
|
+
* refused BY NAME (no byte written to the socket, no lease resolved, no
|
|
180
|
+
* connection attempted) whenever it is not a boolean. Selects `warp on` or
|
|
181
|
+
* `warp off` by branch -- never a caller-supplied string concatenated into
|
|
182
|
+
* the command line -- and answers with the framed response, which per the
|
|
183
|
+
* existing measurement reports warp's own state, so the answer carries the
|
|
184
|
+
* OBSERVED state rather than an assumption that the write took. There is no
|
|
185
|
+
* runtime `WarpMode` *resource* on stock; this is a monitor *command*, and
|
|
186
|
+
* warp requested at launch time is a separate mechanism -- this tool changes
|
|
187
|
+
* neither of those facts.
|
|
188
|
+
*/
|
|
189
|
+
export async function handleWarpSet(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
190
|
+
const enabled = args.enabled;
|
|
191
|
+
if (typeof enabled !== "boolean") {
|
|
192
|
+
return isErrorText(
|
|
193
|
+
`vice_warp_set: "enabled" must be a boolean (got ${JSON.stringify(enabled)}) -- refusing before any ` +
|
|
194
|
+
`text-monitor byte is written`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const command = enabled ? "warp on" : "warp off";
|
|
198
|
+
return withTextTool("vice_warp_set", deps, async (client) => {
|
|
199
|
+
const response = await client.command(command);
|
|
200
|
+
return derivedAnswer({
|
|
201
|
+
requested: enabled,
|
|
202
|
+
response,
|
|
203
|
+
note:
|
|
204
|
+
"there is no runtime WarpMode resource on stock -- this is a monitor command, and warp requested at " +
|
|
205
|
+
"launch time is a separate mechanism; the response above carries warp's OWN observed state, not an " +
|
|
206
|
+
"assumption that this write took",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** True iff `value` is a representable, non-negative whole number bounded
|
|
212
|
+
* to the C64's 16-bit address space -- the shared narrowing for
|
|
213
|
+
* `startAddress`/`endAddress`. Declared locally, per this module tree's
|
|
214
|
+
* own "repeated per file, never centrally imported" convention
|
|
215
|
+
* (disasm-decoder.ts). */
|
|
216
|
+
function isValidAddressArg(value: unknown): value is number {
|
|
217
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 0xffff;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** True iff `value` is a representable integer 1 through 4096 -- the
|
|
221
|
+
* `maxRanges` narrowing. */
|
|
222
|
+
function isValidMaxRangesArg(value: unknown): value is number {
|
|
223
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 4096;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Per-column execute counts over the WHOLE parsed map (never the
|
|
227
|
+
* `maxRanges`-truncated projection) -- so the counts stay meaningful even
|
|
228
|
+
* when the emitted range list itself is truncated. */
|
|
229
|
+
function executeCounts(map: AccessMap): { io: number; rom: number; ram: number } {
|
|
230
|
+
let io = 0;
|
|
231
|
+
let rom = 0;
|
|
232
|
+
let ram = 0;
|
|
233
|
+
for (const entry of map.entries) {
|
|
234
|
+
if (entry.io.execute) io++;
|
|
235
|
+
if (entry.rom.execute) rom++;
|
|
236
|
+
if (entry.ram.execute) ram++;
|
|
237
|
+
}
|
|
238
|
+
return { io, rom, ram };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `vice_memmap_show` -- dials the single allowlisted, unparameterized verb
|
|
243
|
+
* `memmapshow` (D-42-1: `startAddress`/`endAddress`/`maxRanges` are
|
|
244
|
+
* client-side projection FILTERS applied after parsing; none of the three
|
|
245
|
+
* ever reaches the socket, and the dialed command is the frozen literal,
|
|
246
|
+
* never a built string). Refuses any argument outside its documented
|
|
247
|
+
* bounds BEFORE any lease is resolved or any byte is written, mirroring
|
|
248
|
+
* `handleWarpSet()`'s shape. Retrofitted in plan 42-07 with the same
|
|
249
|
+
* classify-before-parse ordering the four PARSE-02 handlers use: `chis`
|
|
250
|
+
* shares FEATURE_CPUMEMHISTORY with `memmapshow`, so a disabled build is
|
|
251
|
+
* named by capability, command, binary and remedy -- never a parser
|
|
252
|
+
* refusal. Plan 42-01 wrote this handler before text-capability-probe.ts
|
|
253
|
+
* existed, so it originally handed a disabled-stub reply straight to the
|
|
254
|
+
* parser (a parse error that reads like a defect in this project, exactly
|
|
255
|
+
* the outcome PARSE-04 forbids). On a genuine parse refusal, answers
|
|
256
|
+
* `isErrorText` naming the tool, the refusal code, and the offending
|
|
257
|
+
* line/lineNumber -- never a partial or best-effort access map.
|
|
258
|
+
*/
|
|
259
|
+
export async function handleMemmapShow(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
260
|
+
const { startAddress, endAddress, maxRanges } = args;
|
|
261
|
+
|
|
262
|
+
if (startAddress !== undefined && !isValidAddressArg(startAddress)) {
|
|
263
|
+
return isErrorText(
|
|
264
|
+
`vice_memmap_show: "startAddress" must be an integer 0 through 65535 (got ${JSON.stringify(startAddress)}) -- ` +
|
|
265
|
+
`refusing before any text-monitor byte is written`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (endAddress !== undefined && !isValidAddressArg(endAddress)) {
|
|
269
|
+
return isErrorText(
|
|
270
|
+
`vice_memmap_show: "endAddress" must be an integer 0 through 65535 (got ${JSON.stringify(endAddress)}) -- ` +
|
|
271
|
+
`refusing before any text-monitor byte is written`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
startAddress !== undefined &&
|
|
276
|
+
endAddress !== undefined &&
|
|
277
|
+
isValidAddressArg(startAddress) &&
|
|
278
|
+
isValidAddressArg(endAddress) &&
|
|
279
|
+
startAddress > endAddress
|
|
280
|
+
) {
|
|
281
|
+
return isErrorText(
|
|
282
|
+
`vice_memmap_show: "startAddress" (${JSON.stringify(startAddress)}) must not be greater than "endAddress" ` +
|
|
283
|
+
`(${JSON.stringify(endAddress)}) -- refusing before any text-monitor byte is written`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (maxRanges !== undefined && !isValidMaxRangesArg(maxRanges)) {
|
|
287
|
+
return isErrorText(
|
|
288
|
+
`vice_memmap_show: "maxRanges" must be an integer 1 through 4096 (got ${JSON.stringify(maxRanges)}) -- ` +
|
|
289
|
+
`refusing before any text-monitor byte is written`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { identity, brokerIdentity } = await capabilityIdentityFor(deps);
|
|
294
|
+
const identityWarning = textCapabilityIdentityWarning(identity, brokerIdentity);
|
|
295
|
+
|
|
296
|
+
return withTextTool("vice_memmap_show", deps, async (client) => {
|
|
297
|
+
const response = await client.command("memmapshow", { timeoutMs: 30000 });
|
|
298
|
+
|
|
299
|
+
const classification = classifyTextCapabilityResponse("memmapshow", response);
|
|
300
|
+
if (classification.outcome !== "capable") {
|
|
301
|
+
const verdict = await probeTextCapability({ command: "memmapshow", identity, brokerIdentity, dial: async () => response });
|
|
302
|
+
return isErrorText(withIdentityWarning(`vice_memmap_show: ${textCapabilityRefusalMessage([verdict])}`, identityWarning));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const parsed = parseAccessMap(response);
|
|
306
|
+
if (!parsed.ok) {
|
|
307
|
+
return isErrorText(
|
|
308
|
+
withIdentityWarning(
|
|
309
|
+
`vice_memmap_show: memmapshow's response could not be parsed (${parsed.refusal.code} at line ` +
|
|
310
|
+
`${parsed.refusal.lineNumber}: ${JSON.stringify(parsed.refusal.line)}) -- ${parsed.refusal.message}`,
|
|
311
|
+
identityWarning,
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const rangesOpts: AccessMapRangesOptions = {};
|
|
317
|
+
if (isValidAddressArg(startAddress)) rangesOpts.startAddress = startAddress;
|
|
318
|
+
if (isValidAddressArg(endAddress)) rangesOpts.endAddress = endAddress;
|
|
319
|
+
if (isValidMaxRangesArg(maxRanges)) rangesOpts.maxRanges = maxRanges;
|
|
320
|
+
const projection = accessMapRanges(parsed.value, rangesOpts);
|
|
321
|
+
|
|
322
|
+
return derivedAnswer({
|
|
323
|
+
command: "memmapshow",
|
|
324
|
+
...projection,
|
|
325
|
+
executeCounts: executeCounts(parsed.value),
|
|
326
|
+
...(identityWarning !== "" ? { identityWarning } : {}),
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* `vice_memmap_zap` -- takes NO parameters at all (D-42-1 again, the
|
|
333
|
+
* strictest form: not even a client-side projection filter exists here to
|
|
334
|
+
* validate). Dials exactly two frozen allowlisted literals in sequence:
|
|
335
|
+
* `memmapzap` (plan 43-01, EVID-05/EVID-06's proven-dialable bracket-clear
|
|
336
|
+
* primitive), then `memmapshow`, so the answer carries an OBSERVABLE
|
|
337
|
+
* post-condition rather than a bare acknowledgement -- `memmapzap` alone
|
|
338
|
+
* returns nothing that proves anything was cleared.
|
|
339
|
+
*
|
|
340
|
+
* The build-capability verdict is BORROWED from classifying the second
|
|
341
|
+
* dial's reply (`classifyTextCapabilityResponse("memmapshow", ...)`),
|
|
342
|
+
* exactly `handleMemmapShow`'s own ordering above -- deliberately NOT by
|
|
343
|
+
* adding `"memmapzap"` to `CPUHISTORY_GATED_COMMANDS`
|
|
344
|
+
* (text-capability-probe.ts). Whether a build without FEATURE_CPUMEMHISTORY
|
|
345
|
+
* prints that same disabled stub for `memmapzap` itself is NOT known from
|
|
346
|
+
* any source this project has read; `memmapshow`'s own disabled stub IS
|
|
347
|
+
* source-traced (text-capability-probe.ts's own header). Classifying the
|
|
348
|
+
* `memmapshow` reply instead of inventing a fresh claim about `memmapzap`'s
|
|
349
|
+
* own stub means this handler asserts nothing beyond what is already
|
|
350
|
+
* proven -- and it still refuses correctly on a disabled build, because
|
|
351
|
+
* `memmapshow` shares the exact same build flag and would refuse right
|
|
352
|
+
* alongside it.
|
|
353
|
+
*
|
|
354
|
+
* On a genuine parse refusal of the `memmapshow` reply, answers
|
|
355
|
+
* `isErrorText` naming the tool, the refusal code, the line number and the
|
|
356
|
+
* offending line -- never a partial or best-effort answer, mirroring
|
|
357
|
+
* `handleMemmapShow`'s own discipline exactly -- WITH ONE MEASURED
|
|
358
|
+
* EXCEPTION, discovered live against genuine stock VICE (plan 43-03 Task 1):
|
|
359
|
+
* `no-data-lines` (header present, zero data lines -- `parseAccessMap()`'s
|
|
360
|
+
* own deliberate refusal, `textmon-memmap.ts`'s "never decoded as a
|
|
361
|
+
* zero-entry access map") is the GUARANTEED shape of a real `memmapshow`
|
|
362
|
+
* dialed immediately after a real `memmapzap`, inside this SAME
|
|
363
|
+
* `withTextChannelLock()` hold, with the machine halted the whole time --
|
|
364
|
+
* nothing can have executed between the two dials, so there is no
|
|
365
|
+
* "truncated wire reply vs. genuinely nothing recorded" ambiguity left to
|
|
366
|
+
* guard against for THIS caller specifically (the general ambiguity
|
|
367
|
+
* `parseAccessMap()`'s refusal exists to catch is real for an ARBITRARY
|
|
368
|
+
* caller of `memmapshow`, which `handleMemmapShow` above still refuses on,
|
|
369
|
+
* unchanged). Treated here, and ONLY here, as a confirmed zero-entry map
|
|
370
|
+
* (`{ entries: [] }`) rather than a refusal -- the literal fact this
|
|
371
|
+
* exact caller has already proven.
|
|
372
|
+
*
|
|
373
|
+
* The answer never carries a `ranges` key: this tool answers "is the map
|
|
374
|
+
* clear", not "what is in it" -- `vice_memmap_show` is the verb for the
|
|
375
|
+
* latter. It answers only the post-zap `addressesWithRecordedAccess` and
|
|
376
|
+
* `addressesQueried` (accessMapRanges()'s own denominator discipline,
|
|
377
|
+
* unbounded -- no `startAddress`/`endAddress`/`maxRanges` options exist here
|
|
378
|
+
* to narrow it) plus the same `executeCounts` triple `handleMemmapShow`
|
|
379
|
+
* reports.
|
|
380
|
+
*/
|
|
381
|
+
export async function handleMemmapZap(_args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
382
|
+
const { identity, brokerIdentity } = await capabilityIdentityFor(deps);
|
|
383
|
+
const identityWarning = textCapabilityIdentityWarning(identity, brokerIdentity);
|
|
384
|
+
|
|
385
|
+
return withTextTool("vice_memmap_zap", deps, async (client) => {
|
|
386
|
+
await client.command("memmapzap", { timeoutMs: 30000 });
|
|
387
|
+
const response = await client.command("memmapshow", { timeoutMs: 30000 });
|
|
388
|
+
|
|
389
|
+
const classification = classifyTextCapabilityResponse("memmapshow", response);
|
|
390
|
+
if (classification.outcome !== "capable") {
|
|
391
|
+
const verdict = await probeTextCapability({ command: "memmapshow", identity, brokerIdentity, dial: async () => response });
|
|
392
|
+
return isErrorText(withIdentityWarning(`vice_memmap_zap: ${textCapabilityRefusalMessage([verdict])}`, identityWarning));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const parsed = parseAccessMap(response);
|
|
396
|
+
let accessMap: AccessMap;
|
|
397
|
+
if (parsed.ok) {
|
|
398
|
+
accessMap = parsed.value;
|
|
399
|
+
} else if (parsed.refusal.code === "no-data-lines") {
|
|
400
|
+
// Live-measured (plan 43-03 Task 1): this IS what a real memmapzap
|
|
401
|
+
// followed immediately by memmapshow, in the same locked session,
|
|
402
|
+
// looks like -- see the doc comment above.
|
|
403
|
+
accessMap = { entries: [] };
|
|
404
|
+
} else {
|
|
405
|
+
return isErrorText(
|
|
406
|
+
withIdentityWarning(
|
|
407
|
+
`vice_memmap_zap: memmapshow's response (dialed after memmapzap) could not be parsed (${parsed.refusal.code} at line ` +
|
|
408
|
+
`${parsed.refusal.lineNumber}: ${JSON.stringify(parsed.refusal.line)}) -- ${parsed.refusal.message}`,
|
|
409
|
+
identityWarning,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const projection = accessMapRanges(accessMap, {});
|
|
415
|
+
|
|
416
|
+
return derivedAnswer({
|
|
417
|
+
command: "memmapzap",
|
|
418
|
+
addressesWithRecordedAccess: projection.addressesWithRecordedAccess,
|
|
419
|
+
addressesQueried: projection.addressesQueried,
|
|
420
|
+
executeCounts: executeCounts(accessMap),
|
|
421
|
+
...(identityWarning !== "" ? { identityWarning } : {}),
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// Plan 42-07: the four remaining text formats -- chis, prof flat, bt, io --
|
|
428
|
+
// plus the shared capability-identity helper every one of the five text
|
|
429
|
+
// tools in this file (including handleMemmapShow above, retrofitted) uses to
|
|
430
|
+
// classify a raw reply for build capability BEFORE handing it to a parser
|
|
431
|
+
// (PARSE-04, D-42-2). See text-capability-probe.ts's own header comment for
|
|
432
|
+
// the full three-outcome design this section leans on.
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
/** THE ONE place that decides what identity a capability answer is
|
|
436
|
+
* attributed to (D-42-2), used by all five text tools in this file.
|
|
437
|
+
* `identity` comes from `deps.resolvedBinaryPath`/
|
|
438
|
+
* `deps.resolvedBinaryPathIsResolved` -- the SAME single dispatch-layer
|
|
439
|
+
* resolution `stock-dispatch.ts`'s own `StockDispatchDeps` doc comment
|
|
440
|
+
* documents, never re-resolved here. Every tool registered in this file
|
|
441
|
+
* runs ONLY on the stock backend (STOCK_DERIVED_TOOLS), so `backend` is
|
|
442
|
+
* always the "stock" literal -- never invented for a caller this module
|
|
443
|
+
* could not actually be talking to.
|
|
444
|
+
*
|
|
445
|
+
* `brokerIdentity`, when obtainable, is the broker's OWN reported identity
|
|
446
|
+
* (`BrokerControlSession.hostState()`'s `backend`/`vice_bin` fields) for
|
|
447
|
+
* `probeTextCapability()`'s own cross-check (D-42-2). A failed `hostState()`
|
|
448
|
+
* call (a broker predating the field, no lease at all, or a control-plane
|
|
449
|
+
* hiccup) is ABSENT EVIDENCE, never disagreement -- `brokerIdentity` is
|
|
450
|
+
* simply omitted, matching `probeTextCapability()`'s own documented
|
|
451
|
+
* treatment of an omitted broker identity.
|
|
452
|
+
*/
|
|
453
|
+
async function capabilityIdentityFor(
|
|
454
|
+
deps: StockDispatchDeps,
|
|
455
|
+
): Promise<{ identity: TextCapabilityIdentity; brokerIdentity?: TextCapabilityBrokerIdentity }> {
|
|
456
|
+
const identity: TextCapabilityIdentity = {
|
|
457
|
+
backend: "stock",
|
|
458
|
+
binPath: deps.resolvedBinaryPath ?? "",
|
|
459
|
+
resolved: deps.resolvedBinaryPathIsResolved ?? false,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const leaseOutcome = await deps.ensureLease();
|
|
463
|
+
if (!leaseOutcome.ok || leaseOutcome.lease === null) {
|
|
464
|
+
return { identity };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const hostStateResult = await leaseOutcome.lease.brokerControl.hostState();
|
|
469
|
+
if (!hostStateResult.ok) return { identity };
|
|
470
|
+
return {
|
|
471
|
+
identity,
|
|
472
|
+
brokerIdentity: { backend: hostStateResult.hostState.backend, binPath: hostStateResult.hostState.vice_bin },
|
|
473
|
+
};
|
|
474
|
+
} catch {
|
|
475
|
+
return { identity };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Appends `warning` (from {@link textCapabilityIdentityWarning}) on its own
|
|
480
|
+
* line after `text` when non-empty; returns `text` unchanged when there is
|
|
481
|
+
* nothing to report. Used on every POST-LEASE error path in every handler
|
|
482
|
+
* below (plan 42-13, G3) -- never on a pre-dial argument-validation refusal,
|
|
483
|
+
* which returns before an identity is ever resolved and has nothing to
|
|
484
|
+
* attribute a warning to. */
|
|
485
|
+
function withIdentityWarning(text: string, warning: string): string {
|
|
486
|
+
return warning === "" ? text : `${text}\n${warning}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* `vice_cpu_history` -- dials `chis`, optionally parameterized with a
|
|
491
|
+
* caller-chosen decimal row count via `buildTextCommand()` (the ONE place
|
|
492
|
+
* such a string is built, D-42-1). An omitted `count` dials the bare frozen
|
|
493
|
+
* verb; a supplied one is validated and rendered by the builder alone --
|
|
494
|
+
* this handler never states or duplicates the builder's own bound.
|
|
495
|
+
* Classifies the raw reply for build capability before parsing (PARSE-04):
|
|
496
|
+
* `chis` shares FEATURE_CPUMEMHISTORY with `memmapshow`, so a disabled build
|
|
497
|
+
* is named by capability, command, binary and remedy -- never a parser
|
|
498
|
+
* refusal.
|
|
499
|
+
*/
|
|
500
|
+
export async function handleCpuHistory(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
501
|
+
const { count } = args;
|
|
502
|
+
let command = "chis";
|
|
503
|
+
if (count !== undefined) {
|
|
504
|
+
const built = buildTextCommand("chis", count);
|
|
505
|
+
if (!built.ok) {
|
|
506
|
+
return isErrorText(`vice_cpu_history: ${built.message} -- refusing before any text-monitor byte is written`);
|
|
507
|
+
}
|
|
508
|
+
command = built.command;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const { identity, brokerIdentity } = await capabilityIdentityFor(deps);
|
|
512
|
+
const identityWarning = textCapabilityIdentityWarning(identity, brokerIdentity);
|
|
513
|
+
|
|
514
|
+
return withTextTool("vice_cpu_history", deps, async (client) => {
|
|
515
|
+
const response = await client.command(command, { timeoutMs: 30000 });
|
|
516
|
+
|
|
517
|
+
const classification = classifyTextCapabilityResponse("chis", response);
|
|
518
|
+
if (classification.outcome !== "capable") {
|
|
519
|
+
const verdict = await probeTextCapability({ command: "chis", identity, brokerIdentity, dial: async () => response });
|
|
520
|
+
return isErrorText(withIdentityWarning(`vice_cpu_history: ${textCapabilityRefusalMessage([verdict])}`, identityWarning));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const parsed = parseCpuHistory(response);
|
|
524
|
+
if (!parsed.ok) {
|
|
525
|
+
return isErrorText(
|
|
526
|
+
withIdentityWarning(
|
|
527
|
+
`vice_cpu_history: chis's response could not be parsed (${parsed.refusal.code} at line ` +
|
|
528
|
+
`${parsed.refusal.lineNumber}: ${JSON.stringify(parsed.refusal.line)}) -- ${parsed.refusal.message}`,
|
|
529
|
+
identityWarning,
|
|
530
|
+
),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return derivedAnswer({
|
|
535
|
+
command,
|
|
536
|
+
entries: parsed.value.entries,
|
|
537
|
+
count: parsed.value.entries.length,
|
|
538
|
+
...(identityWarning !== "" ? { identityWarning } : {}),
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* `vice_profile_flat` -- dials `prof flat`, optionally parameterized with a
|
|
545
|
+
* caller-chosen decimal row count via `buildTextCommand()`, exactly mirroring
|
|
546
|
+
* `handleCpuHistory()`'s argument shape. `prof flat` carries NO build-time
|
|
547
|
+
* guard at all -- the classifier can only ever return `capable` or
|
|
548
|
+
* `indeterminate` for this verb -- but is still classified before parsing so
|
|
549
|
+
* an indeterminate (empty/unframeable) reply is a named state rather than a
|
|
550
|
+
* silent empty success.
|
|
551
|
+
*/
|
|
552
|
+
export async function handleProfileFlat(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
553
|
+
const { count } = args;
|
|
554
|
+
let command = "prof flat";
|
|
555
|
+
if (count !== undefined) {
|
|
556
|
+
const built = buildTextCommand("prof flat", count);
|
|
557
|
+
if (!built.ok) {
|
|
558
|
+
return isErrorText(`vice_profile_flat: ${built.message} -- refusing before any text-monitor byte is written`);
|
|
559
|
+
}
|
|
560
|
+
command = built.command;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const { identity, brokerIdentity } = await capabilityIdentityFor(deps);
|
|
564
|
+
const identityWarning = textCapabilityIdentityWarning(identity, brokerIdentity);
|
|
565
|
+
|
|
566
|
+
return withTextTool("vice_profile_flat", deps, async (client) => {
|
|
567
|
+
const response = await client.command(command, { timeoutMs: 30000 });
|
|
568
|
+
|
|
569
|
+
const classification = classifyTextCapabilityResponse("prof flat", response);
|
|
570
|
+
if (classification.outcome !== "capable") {
|
|
571
|
+
const verdict = await probeTextCapability({ command: "prof flat", identity, brokerIdentity, dial: async () => response });
|
|
572
|
+
return isErrorText(withIdentityWarning(`vice_profile_flat: ${textCapabilityRefusalMessage([verdict])}`, identityWarning));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const parsed = parseFlatProfile(response);
|
|
576
|
+
if (!parsed.ok) {
|
|
577
|
+
// A profiler that was never started (profiling-not-started, plan
|
|
578
|
+
// 42-13) is a named state the owning module recognises, not a parse
|
|
579
|
+
// defect -- rendering it through the wrapper below would make a
|
|
580
|
+
// legitimate external condition (the profiler subsystem simply has
|
|
581
|
+
// nothing recorded yet) read as a bug in this project. This changes
|
|
582
|
+
// the DESCRIPTION of the cold state only: the separate, still-open
|
|
583
|
+
// inability for this tree to start profiling on the user's behalf
|
|
584
|
+
// (Window #55) is unaffected. Every other code keeps the wrapper.
|
|
585
|
+
if (parsed.refusal.code === "profiling-not-started") {
|
|
586
|
+
return isErrorText(withIdentityWarning(`vice_profile_flat: ${parsed.refusal.message}`, identityWarning));
|
|
587
|
+
}
|
|
588
|
+
return isErrorText(
|
|
589
|
+
withIdentityWarning(
|
|
590
|
+
`vice_profile_flat: prof flat's response could not be parsed (${parsed.refusal.code} at line ` +
|
|
591
|
+
`${parsed.refusal.lineNumber}: ${JSON.stringify(parsed.refusal.line)}) -- ${parsed.refusal.message}`,
|
|
592
|
+
identityWarning,
|
|
593
|
+
),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return derivedAnswer({
|
|
598
|
+
command,
|
|
599
|
+
entries: parsed.value.entries,
|
|
600
|
+
count: parsed.value.entries.length,
|
|
601
|
+
decimalSeparator: parsed.value.decimalSeparator,
|
|
602
|
+
...(identityWarning !== "" ? { identityWarning } : {}),
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** True iff `value` is a representable integer 1 through 64 -- the shared
|
|
608
|
+
* bound the fork's own `vice_backtrace` tool description already declares
|
|
609
|
+
* ("Max stack frames to show (default: 16, max: 64)"). This is the ONE
|
|
610
|
+
* exception D-42-1 states explicitly: `depth` is a client-side projection
|
|
611
|
+
* FILTER applied to the PARSED frame list, never part of the "bt" command
|
|
612
|
+
* itself (the verb takes no wire parameter at all), so it is validated
|
|
613
|
+
* locally rather than through `buildTextCommand()`. */
|
|
614
|
+
function isValidBacktraceDepthArg(value: unknown): value is number {
|
|
615
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 64;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* `vice_backtrace` -- takes the name the fork's own tool already advertises
|
|
620
|
+
* (D-42-4): landing the text-monitor implementation under the SAME name,
|
|
621
|
+
* with a backward-compatible argument shape (the fork's optional numeric
|
|
622
|
+
* `depth` stays optional and stays numeric), makes this a SHARED tool
|
|
623
|
+
* rather than a second vocabulary for one capability. Dials the bare,
|
|
624
|
+
* unparameterized `bt` verb -- `depth`, when supplied, truncates the
|
|
625
|
+
* PARSED frames only, and the answer reports both the returned count and
|
|
626
|
+
* the total so truncation is never silently a function of the argument.
|
|
627
|
+
* `bt` carries no build-time guard; classified before parsing anyway so an
|
|
628
|
+
* indeterminate reply is a named state.
|
|
629
|
+
*/
|
|
630
|
+
export async function handleBacktrace(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
631
|
+
const { depth } = args;
|
|
632
|
+
if (depth !== undefined && !isValidBacktraceDepthArg(depth)) {
|
|
633
|
+
return isErrorText(
|
|
634
|
+
`vice_backtrace: "depth" must be an integer 1 through 64 (got ${JSON.stringify(depth)}) -- refusing before ` +
|
|
635
|
+
`any text-monitor byte is written`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const { identity, brokerIdentity } = await capabilityIdentityFor(deps);
|
|
640
|
+
const identityWarning = textCapabilityIdentityWarning(identity, brokerIdentity);
|
|
641
|
+
|
|
642
|
+
return withTextTool("vice_backtrace", deps, async (client) => {
|
|
643
|
+
const response = await client.command("bt", { timeoutMs: 30000 });
|
|
644
|
+
|
|
645
|
+
const classification = classifyTextCapabilityResponse("bt", response);
|
|
646
|
+
if (classification.outcome !== "capable") {
|
|
647
|
+
const verdict = await probeTextCapability({ command: "bt", identity, brokerIdentity, dial: async () => response });
|
|
648
|
+
return isErrorText(withIdentityWarning(`vice_backtrace: ${textCapabilityRefusalMessage([verdict])}`, identityWarning));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const parsed = parseBacktrace(response);
|
|
652
|
+
if (!parsed.ok) {
|
|
653
|
+
return isErrorText(
|
|
654
|
+
withIdentityWarning(
|
|
655
|
+
`vice_backtrace: bt's response could not be parsed (${parsed.refusal.code} at line ` +
|
|
656
|
+
`${parsed.refusal.lineNumber}: ${JSON.stringify(parsed.refusal.line)}) -- ${parsed.refusal.message}`,
|
|
657
|
+
identityWarning,
|
|
658
|
+
),
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const totalCount = parsed.value.frames.length;
|
|
663
|
+
const truncated = isValidBacktraceDepthArg(depth) && depth < totalCount;
|
|
664
|
+
const frames = isValidBacktraceDepthArg(depth) ? parsed.value.frames.slice(0, depth) : parsed.value.frames;
|
|
665
|
+
|
|
666
|
+
return derivedAnswer({
|
|
667
|
+
command: "bt",
|
|
668
|
+
currentPc: parsed.value.currentPc,
|
|
669
|
+
frames,
|
|
670
|
+
returnedCount: frames.length,
|
|
671
|
+
totalCount,
|
|
672
|
+
truncated,
|
|
673
|
+
...(identityWarning !== "" ? { identityWarning } : {}),
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* `vice_io_registers` -- dials `io $aaaa` for a REQUIRED `address` argument:
|
|
680
|
+
* unlike the other three parameterized tools, this one refuses a MISSING
|
|
681
|
+
* address outright, because the bare "io" verb dumps every chip and this
|
|
682
|
+
* tool decodes the chip covering exactly one. `address` is validated and
|
|
683
|
+
* rendered by `buildTextCommand()` alone. `io` carries no build-time guard
|
|
684
|
+
* -- `classifyTextCapabilityResponse()` can only ever return `capable` or
|
|
685
|
+
* `indeterminate` for this verb -- but it still degrades PER-CHIP at
|
|
686
|
+
* runtime with its own two fixed strings, a fact the classifier alone
|
|
687
|
+
* cannot see; the probe module's own renderer (`textCapabilityRefusalMessage()`)
|
|
688
|
+
* is always consulted below, whatever this classification said, since that
|
|
689
|
+
* is the one place the chip-degradation-vs-missing-capability distinction
|
|
690
|
+
* is drawn.
|
|
691
|
+
*
|
|
692
|
+
* CR-02 (corrected): `io`'s outcome is decided by THIS CALL's OWN `address`
|
|
693
|
+
* argument, not by a property of the binary -- so it must never be
|
|
694
|
+
* classified through `probeTextCapability()`'s memoised entry point. The
|
|
695
|
+
* old wiring did exactly that, and a second call to a different address
|
|
696
|
+
* silently received the FIRST call's cached verdict: one call's evidence
|
|
697
|
+
* answering another call's question. This handler instead builds its
|
|
698
|
+
* verdict directly from the response THIS call just received, via
|
|
699
|
+
* `textCapabilityVerdictFor()` (no dial, no cache read, no cache write),
|
|
700
|
+
* and hands that verdict to the unchanged `textCapabilityRefusalMessage()`
|
|
701
|
+
* -- still the one place the chip-degradation-vs-missing-capability
|
|
702
|
+
* distinction is drawn, just applied to the right response. Do not
|
|
703
|
+
* "simplify" this back onto `probeTextCapability()`; `io` is additionally
|
|
704
|
+
* excluded from that cache's domain structurally (`NEVER_CACHED_COMMANDS`
|
|
705
|
+
* in text-capability-probe.ts) so this handler could not reach it that way
|
|
706
|
+
* even by accident.
|
|
707
|
+
*/
|
|
708
|
+
export async function handleIoRegisters(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
709
|
+
const { address } = args;
|
|
710
|
+
if (address === undefined) {
|
|
711
|
+
return isErrorText(
|
|
712
|
+
`vice_io_registers: "address" is REQUIRED (the bare "io" verb dumps every chip; this tool decodes the chip ` +
|
|
713
|
+
`covering ONE address) -- refusing before any text-monitor byte is written`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const built = buildTextCommand("io", address);
|
|
717
|
+
if (!built.ok) {
|
|
718
|
+
return isErrorText(`vice_io_registers: ${built.message} -- refusing before any text-monitor byte is written`);
|
|
719
|
+
}
|
|
720
|
+
const command = built.command;
|
|
721
|
+
|
|
722
|
+
const { identity, brokerIdentity } = await capabilityIdentityFor(deps);
|
|
723
|
+
const identityWarning = textCapabilityIdentityWarning(identity, brokerIdentity);
|
|
724
|
+
|
|
725
|
+
return withTextTool("vice_io_registers", deps, async (client) => {
|
|
726
|
+
const response = await client.command(command, { timeoutMs: 30000 });
|
|
727
|
+
|
|
728
|
+
// "io" is never gated behind FEATURE_CPUMEMHISTORY -- classifyTextCapabilityResponse()
|
|
729
|
+
// only ever returns "capable" or "indeterminate" for this verb (see
|
|
730
|
+
// CPUHISTORY_GATED_COMMANDS in text-capability-probe.ts), so "capable" here is
|
|
731
|
+
// not the same as "nothing to refuse": the probe module's own renderer is
|
|
732
|
+
// always consulted below, since it is what additionally catches io's own
|
|
733
|
+
// per-chip runtime degradation. For this verb the classifier can therefore
|
|
734
|
+
// return only capable or indeterminate, NEVER missing -- that invariant is
|
|
735
|
+
// stated here in prose rather than by a computed-and-discarded call, since
|
|
736
|
+
// this handler has nothing further to do with the classifier's answer.
|
|
737
|
+
//
|
|
738
|
+
// CR-02: this verdict is built from the response THIS call just
|
|
739
|
+
// received, via textCapabilityVerdictFor() -- NOT via probeTextCapability(),
|
|
740
|
+
// which would classify on a possibly-stale cached verdict built from a
|
|
741
|
+
// DIFFERENT call's address. io's outcome is per-call, per-address content,
|
|
742
|
+
// never a binary-wide capability fact, so it never reaches the memoised
|
|
743
|
+
// entry point at all.
|
|
744
|
+
const verdict = textCapabilityVerdictFor({ command: "io", response, identity, ...(brokerIdentity !== undefined ? { brokerIdentity } : {}) });
|
|
745
|
+
const refusalMessage = textCapabilityRefusalMessage([verdict]);
|
|
746
|
+
if (refusalMessage !== "") {
|
|
747
|
+
return isErrorText(withIdentityWarning(`vice_io_registers: ${refusalMessage}`, identityWarning));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const parsed = parseIoRegisters(response);
|
|
751
|
+
if (!parsed.ok) {
|
|
752
|
+
// A chip this parser does not decode (unsupported-chip, plan 42-11,
|
|
753
|
+
// WR-02) is a legitimately different reply whose register dump read
|
|
754
|
+
// cleanly -- reporting it through the parse-failure wrapper below
|
|
755
|
+
// would make an external chip difference read as a defect in this
|
|
756
|
+
// project. Render the parser's own message verbatim, under the tool
|
|
757
|
+
// name only. Every other refusal code keeps the wrapper unchanged.
|
|
758
|
+
if (parsed.refusal.code === "unsupported-chip") {
|
|
759
|
+
return isErrorText(withIdentityWarning(`vice_io_registers: ${parsed.refusal.message}`, identityWarning));
|
|
760
|
+
}
|
|
761
|
+
return isErrorText(
|
|
762
|
+
withIdentityWarning(
|
|
763
|
+
`vice_io_registers: io's response could not be parsed (${parsed.refusal.code} at line ` +
|
|
764
|
+
`${parsed.refusal.lineNumber}: ${JSON.stringify(parsed.refusal.line)}) -- ${parsed.refusal.message}`,
|
|
765
|
+
identityWarning,
|
|
766
|
+
),
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return derivedAnswer({
|
|
771
|
+
command,
|
|
772
|
+
sections: parsed.value.sections,
|
|
773
|
+
unrecognisedLines: parsed.value.unrecognisedLines,
|
|
774
|
+
unrecognisedLineCount: parsed.value.unrecognisedLines.length,
|
|
775
|
+
...(identityWarning !== "" ? { identityWarning } : {}),
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
}
|