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/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
@@ -12,11 +12,23 @@ const LINE_CAP = 5000;
12
12
  export class ProcessRegistry {
13
13
  procs = new Map();
14
14
  counter = 0;
15
- run(cfg, extraArgs) {
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 {