zcode-acp-server 0.2.0 → 0.3.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 +101 -15
- package/README.zh-CN.md +68 -13
- package/dist/bin/hub.d.ts +16 -0
- package/dist/bin/hub.d.ts.map +1 -0
- package/dist/bin/hub.js +41 -0
- package/dist/bin/hub.js.map +1 -0
- package/dist/handlers/account.d.ts +43 -0
- package/dist/handlers/account.d.ts.map +1 -0
- package/dist/handlers/account.js +59 -0
- package/dist/handlers/account.js.map +1 -0
- package/dist/handlers/io.d.ts +20 -1
- package/dist/handlers/io.d.ts.map +1 -1
- package/dist/handlers/io.js +57 -2
- package/dist/handlers/io.js.map +1 -1
- package/dist/handlers/replay.d.ts +79 -0
- package/dist/handlers/replay.d.ts.map +1 -0
- package/dist/handlers/replay.js +252 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +64 -65
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +23 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +67 -6
- package/dist/handlers/slash.js.map +1 -1
- package/dist/index.js +47 -17
- package/dist/index.js.map +1 -1
- package/dist/remote/broadcast.d.ts +47 -0
- package/dist/remote/broadcast.d.ts.map +1 -0
- package/dist/remote/broadcast.js +121 -0
- package/dist/remote/broadcast.js.map +1 -0
- package/dist/remote/config.d.ts +32 -0
- package/dist/remote/config.d.ts.map +1 -0
- package/dist/remote/config.js +65 -0
- package/dist/remote/config.js.map +1 -0
- package/dist/remote/endpoint.d.ts +30 -0
- package/dist/remote/endpoint.d.ts.map +1 -0
- package/dist/remote/endpoint.js +213 -0
- package/dist/remote/endpoint.js.map +1 -0
- package/dist/remote/hub-server.d.ts +41 -0
- package/dist/remote/hub-server.d.ts.map +1 -0
- package/dist/remote/hub-server.js +346 -0
- package/dist/remote/hub-server.js.map +1 -0
- package/dist/server.d.ts +41 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +69 -11
- package/dist/server.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +17 -1
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +47 -15
- package/docs/BACKLOG.md +3 -1
- package/docs/DEVELOPMENT.md +26 -0
- package/docs/PROTOCOL.md +67 -27
- package/docs/REMOTE-CLIENTS.md +260 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +39 -6
- package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
- package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
- package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
- package/docs/proposals/0001-tail-session-replay.md +136 -0
- package/docs/proposals/0002-plan-quota-usage.md +81 -0
- 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":"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"}
|
|
@@ -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,30 @@
|
|
|
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
|
+
* Start the loopback endpoint and hub registration. Never throws — failures
|
|
27
|
+
* warn and leave the bridge running stdio-only.
|
|
28
|
+
*/
|
|
29
|
+
export declare function startRemoteEndpoint(server: ZcodeAcpServer, app: acp.AgentApp, config: RemoteConfig): Promise<RemoteEndpointHandle | null>;
|
|
30
|
+
//# 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;AA8BD;;;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,213 @@
|
|
|
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
|
+
/** Session summaries for the hub's discovery API. */
|
|
37
|
+
function sessionsPayload(server) {
|
|
38
|
+
return Array.from(server.sessionSummaries.entries(), ([sessionId, s]) => ({
|
|
39
|
+
sessionId,
|
|
40
|
+
...(s.title !== undefined ? { title: s.title } : {}),
|
|
41
|
+
updatedAt: s.updatedAt,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async function postJson(url, body, timeoutMs = 3000) {
|
|
45
|
+
return fetch(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the loopback endpoint and hub registration. Never throws — failures
|
|
54
|
+
* warn and leave the bridge running stdio-only.
|
|
55
|
+
*/
|
|
56
|
+
export async function startRemoteEndpoint(server, app, config) {
|
|
57
|
+
const acpServer = new AcpServer({ agent: app });
|
|
58
|
+
const acpHttpHandler = createNodeHttpHandler(acpServer);
|
|
59
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
60
|
+
const upgradeHandler = createNodeWebSocketUpgradeHandler(acpServer, wss);
|
|
61
|
+
const httpServer = createServer((req, res) => {
|
|
62
|
+
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
63
|
+
if (path === "/acp")
|
|
64
|
+
acpHttpHandler(req, res);
|
|
65
|
+
else {
|
|
66
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
67
|
+
res.end("not found");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
71
|
+
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
72
|
+
if (path === "/acp")
|
|
73
|
+
upgradeHandler(req, socket, head);
|
|
74
|
+
else
|
|
75
|
+
socket.destroy();
|
|
76
|
+
});
|
|
77
|
+
// Port scan: several Zed windows spawn several bridges, each takes the next
|
|
78
|
+
// free port starting at ZCODE_ACP_REMOTE_PORT.
|
|
79
|
+
let port = 0;
|
|
80
|
+
for (let probe = 0; probe < MAX_PORT_PROBES; probe++) {
|
|
81
|
+
if (await tryListen(httpServer, config.bridgePort + probe)) {
|
|
82
|
+
port = config.bridgePort + probe;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!port) {
|
|
87
|
+
warn(`remote: no free loopback port in ${config.bridgePort}..${config.bridgePort + MAX_PORT_PROBES - 1} — remote disabled`);
|
|
88
|
+
wss.close();
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// The listener must not keep the process alive on its own (ADR-0001: the
|
|
92
|
+
// bridge's lifetime follows the stdio editor, not remote clients).
|
|
93
|
+
httpServer.unref();
|
|
94
|
+
httpServer.on("error", (e) => warn(`remote: endpoint error: ${e.message}`));
|
|
95
|
+
log(`remote: ACP endpoint listening on 127.0.0.1:${port}/acp`);
|
|
96
|
+
// ---- hub registration (heartbeat loop) ----
|
|
97
|
+
const instanceId = String(process.pid);
|
|
98
|
+
let stopped = false;
|
|
99
|
+
let authRejected = false;
|
|
100
|
+
let spawnThrottledUntil = 0;
|
|
101
|
+
const payload = () => ({
|
|
102
|
+
token: config.token,
|
|
103
|
+
id: instanceId,
|
|
104
|
+
port,
|
|
105
|
+
pid: process.pid,
|
|
106
|
+
workspace: server.workspaceLabel(),
|
|
107
|
+
sessions: sessionsPayload(server),
|
|
108
|
+
// Lets the hub detect that it is older than this bridge and restart
|
|
109
|
+
// itself (we then re-spawn it from this dist — see registerOnce).
|
|
110
|
+
version: AGENT_INFO.version,
|
|
111
|
+
});
|
|
112
|
+
const spawnHub = () => {
|
|
113
|
+
try {
|
|
114
|
+
// dist/remote/endpoint.js → dist/bin/hub.js (one level up, then bin/).
|
|
115
|
+
const hubJs = fileURLToPath(new URL("../bin/hub.js", import.meta.url));
|
|
116
|
+
const child = spawn(process.execPath, [hubJs], {
|
|
117
|
+
detached: true,
|
|
118
|
+
// Surface the daemon's stderr through the bridge's diagnostics — a
|
|
119
|
+
// detached "ignore" pipe silently eats startup failures.
|
|
120
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
121
|
+
env: {
|
|
122
|
+
...process.env,
|
|
123
|
+
ZCODE_ACP_HUB_PORT: String(config.hubPort),
|
|
124
|
+
ZCODE_ACP_HUB_HOST: config.hubHost,
|
|
125
|
+
ZCODE_ACP_REMOTE_TOKEN: config.token,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
child.stderr?.on("data", (d) => {
|
|
129
|
+
for (const line of d.toString().split("\n")) {
|
|
130
|
+
if (line.trim())
|
|
131
|
+
warn(`remote: hub: ${line}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Spawn failures (ENOENT when run from src without a build) arrive as an
|
|
135
|
+
// async 'error' event — without a listener Node crashes the bridge.
|
|
136
|
+
child.once("error", (e) => {
|
|
137
|
+
warn(`remote: hub spawn failed: ${e.message}`);
|
|
138
|
+
});
|
|
139
|
+
child.unref();
|
|
140
|
+
log(`remote: spawned hub on port ${config.hubPort} (pid ${child.pid})`);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
warn(`remote: hub spawn failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const registerOnce = async () => {
|
|
147
|
+
if (stopped || authRejected)
|
|
148
|
+
return;
|
|
149
|
+
try {
|
|
150
|
+
const res = await postJson(`http://127.0.0.1:${config.hubPort}/api/register`, payload());
|
|
151
|
+
if (res.status === 401) {
|
|
152
|
+
authRejected = true;
|
|
153
|
+
warn("remote: hub rejected the token (401) — registration stopped, check ZCODE_ACP_REMOTE_TOKEN");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Version handshake: the hub saw a newer bridge and is exiting. It is
|
|
157
|
+
// gone by now (it exits ~0.5s after replying) — re-spawn it from THIS
|
|
158
|
+
// dist (the upgraded code) and re-register. Throttled like the spawn
|
|
159
|
+
// below so a hub that keeps answering `restarting` can't loop us.
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
const body = (await res.json().catch(() => null));
|
|
162
|
+
if (body?.restarting) {
|
|
163
|
+
log("remote: hub is older than this bridge — respawning upgraded hub");
|
|
164
|
+
const respawn = setTimeout(() => {
|
|
165
|
+
if (stopped || authRejected)
|
|
166
|
+
return;
|
|
167
|
+
if (Date.now() < spawnThrottledUntil)
|
|
168
|
+
return;
|
|
169
|
+
spawnThrottledUntil = Date.now() + SPAWN_THROTTLE_MS;
|
|
170
|
+
spawnHub();
|
|
171
|
+
const retry = setTimeout(() => void registerOnce(), 1500);
|
|
172
|
+
retry.unref();
|
|
173
|
+
}, 2000);
|
|
174
|
+
respawn.unref();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Hub unreachable: (re)spawn it, throttled so a failing spawn can't
|
|
180
|
+
// storm, then retry registration shortly after the daemon warms up
|
|
181
|
+
// instead of waiting a full heartbeat cycle.
|
|
182
|
+
if (Date.now() >= spawnThrottledUntil) {
|
|
183
|
+
spawnThrottledUntil = Date.now() + SPAWN_THROTTLE_MS;
|
|
184
|
+
spawnHub();
|
|
185
|
+
const retry = setTimeout(() => void registerOnce(), 1500);
|
|
186
|
+
retry.unref();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
void registerOnce();
|
|
191
|
+
const heartbeat = setInterval(() => void registerOnce(), HEARTBEAT_MS);
|
|
192
|
+
heartbeat.unref();
|
|
193
|
+
return {
|
|
194
|
+
port,
|
|
195
|
+
async stop() {
|
|
196
|
+
stopped = true;
|
|
197
|
+
clearInterval(heartbeat);
|
|
198
|
+
try {
|
|
199
|
+
await postJson(`http://127.0.0.1:${config.hubPort}/api/unregister`, { token: config.token, id: instanceId }, 1500);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Hub gone or unreachable — its heartbeat TTL will drop us anyway.
|
|
203
|
+
}
|
|
204
|
+
for (const client of wss.clients)
|
|
205
|
+
client.terminate();
|
|
206
|
+
wss.close();
|
|
207
|
+
await acpServer.close().catch(() => undefined);
|
|
208
|
+
httpServer.closeAllConnections?.();
|
|
209
|
+
httpServer.close();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//# 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"}
|