zcode-acp-server 0.2.0 → 0.3.1

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.
Files changed (64) hide show
  1. package/README.md +101 -15
  2. package/README.zh-CN.md +68 -13
  3. package/dist/bin/hub.d.ts +16 -0
  4. package/dist/bin/hub.d.ts.map +1 -0
  5. package/dist/bin/hub.js +41 -0
  6. package/dist/bin/hub.js.map +1 -0
  7. package/dist/handlers/account.d.ts +43 -0
  8. package/dist/handlers/account.d.ts.map +1 -0
  9. package/dist/handlers/account.js +59 -0
  10. package/dist/handlers/account.js.map +1 -0
  11. package/dist/handlers/io.d.ts +20 -1
  12. package/dist/handlers/io.d.ts.map +1 -1
  13. package/dist/handlers/io.js +57 -2
  14. package/dist/handlers/io.js.map +1 -1
  15. package/dist/handlers/replay.d.ts +79 -0
  16. package/dist/handlers/replay.d.ts.map +1 -0
  17. package/dist/handlers/replay.js +256 -0
  18. package/dist/handlers/replay.js.map +1 -0
  19. package/dist/handlers/session.d.ts.map +1 -1
  20. package/dist/handlers/session.js +83 -68
  21. package/dist/handlers/session.js.map +1 -1
  22. package/dist/handlers/slash.d.ts +23 -1
  23. package/dist/handlers/slash.d.ts.map +1 -1
  24. package/dist/handlers/slash.js +67 -6
  25. package/dist/handlers/slash.js.map +1 -1
  26. package/dist/index.js +47 -17
  27. package/dist/index.js.map +1 -1
  28. package/dist/remote/broadcast.d.ts +47 -0
  29. package/dist/remote/broadcast.d.ts.map +1 -0
  30. package/dist/remote/broadcast.js +121 -0
  31. package/dist/remote/broadcast.js.map +1 -0
  32. package/dist/remote/config.d.ts +32 -0
  33. package/dist/remote/config.d.ts.map +1 -0
  34. package/dist/remote/config.js +65 -0
  35. package/dist/remote/config.js.map +1 -0
  36. package/dist/remote/endpoint.d.ts +43 -0
  37. package/dist/remote/endpoint.d.ts.map +1 -0
  38. package/dist/remote/endpoint.js +222 -0
  39. package/dist/remote/endpoint.js.map +1 -0
  40. package/dist/remote/hub-server.d.ts +41 -0
  41. package/dist/remote/hub-server.d.ts.map +1 -0
  42. package/dist/remote/hub-server.js +346 -0
  43. package/dist/remote/hub-server.js.map +1 -0
  44. package/dist/server.d.ts +62 -7
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +99 -11
  47. package/dist/server.js.map +1 -1
  48. package/dist/utils.d.ts +1 -1
  49. package/dist/utils.d.ts.map +1 -1
  50. package/dist/utils.js +17 -1
  51. package/dist/utils.js.map +1 -1
  52. package/docs/ARCHITECTURE.md +47 -15
  53. package/docs/BACKLOG.md +3 -1
  54. package/docs/DEVELOPMENT.md +26 -0
  55. package/docs/PROTOCOL.md +67 -27
  56. package/docs/REMOTE-CLIENTS.md +264 -0
  57. package/docs/REPLAY-GUIDE.md +131 -0
  58. package/docs/TROUBLESHOOTING.md +51 -6
  59. package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
  60. package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
  61. package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
  62. package/docs/proposals/0001-tail-session-replay.md +136 -0
  63. package/docs/proposals/0002-plan-quota-usage.md +81 -0
  64. package/package.json +5 -2
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Multi-client broadcast layer for remote access.
3
+ *
4
+ * The bridge historically served ONE ACP client (the editor over stdio). With
5
+ * remote access enabled, additional clients attach over WebSocket; every
6
+ * agent-originated message must reach all of them. This module owns the client
7
+ * registry and a stable proxy that quacks like an `AgentContext`:
8
+ *
9
+ * - `notify` fans out to every client; a single dead/slow client is warned
10
+ * about and never fails the others.
11
+ * - `request` (permission / elicitation) is sent to every client and the FIRST
12
+ * response wins. Losers are aborted via `cancellationSignal`, which makes
13
+ * the SDK emit `$/cancel_request` so the losing editor dismisses its dialog
14
+ * (verified against Zed's ACP client).
15
+ *
16
+ * Loser promises settle late (the peer answers the cancellation eventually) —
17
+ * every raced promise carries a no-op catch so late settlements can't surface
18
+ * as unhandledRejection (Node ≥15 crashes on those by default).
19
+ */
20
+ import { warn } from "../utils.js";
21
+ /**
22
+ * Track every connection opened on the app (stdio editor + remote WebSocket)
23
+ * in the registry, removing each on close. Wired once by the entry point
24
+ * BEFORE `connect()` so the stdio connection is captured too.
25
+ */
26
+ export function trackConnections(app, clients) {
27
+ app.onConnect((conn) => {
28
+ clients.add(conn.client);
29
+ void conn.closed.then(() => clients.remove(conn.client));
30
+ });
31
+ }
32
+ /**
33
+ * Registry of connected ACP clients (stdio editor + remote WebSocket clients).
34
+ * Membership is managed by the entry point via the SDK's per-connection
35
+ * lifecycle; the broadcast proxy reads membership live on every call.
36
+ */
37
+ export class ClientRegistry {
38
+ clients = new Set();
39
+ proxy = null;
40
+ add(cx) {
41
+ this.clients.add(cx);
42
+ }
43
+ remove(cx) {
44
+ this.clients.delete(cx);
45
+ }
46
+ get size() {
47
+ return this.clients.size;
48
+ }
49
+ /** Stable broadcast proxy satisfying the `AgentContext` call surface. */
50
+ broadcast() {
51
+ if (!this.proxy)
52
+ this.proxy = createBroadcastProxy(this);
53
+ return this.proxy;
54
+ }
55
+ snapshot() {
56
+ return Array.from(this.clients);
57
+ }
58
+ }
59
+ /** Build the stable proxy once per registry (module factory: no `this` alias). */
60
+ function createBroadcastProxy(registry) {
61
+ const proxy = Object.create(null);
62
+ proxy.notify = (method, params) => notifyAll(registry, method, params);
63
+ proxy.request = (method, params, options) => requestAny(registry, method, params, options);
64
+ return proxy;
65
+ }
66
+ async function notifyAll(registry, method, params) {
67
+ const results = await Promise.allSettled(registry.snapshot().map((cx) => cx.notify(method, params)));
68
+ for (const r of results) {
69
+ if (r.status === "rejected") {
70
+ warn(`broadcast: notify ${method} failed on one client: ` +
71
+ `${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
72
+ }
73
+ }
74
+ }
75
+ async function requestAny(registry, method, params, options) {
76
+ const clients = registry.snapshot();
77
+ if (clients.length === 0) {
78
+ throw new Error(`broadcast: no connected clients (${method})`);
79
+ }
80
+ const controllers = clients.map(() => new AbortController());
81
+ // Link a caller-provided signal: aborting it cancels EVERY inner request.
82
+ const outerSignal = options?.cancellationSignal;
83
+ const onOuterAbort = () => {
84
+ for (const c of controllers)
85
+ c.abort();
86
+ };
87
+ if (outerSignal) {
88
+ if (outerSignal.aborted)
89
+ onOuterAbort();
90
+ else
91
+ outerSignal.addEventListener("abort", onOuterAbort, { once: true });
92
+ }
93
+ const attempts = clients.map((cx, i) => {
94
+ const promise = cx.request(method, params, {
95
+ ...options,
96
+ cancellationSignal: controllers[i].signal,
97
+ });
98
+ // Mark handled: losing promises settle AFTER Promise.any is done.
99
+ promise.catch(() => undefined);
100
+ return promise.then((value) => ({ value, index: i }));
101
+ });
102
+ try {
103
+ const winner = await Promise.any(attempts);
104
+ for (let i = 0; i < controllers.length; i++) {
105
+ if (i !== winner.index)
106
+ controllers[i].abort();
107
+ }
108
+ return winner.value;
109
+ }
110
+ catch (e) {
111
+ // All clients failed — surface the first error like a single client would.
112
+ if (e instanceof AggregateError)
113
+ throw e.errors[0] ?? e;
114
+ throw e;
115
+ }
116
+ finally {
117
+ if (outerSignal)
118
+ outerSignal.removeEventListener("abort", onOuterAbort);
119
+ }
120
+ }
121
+ //# sourceMappingURL=broadcast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../../src/remote/broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQnC;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAiB,EAAE,OAAuB;IACzE,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;QACrB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzB,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC;AAQD;;;;GAIG;AACH,MAAM,OAAO,cAAc;IACR,OAAO,GAAG,IAAI,GAAG,EAAc,CAAC;IACzC,KAAK,GAA4B,IAAI,CAAC;IAE9C,GAAG,CAAC,EAAc;QAChB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvB,CAAC;IAED,MAAM,CAAC,EAAc;QACnB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED,yEAAyE;IACzE,SAAS;QACP,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,QAAQ;QACN,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;CACF;AAED,kFAAkF;AAClF,SAAS,oBAAoB,CAAC,QAAwB;IACpD,MAAM,KAAK,GAA4B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,MAAM,GAAG,CAAC,MAAc,EAAE,MAAgB,EAAiB,EAAE,CACjE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,CAAC,OAAO,GAAG,CACd,MAAc,EACd,MAAgB,EAChB,OAAgC,EACd,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACrE,OAAO,KAAoC,CAAC;AAC9C,CAAC;AAED,KAAK,UAAU,SAAS,CACtB,QAAwB,EACxB,MAAc,EACd,MAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAC3D,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC5B,IAAI,CACF,qBAAqB,MAAM,yBAAyB;gBAClD,GAAG,CAAC,CAAC,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CACvE,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,QAAwB,EACxB,MAAc,EACd,MAAgB,EAChB,OAAgC;IAEhC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;IACpC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,GAAG,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC;IAC7D,0EAA0E;IAC1E,MAAM,WAAW,GAAG,OAAO,EAAE,kBAAkB,CAAC;IAChD,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,KAAK,MAAM,CAAC,IAAI,WAAW;YAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACzC,CAAC,CAAC;IACF,IAAI,WAAW,EAAE,CAAC;QAChB,IAAI,WAAW,CAAC,OAAO;YAAE,YAAY,EAAE,CAAC;;YACnC,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;YACzC,GAAG,OAAO;YACV,kBAAkB,EAAE,WAAW,CAAC,CAAC,CAAE,CAAC,MAAM;SAC3C,CAAC,CAAC;QACH,kEAAkE;QAClE,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC/B,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAc,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IACH,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,IAAI,CAAC,KAAK,MAAM,CAAC,KAAK;gBAAE,WAAW,CAAC,CAAC,CAAE,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,2EAA2E;QAC3E,IAAI,CAAC,YAAY,cAAc;YAAE,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,CAAC,CAAC;IACV,CAAC;YAAS,CAAC;QACT,IAAI,WAAW;YAAE,WAAW,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Remote access configuration from environment variables.
3
+ *
4
+ * Remote access is opt-in via ZCODE_ACP_REMOTE=1 and REQUIRES a token — the
5
+ * endpoint is expected to sit behind a public tunnel (Cloudflare Tunnel, frp),
6
+ * so "loopback-only" is never a safe assumption here. A missing token disables
7
+ * the feature with a warning instead of failing the bridge: the stdio link to
8
+ * the editor must keep working no matter what.
9
+ *
10
+ * Variables:
11
+ * ZCODE_ACP_REMOTE=1 enable the remote endpoint (gate)
12
+ * ZCODE_ACP_REMOTE_TOKEN=<s> auth token (mandatory when enabled)
13
+ * ZCODE_ACP_HUB_PORT=8377 hub's fixed port (the one a tunnel maps)
14
+ * ZCODE_ACP_HUB_HOST=127.0.0.1 hub bind address (e.g. 0.0.0.0 for a
15
+ * containerized tunnel agent)
16
+ * ZCODE_ACP_REMOTE_PORT=8378 bridge endpoint start port (auto-increment
17
+ * when taken; loopback only)
18
+ */
19
+ export interface RemoteConfig {
20
+ token: string;
21
+ hubPort: number;
22
+ hubHost: string;
23
+ bridgePort: number;
24
+ }
25
+ export declare const DEFAULT_HUB_PORT = 8377;
26
+ export declare const DEFAULT_BRIDGE_PORT = 8378;
27
+ export declare const DEFAULT_HUB_HOST = "127.0.0.1";
28
+ /** Parse remote config; null = disabled (or misconfigured → warned). */
29
+ export declare function parseRemoteConfig(env?: NodeJS.ProcessEnv): RemoteConfig | null;
30
+ /** Parse hub-side config for the standalone `zcode-acp-hub` bin. */
31
+ export declare function parseHubConfig(env?: NodeJS.ProcessEnv): RemoteConfig | null;
32
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAY5C,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,GAAG,IAAI,CAiB3F;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,GAAG,IAAI,CAYxF"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Remote access configuration from environment variables.
3
+ *
4
+ * Remote access is opt-in via ZCODE_ACP_REMOTE=1 and REQUIRES a token — the
5
+ * endpoint is expected to sit behind a public tunnel (Cloudflare Tunnel, frp),
6
+ * so "loopback-only" is never a safe assumption here. A missing token disables
7
+ * the feature with a warning instead of failing the bridge: the stdio link to
8
+ * the editor must keep working no matter what.
9
+ *
10
+ * Variables:
11
+ * ZCODE_ACP_REMOTE=1 enable the remote endpoint (gate)
12
+ * ZCODE_ACP_REMOTE_TOKEN=<s> auth token (mandatory when enabled)
13
+ * ZCODE_ACP_HUB_PORT=8377 hub's fixed port (the one a tunnel maps)
14
+ * ZCODE_ACP_HUB_HOST=127.0.0.1 hub bind address (e.g. 0.0.0.0 for a
15
+ * containerized tunnel agent)
16
+ * ZCODE_ACP_REMOTE_PORT=8378 bridge endpoint start port (auto-increment
17
+ * when taken; loopback only)
18
+ */
19
+ import { warn } from "../utils.js";
20
+ export const DEFAULT_HUB_PORT = 8377;
21
+ export const DEFAULT_BRIDGE_PORT = 8378;
22
+ export const DEFAULT_HUB_HOST = "127.0.0.1";
23
+ function parsePort(raw, fallback, envName) {
24
+ if (!raw)
25
+ return fallback;
26
+ const port = Number.parseInt(raw, 10);
27
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
28
+ warn(`remote: invalid ${envName}="${raw}", falling back to ${fallback}`);
29
+ return fallback;
30
+ }
31
+ return port;
32
+ }
33
+ /** Parse remote config; null = disabled (or misconfigured → warned). */
34
+ export function parseRemoteConfig(env = process.env) {
35
+ const gate = (env.ZCODE_ACP_REMOTE ?? "").trim().toLowerCase();
36
+ if (!["1", "true", "yes", "on"].includes(gate))
37
+ return null;
38
+ const token = (env.ZCODE_ACP_REMOTE_TOKEN ?? "").trim();
39
+ if (!token) {
40
+ warn("remote: ZCODE_ACP_REMOTE is enabled but ZCODE_ACP_REMOTE_TOKEN is missing — " +
41
+ "remote access disabled (stdio unaffected)");
42
+ return null;
43
+ }
44
+ return {
45
+ token,
46
+ hubPort: parsePort(env.ZCODE_ACP_HUB_PORT, DEFAULT_HUB_PORT, "ZCODE_ACP_HUB_PORT"),
47
+ hubHost: (env.ZCODE_ACP_HUB_HOST ?? "").trim() || DEFAULT_HUB_HOST,
48
+ bridgePort: parsePort(env.ZCODE_ACP_REMOTE_PORT, DEFAULT_BRIDGE_PORT, "ZCODE_ACP_REMOTE_PORT"),
49
+ };
50
+ }
51
+ /** Parse hub-side config for the standalone `zcode-acp-hub` bin. */
52
+ export function parseHubConfig(env = process.env) {
53
+ const token = (env.ZCODE_ACP_REMOTE_TOKEN ?? "").trim();
54
+ if (!token) {
55
+ warn("hub: ZCODE_ACP_REMOTE_TOKEN is required — refusing to start without auth");
56
+ return null;
57
+ }
58
+ return {
59
+ token,
60
+ hubPort: parsePort(env.ZCODE_ACP_HUB_PORT, DEFAULT_HUB_PORT, "ZCODE_ACP_HUB_PORT"),
61
+ hubHost: (env.ZCODE_ACP_HUB_HOST ?? "").trim() || DEFAULT_HUB_HOST,
62
+ bridgePort: parsePort(env.ZCODE_ACP_REMOTE_PORT, DEFAULT_BRIDGE_PORT, "ZCODE_ACP_REMOTE_PORT"),
63
+ };
64
+ }
65
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AASnC,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AACrC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE5C,SAAS,SAAS,CAAC,GAAuB,EAAE,QAAgB,EAAE,OAAe;IAC3E,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;QACxD,IAAI,CAAC,mBAAmB,OAAO,KAAK,GAAG,sBAAsB,QAAQ,EAAE,CAAC,CAAC;QACzE,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,iBAAiB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACpE,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/D,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,CACF,8EAA8E;YAC5E,2CAA2C,CAC9C,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO;QACL,KAAK;QACL,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,kBAAkB,EAAE,gBAAgB,EAAE,oBAAoB,CAAC;QAClF,OAAO,EAAE,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,gBAAgB;QAClE,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,qBAAqB,EAAE,mBAAmB,EAAE,uBAAuB,CAAC;KAC/F,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,cAAc,CAAC,MAAyB,OAAO,CAAC,GAAG;IACjE,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACjF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO;QACL,KAAK;QACL,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,kBAAkB,EAAE,gBAAgB,EAAE,oBAAoB,CAAC;QAClF,OAAO,EAAE,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,gBAAgB;QAClE,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,qBAAqB,EAAE,mBAAmB,EAAE,uBAAuB,CAAC;KAC/F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Loopback ACP endpoint + hub registration for remote access.
3
+ *
4
+ * When ZCODE_ACP_REMOTE is enabled, the bridge serves the SAME AgentApp that
5
+ * handles the stdio editor connection on a loopback HTTP/WebSocket endpoint
6
+ * (SDK AcpServer transport). Each remote connection gets its own JSON-RPC id
7
+ * space; fan-out to all clients is handled by the broadcast registry, not
8
+ * here. This endpoint is intentionally NOT exposed to the network — the hub
9
+ * (`zcode-acp-hub`) is the single public entry and proxies into it.
10
+ *
11
+ * The bridge also registers itself with the hub (spawning one if none is
12
+ * listening) and re-registers every 10s as a heartbeat carrying fresh session
13
+ * summaries. Everything here is best-effort: any failure warns and disables
14
+ * the remote side without touching the stdio link.
15
+ */
16
+ import type * as acp from "@agentclientprotocol/sdk";
17
+ import type { ZcodeAcpServer } from "../server.js";
18
+ import type { RemoteConfig } from "./config.js";
19
+ export interface RemoteEndpointHandle {
20
+ /** Actual loopback port the endpoint bound (may differ from config). */
21
+ port: number;
22
+ /** Stop the endpoint and unregister from the hub (best-effort). */
23
+ stop(): Promise<void>;
24
+ }
25
+ /**
26
+ * Session summaries for the hub's discovery API. Only sessions with real
27
+ * interaction (`hasActivity`) are pushed: an editor restart auto-resumes its
28
+ * stored placeholder, materializing an empty backend session — pushing that
29
+ * would make remote clients list (and open) a conversation that never
30
+ * happened. The hub replaces the whole list on every register, so a session
31
+ * that gains activity shows up within one heartbeat (~10s).
32
+ */
33
+ export declare function sessionsPayload(server: ZcodeAcpServer): Array<{
34
+ sessionId: string;
35
+ title?: string;
36
+ updatedAt: number;
37
+ }>;
38
+ /**
39
+ * Start the loopback endpoint and hub registration. Never throws — failures
40
+ * warn and leave the bridge running stdio-only.
41
+ */
42
+ export declare function startRemoteEndpoint(server: ZcodeAcpServer, app: acp.AgentApp, config: RemoteConfig): Promise<RemoteEndpointHandle | null>;
43
+ //# sourceMappingURL=endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../../src/remote/endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAQrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAUD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,cAAc,GACrB,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAQjE;AAWD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,GAAG,CAAC,QAAQ,EACjB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAiKtC"}
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Loopback ACP endpoint + hub registration for remote access.
3
+ *
4
+ * When ZCODE_ACP_REMOTE is enabled, the bridge serves the SAME AgentApp that
5
+ * handles the stdio editor connection on a loopback HTTP/WebSocket endpoint
6
+ * (SDK AcpServer transport). Each remote connection gets its own JSON-RPC id
7
+ * space; fan-out to all clients is handled by the broadcast registry, not
8
+ * here. This endpoint is intentionally NOT exposed to the network — the hub
9
+ * (`zcode-acp-hub`) is the single public entry and proxies into it.
10
+ *
11
+ * The bridge also registers itself with the hub (spawning one if none is
12
+ * listening) and re-registers every 10s as a heartbeat carrying fresh session
13
+ * summaries. Everything here is best-effort: any failure warns and disables
14
+ * the remote side without touching the stdio link.
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ import { createServer } from "node:http";
18
+ import { fileURLToPath } from "node:url";
19
+ import { AcpServer } from "@agentclientprotocol/sdk/experimental/server";
20
+ import { createNodeHttpHandler, createNodeWebSocketUpgradeHandler, } from "@agentclientprotocol/sdk/experimental/node";
21
+ import { WebSocketServer } from "ws";
22
+ import { AGENT_INFO, log, warn } from "../utils.js";
23
+ /** How often the bridge re-registers with the hub (also the heartbeat). */
24
+ const HEARTBEAT_MS = 10_000;
25
+ /** Minimum spacing between hub spawn attempts (avoids spawn storms). */
26
+ const SPAWN_THROTTLE_MS = 60_000;
27
+ /** Max ports probed above ZCODE_ACP_REMOTE_PORT before giving up. */
28
+ const MAX_PORT_PROBES = 100;
29
+ /** Probe one loopback port; false = taken or otherwise unusable. */
30
+ function tryListen(server, port) {
31
+ return new Promise((resolve) => {
32
+ server.once("error", () => resolve(false));
33
+ server.listen(port, "127.0.0.1", () => resolve(true));
34
+ });
35
+ }
36
+ /**
37
+ * Session summaries for the hub's discovery API. Only sessions with real
38
+ * interaction (`hasActivity`) are pushed: an editor restart auto-resumes its
39
+ * stored placeholder, materializing an empty backend session — pushing that
40
+ * would make remote clients list (and open) a conversation that never
41
+ * happened. The hub replaces the whole list on every register, so a session
42
+ * that gains activity shows up within one heartbeat (~10s).
43
+ */
44
+ export function sessionsPayload(server) {
45
+ return Array.from(server.sessionSummaries.entries())
46
+ .filter(([, s]) => s.hasActivity)
47
+ .map(([sessionId, s]) => ({
48
+ sessionId,
49
+ ...(s.title !== undefined ? { title: s.title } : {}),
50
+ updatedAt: s.updatedAt,
51
+ }));
52
+ }
53
+ async function postJson(url, body, timeoutMs = 3000) {
54
+ return fetch(url, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify(body),
58
+ signal: AbortSignal.timeout(timeoutMs),
59
+ });
60
+ }
61
+ /**
62
+ * Start the loopback endpoint and hub registration. Never throws — failures
63
+ * warn and leave the bridge running stdio-only.
64
+ */
65
+ export async function startRemoteEndpoint(server, app, config) {
66
+ const acpServer = new AcpServer({ agent: app });
67
+ const acpHttpHandler = createNodeHttpHandler(acpServer);
68
+ const wss = new WebSocketServer({ noServer: true });
69
+ const upgradeHandler = createNodeWebSocketUpgradeHandler(acpServer, wss);
70
+ const httpServer = createServer((req, res) => {
71
+ const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
72
+ if (path === "/acp")
73
+ acpHttpHandler(req, res);
74
+ else {
75
+ res.writeHead(404, { "Content-Type": "text/plain" });
76
+ res.end("not found");
77
+ }
78
+ });
79
+ httpServer.on("upgrade", (req, socket, head) => {
80
+ const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
81
+ if (path === "/acp")
82
+ upgradeHandler(req, socket, head);
83
+ else
84
+ socket.destroy();
85
+ });
86
+ // Port scan: several Zed windows spawn several bridges, each takes the next
87
+ // free port starting at ZCODE_ACP_REMOTE_PORT.
88
+ let port = 0;
89
+ for (let probe = 0; probe < MAX_PORT_PROBES; probe++) {
90
+ if (await tryListen(httpServer, config.bridgePort + probe)) {
91
+ port = config.bridgePort + probe;
92
+ break;
93
+ }
94
+ }
95
+ if (!port) {
96
+ warn(`remote: no free loopback port in ${config.bridgePort}..${config.bridgePort + MAX_PORT_PROBES - 1} — remote disabled`);
97
+ wss.close();
98
+ return null;
99
+ }
100
+ // The listener must not keep the process alive on its own (ADR-0001: the
101
+ // bridge's lifetime follows the stdio editor, not remote clients).
102
+ httpServer.unref();
103
+ httpServer.on("error", (e) => warn(`remote: endpoint error: ${e.message}`));
104
+ log(`remote: ACP endpoint listening on 127.0.0.1:${port}/acp`);
105
+ // ---- hub registration (heartbeat loop) ----
106
+ const instanceId = String(process.pid);
107
+ let stopped = false;
108
+ let authRejected = false;
109
+ let spawnThrottledUntil = 0;
110
+ const payload = () => ({
111
+ token: config.token,
112
+ id: instanceId,
113
+ port,
114
+ pid: process.pid,
115
+ workspace: server.workspaceLabel(),
116
+ sessions: sessionsPayload(server),
117
+ // Lets the hub detect that it is older than this bridge and restart
118
+ // itself (we then re-spawn it from this dist — see registerOnce).
119
+ version: AGENT_INFO.version,
120
+ });
121
+ const spawnHub = () => {
122
+ try {
123
+ // dist/remote/endpoint.js → dist/bin/hub.js (one level up, then bin/).
124
+ const hubJs = fileURLToPath(new URL("../bin/hub.js", import.meta.url));
125
+ const child = spawn(process.execPath, [hubJs], {
126
+ detached: true,
127
+ // Surface the daemon's stderr through the bridge's diagnostics — a
128
+ // detached "ignore" pipe silently eats startup failures.
129
+ stdio: ["ignore", "ignore", "pipe"],
130
+ env: {
131
+ ...process.env,
132
+ ZCODE_ACP_HUB_PORT: String(config.hubPort),
133
+ ZCODE_ACP_HUB_HOST: config.hubHost,
134
+ ZCODE_ACP_REMOTE_TOKEN: config.token,
135
+ },
136
+ });
137
+ child.stderr?.on("data", (d) => {
138
+ for (const line of d.toString().split("\n")) {
139
+ if (line.trim())
140
+ warn(`remote: hub: ${line}`);
141
+ }
142
+ });
143
+ // Spawn failures (ENOENT when run from src without a build) arrive as an
144
+ // async 'error' event — without a listener Node crashes the bridge.
145
+ child.once("error", (e) => {
146
+ warn(`remote: hub spawn failed: ${e.message}`);
147
+ });
148
+ child.unref();
149
+ log(`remote: spawned hub on port ${config.hubPort} (pid ${child.pid})`);
150
+ }
151
+ catch (e) {
152
+ warn(`remote: hub spawn failed: ${e instanceof Error ? e.message : String(e)}`);
153
+ }
154
+ };
155
+ const registerOnce = async () => {
156
+ if (stopped || authRejected)
157
+ return;
158
+ try {
159
+ const res = await postJson(`http://127.0.0.1:${config.hubPort}/api/register`, payload());
160
+ if (res.status === 401) {
161
+ authRejected = true;
162
+ warn("remote: hub rejected the token (401) — registration stopped, check ZCODE_ACP_REMOTE_TOKEN");
163
+ return;
164
+ }
165
+ // Version handshake: the hub saw a newer bridge and is exiting. It is
166
+ // gone by now (it exits ~0.5s after replying) — re-spawn it from THIS
167
+ // dist (the upgraded code) and re-register. Throttled like the spawn
168
+ // below so a hub that keeps answering `restarting` can't loop us.
169
+ if (res.ok) {
170
+ const body = (await res.json().catch(() => null));
171
+ if (body?.restarting) {
172
+ log("remote: hub is older than this bridge — respawning upgraded hub");
173
+ const respawn = setTimeout(() => {
174
+ if (stopped || authRejected)
175
+ return;
176
+ if (Date.now() < spawnThrottledUntil)
177
+ return;
178
+ spawnThrottledUntil = Date.now() + SPAWN_THROTTLE_MS;
179
+ spawnHub();
180
+ const retry = setTimeout(() => void registerOnce(), 1500);
181
+ retry.unref();
182
+ }, 2000);
183
+ respawn.unref();
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ // Hub unreachable: (re)spawn it, throttled so a failing spawn can't
189
+ // storm, then retry registration shortly after the daemon warms up
190
+ // instead of waiting a full heartbeat cycle.
191
+ if (Date.now() >= spawnThrottledUntil) {
192
+ spawnThrottledUntil = Date.now() + SPAWN_THROTTLE_MS;
193
+ spawnHub();
194
+ const retry = setTimeout(() => void registerOnce(), 1500);
195
+ retry.unref();
196
+ }
197
+ }
198
+ };
199
+ void registerOnce();
200
+ const heartbeat = setInterval(() => void registerOnce(), HEARTBEAT_MS);
201
+ heartbeat.unref();
202
+ return {
203
+ port,
204
+ async stop() {
205
+ stopped = true;
206
+ clearInterval(heartbeat);
207
+ try {
208
+ await postJson(`http://127.0.0.1:${config.hubPort}/api/unregister`, { token: config.token, id: instanceId }, 1500);
209
+ }
210
+ catch {
211
+ // Hub gone or unreachable — its heartbeat TTL will drop us anyway.
212
+ }
213
+ for (const client of wss.clients)
214
+ client.terminate();
215
+ wss.close();
216
+ await acpServer.close().catch(() => undefined);
217
+ httpServer.closeAllConnections?.();
218
+ httpServer.close();
219
+ },
220
+ };
221
+ }
222
+ //# sourceMappingURL=endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../src/remote/endpoint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * zcode-acp-hub — machine-level singleton for remote access.
3
+ *
4
+ * The hub is the ONLY public entry point (the port a tunnel maps). It does
5
+ * exactly three things (ADR-0002): token auth, instance discovery, and
6
+ * byte-level WebSocket proxying from a remote client to one bridge's loopback
7
+ * ACP endpoint. It holds no session state and understands no ACP — a proxied
8
+ * connection stays bound to one instance for its whole lifetime.
9
+ *
10
+ * Bridges register via POST /api/register every 10s (the registration doubles
11
+ * as the heartbeat; entries older than the heartbeat TTL are pruned). A client
12
+ * that needs an immediately-honest list (e.g. a phone app's pull-to-refresh)
13
+ * passes ?probe=1 to /api/instances: the hub TCP-probes each registered
14
+ * loopback port and prunes unreachable bridges before answering — no periodic
15
+ * probing, the cost is paid only when someone refreshes. When no instance is
16
+ * registered and no proxy is active for `idleExitMs`, the hub exits — the
17
+ * next bridge re-spawns it on demand.
18
+ */
19
+ export interface HubOptions {
20
+ port: number;
21
+ host: string;
22
+ token: string;
23
+ /** Registration TTL before an instance is pruned (default 30s). */
24
+ heartbeatTimeoutMs?: number;
25
+ /** Idle time with zero instances and zero proxies before exit (default 10min). */
26
+ idleExitMs?: number;
27
+ /** WebSocket keepalive ping interval (default 30s; tunnels drop idle links). */
28
+ pingIntervalMs?: number;
29
+ }
30
+ export interface HubHandle {
31
+ port: number;
32
+ close(): Promise<void>;
33
+ }
34
+ /**
35
+ * Start the hub. Resolves once listening; rejects on bind failure (including
36
+ * EADDRINUSE when another hub already owns the port).
37
+ */
38
+ export declare function startHub(options: HubOptions & {
39
+ onIdleExit?: () => void;
40
+ }): Promise<HubHandle>;
41
+ //# sourceMappingURL=hub-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-server.d.ts","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAUH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAqGD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAwQ9F"}