c64bridge 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/skills/assembly-program/SKILL.md +37 -0
- package/.github/skills/basic-program/SKILL.md +40 -0
- package/.github/skills/cross-platform-demo/SKILL.md +32 -0
- package/.github/skills/drive-manager/SKILL.md +33 -0
- package/.github/skills/graphics-demo/SKILL.md +30 -0
- package/.github/skills/hello-world/SKILL.md +36 -0
- package/.github/skills/memory-debug/SKILL.md +32 -0
- package/.github/skills/pr-converge/SKILL.md +33 -0
- package/.github/skills/printer-job/SKILL.md +30 -0
- package/.github/skills/sid-music/SKILL.md +35 -0
- package/.github/skills/software-launch/SKILL.md +31 -0
- package/.github/skills/stream-control/SKILL.md +29 -0
- package/.github/skills/system-control/SKILL.md +32 -0
- package/AGENTS.md +28 -7
- package/README.md +70 -15
- package/data/assembly/assembly-spec.md +2 -0
- package/dist/c64Client.js +238 -60
- package/dist/chargen.js +0 -4
- package/dist/config.js +53 -28
- package/dist/device.js +92 -37
- package/dist/diagnostics.js +66 -5
- package/dist/hostPaths.js +128 -0
- package/dist/logger.js +3 -2
- package/dist/loggingHttpClient.js +71 -2
- package/dist/mcp-server.js +416 -291
- package/dist/numericArg.js +55 -0
- package/dist/petscii.js +13 -6
- package/dist/platform.js +12 -3
- package/dist/prompts/registry.js +6 -1
- package/dist/redact.js +166 -0
- package/dist/tools/access.js +66 -0
- package/dist/tools/debug.js +6 -0
- package/dist/tools/developer.js +77 -36
- package/dist/tools/errors.js +54 -3
- package/dist/tools/input.js +25 -8
- package/dist/tools/machineControl.js +113 -21
- package/dist/tools/memory.js +135 -30
- package/dist/tools/meta/artifacts.js +5 -5
- package/dist/tools/meta/background.js +216 -54
- package/dist/tools/meta/compilation.js +6 -5
- package/dist/tools/meta/config.js +20 -10
- package/dist/tools/meta/configInventory.js +25 -2
- package/dist/tools/meta/diagnostics.js +2 -1
- package/dist/tools/meta/filesystem.js +16 -14
- package/dist/tools/meta/graphics.js +48 -20
- package/dist/tools/meta/memory.js +45 -19
- package/dist/tools/meta/program.js +10 -7
- package/dist/tools/meta/screen.js +116 -25
- package/dist/tools/pauseGuard.js +23 -0
- package/dist/tools/pollValidator.js +53 -8
- package/dist/tools/programRunners.js +62 -18
- package/dist/tools/registry/batch.js +17 -11
- package/dist/tools/registry/config.js +9 -2
- package/dist/tools/registry/disk.js +12 -2
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/extract.js +3 -0
- package/dist/tools/registry/graphics.js +59 -6
- package/dist/tools/registry/index.js +17 -1
- package/dist/tools/registry/memory.js +11 -1
- package/dist/tools/registry/platform.js +4 -1
- package/dist/tools/registry/printer.js +2 -1
- package/dist/tools/registry/program.js +5 -1
- package/dist/tools/registry/rag.js +1 -0
- package/dist/tools/registry/sound.js +12 -1
- package/dist/tools/registry/stream.js +2 -1
- package/dist/tools/registry/system.js +8 -1
- package/dist/tools/schema.js +7 -7
- package/dist/tools/streaming.js +34 -4
- package/dist/tools/translation/assembler.js +247 -24
- package/dist/tools/translation/basicDetokenizer.js +80 -0
- package/dist/tools/translation/basicTokenizer.js +8 -3
- package/dist/tools/translation/disassembler.js +14 -0
- package/dist/tools/types.js +258 -7
- package/dist/tools/vice.js +5 -16
- package/dist/vicBitmap.js +1 -1
- package/dist/vice/mockServer.js +3 -0
- package/dist/vice/process.js +19 -0
- package/dist/vice/resourcePolicy.js +36 -0
- package/dist/vice/viceClient.js +22 -5
- package/doc/vice/support-matrix.md +17 -11
- package/mcp/schemas/c64_batch.schema.json +2 -1
- package/mcp/schemas/c64_config.schema.json +33 -466
- package/mcp/schemas/c64_debug.schema.json +3 -602
- package/mcp/schemas/c64_disk.schema.json +17 -344
- package/mcp/schemas/c64_drive.schema.json +2 -157
- package/mcp/schemas/c64_extract.schema.json +10 -364
- package/mcp/schemas/c64_graphics.schema.json +19 -426
- package/mcp/schemas/c64_input.schema.json +5 -318
- package/mcp/schemas/c64_memory.schema.json +23 -491
- package/mcp/schemas/c64_printer.schema.json +3 -258
- package/mcp/schemas/c64_program.schema.json +16 -491
- package/mcp/schemas/c64_rag.schema.json +1 -81
- package/mcp/schemas/c64_select_backend.schema.json +2 -36
- package/mcp/schemas/c64_sound.schema.json +20 -822
- package/mcp/schemas/c64_stream.schema.json +4 -75
- package/mcp/schemas/c64_system.schema.json +28 -348
- package/mcp/schemas/c64_vice.schema.json +2 -71
- package/mcp/server.json +1 -1
- package/mcp/tools.json +1990 -6900
- package/mcp.json +21 -1
- package/package.json +19 -8
- package/scripts/mockC64Server.mjs +44 -0
- package/scripts/run-coverage.mjs +27 -1
- package/scripts/smoke-run.sh +3 -2
- package/scripts/start.mjs +23 -1
- package/server.json +53 -3
- package/dist/.tsbuildinfo +0 -1
- package/doc/agent-state-spec.md +0 -243
- package/doc/plans/bun/baseline-metrics.md +0 -16
- package/doc/plans/bun/migration-checklist.md +0 -55
- package/doc/plans/bun/migration.md +0 -83
- package/doc/plans/improve-knowledge/notes.md +0 -43
- package/doc/plans/improve-knowledge/plan.md +0 -121
- package/doc/plans/improve-knowledge/rollout.md +0 -110
- package/doc/plans/meta-tools/README.md +0 -110
- package/doc/plans/meta-tools/analysis.md +0 -272
- package/doc/plans/meta-tools/catalog.md +0 -435
- package/doc/plans/meta-tools/notes.md +0 -192
- package/doc/plans/meta-tools/rollout.md +0 -368
- package/doc/plans/prompts/README.md +0 -96
- package/doc/plans/prompts/user-prompt-ideas.md +0 -147
- package/doc/plans/tool-consolidation/notes.md +0 -28
- package/doc/plans/tool-consolidation/plan.md +0 -169
- package/doc/plans/tool-consolidation/rollout.md +0 -104
- package/doc/plans/vice-backend-switch/plan.md +0 -486
- package/doc/plans/vice-backend-switch-2/plan.md +0 -48
- package/doc/plans/vice-support/notes.md +0 -63
- package/doc/plans/vice-support/plan.md +0 -88
- package/doc/plans/vice-support/rollout.md +0 -99
- package/doc/reviews/audit.md +0 -143
- package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
- package/doc/reviews/hardening/01-fable/prompt.md +0 -98
- package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
- package/doc/reviews/hardening/01-fable/review.md +0 -1234
- package/doc/reviews/vice-support-review.md +0 -185
- package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
|
@@ -5,6 +5,7 @@ Copyright (C) 2025 Christian Gleissner
|
|
|
5
5
|
Licensed under the GNU General Public License v2.0 or later.
|
|
6
6
|
See <https://www.gnu.org/licenses/> for details.
|
|
7
7
|
*/
|
|
8
|
+
const LOAD_CHECK_BYTES = 16;
|
|
8
9
|
/**
|
|
9
10
|
* Load poll configuration from environment variables with defaults.
|
|
10
11
|
*/
|
|
@@ -156,7 +157,49 @@ async function pollBasicOutcome(client, logger, config) {
|
|
|
156
157
|
* to determine if program is alive or crashed.
|
|
157
158
|
* First waits for RUN to appear, then monitors for activity across multiple memory regions.
|
|
158
159
|
*/
|
|
159
|
-
|
|
160
|
+
function toHex(bytes) {
|
|
161
|
+
return Array.from(bytes, (value) => value.toString(16).toUpperCase().padStart(2, "0")).join(" ");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Reads back up to 16 bytes at the load address and compares them with the
|
|
165
|
+
* assembled program. A U2-family firmware answers run_prg with success without
|
|
166
|
+
* loading anything, so this is the evidence that the program reached memory.
|
|
167
|
+
* Needs confirmation on U2 hardware; it is exercised against mocks only.
|
|
168
|
+
*/
|
|
169
|
+
async function checkProgramLoaded(client, logger, program) {
|
|
170
|
+
const length = Math.min(LOAD_CHECK_BYTES, program.bytes.length, 0x10000 - program.address);
|
|
171
|
+
if (length <= 0) {
|
|
172
|
+
return { loadCheck: "skipped" };
|
|
173
|
+
}
|
|
174
|
+
const expected = program.bytes.subarray(0, length);
|
|
175
|
+
let found;
|
|
176
|
+
try {
|
|
177
|
+
found = await client.readMemoryRaw(program.address, length);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
logger.debug("Unable to read back the loaded program", { error });
|
|
181
|
+
return { loadCheck: "unavailable" };
|
|
182
|
+
}
|
|
183
|
+
if (!buffersEqual(found.subarray(0, length), expected)) {
|
|
184
|
+
return { loadCheck: "matched", load: { address: program.address, expected: toHex(expected), found: toHex(found.subarray(0, length)) } };
|
|
185
|
+
}
|
|
186
|
+
return { loadCheck: "matched" };
|
|
187
|
+
}
|
|
188
|
+
async function pollAsmOutcome(client, logger, config, program) {
|
|
189
|
+
let loadCheck = "skipped";
|
|
190
|
+
if (program) {
|
|
191
|
+
const check = await checkProgramLoaded(client, logger, program);
|
|
192
|
+
loadCheck = check.loadCheck;
|
|
193
|
+
if (check.load) {
|
|
194
|
+
return {
|
|
195
|
+
status: "not_loaded",
|
|
196
|
+
type: "ASM",
|
|
197
|
+
reason: `memory at $${program.address.toString(16).toUpperCase().padStart(4, "0")} does not contain the assembled program`,
|
|
198
|
+
loadCheck: "matched",
|
|
199
|
+
load: check.load,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
160
203
|
const startTime = Date.now();
|
|
161
204
|
let pollCount = 0;
|
|
162
205
|
let runDetected = false;
|
|
@@ -173,7 +216,7 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
173
216
|
if (returnedToBasicAfterSys(screen)) {
|
|
174
217
|
// The typed SYS line is followed by READY., so the routine ran and returned with RTS.
|
|
175
218
|
logger.debug("ASM program returned to BASIC", { elapsed: Date.now() - startTime });
|
|
176
|
-
return { status: "ok", type: "ASM" };
|
|
219
|
+
return { status: "ok", type: "ASM", loadCheck };
|
|
177
220
|
}
|
|
178
221
|
if (upperScreen.includes("RUN") || upperScreen.includes("SYS") || upperScreen.includes("ERROR")) {
|
|
179
222
|
runDetected = true;
|
|
@@ -204,7 +247,7 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
204
247
|
// A routine that works for a while before RTS shows READY. only after the first screen read.
|
|
205
248
|
if (returnedToBasicAfterSys(await client.readScreen())) {
|
|
206
249
|
logger.debug("ASM program returned to BASIC", { pollCount, elapsed: Date.now() - startTime });
|
|
207
|
-
return { status: "ok", type: "ASM" };
|
|
250
|
+
return { status: "ok", type: "ASM", loadCheck };
|
|
208
251
|
}
|
|
209
252
|
}
|
|
210
253
|
catch (error) {
|
|
@@ -235,14 +278,16 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
235
278
|
return {
|
|
236
279
|
status: "ok",
|
|
237
280
|
type: "ASM",
|
|
281
|
+
loadCheck,
|
|
238
282
|
};
|
|
239
283
|
}
|
|
240
|
-
// No
|
|
241
|
-
logger.debug("ASM program
|
|
284
|
+
// No screen change: the program may be running silently, so this is not evidence of a crash.
|
|
285
|
+
logger.debug("ASM program unconfirmed - no screen progression", { pollCount, elapsed: Date.now() - startTime });
|
|
242
286
|
return {
|
|
243
|
-
status: "
|
|
287
|
+
status: "unconfirmed",
|
|
244
288
|
type: "ASM",
|
|
245
289
|
reason: "no program-visible screen progression within window",
|
|
290
|
+
loadCheck,
|
|
246
291
|
};
|
|
247
292
|
}
|
|
248
293
|
/**
|
|
@@ -254,7 +299,7 @@ async function pollAsmOutcome(client, logger, config) {
|
|
|
254
299
|
* @param config - Optional poll configuration (uses environment defaults if not provided)
|
|
255
300
|
* @returns Promise resolving to poll result
|
|
256
301
|
*/
|
|
257
|
-
export async function pollForProgramOutcome(type, client, logger, config) {
|
|
302
|
+
export async function pollForProgramOutcome(type, client, logger, config, program) {
|
|
258
303
|
const defaults = loadPollConfig();
|
|
259
304
|
const pollConfig = {
|
|
260
305
|
maxMs: config?.maxMs ?? defaults.maxMs,
|
|
@@ -271,5 +316,5 @@ export async function pollForProgramOutcome(type, client, logger, config) {
|
|
|
271
316
|
if (type === "BASIC") {
|
|
272
317
|
return pollBasicOutcome(client, logger, pollConfig);
|
|
273
318
|
}
|
|
274
|
-
return pollAsmOutcome(client, logger, pollConfig);
|
|
319
|
+
return pollAsmOutcome(client, logger, pollConfig, program);
|
|
275
320
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { resolveHostPath } from "../hostPaths.js";
|
|
2
3
|
import { assemblyToPrgDetailed, AssemblyError } from "./translation/assembler.js";
|
|
3
4
|
import { basicToPrg } from "./translation/basicTokenizer.js";
|
|
4
5
|
import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
|
|
@@ -117,10 +118,13 @@ function attemptAutoFixBasicProgram(program, errors) {
|
|
|
117
118
|
}
|
|
118
119
|
if (notes.length > 0) {
|
|
119
120
|
const updated = content.length > 0 ? `${lineNumber} ${content}` : `${lineNumber}`;
|
|
121
|
+
const before = rawLine.trim();
|
|
120
122
|
lines[index] = updated;
|
|
121
123
|
changes.push({
|
|
122
124
|
line: lineNumber,
|
|
123
125
|
notes,
|
|
126
|
+
before,
|
|
127
|
+
after: updated,
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
130
|
}
|
|
@@ -206,6 +210,10 @@ const uploadBasicArgsSchema = objectSchema({
|
|
|
206
210
|
description: "Run post-execution polling to ensure the BASIC program executed without errors.",
|
|
207
211
|
default: false,
|
|
208
212
|
}),
|
|
213
|
+
autoFix: optionalSchema(booleanSchema({
|
|
214
|
+
description: "When a BASIC error is detected on screen, append a missing closing quote or parentheses to the failing line and run the program again (default true). The rewritten lines are reported with before and after text. Set false for programs with side effects, which must not run twice.",
|
|
215
|
+
default: true,
|
|
216
|
+
})),
|
|
209
217
|
},
|
|
210
218
|
required: ["program"],
|
|
211
219
|
additionalProperties: false,
|
|
@@ -218,7 +226,7 @@ const uploadAsmArgsSchema = objectSchema({
|
|
|
218
226
|
minLength: 1,
|
|
219
227
|
}),
|
|
220
228
|
verify: booleanSchema({
|
|
221
|
-
description: "
|
|
229
|
+
description: "Treat missing evidence of execution as an error. The bytes read back at the load address must always match the assembled program (a mismatch is an error either way). With verify:true, a program that loaded but changed nothing on screen is an error; with verify:false it returns success with execution:\"unconfirmed\" (SID players, border effects and JMP * loops change no screen RAM).",
|
|
222
230
|
default: false,
|
|
223
231
|
}),
|
|
224
232
|
},
|
|
@@ -287,7 +295,7 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
287
295
|
errors: errors.map((error) => ({ line: error.line, type: error.type })),
|
|
288
296
|
});
|
|
289
297
|
const normalizedErrors = normalizeRuntimeErrors(errors);
|
|
290
|
-
const fixAttempt = attemptAutoFixBasicProgram(activeProgram, errors);
|
|
298
|
+
const fixAttempt = parsed.autoFix === false ? undefined : attemptAutoFixBasicProgram(activeProgram, errors);
|
|
291
299
|
if (!fixAttempt) {
|
|
292
300
|
const data = {
|
|
293
301
|
kind: "basic_runtime_error",
|
|
@@ -296,9 +304,12 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
296
304
|
...(screenOutput ? { screen: screenOutput } : {}),
|
|
297
305
|
autoFix: {
|
|
298
306
|
attempted: false,
|
|
307
|
+
...(parsed.autoFix === false ? { disabled: true } : {}),
|
|
299
308
|
},
|
|
300
309
|
};
|
|
301
|
-
return structuredExecutionError(
|
|
310
|
+
return structuredExecutionError(parsed.autoFix === false
|
|
311
|
+
? "Detected BASIC runtime errors after execution. Auto-fix is disabled (autoFix:false), so the program was not rewritten or run again."
|
|
312
|
+
: "Detected BASIC runtime errors after execution.", data);
|
|
302
313
|
}
|
|
303
314
|
ctx.logger.info("Attempting BASIC auto-fix", {
|
|
304
315
|
changes: fixAttempt.changes.map((change) => ({
|
|
@@ -384,9 +395,14 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
384
395
|
}));
|
|
385
396
|
}
|
|
386
397
|
}
|
|
398
|
+
const changedLines = autoFixInfo
|
|
399
|
+
? autoFixInfo.changes.map((change) => `line ${change.line}: "${change.before}" -> "${change.after}"`).join("; ")
|
|
400
|
+
: "";
|
|
387
401
|
const message = autoFixInfo
|
|
388
|
-
?
|
|
389
|
-
:
|
|
402
|
+
? `Detected BASIC errors on execution; applied auto-fix and ran the program a second time (changed lines: ${changedLines}). The second run showed no BASIC error on screen.${verified ? "" : " Execution was not verified."}`
|
|
403
|
+
: verified
|
|
404
|
+
? "BASIC program uploaded and executed successfully (verified)."
|
|
405
|
+
: "BASIC program uploaded and started. No BASIC error was visible on the screen immediately afterwards; pass verify:true to poll for the outcome.";
|
|
390
406
|
const metadata = {
|
|
391
407
|
success: true,
|
|
392
408
|
entryAddress,
|
|
@@ -399,6 +415,7 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
399
415
|
applied: true,
|
|
400
416
|
changes: autoFixInfo.changes,
|
|
401
417
|
originalErrors: autoFixInfo.originalErrors,
|
|
418
|
+
programSource: activeProgram,
|
|
402
419
|
},
|
|
403
420
|
}
|
|
404
421
|
: {}),
|
|
@@ -414,8 +431,10 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
414
431
|
...(autoFixInfo
|
|
415
432
|
? {
|
|
416
433
|
autoFix: {
|
|
434
|
+
applied: true,
|
|
417
435
|
changes: autoFixInfo.changes,
|
|
418
436
|
originalErrors: autoFixInfo.originalErrors,
|
|
437
|
+
programSource: activeProgram,
|
|
419
438
|
},
|
|
420
439
|
}
|
|
421
440
|
: {}),
|
|
@@ -452,18 +471,35 @@ async function executeUploadRunAsm(rawArgs, ctx) {
|
|
|
452
471
|
details: extractFailureDetails(result.details),
|
|
453
472
|
}));
|
|
454
473
|
}
|
|
455
|
-
//
|
|
474
|
+
// Read back the program bytes (load evidence), then poll for signs of execution.
|
|
456
475
|
let verified = false;
|
|
476
|
+
let execution;
|
|
477
|
+
let loadCheck;
|
|
478
|
+
const loadAddress = prg.length >= 2 ? prg.readUInt16LE(0) : 0;
|
|
457
479
|
try {
|
|
458
|
-
const outcome = await pollForProgramOutcome("ASM", ctx.client, ctx.logger
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
480
|
+
const outcome = await pollForProgramOutcome("ASM", ctx.client, ctx.logger, undefined, {
|
|
481
|
+
address: loadAddress,
|
|
482
|
+
bytes: prg.subarray(2),
|
|
483
|
+
});
|
|
484
|
+
if (outcome.type === "ASM") {
|
|
485
|
+
loadCheck = outcome.loadCheck;
|
|
486
|
+
if (outcome.status === "not_loaded") {
|
|
487
|
+
ctx.logger.warn("ASM program was not loaded", { reason: outcome.reason });
|
|
488
|
+
return toolErrorResult(new ToolExecutionError(`Assembly program was not loaded: ${outcome.reason}. The backend reported success, but the bytes read back at the load address differ from the assembled program (a program that overwrites its own first bytes also looks like this).`, { details: { reason: outcome.reason, ...(outcome.load ? { loadAddress: outcome.load.address, expected: outcome.load.expected, found: outcome.load.found } : {}) } }));
|
|
489
|
+
}
|
|
490
|
+
if (outcome.status === "unconfirmed") {
|
|
491
|
+
if (shouldVerify) {
|
|
492
|
+
ctx.logger.warn("ASM execution could not be verified", { reason: outcome.reason });
|
|
493
|
+
return toolErrorResult(new ToolExecutionError("Assembly program was loaded, but no screen change was observed, so its execution could not be verified", { details: { reason: outcome.reason, loadCheck: outcome.loadCheck, execution: "unconfirmed" } }));
|
|
494
|
+
}
|
|
495
|
+
execution = "unconfirmed";
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
execution = "confirmed";
|
|
499
|
+
if (shouldVerify) {
|
|
500
|
+
verified = true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
467
503
|
}
|
|
468
504
|
}
|
|
469
505
|
catch (pollError) {
|
|
@@ -480,13 +516,20 @@ async function executeUploadRunAsm(rawArgs, ctx) {
|
|
|
480
516
|
entryAddress,
|
|
481
517
|
prgSize: prg.length,
|
|
482
518
|
resources: ["c64://assembly/6510-spec", "c64://guide/bootstrap"],
|
|
519
|
+
...(loadCheck ? { loadCheck } : {}),
|
|
520
|
+
...(execution ? { execution } : {}),
|
|
483
521
|
...(shouldVerify && verified ? { verified: true } : {}),
|
|
484
522
|
};
|
|
485
|
-
const
|
|
523
|
+
const message = execution === "unconfirmed"
|
|
524
|
+
? "Assembly program assembled, uploaded and loaded. No screen change was observed, so it is not confirmed that the program is running (a SID player, a border effect or a JMP * loop looks like this)."
|
|
525
|
+
: "Assembly program assembled, uploaded, and executed successfully.";
|
|
526
|
+
const base = textResult(message, {
|
|
486
527
|
success: true,
|
|
487
528
|
entryAddress,
|
|
488
529
|
prgSize: prg.length,
|
|
489
530
|
details: result.details ?? null,
|
|
531
|
+
...(loadCheck ? { loadCheck } : {}),
|
|
532
|
+
...(execution ? { execution } : {}),
|
|
490
533
|
...(shouldVerify && verified ? { verified: true } : {}),
|
|
491
534
|
});
|
|
492
535
|
return { ...base, structuredContent: { type: "json", data } };
|
|
@@ -550,8 +593,9 @@ async function executeRunPrg(rawArgs, ctx) {
|
|
|
550
593
|
if (ctx.platform.id === "vice") {
|
|
551
594
|
clearViceSymbols();
|
|
552
595
|
if (parsed.symbolsFile) {
|
|
596
|
+
const symbolsPath = resolveHostPath(parsed.symbolsFile, { purpose: "read" });
|
|
553
597
|
try {
|
|
554
|
-
const content = await fs.promises.readFile(
|
|
598
|
+
const content = await fs.promises.readFile(symbolsPath, "utf8");
|
|
555
599
|
const syms = parseViceSymbolFile(content);
|
|
556
600
|
setViceSymbols(syms.entries());
|
|
557
601
|
symbolsLoaded = syms.size;
|
|
@@ -718,7 +762,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
718
762
|
"Stage PRG files without running when the user wants to inspect memory first.",
|
|
719
763
|
"Confirm the Ultimate filesystem path (e.g. //USB0/demo.prg) is accessible before invoking.",
|
|
720
764
|
],
|
|
721
|
-
supportedPlatforms: ["c64u", "u2"],
|
|
765
|
+
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
722
766
|
async execute(args, ctx) {
|
|
723
767
|
return executeLoadPrg(args, ctx);
|
|
724
768
|
},
|
|
@@ -2,7 +2,8 @@ import { defineToolModule, } from "../types.js";
|
|
|
2
2
|
import { getPlatformStatus } from "../../platform.js";
|
|
3
3
|
import { arraySchema, booleanSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
|
|
4
4
|
import { jsonResult } from "../responses.js";
|
|
5
|
-
import {
|
|
5
|
+
import { unknownErrorResult } from "../errors.js";
|
|
6
|
+
export const MAX_BATCH_COMMANDS = 64;
|
|
6
7
|
const commandSchema = objectSchema({
|
|
7
8
|
description: "A single batch command.",
|
|
8
9
|
properties: {
|
|
@@ -23,6 +24,7 @@ const executeBatchArgsSchema = objectSchema({
|
|
|
23
24
|
commands: arraySchema(commandSchema, {
|
|
24
25
|
description: "Ordered list of commands to execute.",
|
|
25
26
|
minItems: 1,
|
|
27
|
+
maxItems: MAX_BATCH_COMMANDS,
|
|
26
28
|
}),
|
|
27
29
|
stopOnError: optionalSchema(booleanSchema({
|
|
28
30
|
description: "Halt the batch after the first failure. Default: true.",
|
|
@@ -49,6 +51,9 @@ export const batchModuleGroup = defineToolModule({
|
|
|
49
51
|
description: "Execute multiple c64bridge tool calls in a single request. Reduces latency for multi-step workflows.",
|
|
50
52
|
summary: "Runs an ordered list of tool commands and returns all results with per-command timing.",
|
|
51
53
|
inputSchema: executeBatchArgsSchema.jsonSchema,
|
|
54
|
+
// Annotated as the worst case; the access check runs per command when it is dispatched.
|
|
55
|
+
effect: "destructive",
|
|
56
|
+
effectOf: () => "read",
|
|
52
57
|
tags: ["batch", "orchestration", "grouped"],
|
|
53
58
|
examples: [
|
|
54
59
|
{
|
|
@@ -80,6 +85,7 @@ export const batchModuleGroup = defineToolModule({
|
|
|
80
85
|
// Lazy import to avoid circular dependency at module-init time.
|
|
81
86
|
const { toolRegistry } = await import("./index.js");
|
|
82
87
|
const results = [];
|
|
88
|
+
const imageBlocks = [];
|
|
83
89
|
let succeeded = 0;
|
|
84
90
|
let failed = 0;
|
|
85
91
|
const batchStart = Date.now();
|
|
@@ -120,8 +126,11 @@ export const batchModuleGroup = defineToolModule({
|
|
|
120
126
|
});
|
|
121
127
|
const elapsedMs = Date.now() - cmdStart;
|
|
122
128
|
const isError = toolResult.isError === true || toolResult.metadata?.success === false;
|
|
123
|
-
const text = toolResult.content.
|
|
129
|
+
const text = toolResult.content.flatMap((c) => (c.type === "text" ? [c.text] : [])).join("\n");
|
|
124
130
|
const resultPayload = toolResult.structuredContent?.data ?? text;
|
|
131
|
+
// Image blocks (capture_frame format "png") have no text form, so they are passed through after the summary.
|
|
132
|
+
const images = toolResult.content.filter((c) => c.type === "image");
|
|
133
|
+
imageBlocks.push(...images);
|
|
125
134
|
if (isError) {
|
|
126
135
|
results.push({ index: i, tool: toolName, description: label, success: false, error: text, elapsedMs });
|
|
127
136
|
failed++;
|
|
@@ -129,13 +138,14 @@ export const batchModuleGroup = defineToolModule({
|
|
|
129
138
|
break;
|
|
130
139
|
}
|
|
131
140
|
else {
|
|
132
|
-
results.push({ index: i, tool: toolName, description: label, success: true, result: resultPayload, elapsedMs });
|
|
141
|
+
results.push({ index: i, tool: toolName, description: label, success: true, result: resultPayload, elapsedMs, ...(images.length > 0 ? { images: images.length } : {}) });
|
|
133
142
|
succeeded++;
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
catch (err) {
|
|
137
146
|
const elapsedMs = Date.now() - cmdStart;
|
|
138
|
-
|
|
147
|
+
// Keep the validation detail (allowed values, bounds) that the CallTool error path would show.
|
|
148
|
+
const message = unknownErrorResult(err).content.flatMap((c) => (c.type === "text" ? [c.text] : [])).join("\n");
|
|
139
149
|
results.push({ index: i, tool: toolName, description: label, success: false, error: message, elapsedMs });
|
|
140
150
|
failed++;
|
|
141
151
|
if (stopOnError)
|
|
@@ -152,15 +162,11 @@ export const batchModuleGroup = defineToolModule({
|
|
|
152
162
|
results,
|
|
153
163
|
};
|
|
154
164
|
ctx.logger.info("Batch complete", { total: parsed.commands.length, succeeded, failed, elapsedMs: totalElapsedMs });
|
|
155
|
-
|
|
165
|
+
const summaryResult = jsonResult(summary, { success: failed === 0, succeeded, failed, total: parsed.commands.length });
|
|
166
|
+
const outcome = imageBlocks.length > 0 ? { ...summaryResult, content: [...summaryResult.content, ...imageBlocks] } : summaryResult;
|
|
167
|
+
return failed > 0 ? { ...outcome, isError: true } : outcome;
|
|
156
168
|
}
|
|
157
169
|
catch (error) {
|
|
158
|
-
if (error instanceof ToolValidationError) {
|
|
159
|
-
return {
|
|
160
|
-
content: [{ type: "text", text: error.message }],
|
|
161
|
-
isError: true,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
170
|
return unknownErrorResult(error);
|
|
165
171
|
}
|
|
166
172
|
},
|
|
@@ -60,7 +60,7 @@ const configOperations = [
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
op: "batch_update",
|
|
63
|
-
schema: extendSchemaWithOp("batch_update", ensureDescriptor(developerDescriptorIndex, "config_batch_update").inputSchema, { description: "Apply multiple configuration updates in a single request." }),
|
|
63
|
+
schema: extendSchemaWithOp("batch_update", ensureDescriptor(developerDescriptorIndex, "config_batch_update").inputSchema, { description: "Apply multiple configuration updates in a single request. Pass `updates` as {category: {item: value}}; categories as top-level keys are still accepted at runtime." }),
|
|
64
64
|
handler: async (rawArgs, ctx) => invokeModuleTool(developerModule, "config_batch_update", rawArgs, ctx),
|
|
65
65
|
},
|
|
66
66
|
{
|
|
@@ -169,6 +169,13 @@ export const configModuleGroup = defineToolModule({
|
|
|
169
169
|
description: "Configuration operations available via the c64_config tool.",
|
|
170
170
|
variants: configOperations.map((operation) => operation.schema),
|
|
171
171
|
}),
|
|
172
|
+
operationEffects: {
|
|
173
|
+
list: "read", get: "read", set: "write", batch_update: "write", load_flash: "destructive",
|
|
174
|
+
save_flash: "destructive", reset_defaults: "destructive", read_debugreg: "read",
|
|
175
|
+
write_debugreg: "destructive", info: "read", version: "read", snapshot: "write",
|
|
176
|
+
restore: "write", diff: "read", shuffle: "write",
|
|
177
|
+
},
|
|
178
|
+
effectOf: (op, args) => (op === "restore" && args.applyToFlash === true ? "destructive" : undefined),
|
|
172
179
|
tags: ["config", "diagnostics", "grouped"],
|
|
173
180
|
operationPlatforms: {
|
|
174
181
|
load_flash: ["c64u", "u2"],
|
|
@@ -200,7 +207,7 @@ export const configModuleGroup = defineToolModule({
|
|
|
200
207
|
{
|
|
201
208
|
name: "Snapshot config",
|
|
202
209
|
description: "Write configuration snapshot to disk",
|
|
203
|
-
arguments: { op: "snapshot", path: "./snapshots/
|
|
210
|
+
arguments: { op: "snapshot", path: "./snapshots/c64.json" },
|
|
204
211
|
},
|
|
205
212
|
],
|
|
206
213
|
execute: createOperationDispatcher("c64_config", configOperationHandlers),
|
|
@@ -251,6 +251,11 @@ const diskOperations = [
|
|
|
251
251
|
schema: extendSchemaWithOp("find_and_run", ensureDescriptor(metaDescriptorIndex, "find_and_run_program_by_name").inputSchema, { description: "Search for a PRG/CRT by name substring and run the first match." }),
|
|
252
252
|
handler: async (rawArgs, ctx) => invokeModuleTool(metaModule, "find_and_run_program_by_name", rawArgs, ctx),
|
|
253
253
|
},
|
|
254
|
+
{
|
|
255
|
+
op: "find_paths",
|
|
256
|
+
schema: extendSchemaWithOp("find_paths", ensureDescriptor(metaDescriptorIndex, "find_paths_by_name").inputSchema, { description: "Search Ultimate storage for paths whose names contain a substring, without running anything." }),
|
|
257
|
+
handler: async (rawArgs, ctx) => invokeModuleTool(metaModule, "find_paths_by_name", rawArgs, ctx),
|
|
258
|
+
},
|
|
254
259
|
];
|
|
255
260
|
const diskOperationHandlers = createOperationHandlers(diskOperations);
|
|
256
261
|
export const diskModuleGroup = defineToolModule({
|
|
@@ -258,7 +263,7 @@ export const diskModuleGroup = defineToolModule({
|
|
|
258
263
|
summary: "Grouped disk image management, mounting, and discovery tools.",
|
|
259
264
|
supportedPlatforms: ["c64u", "u2", "vice"],
|
|
260
265
|
resources: ["c64://guide/bootstrap"],
|
|
261
|
-
prompts: ["drive-
|
|
266
|
+
prompts: ["drive-manager"],
|
|
262
267
|
defaultTags: ["storage", "drive"],
|
|
263
268
|
workflowHints: [
|
|
264
269
|
"Summarise drive state before and after mounts so the user can confirm hardware changes.",
|
|
@@ -273,14 +278,19 @@ export const diskModuleGroup = defineToolModule({
|
|
|
273
278
|
description: "Disk operations available via the c64_disk tool.",
|
|
274
279
|
variants: diskOperations.map((operation) => operation.schema),
|
|
275
280
|
}),
|
|
281
|
+
operationEffects: {
|
|
282
|
+
list_drives: "read", mount: "write", unmount: "write", file_info: "read",
|
|
283
|
+
create_image: "destructive", find_and_run: "write", find_paths: "read",
|
|
284
|
+
},
|
|
276
285
|
tags: ["storage", "drive", "grouped"],
|
|
277
286
|
// The VICE Binary Monitor has no command to detach a disk image, so unmount is Ultimate-only.
|
|
278
|
-
operationPlatforms: { unmount: ["c64u", "u2"], file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"] },
|
|
287
|
+
operationPlatforms: { unmount: ["c64u", "u2"], file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"], find_paths: ["c64u", "u2"] },
|
|
279
288
|
operationToolNames: {
|
|
280
289
|
unmount: "drive_remove",
|
|
281
290
|
file_info: "file_info",
|
|
282
291
|
create_image: "create_image",
|
|
283
292
|
find_and_run: "find_and_run_program_by_name",
|
|
293
|
+
find_paths: "find_paths_by_name",
|
|
284
294
|
},
|
|
285
295
|
examples: [
|
|
286
296
|
{
|
|
@@ -49,6 +49,9 @@ export const driveModuleGroup = defineToolModule({
|
|
|
49
49
|
description: "Drive operations available via the c64_drive tool.",
|
|
50
50
|
variants: driveOperations.map((operation) => operation.schema),
|
|
51
51
|
}),
|
|
52
|
+
operationEffects: {
|
|
53
|
+
reset: "write", power_on: "write", power_off: "write", load_rom: "destructive", set_mode: "write",
|
|
54
|
+
},
|
|
52
55
|
tags: ["drive", "hardware", "grouped"],
|
|
53
56
|
operationPlatforms: { load_rom: ["c64u", "u2"] },
|
|
54
57
|
operationToolNames: { load_rom: "drive_load_rom" },
|
|
@@ -49,6 +49,9 @@ export const extractModule = defineToolModule({
|
|
|
49
49
|
description: "Extraction operations available via the c64_extract tool.",
|
|
50
50
|
variants: extractOperations.map((operation) => operation.schema),
|
|
51
51
|
}),
|
|
52
|
+
operationEffects: {
|
|
53
|
+
sprites: "write", charset: "write", memory_dump: "write", fs_stats: "read", firmware_health: "read",
|
|
54
|
+
},
|
|
52
55
|
tags: ["extract", "diagnostics", "grouped"],
|
|
53
56
|
operationPlatforms: { fs_stats: ["c64u", "u2"] },
|
|
54
57
|
operationToolNames: { fs_stats: "filesystem_stats_by_extension" },
|
|
@@ -3,8 +3,10 @@ import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema,
|
|
|
3
3
|
import { graphicsModule, graphicsOperationHandlers as groupedGraphicsHandlers } from "../graphics.js";
|
|
4
4
|
import { booleanSchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
|
|
5
5
|
import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, } from "./utils.js";
|
|
6
|
+
import { Jimp } from "jimp";
|
|
6
7
|
import { jsonResult } from "../responses.js";
|
|
7
|
-
import {
|
|
8
|
+
import { VIC_PALETTE } from "../../vicBitmap.js";
|
|
9
|
+
import { ToolError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "../errors.js";
|
|
8
10
|
const graphicsDescriptorIndex = buildDescriptorIndex(graphicsModule);
|
|
9
11
|
const captureFrameArgsSchema = objectSchema({
|
|
10
12
|
description: "Capture one or more complete video frames from the active backend.",
|
|
@@ -18,9 +20,13 @@ const captureFrameArgsSchema = objectSchema({
|
|
|
18
20
|
default: 1,
|
|
19
21
|
}), 1),
|
|
20
22
|
includePixels: optionalSchema(booleanSchema({
|
|
21
|
-
description: "Include pixel payload bytes in the response.",
|
|
22
|
-
default:
|
|
23
|
-
}),
|
|
23
|
+
description: "Include raw pixel payload bytes in the response. Off by default because a frame is about 100 KB; the response is rejected above 2 MiB, so lower count when it is on.",
|
|
24
|
+
default: false,
|
|
25
|
+
}), false),
|
|
26
|
+
format: optionalSchema(stringSchema({
|
|
27
|
+
description: "Set to png to receive each frame as an MCP image block (C64 palette). At most 4 frames per call.",
|
|
28
|
+
enum: ["png"],
|
|
29
|
+
})),
|
|
24
30
|
encoding: optionalSchema(stringSchema({
|
|
25
31
|
description: "Encoding used for the pixel payload when included.",
|
|
26
32
|
enum: ["base64", "hex"],
|
|
@@ -30,6 +36,30 @@ const captureFrameArgsSchema = objectSchema({
|
|
|
30
36
|
required: ["op"],
|
|
31
37
|
additionalProperties: false,
|
|
32
38
|
});
|
|
39
|
+
/** Largest raw pixel payload (encoded size, all frames) one capture_frame response may carry. */
|
|
40
|
+
export const MAX_CAPTURE_PAYLOAD_BYTES = 2 * 1024 * 1024;
|
|
41
|
+
export const MAX_PNG_FRAMES = 4;
|
|
42
|
+
/** Renders one captured frame (one palette index per byte) as a PNG image content block. */
|
|
43
|
+
export async function frameToPngContent(frame) {
|
|
44
|
+
const { width, height } = frame;
|
|
45
|
+
if (frame.bitsPerPixel > 8 || frame.pixels.length < width * height) {
|
|
46
|
+
throw new ToolValidationError("PNG output needs indexed frames with one byte per pixel; use includePixels for raw data", {
|
|
47
|
+
path: "$.format",
|
|
48
|
+
details: { bitsPerPixel: frame.bitsPerPixel, width, height, byteLength: frame.pixels.length },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
52
|
+
for (let i = 0; i < width * height; i += 1) {
|
|
53
|
+
const [r, g, b] = VIC_PALETTE[frame.pixels[i] & 0x0f];
|
|
54
|
+
rgba[i * 4] = r;
|
|
55
|
+
rgba[i * 4 + 1] = g;
|
|
56
|
+
rgba[i * 4 + 2] = b;
|
|
57
|
+
rgba[i * 4 + 3] = 0xff;
|
|
58
|
+
}
|
|
59
|
+
const image = new Jimp({ width, height, data: rgba });
|
|
60
|
+
const png = await image.getBuffer("image/png");
|
|
61
|
+
return { type: "image", data: Buffer.from(png).toString("base64"), mimeType: "image/png" };
|
|
62
|
+
}
|
|
33
63
|
// ---------------------------------------------------------------------------
|
|
34
64
|
// get_display_state helpers — VIC-II register decoding
|
|
35
65
|
// ---------------------------------------------------------------------------
|
|
@@ -107,10 +137,27 @@ const graphicsOperations = [
|
|
|
107
137
|
count: parsed.count,
|
|
108
138
|
includePixels: parsed.includePixels,
|
|
109
139
|
encoding: parsed.encoding,
|
|
140
|
+
format: parsed.format,
|
|
110
141
|
});
|
|
142
|
+
if (parsed.format === "png" && (parsed.count ?? 1) > MAX_PNG_FRAMES) {
|
|
143
|
+
throw new ToolValidationError(`count must be at most ${MAX_PNG_FRAMES} when format is png`, {
|
|
144
|
+
path: "$.count",
|
|
145
|
+
details: { count: parsed.count, maximum: MAX_PNG_FRAMES },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
111
148
|
const capture = await ctx.client.captureFrames({ count: parsed.count });
|
|
112
|
-
const includePixels = parsed.includePixels
|
|
149
|
+
const includePixels = parsed.includePixels === true;
|
|
113
150
|
const encoding = parsed.encoding ?? "base64";
|
|
151
|
+
if (includePixels) {
|
|
152
|
+
const rawBytes = capture.frames.reduce((sum, frame) => sum + frame.pixels.length, 0);
|
|
153
|
+
const encodedBytes = encoding === "hex" ? rawBytes * 2 : Math.ceil(rawBytes / 3) * 4;
|
|
154
|
+
if (encodedBytes > MAX_CAPTURE_PAYLOAD_BYTES) {
|
|
155
|
+
throw new ToolValidationError(`Raw pixel response would be ${encodedBytes} bytes, above the ${MAX_CAPTURE_PAYLOAD_BYTES} byte limit; lower count or use format png`, { path: "$.count", details: { count: parsed.count, encodedBytes, limitBytes: MAX_CAPTURE_PAYLOAD_BYTES } });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const images = parsed.format === "png"
|
|
159
|
+
? await Promise.all(capture.frames.map((frame) => frameToPngContent(frame)))
|
|
160
|
+
: [];
|
|
114
161
|
const frames = capture.frames.map((frame, index) => {
|
|
115
162
|
const pixels = includePixels
|
|
116
163
|
? {
|
|
@@ -131,7 +178,7 @@ const graphicsOperations = [
|
|
|
131
178
|
...(pixels ? { pixels } : {}),
|
|
132
179
|
};
|
|
133
180
|
});
|
|
134
|
-
|
|
181
|
+
const result = jsonResult({
|
|
135
182
|
backend: capture.backend,
|
|
136
183
|
count: frames.length,
|
|
137
184
|
frames,
|
|
@@ -141,7 +188,9 @@ const graphicsOperations = [
|
|
|
141
188
|
count: frames.length,
|
|
142
189
|
includePixels,
|
|
143
190
|
encoding: includePixels ? encoding : null,
|
|
191
|
+
format: parsed.format ?? null,
|
|
144
192
|
});
|
|
193
|
+
return images.length > 0 ? { ...result, content: [...result.content, ...images] } : result;
|
|
145
194
|
}
|
|
146
195
|
catch (error) {
|
|
147
196
|
if (error instanceof ToolError) {
|
|
@@ -223,6 +272,10 @@ export const graphicsModuleGroup = defineToolModule({
|
|
|
223
272
|
description: "Graphics operations available via the c64_graphics tool.",
|
|
224
273
|
variants: graphicsOperations.map((operation) => operation.schema),
|
|
225
274
|
}),
|
|
275
|
+
operationEffects: {
|
|
276
|
+
capture_frame: "read", render_petscii_art: "write", render_petscii_text: "write",
|
|
277
|
+
render_sprite: "write", render_bitmap: "write", get_display_state: "read",
|
|
278
|
+
},
|
|
226
279
|
tags: ["graphics", "vic", "grouped"],
|
|
227
280
|
examples: [
|
|
228
281
|
{
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ToolValidationError } from "../errors.js";
|
|
2
|
+
import { assertAccess } from "../access.js";
|
|
1
3
|
import { getPlatformStatus, setPlatform } from "../../platform.js";
|
|
2
4
|
import { programModule } from "./program.js";
|
|
3
5
|
import { memoryModuleGroup as memoryModule } from "./memory.js";
|
|
@@ -56,11 +58,25 @@ export const toolRegistry = {
|
|
|
56
58
|
};
|
|
57
59
|
const entry = toolMap.get(name);
|
|
58
60
|
if (!entry) {
|
|
59
|
-
|
|
61
|
+
const allowed = Array.from(toolMap.keys()).sort();
|
|
62
|
+
throw new ToolValidationError(`Unknown tool: ${name}`, {
|
|
63
|
+
path: "$.name",
|
|
64
|
+
details: { allowed, received: name },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Single enforcement point for C64BRIDGE_ACCESS. Batch commands and background task starts
|
|
68
|
+
// also arrive here (the batch tool and c64_system start_task declare their effect per call).
|
|
69
|
+
const effect = entry.module.effectOf ? entry.module.effectOf(name, args) : "destructive";
|
|
70
|
+
if (effect) {
|
|
71
|
+
assertAccess(name, getOperation(args), effect);
|
|
60
72
|
}
|
|
61
73
|
return entry.module.invoke(name, args, enrichedCtx);
|
|
62
74
|
},
|
|
63
75
|
};
|
|
76
|
+
function getOperation(args) {
|
|
77
|
+
const op = typeof args === "object" && args !== null ? args.op : undefined;
|
|
78
|
+
return typeof op === "string" && op.length > 0 ? op : undefined;
|
|
79
|
+
}
|
|
64
80
|
export function describeToolModules() {
|
|
65
81
|
return modules.map((module) => ({
|
|
66
82
|
domain: module.domain,
|