@linzumi/cli 0.0.20-beta → 0.0.22-beta
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 +65 -62
- package/bin/linzumi.js +10 -18
- package/dist/assets/linzumi-logo.svg +1 -0
- package/dist/index.js +9135 -0
- package/package.json +9 -4
- package/src/agentBootstrap.ts +0 -872
- package/src/authCache.ts +0 -157
- package/src/authResolution.ts +0 -77
- package/src/boundedCache.ts +0 -57
- package/src/channelSession.ts +0 -4301
- package/src/channelSessionSupport.ts +0 -308
- package/src/codexAppServer.ts +0 -380
- package/src/codexOutput.ts +0 -846
- package/src/codexRuntimeOptions.ts +0 -80
- package/src/dependencyStatus.ts +0 -198
- package/src/forwardTunnel.ts +0 -859
- package/src/forwardTunnelProtocol.ts +0 -324
- package/src/index.ts +0 -1080
- package/src/json.ts +0 -49
- package/src/kandanQueue.ts +0 -113
- package/src/kandanTls.ts +0 -86
- package/src/localCapabilities.ts +0 -143
- package/src/localCodexMessageState.ts +0 -135
- package/src/localCodexTurnState.ts +0 -108
- package/src/localConfig.ts +0 -99
- package/src/localEditor.ts +0 -1061
- package/src/localEditorRuntime.ts +0 -717
- package/src/localForwarding.ts +0 -523
- package/src/oauth.ts +0 -425
- package/src/pendingKandanMessageQueue.ts +0 -109
- package/src/phoenix.ts +0 -359
- package/src/portForwardApproval.ts +0 -181
- package/src/portForwardWatcher.ts +0 -404
- package/src/protocol.ts +0 -321
- package/src/runner.ts +0 -943
- package/src/runnerConsoleReporter.ts +0 -142
- package/src/runnerLogger.ts +0 -50
- package/src/streamDeltaCoalescing.ts +0 -129
- package/src/streamDeltaQueue.ts +0 -102
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-04-26
|
|
3
|
-
Spec: plans/2026-04-26-local-runner-port-forward-approval.md
|
|
4
|
-
Relationship: Detects descendant listener ports for the local Codex runner so
|
|
5
|
-
Kandan can prompt the authorized listener user before exposing a local
|
|
6
|
-
service through the runner forward route.
|
|
7
|
-
*/
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
-
import { basename } from "node:path";
|
|
10
|
-
|
|
11
|
-
export type ProcessRow = {
|
|
12
|
-
readonly pid: number;
|
|
13
|
-
readonly ppid: number;
|
|
14
|
-
readonly command: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type ListenSocketRow = {
|
|
18
|
-
readonly pid: number;
|
|
19
|
-
readonly port: number;
|
|
20
|
-
readonly command: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type PortForwardCandidate = {
|
|
24
|
-
readonly port: number;
|
|
25
|
-
readonly pid: number;
|
|
26
|
-
readonly command: string;
|
|
27
|
-
readonly cwd?: string | undefined;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type PortForwardWatcher = {
|
|
31
|
-
readonly close: () => void;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type PortForwardWatcherOptions = {
|
|
35
|
-
readonly rootPid?: number | undefined;
|
|
36
|
-
readonly intervalMs?: number | undefined;
|
|
37
|
-
readonly debounceMs?: number | undefined;
|
|
38
|
-
readonly scanProcesses?: (() => readonly ProcessRow[]) | undefined;
|
|
39
|
-
readonly scanListenSockets?: (() => readonly ListenSocketRow[]) | undefined;
|
|
40
|
-
readonly scanProcessCwds?: ((pids: readonly number[]) => ReadonlyMap<number, string>) | undefined;
|
|
41
|
-
readonly nowMs?: (() => number) | undefined;
|
|
42
|
-
readonly onCandidate: (candidate: PortForwardCandidate) => void | Promise<void>;
|
|
43
|
-
readonly onError?: ((error: Error) => void) | undefined;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const defaultIntervalMs = 2_000;
|
|
47
|
-
const defaultDebounceMs = 750;
|
|
48
|
-
|
|
49
|
-
export type ObservedPortForwardCandidate = {
|
|
50
|
-
readonly candidate: PortForwardCandidate;
|
|
51
|
-
readonly firstSeenAtMs: number;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export function startPortForwardWatcher(
|
|
55
|
-
options: PortForwardWatcherOptions,
|
|
56
|
-
): PortForwardWatcher {
|
|
57
|
-
const rootPid = options.rootPid ?? process.pid;
|
|
58
|
-
const intervalMs = options.intervalMs ?? defaultIntervalMs;
|
|
59
|
-
const debounceMs = options.debounceMs ?? defaultDebounceMs;
|
|
60
|
-
const scanProcesses = options.scanProcesses ?? readProcessRows;
|
|
61
|
-
const scanListenSockets = options.scanListenSockets ?? readListenSocketRows;
|
|
62
|
-
const scanProcessCwds = options.scanProcessCwds ?? readProcessCwdRows;
|
|
63
|
-
const nowMs = options.nowMs ?? Date.now;
|
|
64
|
-
const candidateStabilityByPort = new Map<number, ObservedPortForwardCandidate>();
|
|
65
|
-
const emittedByPort = new Map<number, PortForwardCandidate>();
|
|
66
|
-
const inFlight = { value: false };
|
|
67
|
-
|
|
68
|
-
const scan = () => {
|
|
69
|
-
if (inFlight.value) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
inFlight.value = true;
|
|
74
|
-
void Promise.resolve()
|
|
75
|
-
.then(async () => {
|
|
76
|
-
const descendants = descendantPidSet(scanProcesses(), rootPid);
|
|
77
|
-
const sockets = scanListenSockets();
|
|
78
|
-
const candidatePids = sockets
|
|
79
|
-
.filter(socket => descendants.has(socket.pid))
|
|
80
|
-
.map(socket => socket.pid);
|
|
81
|
-
const candidates = detectedForwardCandidates(
|
|
82
|
-
sockets,
|
|
83
|
-
descendants,
|
|
84
|
-
scanProcessCwds(candidatePids),
|
|
85
|
-
);
|
|
86
|
-
const stable = stableForwardCandidates(
|
|
87
|
-
candidateStabilityByPort,
|
|
88
|
-
candidates,
|
|
89
|
-
nowMs(),
|
|
90
|
-
debounceMs,
|
|
91
|
-
);
|
|
92
|
-
const changes = changedForwardCandidates(emittedByPort, stable.stableCandidates);
|
|
93
|
-
candidateStabilityByPort.clear();
|
|
94
|
-
emittedByPort.clear();
|
|
95
|
-
|
|
96
|
-
for (const [port, observed] of stable.nextObservedByPort) {
|
|
97
|
-
candidateStabilityByPort.set(port, observed);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const [port, candidate] of changes.nextObservedByPort) {
|
|
101
|
-
emittedByPort.set(port, candidate);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (const candidate of changes.changedCandidates) {
|
|
105
|
-
await options.onCandidate(candidate);
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
.catch(error => {
|
|
109
|
-
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
-
})
|
|
111
|
-
.finally(() => {
|
|
112
|
-
inFlight.value = false;
|
|
113
|
-
});
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
scan();
|
|
117
|
-
const interval = setInterval(scan, intervalMs);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
close: () => clearInterval(interval),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function changedForwardCandidates(
|
|
125
|
-
previousObservedByPort: ReadonlyMap<number, PortForwardCandidate>,
|
|
126
|
-
candidates: readonly PortForwardCandidate[],
|
|
127
|
-
): {
|
|
128
|
-
readonly nextObservedByPort: Map<number, PortForwardCandidate>;
|
|
129
|
-
readonly changedCandidates: PortForwardCandidate[];
|
|
130
|
-
} {
|
|
131
|
-
const nextObservedByPort = new Map<number, PortForwardCandidate>();
|
|
132
|
-
const changedCandidates: PortForwardCandidate[] = [];
|
|
133
|
-
|
|
134
|
-
for (const candidate of candidates) {
|
|
135
|
-
nextObservedByPort.set(candidate.port, candidate);
|
|
136
|
-
const previous = previousObservedByPort.get(candidate.port);
|
|
137
|
-
|
|
138
|
-
if (previous === undefined || !sameForwardCandidate(previous, candidate)) {
|
|
139
|
-
changedCandidates.push(candidate);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { nextObservedByPort, changedCandidates };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function stableForwardCandidates(
|
|
147
|
-
previousObservedByPort: ReadonlyMap<number, ObservedPortForwardCandidate>,
|
|
148
|
-
candidates: readonly PortForwardCandidate[],
|
|
149
|
-
nowMs: number,
|
|
150
|
-
debounceMs: number,
|
|
151
|
-
): {
|
|
152
|
-
readonly nextObservedByPort: Map<number, ObservedPortForwardCandidate>;
|
|
153
|
-
readonly stableCandidates: PortForwardCandidate[];
|
|
154
|
-
} {
|
|
155
|
-
const nextObservedByPort = new Map<number, ObservedPortForwardCandidate>();
|
|
156
|
-
const stableCandidates: PortForwardCandidate[] = [];
|
|
157
|
-
|
|
158
|
-
for (const candidate of candidates) {
|
|
159
|
-
const previous = previousObservedByPort.get(candidate.port);
|
|
160
|
-
const firstSeenAtMs =
|
|
161
|
-
previous !== undefined && sameForwardCandidate(previous.candidate, candidate)
|
|
162
|
-
? previous.firstSeenAtMs
|
|
163
|
-
: nowMs;
|
|
164
|
-
const observed = { candidate, firstSeenAtMs };
|
|
165
|
-
nextObservedByPort.set(candidate.port, observed);
|
|
166
|
-
|
|
167
|
-
if (debounceMs <= 0 || nowMs - firstSeenAtMs >= debounceMs) {
|
|
168
|
-
stableCandidates.push(candidate);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { nextObservedByPort, stableCandidates };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function sameForwardCandidate(
|
|
176
|
-
left: PortForwardCandidate,
|
|
177
|
-
right: PortForwardCandidate,
|
|
178
|
-
): boolean {
|
|
179
|
-
return (
|
|
180
|
-
left.port === right.port &&
|
|
181
|
-
left.pid === right.pid &&
|
|
182
|
-
left.command === right.command &&
|
|
183
|
-
left.cwd === right.cwd
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function descendantPidSet(
|
|
188
|
-
rows: readonly ProcessRow[],
|
|
189
|
-
rootPid: number,
|
|
190
|
-
): Set<number> {
|
|
191
|
-
const childrenByParent = new Map<number, number[]>();
|
|
192
|
-
|
|
193
|
-
for (const row of rows) {
|
|
194
|
-
const existing = childrenByParent.get(row.ppid) ?? [];
|
|
195
|
-
childrenByParent.set(row.ppid, [...existing, row.pid]);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const descendants = new Set<number>();
|
|
199
|
-
const queue = [...(childrenByParent.get(rootPid) ?? [])];
|
|
200
|
-
|
|
201
|
-
while (queue.length > 0) {
|
|
202
|
-
const pid = queue.shift();
|
|
203
|
-
|
|
204
|
-
if (pid === undefined || descendants.has(pid)) {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
descendants.add(pid);
|
|
209
|
-
queue.push(...(childrenByParent.get(pid) ?? []));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return descendants;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function detectedForwardCandidates(
|
|
216
|
-
sockets: readonly ListenSocketRow[],
|
|
217
|
-
descendantPids: ReadonlySet<number>,
|
|
218
|
-
processCwds: ReadonlyMap<number, string> = new Map(),
|
|
219
|
-
): PortForwardCandidate[] {
|
|
220
|
-
return sockets
|
|
221
|
-
.filter(socket => descendantPids.has(socket.pid))
|
|
222
|
-
.filter(socket => socket.port > 0 && socket.port < 65_536)
|
|
223
|
-
.sort((left, right) => left.port - right.port)
|
|
224
|
-
.map(socket => {
|
|
225
|
-
const cwd = processCwds.get(socket.pid);
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
port: socket.port,
|
|
229
|
-
pid: socket.pid,
|
|
230
|
-
command: socket.command,
|
|
231
|
-
...(cwd === undefined ? {} : { cwd }),
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function parseProcessRows(output: string): ProcessRow[] {
|
|
237
|
-
return output
|
|
238
|
-
.split("\n")
|
|
239
|
-
.map(line => line.trim())
|
|
240
|
-
.filter(line => line !== "")
|
|
241
|
-
.map(line => {
|
|
242
|
-
const match = line.match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
243
|
-
|
|
244
|
-
if (match === null) {
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
pid: Number(match[1]),
|
|
250
|
-
ppid: Number(match[2]),
|
|
251
|
-
command: match[3] ?? "",
|
|
252
|
-
};
|
|
253
|
-
})
|
|
254
|
-
.filter((row): row is ProcessRow => row !== undefined);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function parseLsofListenRows(output: string): ListenSocketRow[] {
|
|
258
|
-
const rows: ListenSocketRow[] = [];
|
|
259
|
-
let currentPid: number | undefined;
|
|
260
|
-
let currentCommand: string | undefined;
|
|
261
|
-
|
|
262
|
-
for (const rawLine of output.split("\n")) {
|
|
263
|
-
const line = rawLine.trim();
|
|
264
|
-
|
|
265
|
-
if (line.startsWith("p")) {
|
|
266
|
-
currentPid = Number(line.slice(1));
|
|
267
|
-
currentCommand = undefined;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (line.startsWith("c")) {
|
|
272
|
-
currentCommand = line.slice(1);
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (!line.startsWith("n") || currentPid === undefined) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const port = portFromLsofName(line.slice(1));
|
|
281
|
-
|
|
282
|
-
if (port !== undefined) {
|
|
283
|
-
rows.push({
|
|
284
|
-
pid: currentPid,
|
|
285
|
-
port,
|
|
286
|
-
command: currentCommand ?? "unknown",
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return rows;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function parseLsofCwdRows(output: string): Map<number, string> {
|
|
295
|
-
const rows = new Map<number, string>();
|
|
296
|
-
let currentPid: number | undefined;
|
|
297
|
-
|
|
298
|
-
for (const rawLine of output.split("\n")) {
|
|
299
|
-
const line = rawLine.trim();
|
|
300
|
-
|
|
301
|
-
if (line.startsWith("p")) {
|
|
302
|
-
currentPid = Number(line.slice(1));
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (line.startsWith("n") && currentPid !== undefined) {
|
|
307
|
-
const cwd = line.slice(1).trim();
|
|
308
|
-
|
|
309
|
-
if (cwd !== "") {
|
|
310
|
-
rows.set(currentPid, cwd);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return rows;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function readProcessRows(): ProcessRow[] {
|
|
319
|
-
const result = spawnSync("ps", ["-axo", "pid=,ppid=,command="], {
|
|
320
|
-
encoding: "utf8",
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (result.error !== undefined) {
|
|
324
|
-
throw result.error;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (result.status !== 0) {
|
|
328
|
-
throw new Error(`ps failed with status ${result.status}: ${result.stderr}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return parseProcessRows(result.stdout);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function readListenSocketRows(): ListenSocketRow[] {
|
|
335
|
-
const result = spawnSync(
|
|
336
|
-
"lsof",
|
|
337
|
-
["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"],
|
|
338
|
-
{ encoding: "utf8" },
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
if (result.error !== undefined) {
|
|
342
|
-
throw result.error;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (result.status !== 0) {
|
|
346
|
-
if (result.status === 1 && result.stdout.trim() === "") {
|
|
347
|
-
return [];
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
throw new Error(`lsof failed with status ${result.status}: ${result.stderr}`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return parseLsofListenRows(result.stdout);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function readProcessCwdRows(pids: readonly number[]): ReadonlyMap<number, string> {
|
|
357
|
-
const uniquePids = [...new Set(pids)].filter(pid => Number.isInteger(pid) && pid > 0);
|
|
358
|
-
|
|
359
|
-
if (uniquePids.length === 0) {
|
|
360
|
-
return new Map();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const result = spawnSync(
|
|
364
|
-
"lsof",
|
|
365
|
-
["-a", "-p", uniquePids.join(","), "-d", "cwd", "-Fn"],
|
|
366
|
-
{ encoding: "utf8" },
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
if (result.error !== undefined) {
|
|
370
|
-
throw result.error;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (result.status !== 0) {
|
|
374
|
-
if (result.status === 1 && result.stdout.trim() === "") {
|
|
375
|
-
return new Map();
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
throw new Error(`lsof cwd failed with status ${result.status}: ${result.stderr}`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return parseLsofCwdRows(result.stdout);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function portFromLsofName(name: string): number | undefined {
|
|
385
|
-
const match = name.match(/:(\d+)(?:\s|\(|$)/);
|
|
386
|
-
|
|
387
|
-
if (match === null) {
|
|
388
|
-
return undefined;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const port = Number(match[1]);
|
|
392
|
-
|
|
393
|
-
return Number.isInteger(port) ? port : undefined;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export function commandLabel(command: string): string {
|
|
397
|
-
const trimmed = command.trim();
|
|
398
|
-
|
|
399
|
-
if (trimmed === "") {
|
|
400
|
-
return "unknown";
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return basename(trimmed.split(/\s+/)[0] ?? trimmed);
|
|
404
|
-
}
|
package/src/protocol.ts
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-04-24
|
|
3
|
-
Spec: plans/2026-04-24-local-codex-runner-plan.md
|
|
4
|
-
Relationship: Defines the spec's first local runner protocol types for
|
|
5
|
-
Phoenix frames, Codex JSON-RPC envelopes, runner events, and controls.
|
|
6
|
-
|
|
7
|
-
- Date: 2026-04-24
|
|
8
|
-
Spec: plans/2026-04-24-local-codex-channel-thread-binding-spec.md
|
|
9
|
-
Relationship: Defines the channel-bound runner options used to bind one
|
|
10
|
-
local Codex process to a Kandan channel/thread and listener selector.
|
|
11
|
-
|
|
12
|
-
- Date: 2026-04-25
|
|
13
|
-
Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
|
|
14
|
-
Relationship: Defines the typed runner control that carries a Kandan
|
|
15
|
-
Approve/Deny decision back to the local Codex app-server approval request
|
|
16
|
-
without exposing a browser-to-runner socket.
|
|
17
|
-
|
|
18
|
-
- Date: 2026-04-26
|
|
19
|
-
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
20
|
-
Relationship: Carries the runner streaming flush interval so live Codex
|
|
21
|
-
output can be batched before Kandan persistence without changing the logical
|
|
22
|
-
transcript protocol.
|
|
23
|
-
|
|
24
|
-
- Date: 2026-04-26
|
|
25
|
-
Spec: plans/2026-04-26-local-runner-forwarding-and-editor-plan.md
|
|
26
|
-
Relationship: Defines the typed control used by Kandan to ask a connected
|
|
27
|
-
runner to perform one bounded HTTP request against an explicitly allowed
|
|
28
|
-
local preview port, and the typed control used to ask the runner to start a
|
|
29
|
-
loopback-only local code-server editor for an allowed cwd.
|
|
30
|
-
|
|
31
|
-
- Date: 2026-04-26
|
|
32
|
-
Spec: plans/2026-04-26-local-runner-port-forward-approval.md
|
|
33
|
-
Relationship: Defines the typed approval control Kandan sends after the
|
|
34
|
-
in-app local port prompt is resolved by the authorized listener user.
|
|
35
|
-
|
|
36
|
-
- Date: 2026-04-26
|
|
37
|
-
Spec: plans/2026-04-26-local-runner-subdomain-forwarding-epr.md
|
|
38
|
-
Relationship: Defines the typed WebSocket forwarding controls used by
|
|
39
|
-
isolated preview subdomains.
|
|
40
|
-
|
|
41
|
-
- Date: 2026-05-02
|
|
42
|
-
Spec: plans/2026-05-02-agent-first-zero-to-codex-launch-plan.md
|
|
43
|
-
Relationship: Keeps the npm CLI runner protocol in parity with the legacy
|
|
44
|
-
runner for runtime session setting updates.
|
|
45
|
-
*/
|
|
46
|
-
export type JsonValue =
|
|
47
|
-
| null
|
|
48
|
-
| boolean
|
|
49
|
-
| number
|
|
50
|
-
| string
|
|
51
|
-
| JsonValue[]
|
|
52
|
-
| { readonly [key: string]: JsonValue };
|
|
53
|
-
|
|
54
|
-
export type JsonObject = { readonly [key: string]: JsonValue };
|
|
55
|
-
|
|
56
|
-
export type JsonRpcId = string | number;
|
|
57
|
-
|
|
58
|
-
export type JsonRpcRequest = {
|
|
59
|
-
readonly jsonrpc: "2.0";
|
|
60
|
-
readonly id: JsonRpcId;
|
|
61
|
-
readonly method: string;
|
|
62
|
-
readonly params?: JsonObject;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export type JsonRpcNotification = {
|
|
66
|
-
readonly jsonrpc: "2.0";
|
|
67
|
-
readonly method: string;
|
|
68
|
-
readonly params?: JsonObject;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export type JsonRpcResponse =
|
|
72
|
-
| {
|
|
73
|
-
readonly jsonrpc: "2.0";
|
|
74
|
-
readonly id: JsonRpcId;
|
|
75
|
-
readonly result: JsonValue;
|
|
76
|
-
}
|
|
77
|
-
| {
|
|
78
|
-
readonly jsonrpc: "2.0";
|
|
79
|
-
readonly id: JsonRpcId;
|
|
80
|
-
readonly error: {
|
|
81
|
-
readonly code: number;
|
|
82
|
-
readonly message: string;
|
|
83
|
-
readonly data?: JsonValue;
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export type JsonRpcMessage =
|
|
88
|
-
| JsonRpcRequest
|
|
89
|
-
| JsonRpcNotification
|
|
90
|
-
| JsonRpcResponse;
|
|
91
|
-
|
|
92
|
-
export type PhoenixFrame = readonly [
|
|
93
|
-
joinRef: string | null,
|
|
94
|
-
ref: string | null,
|
|
95
|
-
topic: string,
|
|
96
|
-
event: string,
|
|
97
|
-
payload: JsonValue,
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
export type RunnerToKandanEvent =
|
|
101
|
-
| {
|
|
102
|
-
readonly kind: "instance_started";
|
|
103
|
-
readonly payload: JsonObject;
|
|
104
|
-
}
|
|
105
|
-
| {
|
|
106
|
-
readonly kind: "instance_stopped";
|
|
107
|
-
readonly payload: JsonObject;
|
|
108
|
-
}
|
|
109
|
-
| {
|
|
110
|
-
readonly kind: "codex_notification";
|
|
111
|
-
readonly payload: {
|
|
112
|
-
readonly instanceId: string;
|
|
113
|
-
readonly seq: number;
|
|
114
|
-
readonly method: string;
|
|
115
|
-
readonly params: JsonObject;
|
|
116
|
-
readonly receivedAt: string;
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
| {
|
|
120
|
-
readonly kind: "codex_request";
|
|
121
|
-
readonly payload: JsonObject;
|
|
122
|
-
}
|
|
123
|
-
| {
|
|
124
|
-
readonly kind: "codex_response";
|
|
125
|
-
readonly payload: JsonObject;
|
|
126
|
-
}
|
|
127
|
-
| {
|
|
128
|
-
readonly kind: "codex_error";
|
|
129
|
-
readonly payload: JsonObject;
|
|
130
|
-
}
|
|
131
|
-
| {
|
|
132
|
-
readonly kind: "heartbeat";
|
|
133
|
-
readonly payload: JsonObject;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export type KandanControl =
|
|
137
|
-
| {
|
|
138
|
-
readonly type: "start_instance";
|
|
139
|
-
readonly instanceId?: string;
|
|
140
|
-
readonly cwd?: string;
|
|
141
|
-
readonly workDescription?: string;
|
|
142
|
-
readonly launchTui?: boolean;
|
|
143
|
-
readonly model?: string;
|
|
144
|
-
readonly reasoningEffort?: string;
|
|
145
|
-
readonly approvalPolicy?: string;
|
|
146
|
-
readonly sandbox?: string;
|
|
147
|
-
readonly fast?: boolean;
|
|
148
|
-
}
|
|
149
|
-
| {
|
|
150
|
-
readonly type: "stop_instance";
|
|
151
|
-
readonly instanceId: string;
|
|
152
|
-
}
|
|
153
|
-
| {
|
|
154
|
-
readonly type: "kill_instance";
|
|
155
|
-
readonly instanceId: string;
|
|
156
|
-
}
|
|
157
|
-
| {
|
|
158
|
-
readonly type: "start_turn";
|
|
159
|
-
readonly instanceId: string;
|
|
160
|
-
readonly threadId: string;
|
|
161
|
-
readonly input: JsonValue[];
|
|
162
|
-
}
|
|
163
|
-
| {
|
|
164
|
-
readonly type: "steer_turn";
|
|
165
|
-
readonly instanceId: string;
|
|
166
|
-
readonly threadId: string;
|
|
167
|
-
readonly turnId: string;
|
|
168
|
-
readonly input: JsonValue[];
|
|
169
|
-
}
|
|
170
|
-
| {
|
|
171
|
-
readonly type: "interrupt_turn";
|
|
172
|
-
readonly instanceId: string;
|
|
173
|
-
readonly threadId: string;
|
|
174
|
-
readonly turnId?: string;
|
|
175
|
-
}
|
|
176
|
-
| {
|
|
177
|
-
readonly type: "interrupt_queued_messages";
|
|
178
|
-
readonly instanceId: string;
|
|
179
|
-
readonly threadId: string;
|
|
180
|
-
readonly throughSeq?: number;
|
|
181
|
-
}
|
|
182
|
-
| {
|
|
183
|
-
readonly type: "resolve_codex_approval_request";
|
|
184
|
-
readonly instanceId: string;
|
|
185
|
-
readonly threadId: string;
|
|
186
|
-
readonly sourceSeq: number;
|
|
187
|
-
readonly requestId: string;
|
|
188
|
-
readonly decision: "approve" | "deny";
|
|
189
|
-
}
|
|
190
|
-
| {
|
|
191
|
-
readonly type: "update_session_settings";
|
|
192
|
-
readonly instanceId?: string;
|
|
193
|
-
readonly threadId: string;
|
|
194
|
-
readonly model?: string | null;
|
|
195
|
-
readonly reasoningEffort?: string | null;
|
|
196
|
-
readonly approvalPolicy?: string | null;
|
|
197
|
-
readonly sandbox?: string | null;
|
|
198
|
-
readonly fast?: boolean;
|
|
199
|
-
}
|
|
200
|
-
| {
|
|
201
|
-
readonly type: "resolve_port_forward_request";
|
|
202
|
-
readonly instanceId: string;
|
|
203
|
-
readonly requestId: string;
|
|
204
|
-
readonly decision: "approve" | "deny";
|
|
205
|
-
readonly actorUserId?: number | undefined;
|
|
206
|
-
readonly actorSlug?: string | undefined;
|
|
207
|
-
}
|
|
208
|
-
| {
|
|
209
|
-
readonly type: "read_thread";
|
|
210
|
-
readonly instanceId: string;
|
|
211
|
-
readonly threadId: string;
|
|
212
|
-
readonly includeTurns?: boolean;
|
|
213
|
-
}
|
|
214
|
-
| {
|
|
215
|
-
readonly type: "forward_http_request";
|
|
216
|
-
readonly instanceId?: string;
|
|
217
|
-
readonly requestId: string;
|
|
218
|
-
readonly port: number;
|
|
219
|
-
readonly method: string;
|
|
220
|
-
readonly path: string;
|
|
221
|
-
readonly queryString?: string;
|
|
222
|
-
readonly headers?: JsonValue;
|
|
223
|
-
readonly bodyBase64?: string;
|
|
224
|
-
}
|
|
225
|
-
| {
|
|
226
|
-
readonly type: "forward_websocket_open";
|
|
227
|
-
readonly instanceId?: string;
|
|
228
|
-
readonly socketId: string;
|
|
229
|
-
readonly port: number;
|
|
230
|
-
readonly path: string;
|
|
231
|
-
readonly queryString?: string;
|
|
232
|
-
readonly headers?: JsonValue;
|
|
233
|
-
}
|
|
234
|
-
| {
|
|
235
|
-
readonly type: "forward_websocket_send";
|
|
236
|
-
readonly instanceId?: string;
|
|
237
|
-
readonly socketId: string;
|
|
238
|
-
readonly opcode: "text" | "binary" | "ping" | "pong";
|
|
239
|
-
readonly bodyBase64: string;
|
|
240
|
-
}
|
|
241
|
-
| {
|
|
242
|
-
readonly type: "forward_websocket_close";
|
|
243
|
-
readonly instanceId?: string;
|
|
244
|
-
readonly socketId: string;
|
|
245
|
-
}
|
|
246
|
-
| {
|
|
247
|
-
readonly type: "update_runner_config";
|
|
248
|
-
readonly instanceId?: string;
|
|
249
|
-
readonly allowedCwds: readonly string[];
|
|
250
|
-
readonly configVersion?: number;
|
|
251
|
-
}
|
|
252
|
-
| {
|
|
253
|
-
readonly type: "start_local_editor";
|
|
254
|
-
readonly instanceId?: string;
|
|
255
|
-
readonly requestId?: string;
|
|
256
|
-
readonly cwd: string;
|
|
257
|
-
readonly browserBaseUrl?: string;
|
|
258
|
-
readonly collaboration?: {
|
|
259
|
-
readonly provider: "oct";
|
|
260
|
-
readonly editorSessionId: number;
|
|
261
|
-
readonly runtimeSessionId: string;
|
|
262
|
-
readonly roomId: string;
|
|
263
|
-
readonly extensionAssetPath: string;
|
|
264
|
-
readonly serverAssetPath: string;
|
|
265
|
-
readonly bootstrapToken?: string;
|
|
266
|
-
};
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
export type KandanChannelSessionOptions = {
|
|
270
|
-
readonly workspaceSlug: string;
|
|
271
|
-
readonly channelSlug: string;
|
|
272
|
-
readonly kandanThreadId: string | undefined;
|
|
273
|
-
readonly listenUser: string;
|
|
274
|
-
readonly model?: string | undefined;
|
|
275
|
-
readonly reasoningEffort?: string | undefined;
|
|
276
|
-
readonly sandbox?: string | undefined;
|
|
277
|
-
readonly approvalPolicy?: string | undefined;
|
|
278
|
-
readonly streamFlushMs?: number | undefined;
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
export function parseJsonObject(text: string): JsonObject {
|
|
282
|
-
const parsed = JSON.parse(text);
|
|
283
|
-
|
|
284
|
-
if (isJsonObject(parsed)) {
|
|
285
|
-
return parsed;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
throw new Error("expected JSON object");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function isJsonObject(value: unknown): value is JsonObject {
|
|
292
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function requireString(value: unknown, label: string): string {
|
|
296
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
297
|
-
return value.trim();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function requirePositiveInteger(value: unknown, label: string): number {
|
|
304
|
-
if (Number.isInteger(value) && typeof value === "number" && value > 0) {
|
|
305
|
-
return value;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
throw new Error(`${label} must be a positive integer`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function extractCodexIds(params: JsonObject): JsonObject {
|
|
312
|
-
const entries = [
|
|
313
|
-
["threadId", params.threadId],
|
|
314
|
-
["turnId", params.turnId],
|
|
315
|
-
["itemId", params.itemId],
|
|
316
|
-
["processId", params.processId],
|
|
317
|
-
["requestId", params.requestId],
|
|
318
|
-
].filter((entry): entry is [string, JsonValue] => entry[1] !== undefined);
|
|
319
|
-
|
|
320
|
-
return Object.fromEntries(entries) as JsonObject;
|
|
321
|
-
}
|