@kb-labs/host-agent-core 0.2.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/index.d.ts +260 -0
- package/dist/index.js +560 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { CapabilityCall, IpcStatusResponse } from '@kb-labs/host-agent-contracts';
|
|
2
|
+
import { AdapterCallContext } from '@kb-labs/gateway-contracts';
|
|
3
|
+
import { ILocalTransport } from '@kb-labs/host-agent-transport';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GatewayClient — WebSocket connection to Gateway with:
|
|
7
|
+
* - JWT Bearer auth
|
|
8
|
+
* - hello/connected handshake
|
|
9
|
+
* - heartbeat every 30s
|
|
10
|
+
* - exponential backoff reconnect (1s → 2s → 4s … max 60s)
|
|
11
|
+
* - dispatches incoming `call` messages to registered capability handlers
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type CallHandler = (call: CapabilityCall) => Promise<unknown>;
|
|
15
|
+
/** Adapter call input — used by GatewayTransport */
|
|
16
|
+
interface AdapterCallInput {
|
|
17
|
+
adapter: string;
|
|
18
|
+
method: string;
|
|
19
|
+
args: unknown[];
|
|
20
|
+
timeout?: number;
|
|
21
|
+
context: AdapterCallContext;
|
|
22
|
+
}
|
|
23
|
+
/** Adapter call response — returned by sendAdapterCall */
|
|
24
|
+
interface AdapterCallResponse {
|
|
25
|
+
requestId: string;
|
|
26
|
+
result?: unknown;
|
|
27
|
+
error?: {
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
retryable: boolean;
|
|
31
|
+
details?: unknown;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface GatewayClientOptions {
|
|
35
|
+
/** wss://gateway.example.com */
|
|
36
|
+
gatewayUrl: string;
|
|
37
|
+
agentVersion: string;
|
|
38
|
+
/** Returns current valid access token */
|
|
39
|
+
getAccessToken: () => string;
|
|
40
|
+
/** Called when connection is established and handshake complete */
|
|
41
|
+
onConnected?: (hostId: string, sessionId: string) => void;
|
|
42
|
+
/** Called when disconnected */
|
|
43
|
+
onDisconnected?: () => void;
|
|
44
|
+
/** Called when token needs refresh (triggered before reconnect) */
|
|
45
|
+
onTokenExpired?: () => Promise<void>;
|
|
46
|
+
/** Capabilities this host provides — sent in hello message so Gateway can route by capability */
|
|
47
|
+
capabilities?: string[];
|
|
48
|
+
/** Host type for workspace agent routing */
|
|
49
|
+
hostType?: 'local' | 'cloud';
|
|
50
|
+
/** Workspace info advertised on connect */
|
|
51
|
+
workspaces?: Array<{
|
|
52
|
+
workspaceId: string;
|
|
53
|
+
repoFingerprint?: string;
|
|
54
|
+
branch?: string;
|
|
55
|
+
}>;
|
|
56
|
+
/** Plugin inventory advertised on connect */
|
|
57
|
+
plugins?: Array<{
|
|
58
|
+
id: string;
|
|
59
|
+
version: string;
|
|
60
|
+
}>;
|
|
61
|
+
/** Optional logger for connection diagnostics */
|
|
62
|
+
logger?: {
|
|
63
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
64
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
65
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Pending execute tunnel — callbacks for streaming events from Gateway */
|
|
69
|
+
interface PendingExecute {
|
|
70
|
+
onEvent: (event: unknown) => void;
|
|
71
|
+
onDone: (result: unknown) => void;
|
|
72
|
+
onError: (error: Error) => void;
|
|
73
|
+
}
|
|
74
|
+
declare class GatewayClient {
|
|
75
|
+
private readonly opts;
|
|
76
|
+
private ws;
|
|
77
|
+
private heartbeatTimer;
|
|
78
|
+
private reconnectTimer;
|
|
79
|
+
private backoffMs;
|
|
80
|
+
private stopped;
|
|
81
|
+
private hostId;
|
|
82
|
+
private reconnectCount;
|
|
83
|
+
private disconnectedAt;
|
|
84
|
+
private pendingCalls;
|
|
85
|
+
private handlers;
|
|
86
|
+
/** Pending reverse adapter calls (Host → Gateway → Platform) */
|
|
87
|
+
private pendingAdapterCalls;
|
|
88
|
+
constructor(opts: GatewayClientOptions);
|
|
89
|
+
/** Register a handler for calls to a specific adapter */
|
|
90
|
+
registerHandler(adapter: string, handler: CallHandler): void;
|
|
91
|
+
/**
|
|
92
|
+
* Send an adapter call to Platform via Gateway WS (reverse proxy).
|
|
93
|
+
* Used by GatewayTransport to proxy ctx.llm, ctx.cache, etc. back to Brain.
|
|
94
|
+
*
|
|
95
|
+
* Flow: Host → WS adapter:call → Gateway → HTTP → REST API → platform adapter → result
|
|
96
|
+
*/
|
|
97
|
+
sendAdapterCall(call: AdapterCallInput): Promise<AdapterCallResponse>;
|
|
98
|
+
/**
|
|
99
|
+
* Tunnel an execute request to Gateway via HTTP REST API.
|
|
100
|
+
* CLI → IPC → Host Agent → HTTP POST /api/v1/execute → Gateway → Server.
|
|
101
|
+
* Gateway responds with ndjson stream of ExecutionEvent objects.
|
|
102
|
+
*
|
|
103
|
+
* We use HTTP (not WS) because the WS channel is for Gateway→Host calls,
|
|
104
|
+
* not Host→Gateway execute requests.
|
|
105
|
+
*/
|
|
106
|
+
executeTunnel(requestId: string, command: string, params: Record<string, unknown> | undefined, callbacks: PendingExecute): void;
|
|
107
|
+
private doExecuteHttp;
|
|
108
|
+
/** Cancel a pending execute via Gateway REST API */
|
|
109
|
+
cancelExecute(executionId: string, reason?: string): void;
|
|
110
|
+
connect(): Promise<void>;
|
|
111
|
+
stop(): void;
|
|
112
|
+
private doConnect;
|
|
113
|
+
private onOpen;
|
|
114
|
+
private onMessage;
|
|
115
|
+
private handleCall;
|
|
116
|
+
private onClose;
|
|
117
|
+
/** Reject all pending adapter calls on disconnect (at-most-once semantics) */
|
|
118
|
+
private rejectAllPendingAdapterCalls;
|
|
119
|
+
private scheduleReconnect;
|
|
120
|
+
private startHeartbeat;
|
|
121
|
+
private clearTimers;
|
|
122
|
+
private send;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* GatewayTransport — ITransport implementation over WebSocket via GatewayClient.
|
|
127
|
+
*
|
|
128
|
+
* Used by createProxyPlatform() to proxy platform service calls
|
|
129
|
+
* (LLM, cache, vectorStore, etc.) from Workspace Agent back to Platform
|
|
130
|
+
* through the Gateway WS connection.
|
|
131
|
+
*
|
|
132
|
+
* Flow:
|
|
133
|
+
* plugin → ctx.llm.complete() → LLMProxy → RemoteAdapter.callRemote()
|
|
134
|
+
* → GatewayTransport.send(AdapterCall) → GatewayClient.sendAdapterCall()
|
|
135
|
+
* → WS adapter:call → Gateway → REST API → platform.llm.complete()
|
|
136
|
+
* → adapter:response → GatewayTransport resolves → LLMProxy returns
|
|
137
|
+
*
|
|
138
|
+
* @see ADR-0051: Bidirectional Gateway Protocol
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/** Adapter call structure (matches @kb-labs/core-platform/serializable AdapterCall) */
|
|
142
|
+
interface AdapterCall {
|
|
143
|
+
version?: string;
|
|
144
|
+
type: 'adapter:call';
|
|
145
|
+
requestId: string;
|
|
146
|
+
adapter: string;
|
|
147
|
+
method: string;
|
|
148
|
+
args: unknown[];
|
|
149
|
+
timeout?: number;
|
|
150
|
+
context?: {
|
|
151
|
+
traceId?: string;
|
|
152
|
+
spanId?: string;
|
|
153
|
+
pluginId?: string;
|
|
154
|
+
tenantId?: string;
|
|
155
|
+
executionId?: string;
|
|
156
|
+
[key: string]: unknown;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Adapter response structure (matches @kb-labs/core-platform/serializable AdapterResponse) */
|
|
160
|
+
interface AdapterResponse {
|
|
161
|
+
requestId: string;
|
|
162
|
+
result?: unknown;
|
|
163
|
+
error?: unknown;
|
|
164
|
+
}
|
|
165
|
+
/** ITransport interface (matches @kb-labs/core-runtime/transport) */
|
|
166
|
+
interface ITransport {
|
|
167
|
+
send(call: AdapterCall): Promise<AdapterResponse>;
|
|
168
|
+
close(): Promise<void>;
|
|
169
|
+
isClosed(): boolean;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Transport implementation that proxies adapter calls through Gateway WS.
|
|
173
|
+
*
|
|
174
|
+
* Thin wrapper — all pending/timeout logic lives in GatewayClient.sendAdapterCall().
|
|
175
|
+
*/
|
|
176
|
+
declare class GatewayTransport implements ITransport {
|
|
177
|
+
private readonly client;
|
|
178
|
+
private readonly defaultContext;
|
|
179
|
+
private closed;
|
|
180
|
+
constructor(client: GatewayClient, defaultContext: {
|
|
181
|
+
namespaceId: string;
|
|
182
|
+
hostId: string;
|
|
183
|
+
workspaceId?: string;
|
|
184
|
+
});
|
|
185
|
+
send(call: AdapterCall): Promise<AdapterResponse>;
|
|
186
|
+
close(): Promise<void>;
|
|
187
|
+
isClosed(): boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* IpcServer — accepts IPC requests from CLI/Studio via ILocalTransport.
|
|
192
|
+
* Transport is injected — caller picks unix socket, named pipe, or TCP.
|
|
193
|
+
*
|
|
194
|
+
* Execute requests are tunneled through GatewayClient:
|
|
195
|
+
* CLI → IPC → IpcServer → GatewayClient HTTP → Gateway → Server
|
|
196
|
+
*
|
|
197
|
+
* Cancel requests forwarded to Gateway:
|
|
198
|
+
* CLI → IPC → IpcServer → GatewayClient.cancelExecute() → Gateway REST
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
interface IpcServerOptions {
|
|
202
|
+
transport: ILocalTransport;
|
|
203
|
+
/** Returns current connection status */
|
|
204
|
+
getStatus: () => Omit<IpcStatusResponse, 'type'>;
|
|
205
|
+
/** GatewayClient for tunneling execute requests */
|
|
206
|
+
gatewayClient?: GatewayClient;
|
|
207
|
+
}
|
|
208
|
+
declare class IpcServer {
|
|
209
|
+
private readonly opts;
|
|
210
|
+
constructor(opts: IpcServerOptions);
|
|
211
|
+
start(): Promise<void>;
|
|
212
|
+
stop(): void;
|
|
213
|
+
private handleMessage;
|
|
214
|
+
private handleExecute;
|
|
215
|
+
private handleCancel;
|
|
216
|
+
private parseRequest;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* TokenManager — keeps accessToken fresh.
|
|
221
|
+
* Refreshes 5 minutes before expiry, notifies caller via onRefreshed callback.
|
|
222
|
+
* On repeated failures calls onRefreshFailed so the daemon can re-authenticate or exit.
|
|
223
|
+
*/
|
|
224
|
+
interface TokenPair {
|
|
225
|
+
accessToken: string;
|
|
226
|
+
refreshToken: string;
|
|
227
|
+
expiresIn: number;
|
|
228
|
+
}
|
|
229
|
+
interface TokenManagerOptions {
|
|
230
|
+
/** Fetch initial token pair using stored credentials */
|
|
231
|
+
fetchTokens: () => Promise<TokenPair>;
|
|
232
|
+
/** Rotate token pair using current refreshToken */
|
|
233
|
+
refreshTokens: (refreshToken: string) => Promise<TokenPair>;
|
|
234
|
+
/** Called when a new accessToken is available (e.g. WS reconnect) */
|
|
235
|
+
onRefreshed: (tokens: TokenPair) => void;
|
|
236
|
+
/** Called when all refresh retries are exhausted — daemon should re-authenticate or exit */
|
|
237
|
+
onRefreshFailed?: (error: Error) => void;
|
|
238
|
+
/** Seconds before expiry to trigger refresh (default: 5 * 60) */
|
|
239
|
+
refreshBeforeExpiry?: number;
|
|
240
|
+
/** Max consecutive refresh retry attempts before calling onRefreshFailed (default: 3) */
|
|
241
|
+
maxRefreshRetries?: number;
|
|
242
|
+
}
|
|
243
|
+
declare class TokenManager {
|
|
244
|
+
private readonly opts;
|
|
245
|
+
private tokens;
|
|
246
|
+
private tokenExpiresAt;
|
|
247
|
+
private timer;
|
|
248
|
+
private readonly refreshBefore;
|
|
249
|
+
private readonly maxRetries;
|
|
250
|
+
private retryCount;
|
|
251
|
+
constructor(opts: TokenManagerOptions);
|
|
252
|
+
start(): Promise<string>;
|
|
253
|
+
get accessToken(): string;
|
|
254
|
+
stop(): void;
|
|
255
|
+
private calcExpiresAt;
|
|
256
|
+
private scheduleRefresh;
|
|
257
|
+
private doRefresh;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { type AdapterCall, type AdapterCallInput, type AdapterCallResponse, type AdapterResponse, type CallHandler, GatewayClient, type GatewayClientOptions, GatewayTransport, type ITransport, IpcServer, type IpcServerOptions, TokenManager, type TokenManagerOptions, type TokenPair };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { IpcStatusRequestSchema, IpcExecuteRequestSchema, IpcCancelRequestSchema } from '@kb-labs/host-agent-contracts';
|
|
4
|
+
|
|
5
|
+
// src/ws/gateway-client.ts
|
|
6
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
7
|
+
var HELLO_TIMEOUT_MS = 5e3;
|
|
8
|
+
var BACKOFF_INITIAL_MS = 1e3;
|
|
9
|
+
var BACKOFF_MAX_MS = 6e4;
|
|
10
|
+
var DEFAULT_ADAPTER_CALL_TIMEOUT_MS = 3e4;
|
|
11
|
+
var GatewayClient = class {
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.opts = opts;
|
|
14
|
+
}
|
|
15
|
+
ws = null;
|
|
16
|
+
heartbeatTimer = null;
|
|
17
|
+
reconnectTimer = null;
|
|
18
|
+
backoffMs = BACKOFF_INITIAL_MS;
|
|
19
|
+
stopped = false;
|
|
20
|
+
hostId = null;
|
|
21
|
+
reconnectCount = 0;
|
|
22
|
+
disconnectedAt = null;
|
|
23
|
+
pendingCalls = /* @__PURE__ */ new Map();
|
|
24
|
+
handlers = /* @__PURE__ */ new Map();
|
|
25
|
+
/** Pending reverse adapter calls (Host → Gateway → Platform) */
|
|
26
|
+
pendingAdapterCalls = /* @__PURE__ */ new Map();
|
|
27
|
+
/** Register a handler for calls to a specific adapter */
|
|
28
|
+
registerHandler(adapter, handler) {
|
|
29
|
+
this.handlers.set(adapter, handler);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Send an adapter call to Platform via Gateway WS (reverse proxy).
|
|
33
|
+
* Used by GatewayTransport to proxy ctx.llm, ctx.cache, etc. back to Brain.
|
|
34
|
+
*
|
|
35
|
+
* Flow: Host → WS adapter:call → Gateway → HTTP → REST API → platform adapter → result
|
|
36
|
+
*/
|
|
37
|
+
sendAdapterCall(call) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
40
|
+
reject(new Error("WebSocket not connected"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const requestId = randomUUID();
|
|
44
|
+
const timeoutMs = call.timeout ?? DEFAULT_ADAPTER_CALL_TIMEOUT_MS;
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
this.pendingAdapterCalls.delete(requestId);
|
|
47
|
+
reject(new Error(`Adapter call timed out after ${timeoutMs}ms: ${call.adapter}.${call.method}`));
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
this.pendingAdapterCalls.set(requestId, { resolve, reject, timer });
|
|
50
|
+
this.send({
|
|
51
|
+
type: "adapter:call",
|
|
52
|
+
requestId,
|
|
53
|
+
adapter: call.adapter,
|
|
54
|
+
method: call.method,
|
|
55
|
+
args: call.args,
|
|
56
|
+
timeout: call.timeout,
|
|
57
|
+
context: call.context
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Tunnel an execute request to Gateway via HTTP REST API.
|
|
63
|
+
* CLI → IPC → Host Agent → HTTP POST /api/v1/execute → Gateway → Server.
|
|
64
|
+
* Gateway responds with ndjson stream of ExecutionEvent objects.
|
|
65
|
+
*
|
|
66
|
+
* We use HTTP (not WS) because the WS channel is for Gateway→Host calls,
|
|
67
|
+
* not Host→Gateway execute requests.
|
|
68
|
+
*/
|
|
69
|
+
executeTunnel(requestId, command, params, callbacks) {
|
|
70
|
+
const colonIdx = command.indexOf(":");
|
|
71
|
+
const pluginId = colonIdx > 0 ? command.slice(0, colonIdx) : command;
|
|
72
|
+
const handlerRef = colonIdx > 0 ? command.slice(colonIdx + 1) : command;
|
|
73
|
+
const body = {
|
|
74
|
+
pluginId,
|
|
75
|
+
handlerRef,
|
|
76
|
+
exportName: params?.exportName ?? handlerRef,
|
|
77
|
+
input: params?.input ?? {},
|
|
78
|
+
timeoutMs: params?.timeoutMs ?? void 0
|
|
79
|
+
};
|
|
80
|
+
const token = this.opts.getAccessToken();
|
|
81
|
+
const url = `${this.opts.gatewayUrl}/api/v1/execute`;
|
|
82
|
+
void this.doExecuteHttp(url, token, requestId, body, callbacks);
|
|
83
|
+
}
|
|
84
|
+
async doExecuteHttp(url, token, requestId, body, callbacks) {
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
"Authorization": `Bearer ${token}`
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(body)
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const text = await res.text().catch(() => "");
|
|
96
|
+
callbacks.onError(new Error(`Gateway HTTP ${res.status}: ${text}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!res.body) {
|
|
100
|
+
callbacks.onError(new Error("Gateway returned no response body"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const reader = res.body.getReader();
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
let buffer = "";
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read();
|
|
108
|
+
if (done) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
buffer += decoder.decode(value, { stream: true });
|
|
112
|
+
const lines = buffer.split("\n");
|
|
113
|
+
buffer = lines.pop() ?? "";
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (!trimmed) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const event = JSON.parse(trimmed);
|
|
121
|
+
callbacks.onEvent(event);
|
|
122
|
+
if (event.type === "execution:done") {
|
|
123
|
+
callbacks.onDone(event);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (buffer.trim()) {
|
|
130
|
+
try {
|
|
131
|
+
const event = JSON.parse(buffer.trim());
|
|
132
|
+
callbacks.onEvent(event);
|
|
133
|
+
if (event.type === "execution:done") {
|
|
134
|
+
callbacks.onDone(event);
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
callbacks.onError(err instanceof Error ? err : new Error(String(err)));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Cancel a pending execute via Gateway REST API */
|
|
144
|
+
cancelExecute(executionId, reason) {
|
|
145
|
+
const token = this.opts.getAccessToken();
|
|
146
|
+
const url = `${this.opts.gatewayUrl}/api/v1/execute/${executionId}/cancel`;
|
|
147
|
+
void fetch(url, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"Authorization": `Bearer ${token}`
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({ reason: reason ?? "user" })
|
|
154
|
+
}).catch(() => {
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async connect() {
|
|
158
|
+
this.stopped = false;
|
|
159
|
+
await this.doConnect();
|
|
160
|
+
}
|
|
161
|
+
stop() {
|
|
162
|
+
this.stopped = true;
|
|
163
|
+
this.clearTimers();
|
|
164
|
+
this.ws?.close(1e3, "agent stopped");
|
|
165
|
+
this.ws = null;
|
|
166
|
+
}
|
|
167
|
+
async doConnect() {
|
|
168
|
+
const token = this.opts.getAccessToken();
|
|
169
|
+
const wsUrl = this.opts.gatewayUrl.replace(/^http/, "ws") + "/hosts/connect";
|
|
170
|
+
const isSecureWs = wsUrl.startsWith("wss://");
|
|
171
|
+
const isLoopback = wsUrl.startsWith("ws://localhost") || wsUrl.startsWith("ws://127.0.0.1");
|
|
172
|
+
const isPrivateIp = /^ws:\/\/(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|host\.docker\.internal)/.test(wsUrl);
|
|
173
|
+
const isExplicitlyAllowed = process.env.GATEWAY_ALLOW_WS === "1";
|
|
174
|
+
if (!isSecureWs && !isLoopback && !isPrivateIp && !isExplicitlyAllowed) {
|
|
175
|
+
throw new Error(`GatewayClient: insecure WebSocket URL rejected \u2014 must use wss:// (got ${wsUrl})`);
|
|
176
|
+
}
|
|
177
|
+
this.ws = new WebSocket(wsUrl, {
|
|
178
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
179
|
+
});
|
|
180
|
+
this.ws.on("open", () => this.onOpen());
|
|
181
|
+
this.ws.on("message", (data) => this.onMessage(data.toString()));
|
|
182
|
+
this.ws.on("close", () => this.onClose());
|
|
183
|
+
this.ws.on("error", (err) => {
|
|
184
|
+
this.opts.logger?.warn("WS error", { error: err.message });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
onOpen() {
|
|
188
|
+
this.send({
|
|
189
|
+
type: "hello",
|
|
190
|
+
protocolVersion: "1.0",
|
|
191
|
+
agentVersion: this.opts.agentVersion,
|
|
192
|
+
hostId: this.hostId ?? void 0,
|
|
193
|
+
capabilities: this.opts.capabilities ?? [],
|
|
194
|
+
hostType: this.opts.hostType,
|
|
195
|
+
workspaces: this.opts.workspaces,
|
|
196
|
+
plugins: this.opts.plugins
|
|
197
|
+
});
|
|
198
|
+
const helloTimeout = setTimeout(() => {
|
|
199
|
+
this.ws?.close(1008, "hello timeout");
|
|
200
|
+
}, HELLO_TIMEOUT_MS);
|
|
201
|
+
this.ws.once("message", (data) => {
|
|
202
|
+
clearTimeout(helloTimeout);
|
|
203
|
+
let msg;
|
|
204
|
+
try {
|
|
205
|
+
msg = JSON.parse(data.toString());
|
|
206
|
+
} catch {
|
|
207
|
+
this.ws?.close(1008, "malformed handshake message");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (msg.type === "connected" && msg.hostId && typeof msg.hostId === "string") {
|
|
211
|
+
const reconnectMs = this.disconnectedAt ? Date.now() - this.disconnectedAt : 0;
|
|
212
|
+
this.hostId = msg.hostId;
|
|
213
|
+
this.backoffMs = BACKOFF_INITIAL_MS;
|
|
214
|
+
this.disconnectedAt = null;
|
|
215
|
+
if (this.reconnectCount > 0) {
|
|
216
|
+
this.opts.logger?.info("Reconnected", { hostId: msg.hostId, attempt: this.reconnectCount, reconnectMs });
|
|
217
|
+
}
|
|
218
|
+
this.reconnectCount = 0;
|
|
219
|
+
this.startHeartbeat();
|
|
220
|
+
this.opts.onConnected?.(msg.hostId, msg.sessionId ?? "");
|
|
221
|
+
} else {
|
|
222
|
+
this.ws?.close(1008, "unexpected message during handshake");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
onMessage(raw) {
|
|
227
|
+
let msg;
|
|
228
|
+
try {
|
|
229
|
+
msg = JSON.parse(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const type = msg["type"];
|
|
234
|
+
if (type === "call") {
|
|
235
|
+
void this.handleCall(msg);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (type === "adapter:response") {
|
|
239
|
+
const requestId = msg["requestId"];
|
|
240
|
+
const pending = this.pendingAdapterCalls.get(requestId);
|
|
241
|
+
if (pending) {
|
|
242
|
+
clearTimeout(pending.timer);
|
|
243
|
+
this.pendingAdapterCalls.delete(requestId);
|
|
244
|
+
pending.resolve({ requestId, result: msg["result"] });
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (type === "adapter:error") {
|
|
249
|
+
const requestId = msg["requestId"];
|
|
250
|
+
const pending = this.pendingAdapterCalls.get(requestId);
|
|
251
|
+
if (pending) {
|
|
252
|
+
clearTimeout(pending.timer);
|
|
253
|
+
this.pendingAdapterCalls.delete(requestId);
|
|
254
|
+
const error = msg["error"];
|
|
255
|
+
pending.resolve({ requestId, error });
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async handleCall(call) {
|
|
261
|
+
const handler = this.handlers.get(call.adapter);
|
|
262
|
+
if (!handler) {
|
|
263
|
+
this.send({
|
|
264
|
+
type: "error",
|
|
265
|
+
requestId: call.requestId,
|
|
266
|
+
error: { code: "UNKNOWN_ADAPTER", message: `No handler for adapter: ${call.adapter}`, retryable: false }
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const result = await handler(call);
|
|
272
|
+
this.send({ type: "chunk", requestId: call.requestId, data: result, index: 0 });
|
|
273
|
+
this.send({ type: "result", requestId: call.requestId, done: true });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
276
|
+
this.send({
|
|
277
|
+
type: "error",
|
|
278
|
+
requestId: call.requestId,
|
|
279
|
+
error: { code: "HANDLER_ERROR", message, retryable: false }
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
onClose() {
|
|
284
|
+
this.clearTimers();
|
|
285
|
+
this.disconnectedAt = this.disconnectedAt ?? Date.now();
|
|
286
|
+
this.rejectAllPendingAdapterCalls();
|
|
287
|
+
this.opts.onDisconnected?.();
|
|
288
|
+
if (!this.stopped) {
|
|
289
|
+
this.opts.logger?.warn("Disconnected, scheduling reconnect", { hostId: this.hostId, backoffMs: this.backoffMs });
|
|
290
|
+
this.scheduleReconnect();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** Reject all pending adapter calls on disconnect (at-most-once semantics) */
|
|
294
|
+
rejectAllPendingAdapterCalls() {
|
|
295
|
+
const error = new Error("WebSocket disconnected \u2014 all pending adapter calls rejected");
|
|
296
|
+
for (const [requestId, pending] of this.pendingAdapterCalls) {
|
|
297
|
+
clearTimeout(pending.timer);
|
|
298
|
+
pending.reject(error);
|
|
299
|
+
}
|
|
300
|
+
this.pendingAdapterCalls.clear();
|
|
301
|
+
}
|
|
302
|
+
scheduleReconnect() {
|
|
303
|
+
this.reconnectCount++;
|
|
304
|
+
this.opts.logger?.debug("Reconnect scheduled", { attempt: this.reconnectCount, backoffMs: this.backoffMs });
|
|
305
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
306
|
+
if (this.stopped) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await this.opts.onTokenExpired?.();
|
|
310
|
+
await this.doConnect();
|
|
311
|
+
this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
|
|
312
|
+
}, this.backoffMs);
|
|
313
|
+
}
|
|
314
|
+
startHeartbeat() {
|
|
315
|
+
this.heartbeatTimer = setInterval(() => {
|
|
316
|
+
this.send({ type: "heartbeat" });
|
|
317
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
318
|
+
}
|
|
319
|
+
clearTimers() {
|
|
320
|
+
if (this.heartbeatTimer) {
|
|
321
|
+
clearInterval(this.heartbeatTimer);
|
|
322
|
+
this.heartbeatTimer = null;
|
|
323
|
+
}
|
|
324
|
+
if (this.reconnectTimer) {
|
|
325
|
+
clearTimeout(this.reconnectTimer);
|
|
326
|
+
this.reconnectTimer = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
send(msg) {
|
|
330
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
331
|
+
this.ws.send(JSON.stringify(msg));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// src/transport/gateway-transport.ts
|
|
337
|
+
var GatewayTransport = class {
|
|
338
|
+
constructor(client, defaultContext) {
|
|
339
|
+
this.client = client;
|
|
340
|
+
this.defaultContext = defaultContext;
|
|
341
|
+
}
|
|
342
|
+
closed = false;
|
|
343
|
+
async send(call) {
|
|
344
|
+
if (this.closed) {
|
|
345
|
+
throw new Error("GatewayTransport is closed");
|
|
346
|
+
}
|
|
347
|
+
const response = await this.client.sendAdapterCall({
|
|
348
|
+
adapter: call.adapter,
|
|
349
|
+
method: call.method,
|
|
350
|
+
args: call.args,
|
|
351
|
+
timeout: call.timeout,
|
|
352
|
+
context: {
|
|
353
|
+
namespaceId: this.defaultContext.namespaceId,
|
|
354
|
+
hostId: this.defaultContext.hostId,
|
|
355
|
+
workspaceId: this.defaultContext.workspaceId,
|
|
356
|
+
executionRequestId: call.context?.executionId
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
if (response.error) {
|
|
360
|
+
return {
|
|
361
|
+
requestId: response.requestId,
|
|
362
|
+
error: response.error
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
requestId: response.requestId,
|
|
367
|
+
result: response.result
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async close() {
|
|
371
|
+
this.closed = true;
|
|
372
|
+
}
|
|
373
|
+
isClosed() {
|
|
374
|
+
return this.closed;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var IpcServer = class {
|
|
378
|
+
constructor(opts) {
|
|
379
|
+
this.opts = opts;
|
|
380
|
+
}
|
|
381
|
+
async start() {
|
|
382
|
+
this.opts.transport.onMessage((raw) => void this.handleMessage(raw));
|
|
383
|
+
await this.opts.transport.listen();
|
|
384
|
+
}
|
|
385
|
+
stop() {
|
|
386
|
+
this.opts.transport.close();
|
|
387
|
+
}
|
|
388
|
+
async handleMessage(raw) {
|
|
389
|
+
const req = this.parseRequest(raw);
|
|
390
|
+
if (!req) {
|
|
391
|
+
console.warn("[ipc] Invalid IPC request schema:", JSON.stringify(raw).slice(0, 200));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (req.type === "status") {
|
|
395
|
+
const status = this.opts.getStatus();
|
|
396
|
+
this.opts.transport.send({ type: "status", ...status });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (req.type === "cancel") {
|
|
400
|
+
this.handleCancel(req);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.handleExecute(req);
|
|
404
|
+
}
|
|
405
|
+
handleExecute(req) {
|
|
406
|
+
const { gatewayClient } = this.opts;
|
|
407
|
+
if (!gatewayClient) {
|
|
408
|
+
this.opts.transport.send({
|
|
409
|
+
type: "error",
|
|
410
|
+
requestId: req.requestId,
|
|
411
|
+
code: "NO_GATEWAY",
|
|
412
|
+
message: "GatewayClient not configured \u2014 cannot tunnel execute requests"
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
gatewayClient.executeTunnel(
|
|
417
|
+
req.requestId,
|
|
418
|
+
req.command,
|
|
419
|
+
req.params,
|
|
420
|
+
{
|
|
421
|
+
onEvent: (event) => {
|
|
422
|
+
this.opts.transport.send({
|
|
423
|
+
type: "event",
|
|
424
|
+
requestId: req.requestId,
|
|
425
|
+
data: event
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
onDone: (result) => {
|
|
429
|
+
this.opts.transport.send({
|
|
430
|
+
type: "done",
|
|
431
|
+
requestId: req.requestId,
|
|
432
|
+
result
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
onError: (error) => {
|
|
436
|
+
this.opts.transport.send({
|
|
437
|
+
type: "error",
|
|
438
|
+
requestId: req.requestId,
|
|
439
|
+
code: "TUNNEL_ERROR",
|
|
440
|
+
message: error.message
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
handleCancel(req) {
|
|
447
|
+
const { gatewayClient } = this.opts;
|
|
448
|
+
if (!gatewayClient) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
gatewayClient.cancelExecute(req.executionId, req.reason);
|
|
452
|
+
}
|
|
453
|
+
parseRequest(raw) {
|
|
454
|
+
if (typeof raw !== "object" || raw === null || !("type" in raw)) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const type = raw["type"];
|
|
458
|
+
if (type === "status") {
|
|
459
|
+
return IpcStatusRequestSchema.safeParse(raw).data ?? null;
|
|
460
|
+
}
|
|
461
|
+
if (type === "execute") {
|
|
462
|
+
return IpcExecuteRequestSchema.safeParse(raw).data ?? null;
|
|
463
|
+
}
|
|
464
|
+
if (type === "cancel") {
|
|
465
|
+
return IpcCancelRequestSchema.safeParse(raw).data ?? null;
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/token/token-manager.ts
|
|
472
|
+
var RETRY_DELAY_MS = 3e4;
|
|
473
|
+
var CLOCK_SKEW_MARGIN_S = 60;
|
|
474
|
+
var MAX_EXPIRES_IN_S = 86400 * 365;
|
|
475
|
+
var TokenManager = class {
|
|
476
|
+
constructor(opts) {
|
|
477
|
+
this.opts = opts;
|
|
478
|
+
this.refreshBefore = opts.refreshBeforeExpiry ?? 5 * 60;
|
|
479
|
+
this.maxRetries = opts.maxRefreshRetries ?? 3;
|
|
480
|
+
}
|
|
481
|
+
tokens = null;
|
|
482
|
+
tokenExpiresAt = 0;
|
|
483
|
+
// Unix ms
|
|
484
|
+
timer = null;
|
|
485
|
+
refreshBefore;
|
|
486
|
+
maxRetries;
|
|
487
|
+
retryCount = 0;
|
|
488
|
+
async start() {
|
|
489
|
+
this.tokens = await this.opts.fetchTokens();
|
|
490
|
+
this.tokenExpiresAt = this.calcExpiresAt(this.tokens.expiresIn);
|
|
491
|
+
this.scheduleRefresh(this.tokens);
|
|
492
|
+
return this.tokens.accessToken;
|
|
493
|
+
}
|
|
494
|
+
get accessToken() {
|
|
495
|
+
if (!this.tokens) {
|
|
496
|
+
throw new Error("TokenManager not started");
|
|
497
|
+
}
|
|
498
|
+
if (Date.now() >= this.tokenExpiresAt - CLOCK_SKEW_MARGIN_S * 1e3) {
|
|
499
|
+
throw new Error("accessToken has expired \u2014 refresh has not completed yet");
|
|
500
|
+
}
|
|
501
|
+
return this.tokens.accessToken;
|
|
502
|
+
}
|
|
503
|
+
stop() {
|
|
504
|
+
if (this.timer) {
|
|
505
|
+
clearTimeout(this.timer);
|
|
506
|
+
this.timer = null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
calcExpiresAt(expiresIn) {
|
|
510
|
+
if (expiresIn <= 0 || expiresIn > MAX_EXPIRES_IN_S) {
|
|
511
|
+
throw new Error(`Invalid expiresIn value: ${expiresIn}`);
|
|
512
|
+
}
|
|
513
|
+
return Date.now() + expiresIn * 1e3;
|
|
514
|
+
}
|
|
515
|
+
scheduleRefresh(tokens) {
|
|
516
|
+
if (this.timer) {
|
|
517
|
+
clearTimeout(this.timer);
|
|
518
|
+
}
|
|
519
|
+
const delaySec = tokens.expiresIn - this.refreshBefore;
|
|
520
|
+
if (delaySec <= 0) {
|
|
521
|
+
console.warn("[token-manager] Token expires sooner than refreshBeforeExpiry, refreshing immediately");
|
|
522
|
+
this.timer = setTimeout(() => void this.doRefresh(), 0);
|
|
523
|
+
} else {
|
|
524
|
+
this.timer = setTimeout(() => void this.doRefresh(), delaySec * 1e3);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async doRefresh() {
|
|
528
|
+
if (!this.tokens) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const next = await this.opts.refreshTokens(this.tokens.refreshToken);
|
|
533
|
+
this.tokens = next;
|
|
534
|
+
this.tokenExpiresAt = this.calcExpiresAt(next.expiresIn);
|
|
535
|
+
this.retryCount = 0;
|
|
536
|
+
this.scheduleRefresh(next);
|
|
537
|
+
this.opts.onRefreshed(next);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
this.retryCount++;
|
|
540
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
541
|
+
console.error(`[token-manager] Refresh failed (attempt ${this.retryCount}/${this.maxRetries}):`, error.message);
|
|
542
|
+
if (Date.now() >= this.tokenExpiresAt - CLOCK_SKEW_MARGIN_S * 1e3) {
|
|
543
|
+
console.error("[token-manager] Token expired before refresh succeeded, notifying caller");
|
|
544
|
+
this.opts.onRefreshFailed?.(error);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (this.retryCount >= this.maxRetries) {
|
|
548
|
+
console.error("[token-manager] Max refresh retries exceeded, notifying caller");
|
|
549
|
+
this.opts.onRefreshFailed?.(error);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const delayMs = RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1);
|
|
553
|
+
this.timer = setTimeout(() => void this.doRefresh(), delayMs);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
export { GatewayClient, GatewayTransport, IpcServer, TokenManager };
|
|
559
|
+
//# sourceMappingURL=index.js.map
|
|
560
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ws/gateway-client.ts","../src/transport/gateway-transport.ts","../src/ipc/ipc-server.ts","../src/token/token-manager.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * GatewayClient — WebSocket connection to Gateway with:\n * - JWT Bearer auth\n * - hello/connected handshake\n * - heartbeat every 30s\n * - exponential backoff reconnect (1s → 2s → 4s … max 60s)\n * - dispatches incoming `call` messages to registered capability handlers\n */\n\nimport { randomUUID } from 'node:crypto';\nimport WebSocket from 'ws';\nimport type { CapabilityCall } from '@kb-labs/host-agent-contracts';\nimport type { AdapterCallContext } from '@kb-labs/gateway-contracts';\n\nexport type CallHandler = (call: CapabilityCall) => Promise<unknown>;\n\n/** Adapter call input — used by GatewayTransport */\nexport interface AdapterCallInput {\n adapter: string;\n method: string;\n args: unknown[];\n timeout?: number;\n context: AdapterCallContext;\n}\n\n/** Adapter call response — returned by sendAdapterCall */\nexport interface AdapterCallResponse {\n requestId: string;\n result?: unknown;\n error?: { code: string; message: string; retryable: boolean; details?: unknown };\n}\n\nexport interface GatewayClientOptions {\n /** wss://gateway.example.com */\n gatewayUrl: string;\n agentVersion: string;\n /** Returns current valid access token */\n getAccessToken: () => string;\n /** Called when connection is established and handshake complete */\n onConnected?: (hostId: string, sessionId: string) => void;\n /** Called when disconnected */\n onDisconnected?: () => void;\n /** Called when token needs refresh (triggered before reconnect) */\n onTokenExpired?: () => Promise<void>;\n /** Capabilities this host provides — sent in hello message so Gateway can route by capability */\n capabilities?: string[];\n /** Host type for workspace agent routing */\n hostType?: 'local' | 'cloud';\n /** Workspace info advertised on connect */\n workspaces?: Array<{ workspaceId: string; repoFingerprint?: string; branch?: string }>;\n /** Plugin inventory advertised on connect */\n plugins?: Array<{ id: string; version: string }>;\n /** Optional logger for connection diagnostics */\n logger?: {\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n debug(msg: string, meta?: Record<string, unknown>): void;\n };\n}\n\nconst HEARTBEAT_INTERVAL_MS = 30_000;\nconst HELLO_TIMEOUT_MS = 5_000;\nconst BACKOFF_INITIAL_MS = 1_000;\nconst BACKOFF_MAX_MS = 60_000;\nconst DEFAULT_ADAPTER_CALL_TIMEOUT_MS = 30_000;\n\n/** Pending execute tunnel — callbacks for streaming events from Gateway */\ninterface PendingExecute {\n onEvent: (event: unknown) => void;\n onDone: (result: unknown) => void;\n onError: (error: Error) => void;\n}\n\nexport class GatewayClient {\n private ws: WebSocket | null = null;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private backoffMs = BACKOFF_INITIAL_MS;\n private stopped = false;\n private hostId: string | null = null;\n private reconnectCount = 0;\n private disconnectedAt: number | null = null;\n private pendingCalls = new Map<string, (result: unknown) => void>();\n private handlers = new Map<string, CallHandler>();\n /** Pending reverse adapter calls (Host → Gateway → Platform) */\n private pendingAdapterCalls = new Map<string, {\n resolve: (response: AdapterCallResponse) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n }>();\n\n constructor(private readonly opts: GatewayClientOptions) {}\n\n /** Register a handler for calls to a specific adapter */\n registerHandler(adapter: string, handler: CallHandler): void {\n this.handlers.set(adapter, handler);\n }\n\n /**\n * Send an adapter call to Platform via Gateway WS (reverse proxy).\n * Used by GatewayTransport to proxy ctx.llm, ctx.cache, etc. back to Brain.\n *\n * Flow: Host → WS adapter:call → Gateway → HTTP → REST API → platform adapter → result\n */\n sendAdapterCall(call: AdapterCallInput): Promise<AdapterCallResponse> {\n return new Promise<AdapterCallResponse>((resolve, reject) => {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n reject(new Error('WebSocket not connected'));\n return;\n }\n\n const requestId = randomUUID();\n const timeoutMs = call.timeout ?? DEFAULT_ADAPTER_CALL_TIMEOUT_MS;\n\n const timer = setTimeout(() => {\n this.pendingAdapterCalls.delete(requestId);\n reject(new Error(`Adapter call timed out after ${timeoutMs}ms: ${call.adapter}.${call.method}`));\n }, timeoutMs);\n\n this.pendingAdapterCalls.set(requestId, { resolve, reject, timer });\n\n this.send({\n type: 'adapter:call',\n requestId,\n adapter: call.adapter,\n method: call.method,\n args: call.args,\n timeout: call.timeout,\n context: call.context,\n });\n });\n }\n\n /**\n * Tunnel an execute request to Gateway via HTTP REST API.\n * CLI → IPC → Host Agent → HTTP POST /api/v1/execute → Gateway → Server.\n * Gateway responds with ndjson stream of ExecutionEvent objects.\n *\n * We use HTTP (not WS) because the WS channel is for Gateway→Host calls,\n * not Host→Gateway execute requests.\n */\n executeTunnel(\n requestId: string,\n command: string,\n params: Record<string, unknown> | undefined,\n callbacks: PendingExecute,\n ): void {\n // Parse command: \"pluginId:handlerRef\"\n const colonIdx = command.indexOf(':');\n const pluginId = colonIdx > 0 ? command.slice(0, colonIdx) : command;\n const handlerRef = colonIdx > 0 ? command.slice(colonIdx + 1) : command;\n\n const body = {\n pluginId,\n handlerRef,\n exportName: (params?.exportName as string) ?? handlerRef,\n input: params?.input ?? {},\n timeoutMs: (params?.timeoutMs as number) ?? undefined,\n };\n\n const token = this.opts.getAccessToken();\n const url = `${this.opts.gatewayUrl}/api/v1/execute`;\n\n // Fire-and-forget async fetch with ndjson streaming\n void this.doExecuteHttp(url, token, requestId, body, callbacks);\n }\n\n private async doExecuteHttp(\n url: string,\n token: string,\n requestId: string,\n body: Record<string, unknown>,\n callbacks: PendingExecute,\n ): Promise<void> {\n try {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${token}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n callbacks.onError(new Error(`Gateway HTTP ${res.status}: ${text}`));\n return;\n }\n\n if (!res.body) {\n callbacks.onError(new Error('Gateway returned no response body'));\n return;\n }\n\n // Read ndjson stream\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {break;}\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) {continue;}\n\n try {\n const event = JSON.parse(trimmed) as Record<string, unknown>;\n callbacks.onEvent(event);\n\n if (event.type === 'execution:done') {\n callbacks.onDone(event);\n }\n } catch {\n // Ignore malformed lines\n }\n }\n }\n\n // Process remaining buffer\n if (buffer.trim()) {\n try {\n const event = JSON.parse(buffer.trim()) as Record<string, unknown>;\n callbacks.onEvent(event);\n if (event.type === 'execution:done') {\n callbacks.onDone(event);\n }\n } catch {\n // Ignore\n }\n }\n } catch (err) {\n callbacks.onError(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n /** Cancel a pending execute via Gateway REST API */\n cancelExecute(executionId: string, reason?: string): void {\n const token = this.opts.getAccessToken();\n const url = `${this.opts.gatewayUrl}/api/v1/execute/${executionId}/cancel`;\n\n void fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${token}`,\n },\n body: JSON.stringify({ reason: reason ?? 'user' }),\n }).catch(() => {\n // Best-effort cancel — ignore network errors\n });\n }\n\n async connect(): Promise<void> {\n this.stopped = false;\n await this.doConnect();\n }\n\n stop(): void {\n this.stopped = true;\n this.clearTimers();\n this.ws?.close(1000, 'agent stopped');\n this.ws = null;\n }\n\n private async doConnect(): Promise<void> {\n const token = this.opts.getAccessToken();\n const wsUrl = this.opts.gatewayUrl.replace(/^http/, 'ws') + '/hosts/connect';\n\n // Security model:\n // wss:// — always allowed (encrypted, public or private)\n // ws:// — allowed only on trusted networks:\n // • loopback (localhost, 127.0.0.1)\n // • RFC-1918 private IP ranges (10.x, 172.16-31.x, 192.168.x)\n // • host.docker.internal (macOS/Windows Docker Desktop bridge)\n // • GATEWAY_ALLOW_WS=1 — explicit opt-in for custom setups\n // (workflow-daemon sets this when spawning runtime containers on a\n // trusted Docker bridge network where the service name is not an IP)\n const isSecureWs = wsUrl.startsWith('wss://');\n const isLoopback = wsUrl.startsWith('ws://localhost') || wsUrl.startsWith('ws://127.0.0.1');\n const isPrivateIp = /^ws:\\/\\/(10\\.|172\\.(1[6-9]|2\\d|3[01])\\.|192\\.168\\.|host\\.docker\\.internal)/.test(wsUrl);\n const isExplicitlyAllowed = process.env.GATEWAY_ALLOW_WS === '1';\n if (!isSecureWs && !isLoopback && !isPrivateIp && !isExplicitlyAllowed) {\n throw new Error(`GatewayClient: insecure WebSocket URL rejected — must use wss:// (got ${wsUrl})`);\n }\n\n this.ws = new WebSocket(wsUrl, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n this.ws.on('open', () => this.onOpen());\n this.ws.on('message', (data) => this.onMessage(data.toString()));\n this.ws.on('close', () => this.onClose());\n this.ws.on('error', (err) => { this.opts.logger?.warn('WS error', { error: err.message }); });\n }\n\n private onOpen(): void {\n this.send({\n type: 'hello',\n protocolVersion: '1.0',\n agentVersion: this.opts.agentVersion,\n hostId: this.hostId ?? undefined,\n capabilities: this.opts.capabilities ?? [],\n hostType: this.opts.hostType,\n workspaces: this.opts.workspaces,\n plugins: this.opts.plugins,\n });\n\n // Timeout if no 'connected' reply\n const helloTimeout = setTimeout(() => {\n this.ws?.close(1008, 'hello timeout');\n }, HELLO_TIMEOUT_MS);\n\n this.ws!.once('message', (data) => {\n clearTimeout(helloTimeout);\n let msg: { type: string; hostId?: string; sessionId?: string };\n try {\n msg = JSON.parse(data.toString()) as { type: string; hostId?: string; sessionId?: string };\n } catch {\n this.ws?.close(1008, 'malformed handshake message');\n return;\n }\n if (msg.type === 'connected' && msg.hostId && typeof msg.hostId === 'string') {\n const reconnectMs = this.disconnectedAt ? Date.now() - this.disconnectedAt : 0;\n this.hostId = msg.hostId;\n this.backoffMs = BACKOFF_INITIAL_MS;\n this.disconnectedAt = null;\n if (this.reconnectCount > 0) {\n this.opts.logger?.info('Reconnected', { hostId: msg.hostId, attempt: this.reconnectCount, reconnectMs });\n }\n this.reconnectCount = 0;\n this.startHeartbeat();\n this.opts.onConnected?.(msg.hostId, msg.sessionId ?? '');\n } else {\n this.ws?.close(1008, 'unexpected message during handshake');\n }\n });\n }\n\n private onMessage(raw: string): void {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return;\n }\n\n const type = msg['type'] as string;\n\n if (type === 'call') {\n void this.handleCall(msg as unknown as CapabilityCall);\n return;\n }\n\n // Adapter reverse proxy responses (from Gateway/Platform back to us)\n if (type === 'adapter:response') {\n const requestId = msg['requestId'] as string;\n const pending = this.pendingAdapterCalls.get(requestId);\n if (pending) {\n clearTimeout(pending.timer);\n this.pendingAdapterCalls.delete(requestId);\n pending.resolve({ requestId, result: msg['result'] });\n }\n return;\n }\n\n if (type === 'adapter:error') {\n const requestId = msg['requestId'] as string;\n const pending = this.pendingAdapterCalls.get(requestId);\n if (pending) {\n clearTimeout(pending.timer);\n this.pendingAdapterCalls.delete(requestId);\n const error = msg['error'] as { code: string; message: string; retryable: boolean; details?: unknown };\n pending.resolve({ requestId, error });\n }\n return;\n }\n\n // heartbeat ack — no action needed\n }\n\n private async handleCall(call: CapabilityCall): Promise<void> {\n const handler = this.handlers.get(call.adapter);\n if (!handler) {\n this.send({\n type: 'error',\n requestId: call.requestId,\n error: { code: 'UNKNOWN_ADAPTER', message: `No handler for adapter: ${call.adapter}`, retryable: false },\n });\n return;\n }\n\n try {\n const result = await handler(call);\n this.send({ type: 'chunk', requestId: call.requestId, data: result, index: 0 });\n this.send({ type: 'result', requestId: call.requestId, done: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.send({\n type: 'error',\n requestId: call.requestId,\n error: { code: 'HANDLER_ERROR', message, retryable: false },\n });\n }\n }\n\n private onClose(): void {\n this.clearTimers();\n this.disconnectedAt = this.disconnectedAt ?? Date.now();\n this.rejectAllPendingAdapterCalls();\n this.opts.onDisconnected?.();\n if (!this.stopped) {\n this.opts.logger?.warn('Disconnected, scheduling reconnect', { hostId: this.hostId, backoffMs: this.backoffMs });\n this.scheduleReconnect();\n }\n }\n\n /** Reject all pending adapter calls on disconnect (at-most-once semantics) */\n private rejectAllPendingAdapterCalls(): void {\n const error = new Error('WebSocket disconnected — all pending adapter calls rejected');\n for (const [requestId, pending] of this.pendingAdapterCalls) {\n clearTimeout(pending.timer);\n pending.reject(error);\n }\n this.pendingAdapterCalls.clear();\n }\n\n private scheduleReconnect(): void {\n this.reconnectCount++;\n this.opts.logger?.debug('Reconnect scheduled', { attempt: this.reconnectCount, backoffMs: this.backoffMs });\n this.reconnectTimer = setTimeout(async () => {\n if (this.stopped) {return;}\n await this.opts.onTokenExpired?.();\n await this.doConnect();\n this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);\n }, this.backoffMs);\n }\n\n private startHeartbeat(): void {\n this.heartbeatTimer = setInterval(() => {\n this.send({ type: 'heartbeat' });\n }, HEARTBEAT_INTERVAL_MS);\n }\n\n private clearTimers(): void {\n if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }\n if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }\n }\n\n private send(msg: unknown): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n}\n","/**\n * GatewayTransport — ITransport implementation over WebSocket via GatewayClient.\n *\n * Used by createProxyPlatform() to proxy platform service calls\n * (LLM, cache, vectorStore, etc.) from Workspace Agent back to Platform\n * through the Gateway WS connection.\n *\n * Flow:\n * plugin → ctx.llm.complete() → LLMProxy → RemoteAdapter.callRemote()\n * → GatewayTransport.send(AdapterCall) → GatewayClient.sendAdapterCall()\n * → WS adapter:call → Gateway → REST API → platform.llm.complete()\n * → adapter:response → GatewayTransport resolves → LLMProxy returns\n *\n * @see ADR-0051: Bidirectional Gateway Protocol\n */\n\nimport type { GatewayClient, AdapterCallResponse } from '../ws/gateway-client.js';\n\n/** Adapter call structure (matches @kb-labs/core-platform/serializable AdapterCall) */\nexport interface AdapterCall {\n version?: string;\n type: 'adapter:call';\n requestId: string;\n adapter: string;\n method: string;\n args: unknown[];\n timeout?: number;\n context?: {\n traceId?: string;\n spanId?: string;\n pluginId?: string;\n tenantId?: string;\n executionId?: string;\n [key: string]: unknown;\n };\n}\n\n/** Adapter response structure (matches @kb-labs/core-platform/serializable AdapterResponse) */\nexport interface AdapterResponse {\n requestId: string;\n result?: unknown;\n error?: unknown;\n}\n\n/** ITransport interface (matches @kb-labs/core-runtime/transport) */\nexport interface ITransport {\n send(call: AdapterCall): Promise<AdapterResponse>;\n close(): Promise<void>;\n isClosed(): boolean;\n}\n\n/**\n * Transport implementation that proxies adapter calls through Gateway WS.\n *\n * Thin wrapper — all pending/timeout logic lives in GatewayClient.sendAdapterCall().\n */\nexport class GatewayTransport implements ITransport {\n private closed = false;\n\n constructor(\n private readonly client: GatewayClient,\n private readonly defaultContext: {\n namespaceId: string;\n hostId: string;\n workspaceId?: string;\n },\n ) {}\n\n async send(call: AdapterCall): Promise<AdapterResponse> {\n if (this.closed) {\n throw new Error('GatewayTransport is closed');\n }\n\n const response: AdapterCallResponse = await this.client.sendAdapterCall({\n adapter: call.adapter,\n method: call.method,\n args: call.args,\n timeout: call.timeout,\n context: {\n namespaceId: this.defaultContext.namespaceId,\n hostId: this.defaultContext.hostId,\n workspaceId: this.defaultContext.workspaceId,\n executionRequestId: call.context?.executionId,\n },\n });\n\n // Map GatewayClient response → ITransport AdapterResponse\n if (response.error) {\n return {\n requestId: response.requestId,\n error: response.error,\n };\n }\n\n return {\n requestId: response.requestId,\n result: response.result,\n };\n }\n\n async close(): Promise<void> {\n this.closed = true;\n }\n\n isClosed(): boolean {\n return this.closed;\n }\n}\n","/**\n * IpcServer — accepts IPC requests from CLI/Studio via ILocalTransport.\n * Transport is injected — caller picks unix socket, named pipe, or TCP.\n *\n * Execute requests are tunneled through GatewayClient:\n * CLI → IPC → IpcServer → GatewayClient HTTP → Gateway → Server\n *\n * Cancel requests forwarded to Gateway:\n * CLI → IPC → IpcServer → GatewayClient.cancelExecute() → Gateway REST\n */\n\nimport {\n IpcExecuteRequestSchema,\n IpcCancelRequestSchema,\n IpcStatusRequestSchema,\n} from '@kb-labs/host-agent-contracts';\nimport type { IpcRequest, IpcExecuteRequest, IpcCancelRequest, IpcStatusResponse } from '@kb-labs/host-agent-contracts';\nimport type { ILocalTransport } from '@kb-labs/host-agent-transport';\nimport type { GatewayClient } from '../ws/gateway-client.js';\n\nexport interface IpcServerOptions {\n transport: ILocalTransport;\n /** Returns current connection status */\n getStatus: () => Omit<IpcStatusResponse, 'type'>;\n /** GatewayClient for tunneling execute requests */\n gatewayClient?: GatewayClient;\n}\n\nexport class IpcServer {\n constructor(private readonly opts: IpcServerOptions) {}\n\n async start(): Promise<void> {\n this.opts.transport.onMessage((raw) => void this.handleMessage(raw));\n await this.opts.transport.listen();\n }\n\n stop(): void {\n this.opts.transport.close();\n }\n\n private async handleMessage(raw: unknown): Promise<void> {\n const req = this.parseRequest(raw);\n if (!req) {\n console.warn('[ipc] Invalid IPC request schema:', JSON.stringify(raw).slice(0, 200));\n return;\n }\n\n if (req.type === 'status') {\n const status = this.opts.getStatus();\n this.opts.transport.send({ type: 'status', ...status });\n return;\n }\n\n if (req.type === 'cancel') {\n this.handleCancel(req);\n return;\n }\n\n // execute — tunnel through GatewayClient\n this.handleExecute(req);\n }\n\n private handleExecute(req: IpcExecuteRequest): void {\n const { gatewayClient } = this.opts;\n if (!gatewayClient) {\n this.opts.transport.send({\n type: 'error',\n requestId: req.requestId,\n code: 'NO_GATEWAY',\n message: 'GatewayClient not configured — cannot tunnel execute requests',\n });\n return;\n }\n\n gatewayClient.executeTunnel(\n req.requestId,\n req.command,\n req.params,\n {\n onEvent: (event) => {\n // Forward execution events to CLI via IPC\n this.opts.transport.send({\n type: 'event',\n requestId: req.requestId,\n data: event,\n });\n },\n onDone: (result) => {\n this.opts.transport.send({\n type: 'done',\n requestId: req.requestId,\n result,\n });\n },\n onError: (error) => {\n this.opts.transport.send({\n type: 'error',\n requestId: req.requestId,\n code: 'TUNNEL_ERROR',\n message: error.message,\n });\n },\n },\n );\n }\n\n private handleCancel(req: IpcCancelRequest): void {\n const { gatewayClient } = this.opts;\n if (!gatewayClient) {\n return; // Best-effort — nothing to cancel if no gateway\n }\n\n gatewayClient.cancelExecute(req.executionId, req.reason);\n }\n\n private parseRequest(raw: unknown): IpcRequest | null {\n if (typeof raw !== 'object' || raw === null || !('type' in raw)) { return null; }\n const type = (raw as Record<string, unknown>)['type'];\n if (type === 'status') { return IpcStatusRequestSchema.safeParse(raw).data ?? null; }\n if (type === 'execute') { return IpcExecuteRequestSchema.safeParse(raw).data ?? null; }\n if (type === 'cancel') { return IpcCancelRequestSchema.safeParse(raw).data ?? null; }\n return null;\n }\n}\n","/**\n * TokenManager — keeps accessToken fresh.\n * Refreshes 5 minutes before expiry, notifies caller via onRefreshed callback.\n * On repeated failures calls onRefreshFailed so the daemon can re-authenticate or exit.\n */\n\nexport interface TokenPair {\n accessToken: string;\n refreshToken: string;\n expiresIn: number; // seconds\n}\n\nexport interface TokenManagerOptions {\n /** Fetch initial token pair using stored credentials */\n fetchTokens: () => Promise<TokenPair>;\n /** Rotate token pair using current refreshToken */\n refreshTokens: (refreshToken: string) => Promise<TokenPair>;\n /** Called when a new accessToken is available (e.g. WS reconnect) */\n onRefreshed: (tokens: TokenPair) => void;\n /** Called when all refresh retries are exhausted — daemon should re-authenticate or exit */\n onRefreshFailed?: (error: Error) => void;\n /** Seconds before expiry to trigger refresh (default: 5 * 60) */\n refreshBeforeExpiry?: number;\n /** Max consecutive refresh retry attempts before calling onRefreshFailed (default: 3) */\n maxRefreshRetries?: number;\n}\n\nconst RETRY_DELAY_MS = 30_000;\nconst CLOCK_SKEW_MARGIN_S = 60; // treat token as expired this many seconds before server expiry\nconst MAX_EXPIRES_IN_S = 86400 * 365; // sanity cap: 1 year\n\nexport class TokenManager {\n private tokens: TokenPair | null = null;\n private tokenExpiresAt = 0; // Unix ms\n private timer: ReturnType<typeof setTimeout> | null = null;\n private readonly refreshBefore: number;\n private readonly maxRetries: number;\n private retryCount = 0;\n\n constructor(private readonly opts: TokenManagerOptions) {\n this.refreshBefore = opts.refreshBeforeExpiry ?? 5 * 60;\n this.maxRetries = opts.maxRefreshRetries ?? 3;\n }\n\n async start(): Promise<string> {\n this.tokens = await this.opts.fetchTokens();\n this.tokenExpiresAt = this.calcExpiresAt(this.tokens.expiresIn);\n this.scheduleRefresh(this.tokens);\n return this.tokens.accessToken;\n }\n\n get accessToken(): string {\n if (!this.tokens) { throw new Error('TokenManager not started'); }\n if (Date.now() >= this.tokenExpiresAt - CLOCK_SKEW_MARGIN_S * 1000) {\n throw new Error('accessToken has expired — refresh has not completed yet');\n }\n return this.tokens.accessToken;\n }\n\n stop(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private calcExpiresAt(expiresIn: number): number {\n if (expiresIn <= 0 || expiresIn > MAX_EXPIRES_IN_S) {\n throw new Error(`Invalid expiresIn value: ${expiresIn}`);\n }\n // Track full server lifetime. CLOCK_SKEW_MARGIN_S is applied in the\n // accessToken getter and retry guard, not here — this keeps tokenExpiresAt\n // always later than the proactive refresh schedule time.\n return Date.now() + expiresIn * 1000;\n }\n\n private scheduleRefresh(tokens: TokenPair): void {\n if (this.timer) { clearTimeout(this.timer); }\n const delaySec = tokens.expiresIn - this.refreshBefore;\n if (delaySec <= 0) {\n // Token already near expiry — refresh immediately\n console.warn('[token-manager] Token expires sooner than refreshBeforeExpiry, refreshing immediately');\n this.timer = setTimeout(() => void this.doRefresh(), 0);\n } else {\n this.timer = setTimeout(() => void this.doRefresh(), delaySec * 1000);\n }\n }\n\n private async doRefresh(): Promise<void> {\n if (!this.tokens) { return; }\n try {\n const next = await this.opts.refreshTokens(this.tokens.refreshToken);\n this.tokens = next;\n this.tokenExpiresAt = this.calcExpiresAt(next.expiresIn);\n this.retryCount = 0;\n this.scheduleRefresh(next);\n this.opts.onRefreshed(next);\n } catch (err) {\n this.retryCount++;\n const error = err instanceof Error ? err : new Error(String(err));\n console.error(`[token-manager] Refresh failed (attempt ${this.retryCount}/${this.maxRetries}):`, error.message);\n\n // If the token has already expired (accounting for clock skew), retrying won't help\n if (Date.now() >= this.tokenExpiresAt - CLOCK_SKEW_MARGIN_S * 1000) {\n console.error('[token-manager] Token expired before refresh succeeded, notifying caller');\n this.opts.onRefreshFailed?.(error);\n return;\n }\n\n if (this.retryCount >= this.maxRetries) {\n console.error('[token-manager] Max refresh retries exceeded, notifying caller');\n this.opts.onRefreshFailed?.(error);\n return;\n }\n\n // Exponential backoff: 30s, 60s, 120s\n const delayMs = RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1);\n this.timer = setTimeout(() => void this.doRefresh(), delayMs);\n }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/host-agent-core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rimraf dist",
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"type-check": "tsc --noEmit",
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"lint:fix": "eslint . --fix",
|
|
25
|
+
"test": "vitest run -c ../../vitest.config.ts",
|
|
26
|
+
"test:watch": "vitest -c ../../vitest.config.ts"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@kb-labs/gateway-contracts": "^0.2.0",
|
|
30
|
+
"@kb-labs/host-agent-contracts": "^0.2.0",
|
|
31
|
+
"@kb-labs/host-agent-transport": "^0.2.0",
|
|
32
|
+
"ws": "^8.18.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@kb-labs/devkit": "link:../../../kb-labs-devkit",
|
|
36
|
+
"@types/ws": "^8.5.13",
|
|
37
|
+
"rimraf": "^6",
|
|
38
|
+
"tsup": "^8.5.0",
|
|
39
|
+
"vitest": "^3.2.4"
|
|
40
|
+
}
|
|
41
|
+
}
|