@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.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 CHANGED
@@ -17,73 +17,27 @@ run ACME, DXA and Ghidra themselves, and keep project knowledge local in
17
17
  The design is in [`docs/redesign/`](docs/redesign/README.md), and
18
18
  [`docs/plan.md`](docs/plan.md) tracks the implementation.
19
19
 
20
- ## Install and start
20
+ ## Install
21
21
 
22
- Requirements:
22
+ You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files. ACME, DXA and Ghidra are needed only for the skills that use them.
23
23
 
24
- - Node.js 24 or newer with npx.
25
- - On the machine with VICE: VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files.
26
- - Where the agent runs: ACME, DXA and Ghidra, for the skills that use them.
27
-
28
- Everything runs through npx. Nothing is installed globally.
29
-
30
- ### Release or prerelease
31
-
32
- | Channel | Version |
33
- | --- | --- |
34
- | `latest` | The current release. |
35
- | `next` | The current prerelease, for example `2.0.0-rc.1`. |
36
-
37
- Until 2.0.0 is released, `latest` is the old 1.1.0. Use `next` until then. In the commands below, replace `<channel>` with `latest` or `next`. Use the same channel on both machines.
38
-
39
- ### On one machine
40
-
41
- Start the Host Runtime in its own terminal. Ctrl+C stops it and every emulator:
24
+ Start the Host Runtime and leave it running:
42
25
 
43
26
  ```
44
- npx -y --package=@henols/c64-re-tools@<channel> c64-re-tools-host
27
+ npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host
45
28
  ```
46
29
 
47
- Install into the project, check it, and start the agent:
30
+ Install into your project and start the agent:
48
31
 
49
32
  ```
50
33
  cd my-c64-project
51
- npx -y @henols/c64-re-tools@<channel> install --target claude
52
- npx -y @henols/c64-re-tools@<channel> status
34
+ npx -y @henols/c64-re-tools@latest install --target claude
53
35
  claude
54
36
  ```
55
37
 
56
- ### In a dev container
57
-
58
- On the host, start the Host Runtime on the container bridge with a shared secret:
38
+ `npx -y @henols/c64-re-tools@latest status` shows what works.
59
39
 
60
- ```
61
- export C64RT_HOST_TOKEN=$(openssl rand -hex 16)
62
- npx -y --package=@henols/c64-re-tools@<channel> c64-re-tools-host --listen 172.17.0.1
63
- ```
64
-
65
- In the container, set the same `C64RT_HOST_TOKEN`, then:
66
-
67
- ```
68
- npx -y @henols/c64-re-tools@<channel> install --target claude
69
- npx -y @henols/c64-re-tools@<channel> status
70
- claude
71
- ```
72
-
73
- The container reaches the host as `host.docker.internal`. `status` must show the Host Runtime as reachable.
74
-
75
- ### Update and remove
76
-
77
- ```
78
- npx -y @henols/c64-re-tools@<channel> update # the newest skills into the project
79
- npx -y @henols/c64-re-tools@<channel> uninstall
80
- ```
81
-
82
- Restart the Host Runtime after a new release. `--global` installs into your home directory.
83
-
84
- ### Debugging
85
-
86
- See [docs/debugging.md](docs/debugging.md) for the trace and the field test.
40
+ Until 2.0.0 is released, `@latest` is the old 1.1.0. Use the prerelease instead. For the prerelease, a dev container and debugging, see [docs/debug-install.md](docs/debug-install.md).
87
41
 
88
42
  ### Settings
89
43
 
@@ -94,9 +48,8 @@ See [docs/debugging.md](docs/debugging.md) for the trace and the field test.
94
48
  | `C64RT_ACME`, `C64RT_DXA` | skills, CLI | Full path of the tool when it is not on `PATH`. |
95
49
  | `C64RT_GHIDRA` | skills, CLI | The Ghidra installation directory, when `analyzeHeadless` is not on `PATH`. |
96
50
  | `C64RT_HOST` | MCP, skills, CLI | `host:port` of the Host Runtime when it is not on 127.0.0.1, `host.docker.internal` or `host.containers.internal` at 6464. |
97
- | `C64RT_HOST_TOKEN` | host, MCP, skills, CLI | The shared secret for a Host Runtime that listens beyond loopback. |
98
51
  | `C64RT_VIDEO` | MCP | `pal` (default) or `ntsc`, fixed for the life of the MCP process. |
99
- | `C64RT_TRACE` | host, MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debugging.md](docs/debugging.md). Nothing of the trace reaches the agent. |
52
+ | `C64RT_TRACE` | host, MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debug-install.md](docs/debug-install.md). Nothing of the trace reaches the agent. |
100
53
 
101
54
  Ghidra runs with its own settings directory for each request. The toolkit
102
55
  brings its own NMOS 6510 language (all 256 opcodes); your Ghidra installation
@@ -104,6 +57,8 @@ and settings stay unchanged.
104
57
 
105
58
  ## Use it with an agent
106
59
 
60
+ ## Use it with an agent
61
+
107
62
  1. Install for your harness: `--target claude`, `codex`, `pi`, `opencode`, `gemini`, `copilot`, `cursor` or `windsurf`. Without `--target`, `install` installs for all of them.
108
63
  2. Start the Host Runtime and leave it running.
109
64
  3. Start the harness in the project, for example `claude` or `codex`, or open the project in Cursor or Windsurf. The harness starts the MCP server and finds the skills by itself.
@@ -190,14 +145,7 @@ C64RT_LIVE_VICE=/usr/bin/x64sc C64RT_GHIDRA=/opt/ghidra pnpm test
190
145
 
191
146
  ACME, DXA, c1541 and petcat tests run when the tools are on `PATH`.
192
147
 
193
- To release, push a tag on a commit of `main`. The tag gives the version:
194
-
195
- ```
196
- git tag v2.0.0-rc.2 && git push origin v2.0.0-rc.2 # a prerelease, to npm next
197
- git tag v2.0.0 && git push origin v2.0.0 # a release, to npm latest
198
- ```
199
-
200
- CI checks and tests the tagged commit, publishes it to npm and makes a GitHub Release. The repository keeps the version `0.0.0-development`.
148
+ To release, push a tag. See [docs/releasing.md](docs/releasing.md).
201
149
 
202
150
  ## Layout
203
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.3",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
package/src/host/main.ts CHANGED
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import { DEFAULT_HOST_PORT } from "../protocol/messages.ts";
6
6
  import { ProcessSupervisor } from "../native/processes.ts";
7
7
  import { startTrace, trace } from "../trace.ts";
8
- import { isLoopback, ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
8
+ import { ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
9
9
  import { createToolDispatcher } from "./tools/index.ts";
10
10
  import { checkViceStarts, findVice } from "./vice/process.ts";
11
11
  import { viceSessionFactory } from "./vice/session.ts";
@@ -13,22 +13,19 @@ import { viceSessionFactory } from "./vice/session.ts";
13
13
  const HELP = `c64-re-tools-host
14
14
 
15
15
  Usage:
16
- npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>] [--listen <address> ...]
16
+ npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>]
17
17
  c64-re-tools-host --help
18
18
 
19
19
  Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
20
20
  once (x64sc on PATH, or C64RT_VICE) and does not start if VICE cannot run.
21
- It listens on 127.0.0.1:${DEFAULT_HOST_PORT}, starts one VICE emulator for each
21
+ It listens on port ${DEFAULT_HOST_PORT} on every address, so a dev container
22
+ reaches it as host.docker.internal. It starts one VICE emulator for each
22
23
  connected c64-re-tools-mcp process, and runs the tools that come with VICE
23
24
  (c1541, petcat) for skill scripts. Stop it with Ctrl+C; that stops every
24
25
  emulator and tool it started.
25
26
 
26
27
  Options:
27
28
  --port <port> Listen on this port instead (0 picks a free port).
28
- --listen <address> Also listen on this address, on the same port, for
29
- example the container bridge (172.17.0.1). An address
30
- that is not loopback needs C64RT_HOST_TOKEN: set it to
31
- the same secret for this program and for the clients.
32
29
  --help Show this help.`;
33
30
 
34
31
  function log(line: string): void {
@@ -37,9 +34,9 @@ function log(line: string): void {
37
34
  }
38
35
 
39
36
  async function main(): Promise<number> {
40
- let values: { help?: boolean; port?: string; listen?: string[] };
37
+ let values: { help?: boolean; port?: string };
41
38
  try {
42
- ({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" }, listen: { type: "string", multiple: true } }, strict: true }));
39
+ ({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" } }, strict: true }));
43
40
  } catch (error) {
44
41
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP}\n`);
45
42
  return 2;
@@ -56,13 +53,6 @@ async function main(): Promise<number> {
56
53
  return 2;
57
54
  }
58
55
 
59
- const extraHosts = values.listen ?? [];
60
- const token = process.env.C64RT_HOST_TOKEN;
61
- if (extraHosts.some((address) => !isLoopback(address)) && (token === undefined || token.length < 16)) {
62
- process.stderr.write("--listen with an address that is not loopback needs C64RT_HOST_TOKEN with at least 16 characters, so that only your clients can connect.\n");
63
- return 2;
64
- }
65
-
66
56
  // Without an emulator the runtime is useless: refuse to start, by name and with the remedy.
67
57
  try {
68
58
  findVice(process.env);
@@ -90,9 +80,8 @@ async function main(): Promise<number> {
90
80
  let server;
91
81
  try {
92
82
  server = await startHostServer({
83
+ host: "0.0.0.0",
93
84
  port,
94
- extraHosts,
95
- ...(token === undefined || token === "" ? {} : { token }),
96
85
  log,
97
86
  createViceSession: viceSessionFactory({ supervisor, log }),
98
87
  tools: createToolDispatcher({ supervisor, log }),
@@ -102,15 +91,11 @@ async function main(): Promise<number> {
102
91
  process.stderr.write(`Port ${error.port} on ${error.address} is in use. Is another c64-re-tools-host running?\n`);
103
92
  return 1;
104
93
  }
105
- if (error instanceof ListenError && error.code === "EADDRNOTAVAIL") {
106
- process.stderr.write(`The address ${error.address} in --listen does not belong to this machine.\n`);
107
- return 1;
108
- }
109
94
  throw error;
110
95
  }
111
- const addresses = [server.host, ...extraHosts].map((address) => `${address}:${server.port}`);
112
- process.stdout.write(`c64-re-tools-host listening on ${addresses.join(", ")}\n`);
113
- trace().event("host.listening", { addresses });
96
+ const address = `${server.host}:${server.port}`;
97
+ process.stdout.write(`c64-re-tools-host listening on ${address}\n`);
98
+ trace().event("host.listening", { addresses: [address] });
114
99
 
115
100
  trace().event("host.stop", { signal: await stopSignal(log) });
116
101
  await server.close();
@@ -1,4 +1,3 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
2
1
  import { createServer, type Server, type Socket } from "node:net";
3
2
 
4
3
  import { encodeFrame, MessageReader } from "../protocol/framing.ts";
@@ -37,16 +36,8 @@ export interface HostServerOptions {
37
36
  createViceSession: ViceSessionFactory;
38
37
  /** Native-tool requests on "tool" connections; without it they are refused. */
39
38
  tools?: ToolDispatcher;
40
- /** Defaults to 127.0.0.1. More addresses listen on the same port, for example a container bridge. */
39
+ /** Defaults to 127.0.0.1; 0.0.0.0 listens on every address, so a container reaches it too. */
41
40
  host?: string;
42
- /** Extra addresses to listen on, on the same port as `host`. */
43
- extraHosts?: string[];
44
- /**
45
- * The shared secret. A connection that arrives on an address that is
46
- * not loopback must send it in its hello; without a token such a
47
- * connection is refused.
48
- */
49
- token?: string;
50
41
  /** 0 picks a free port. */
51
42
  port: number;
52
43
  /** How long a new connection may stay silent before it must send hello. */
@@ -67,10 +58,6 @@ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
67
58
  /** How long a refused client may keep its side of the connection open after the host has ended its side. */
68
59
  const REFUSED_CLOSE_MS = 1_000;
69
60
 
70
- function digest(text: string): Buffer {
71
- return createHash("sha256").update(text, "utf8").digest();
72
- }
73
-
74
61
  /** A caught value for the log, with its stack when it has one. */
75
62
  function describe(error: unknown): string {
76
63
  return error instanceof Error ? (error.stack ?? error.message) : String(error);
@@ -95,18 +82,6 @@ class Connection {
95
82
  #isTool = false;
96
83
  readonly #reader = new MessageReader();
97
84
 
98
- /** A connection on a loopback address needs no token; any other needs the host's token. */
99
- #checkToken(token: string | undefined): WireError | undefined {
100
- if (isLoopback(this.#socket.localAddress ?? "")) return undefined;
101
- const expected = this.#options.token;
102
- // Equal-length digests: the comparison takes the same time for any token, also one with multibyte characters.
103
- const matches = expected !== undefined && token !== undefined && timingSafeEqual(digest(token), digest(expected));
104
- if (matches) return undefined;
105
- return {
106
- code: "installation-incomplete",
107
- message: "The c64-re-tools host runtime refused this client: a connection from outside the host needs C64RT_HOST_TOKEN with the same value on both sides.",
108
- };
109
- }
110
85
  readonly #socket: Socket;
111
86
  readonly #options: HostServerOptions;
112
87
  readonly #log: (line: string) => void;
@@ -191,7 +166,7 @@ class Connection {
191
166
  if (this.#state === "handshake") {
192
167
  if (message.type !== "hello") throw new ProtocolError("first message is not hello");
193
168
  clearTimeout(this.#handshakeTimer);
194
- const mismatch = checkHello(message) ?? this.#checkToken(message.token);
169
+ const mismatch = checkHello(message);
195
170
  if (mismatch !== undefined) {
196
171
  this.#log(`refusing a client: ${mismatch.message}`);
197
172
  trace().event("connection.refused", { connection: this.#id, role: message.role, ...mismatch });
@@ -308,12 +283,6 @@ class Connection {
308
283
  }
309
284
  }
310
285
 
311
- /** True for 127.0.0.0/8 and ::1, also in their IPv4-mapped IPv6 form. */
312
- export function isLoopback(address: string): boolean {
313
- const plain = address.startsWith("::ffff:") ? address.slice(7) : address;
314
- return plain === "::1" || /^127\.\d+\.\d+\.\d+$/.test(plain);
315
- }
316
-
317
286
  /** A port as the user gives it: decimal digits only, 0 to 65535. Anything else gives undefined. */
318
287
  export function parsePort(text: string): number | undefined {
319
288
  if (!/^\d{1,5}$/.test(text)) return undefined;
@@ -386,8 +355,6 @@ export async function startHostServer(options: HostServerOptions): Promise<HostS
386
355
  let port: number;
387
356
  try {
388
357
  port = await listen(host, options.port);
389
- // The extra addresses share the port, so one endpoint setting reaches every address.
390
- for (const extra of options.extraHosts ?? []) await listen(extra, port);
391
358
  } catch (error) {
392
359
  await Promise.all(servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
393
360
  throw error;
@@ -150,9 +150,6 @@ export class HostConnection {
150
150
  const connection = new HostConnection(socket);
151
151
  const hello: Hello = { type: "hello", protocol: HOST_PROTOCOL_ID, version: HOST_PROTOCOL_VERSION, role: options.role };
152
152
  if (options.videoStandard !== undefined) hello.videoStandard = options.videoStandard;
153
- // A host that listens beyond loopback checks this shared secret.
154
- const token = (options.env ?? process.env).C64RT_HOST_TOKEN;
155
- if (token !== undefined && token !== "") hello.token = token;
156
153
 
157
154
  const stop = () => void connection.close();
158
155
  signal?.addEventListener("abort", stop, { once: true });
package/src/mcp/main.ts CHANGED
@@ -28,10 +28,7 @@ the agent, through npx with the latest version.
28
28
  Environment:
29
29
  C64RT_VIDEO pal (default) or ntsc; fixed for the life of the process.
30
30
  C64RT_HOST host:port of the Host Runtime when it is not on 127.0.0.1:6464,
31
- host.docker.internal:6464 or host.containers.internal:6464.
32
- C64RT_HOST_TOKEN
33
- the shared secret of a Host Runtime that listens on an address
34
- that is not loopback. Use the same value as the Host Runtime.`;
31
+ host.docker.internal:6464 or host.containers.internal:6464.`;
35
32
 
36
33
  function log(line: string): void {
37
34
  process.stderr.write(`c64-re-tools-mcp: ${line}\n`);
@@ -54,13 +54,8 @@ export interface Hello {
54
54
  role: Role;
55
55
  /** vice-session only; fixed for the session. */
56
56
  videoStandard?: VideoStandard;
57
- /** The shared secret (C64RT_HOST_TOKEN) for a host that listens beyond loopback. */
58
- token?: string;
59
57
  }
60
58
 
61
- /** Longest accepted shared token. */
62
- const MAX_TOKEN_LENGTH = 256;
63
-
64
59
  export interface Ready {
65
60
  type: "ready";
66
61
  }
@@ -166,12 +161,8 @@ export function parseClientMessage(value: unknown): ReceivedClientMessage {
166
161
  if (value.videoStandard !== undefined && !isOneOf(VIDEO_STANDARDS, value.videoStandard)) {
167
162
  throw new ProtocolError("hello has an unknown video standard");
168
163
  }
169
- if (value.token !== undefined && (typeof value.token !== "string" || value.token.length > MAX_TOKEN_LENGTH)) {
170
- throw new ProtocolError("hello has a malformed token");
171
- }
172
164
  const hello: Hello = { type: "hello", protocol: value.protocol, version: value.version, role: value.role };
173
165
  if (value.videoStandard !== undefined) hello.videoStandard = value.videoStandard;
174
- if (value.token !== undefined) hello.token = value.token;
175
166
  return hello;
176
167
  }
177
168
  if (value.type === "request") {