breakpoint-mcp 1.19.1 → 1.23.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/README.md +1 -1
- package/addon/breakpoint_mcp/operations.gd +1 -1
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/runtime_bridge.gd +243 -0
- package/dist/annotations.js +225 -0
- package/dist/capabilities.js +5 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/tools.js +193 -0
- package/dist/index.js +26 -4
- package/dist/peers.js +238 -0
- package/dist/recipes.js +65 -1
- package/dist/schemas.js +36 -0
- package/dist/secret.js +49 -0
- package/dist/tools/processes.js +25 -1
- package/dist/tools/runtime.js +467 -36
- package/dist/toolsets.js +7 -1
- package/package.json +1 -1
package/dist/secret.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
3
4
|
/**
|
|
4
5
|
* Loopback-bridge shared secret (host side).
|
|
5
6
|
*
|
|
@@ -35,4 +36,52 @@ export function resolveBridgeSecret(projectPath, envNames) {
|
|
|
35
36
|
}
|
|
36
37
|
return readProjectSecret(projectPath);
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Mint the per-project secret if it does not exist yet, and return it (or the
|
|
41
|
+
* existing one). Returns null if the file can neither be read nor written — the
|
|
42
|
+
* caller then proceeds exactly as before, with the child minting its own.
|
|
43
|
+
*
|
|
44
|
+
* Why the HOST mints it, ahead of spawning peers (F6 spike, constraint 2):
|
|
45
|
+
* `BridgeSecret.load_or_mint()` in the addon is a check-then-write with no lock
|
|
46
|
+
* — `file_exists → read` ELSE `mint → write`. F6 spawns N children through one
|
|
47
|
+
* funnel, so on a COLD project every child can reach the mint branch before any
|
|
48
|
+
* of them has written. Two peers minting different secrets, each holding its own
|
|
49
|
+
* in memory while the last writer wins on disk, is a peer the host can never
|
|
50
|
+
* authenticate against — and it presents as a flaky spawn, not a clear error.
|
|
51
|
+
* Measured 6/6 clean at 3 simultaneous peers, but that is Godot's ~200 ms init
|
|
52
|
+
* versus a sub-millisecond mint, i.e. timing, not a guarantee; the window is
|
|
53
|
+
* structurally open and would surface on someone else's slower machine.
|
|
54
|
+
*
|
|
55
|
+
* Writing the file first closes it by construction: every child then takes the
|
|
56
|
+
* `file_exists` path (verified 4/4 in the spike, with the file untouched after).
|
|
57
|
+
* An EXISTING secret is never overwritten — the editor may have minted it, and
|
|
58
|
+
* all three parties must agree on one value.
|
|
59
|
+
*
|
|
60
|
+
* Format matches `addons/breakpoint_mcp/bridge_secret.gd` exactly: 32 random
|
|
61
|
+
* bytes, hex-encoded to 64 characters, at `<projectPath>/.godot/breakpoint_mcp.secret`.
|
|
62
|
+
* Note the addon's `_setup_auth()` does NOT read the env overrides — they are
|
|
63
|
+
* host-side only — so the FILE is the mechanism for a host-launched child.
|
|
64
|
+
*/
|
|
65
|
+
export function ensureProjectSecret(projectPath) {
|
|
66
|
+
const existing = readProjectSecret(projectPath);
|
|
67
|
+
if (existing)
|
|
68
|
+
return existing;
|
|
69
|
+
try {
|
|
70
|
+
const dir = path.join(projectPath, ".godot");
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
const secret = randomBytes(32).toString("hex");
|
|
73
|
+
// wx: never clobber a secret another process minted between the read above
|
|
74
|
+
// and this write — losing that race must be a no-op, not an overwrite.
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(path.join(dir, "breakpoint_mcp.secret"), secret, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
77
|
+
return secret;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return readProjectSecret(projectPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
38
87
|
//# sourceMappingURL=secret.js.map
|
package/dist/tools/processes.js
CHANGED
|
@@ -12,11 +12,23 @@ const LINE_CAP = 5000;
|
|
|
12
12
|
export class ProcessRegistry {
|
|
13
13
|
procs = new Map();
|
|
14
14
|
counter = 0;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Spawn a managed Godot child. `env` is an OVERLAY on the inherited
|
|
17
|
+
* environment, not a replacement: omitted (the `godot_run_managed` path) the
|
|
18
|
+
* child inherits exactly as before, byte-identical to pre-F6 behaviour.
|
|
19
|
+
*
|
|
20
|
+
* The overlay is what makes multi-peer work at all — the runtime autoload
|
|
21
|
+
* reads `BREAKPOINT_RUNTIME_PORT` from its environment
|
|
22
|
+
* (`runtime_bridge.gd:74`), so N addressable peers is a per-child env
|
|
23
|
+
* passthrough plus a port allocator, with no protocol, transport or handshake
|
|
24
|
+
* change. See `peers.ts`.
|
|
25
|
+
*/
|
|
26
|
+
run(cfg, extraArgs, env) {
|
|
16
27
|
const id = `godot-${++this.counter}`;
|
|
17
28
|
const child = spawn(cfg.godotBin, ["--path", cfg.projectPath, ...extraArgs], {
|
|
18
29
|
cwd: cfg.projectPath,
|
|
19
30
|
stdio: ["ignore", "pipe", "pipe"],
|
|
31
|
+
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
20
32
|
});
|
|
21
33
|
const m = { id, child, lines: [], seq: 0, exited: false, exitCode: null };
|
|
22
34
|
const ingest = (stream) => (buf) => {
|
|
@@ -43,6 +55,18 @@ export class ProcessRegistry {
|
|
|
43
55
|
get(id) {
|
|
44
56
|
return this.procs.get(id);
|
|
45
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The last `n` captured lines for a managed child, newest last. Used to
|
|
60
|
+
* explain a peer that never bound its port — a project that fails to load
|
|
61
|
+
* dies with the reason on stderr, and without this the caller only ever sees
|
|
62
|
+
* "cannot reach the runtime bridge".
|
|
63
|
+
*/
|
|
64
|
+
tail(id, n = 12) {
|
|
65
|
+
const m = this.procs.get(id);
|
|
66
|
+
if (!m)
|
|
67
|
+
return [];
|
|
68
|
+
return m.lines.slice(-n).map((l) => l.text);
|
|
69
|
+
}
|
|
46
70
|
killAll() {
|
|
47
71
|
for (const m of this.procs.values()) {
|
|
48
72
|
try {
|