@gleapai/kai-bridge 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -4
- package/bin/kai-bridge.mjs +19 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -7
- package/scripts/runtime-smoke.mjs +1 -1
- package/src/api.mjs +8 -0
- package/src/daemon.mjs +142 -93
- package/src/executor.mjs +5 -16
- package/src/git-auth.mjs +38 -0
- package/src/harness-errors.mjs +8 -0
- package/src/harness-install.mjs +73 -0
- package/src/harnesses.mjs +30 -25
- package/src/{hosted-ports.mjs → preview-ports.mjs} +2 -2
- package/src/preview.mjs +14 -20
- package/src/profiles.mjs +6 -5
- package/src/ps.mjs +152 -0
- package/src/repository-setup.mjs +2 -2
- package/src/service.mjs +1 -1
- package/src/setup.mjs +1 -1
- package/src/workspace.mjs +92 -28
- package/fly/Dockerfile +0 -11
- package/fly/UPDATES.md +0 -76
- package/fly/codex-auth.mjs +0 -5
- package/fly/entrypoint.sh +0 -12
- package/fly/login-codex.sh +0 -9
- package/fly/runtime-cli.mjs +0 -15
- package/fly/runtime-cli.sh +0 -10
- package/fly/runtime-launch.mjs +0 -8
- package/fly/runtime-update.mjs +0 -137
- package/fly/start.mjs +0 -7
- package/fly/supervisord.conf +0 -45
- package/src/codex-broker-client.mjs +0 -16
- package/src/codex-broker.mjs +0 -85
- package/src/hosted-auth.mjs +0 -42
- package/src/hosted-git-credential.mjs +0 -24
- package/src/hosted-git.mjs +0 -33
- package/src/hosted-resources.mjs +0 -35
- package/src/hosted-tunnel.mjs +0 -76
- package/src/hosted.mjs +0 -86
package/src/workspace.mjs
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
16
16
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
|
-
import { createHash } from 'node:crypto';
|
|
19
18
|
|
|
20
19
|
import { seedNodeModules } from "./deps.mjs";
|
|
21
20
|
|
|
@@ -23,19 +22,97 @@ function git(cwd, args, opts = {}) {
|
|
|
23
22
|
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...opts }).trim();
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
/** Network ops only: `gitEnv` is the git-auth.mjs fallback, applied to that one command. */
|
|
26
|
+
const withGitEnv = (gitEnv, opts = {}) => (gitEnv ? { ...opts, env: { ...process.env, ...gitEnv } } : opts);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A workspace could not be prepared for a reason that has nothing to do
|
|
30
|
+
* with the task — the Server surfaces `code` as a one-click retry instead
|
|
31
|
+
* of a dead session. `repo` names the checkout for the log line.
|
|
32
|
+
*/
|
|
33
|
+
export class WorkspaceError extends Error {
|
|
34
|
+
constructor(message, { code, repo, cause } = {}) {
|
|
35
|
+
super(message, cause ? { cause } : undefined);
|
|
36
|
+
this.name = "WorkspaceError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.repo = repo;
|
|
30
39
|
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Git refused to update a ref because ANOTHER git process was writing the
|
|
44
|
+
* same repo at that moment: the bridge fetches `origin/<base>` in the
|
|
45
|
+
* user's primary checkout, and IDE auto-fetch / a second session /
|
|
46
|
+
* the user's own `git pull` race it there. The loser sees one of:
|
|
47
|
+
*
|
|
48
|
+
* cannot lock ref 'refs/remotes/origin/master': is at <new> but expected <old>
|
|
49
|
+
* Unable to create '…/refs/remotes/origin/master.lock': File exists.
|
|
50
|
+
* Another git process seems to be running in this repository
|
|
51
|
+
*
|
|
52
|
+
* None of them mean anything is wrong — the ref is simply being moved by
|
|
53
|
+
* someone else — so the fetch is retried, and if it keeps losing the
|
|
54
|
+
* ref the competitor just wrote is used as the base (2026-09-16: a
|
|
55
|
+
* session on ticket #147312 died 54 s in on exactly this, before the
|
|
56
|
+
* agent ever ran).
|
|
57
|
+
*/
|
|
58
|
+
export function isRefLockContention(message) {
|
|
59
|
+
const text = String(message || "");
|
|
60
|
+
return (
|
|
61
|
+
/cannot lock ref/i.test(text) ||
|
|
62
|
+
/\.lock['"]?: File exists/i.test(text) ||
|
|
63
|
+
/Another git process seems to be running/i.test(text)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sleepSync = (ms) => {
|
|
68
|
+
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `git fetch origin <base>` in `primaryPath`, tolerant of ref-lock
|
|
73
|
+
* contention. Returns `{ attempts, stale }` — `stale: true` means every
|
|
74
|
+
* attempt lost the race and the existing `origin/<base>` (which the
|
|
75
|
+
* competitor just updated) is used instead. Any other fetch failure, or
|
|
76
|
+
* contention with no usable `origin/<base>`, throws a WorkspaceError
|
|
77
|
+
* whose `code` the Server turns into a retry offer.
|
|
78
|
+
*/
|
|
79
|
+
export function fetchBase(primaryPath, base, { exec = git, attempts = 4, backoffMs = 400, sleep = sleepSync, repo = primaryPath, gitEnv = null } = {}) {
|
|
80
|
+
let lastError = null;
|
|
81
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
82
|
+
try {
|
|
83
|
+
exec(primaryPath, ["fetch", "origin", base, "--quiet"], withGitEnv(gitEnv));
|
|
84
|
+
return { attempts: attempt, stale: false };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const text = `${err?.stderr || ""}\n${err?.message || ""}`;
|
|
87
|
+
if (!isRefLockContention(text)) {
|
|
88
|
+
throw new WorkspaceError(err?.message || String(err), { code: "workspace_fetch_failed", repo, cause: err });
|
|
89
|
+
}
|
|
90
|
+
lastError = err;
|
|
91
|
+
if (attempt < attempts) sleep(backoffMs * attempt);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Every attempt lost: whoever kept winning has already moved
|
|
95
|
+
// origin/<base> forward, so it is at least as fresh as our fetch
|
|
96
|
+
// would have made it.
|
|
97
|
+
try {
|
|
98
|
+
exec(primaryPath, ["rev-parse", "--verify", "--quiet", `origin/${base}^{commit}`]);
|
|
99
|
+
return { attempts, stale: true };
|
|
100
|
+
} catch {
|
|
101
|
+
throw new WorkspaceError(
|
|
102
|
+
`Git in ${repo} was busy (another fetch was running) and origin/${base} is not available yet — retry the task.`,
|
|
103
|
+
{ code: "workspace_transient", repo, cause: lastError },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function sessionSlug(sessionId, title) {
|
|
31
109
|
const t = String(title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
32
110
|
const id = String(sessionId || "").slice(-8);
|
|
33
111
|
return t ? `${t}-${id}` : `session-${id}`;
|
|
34
112
|
}
|
|
35
113
|
|
|
36
114
|
export function worktreePath(kaiHome, repoName, slug) {
|
|
37
|
-
|
|
38
|
-
return join(kaiHome, "worktrees", repo, slug);
|
|
115
|
+
return join(kaiHome, "worktrees", repoName, slug);
|
|
39
116
|
}
|
|
40
117
|
|
|
41
118
|
/**
|
|
@@ -73,7 +150,7 @@ export function copyPrimaryEnvFiles(primaryPath, cwd) {
|
|
|
73
150
|
* Materialise one repo binding. Returns `{ cwd, mode, branch, base }`.
|
|
74
151
|
* `repo` = `{ name, primaryPath, defaultBranch }`, `binding` = `{ mode, base?, carryUncommitted? }`.
|
|
75
152
|
*/
|
|
76
|
-
export function materializeBinding({ kaiHome, repo, binding, sessionId, title, branchPrefix = "kai" }) {
|
|
153
|
+
export function materializeBinding({ kaiHome, repo, binding, sessionId, title, branchPrefix = "kai", fetch = fetchBase, gitEnv = null }) {
|
|
77
154
|
const mode = binding?.mode === "local" ? "local" : "worktree";
|
|
78
155
|
if (mode === "local") {
|
|
79
156
|
const branch = git(repo.primaryPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -88,7 +165,7 @@ export function materializeBinding({ kaiHome, repo, binding, sessionId, title, b
|
|
|
88
165
|
return { cwd: dir, mode, branch, base, resumed: true };
|
|
89
166
|
}
|
|
90
167
|
mkdirSync(dirname(dir), { recursive: true });
|
|
91
|
-
|
|
168
|
+
const fetched = fetch(repo.primaryPath, base, { repo: repo.name, gitEnv });
|
|
92
169
|
git(repo.primaryPath, ["worktree", "add", "-b", branch, dir, `origin/${base}`]);
|
|
93
170
|
// A fresh worktree has no node_modules; clone the primary checkout's
|
|
94
171
|
// when the lockfiles match so the agent's tests and the preview boot
|
|
@@ -116,7 +193,7 @@ export function materializeBinding({ kaiHome, repo, binding, sessionId, title, b
|
|
|
116
193
|
}
|
|
117
194
|
}
|
|
118
195
|
}
|
|
119
|
-
return { cwd: dir, mode, branch, base, resumed: false, deps };
|
|
196
|
+
return { cwd: dir, mode, branch, base, resumed: false, deps, fetch: fetched };
|
|
120
197
|
}
|
|
121
198
|
|
|
122
199
|
/** The branch checked out at `cwd` (null when it is not a git checkout). */
|
|
@@ -176,22 +253,9 @@ export function discardChanges(cwd) {
|
|
|
176
253
|
export function removeWorktree({ kaiHome, repo, sessionId, title }) {
|
|
177
254
|
const dir = worktreePath(kaiHome, repo.name, sessionSlug(sessionId, title));
|
|
178
255
|
if (!existsSync(dir)) return false;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
git(repo.primaryPath, ['worktree', 'remove', dir]);
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
git(repo.primaryPath, ["worktree", "remove", "--force", dir]);
|
|
187
|
-
} catch {
|
|
188
|
-
rmSync(dir, { recursive: true, force: true });
|
|
189
|
-
try {
|
|
190
|
-
git(repo.primaryPath, ["worktree", "prune"]);
|
|
191
|
-
} catch {
|
|
192
|
-
/* best-effort */
|
|
193
|
-
}
|
|
194
|
-
}
|
|
256
|
+
// Never destroy dirty or unpushed work during session cleanup.
|
|
257
|
+
if (collectChanges(dir).files.length || git(dir, ['log', 'HEAD', '--not', '--remotes', '--oneline'])) return false;
|
|
258
|
+
git(repo.primaryPath, ['worktree', 'remove', dir]);
|
|
195
259
|
return true;
|
|
196
260
|
}
|
|
197
261
|
|
|
@@ -286,7 +350,7 @@ export function ensureCommitExcludes(cwd, { allowDevConfig = false } = {}) {
|
|
|
286
350
|
}
|
|
287
351
|
}
|
|
288
352
|
|
|
289
|
-
export function commitAndPush(cwd, { branch, message, allowEmpty = false, allowDevConfig = false } = {}) {
|
|
353
|
+
export function commitAndPush(cwd, { branch, message, allowEmpty = false, allowDevConfig = false, gitEnv = null } = {}) {
|
|
290
354
|
const out = { committed: false, pushed: false, branch, commitSha: null, remote: null, error: null };
|
|
291
355
|
try {
|
|
292
356
|
ensureCommitExcludes(cwd, { allowDevConfig });
|
|
@@ -298,7 +362,7 @@ export function commitAndPush(cwd, { branch, message, allowEmpty = false, allowD
|
|
|
298
362
|
}
|
|
299
363
|
out.commitSha = git(cwd, ["rev-parse", "HEAD"]);
|
|
300
364
|
out.remote = git(cwd, ["remote", "get-url", "origin"]);
|
|
301
|
-
git(cwd, ["push", "-u", "origin", `HEAD:${branch}`], { timeout: 120_000 });
|
|
365
|
+
git(cwd, ["push", "-u", "origin", `HEAD:${branch}`], withGitEnv(gitEnv, { timeout: 120_000 }));
|
|
302
366
|
out.pushed = true;
|
|
303
367
|
} catch (err) {
|
|
304
368
|
out.error = String(err?.stderr || err?.message || err).trim().slice(0, 500);
|
package/fly/Dockerfile
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
FROM node:24-bookworm-slim
|
|
2
|
-
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl sudo openssh-client lsof procps supervisor xvfb fluxbox x11vnc novnc websockify python3 make g++ && rm -rf /var/lib/apt/lists/*
|
|
3
|
-
RUN useradd --home-dir /data/home/kai --shell /bin/bash kai && printf 'kai ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/kai && chmod 440 /etc/sudoers.d/kai
|
|
4
|
-
WORKDIR /opt/kai-bridge
|
|
5
|
-
COPY package.json package-lock.json ./
|
|
6
|
-
RUN npm ci --ignore-scripts
|
|
7
|
-
COPY . .
|
|
8
|
-
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
|
|
9
|
-
RUN node scripts/postinstall.mjs && node scripts/runtime-smoke.mjs && chmod +x src/codex-broker-client.mjs src/hosted-git.mjs fly/runtime-cli.sh fly/entrypoint.sh fly/login-codex.sh && for cli in codex claude git; do ln -s /opt/kai-bridge/fly/runtime-cli.sh /usr/local/bin/$cli; done && npx playwright install --with-deps chromium
|
|
10
|
-
ENV KAI_HOSTED=1 KAI_HOME=/data/home/kai/.kai HOME=/data/home/kai KAI_BRIDGE_NO_SELF_UPDATE=1 DISABLE_AUTOUPDATER=1
|
|
11
|
-
ENTRYPOINT ["/opt/kai-bridge/fly/entrypoint.sh"]
|
package/fly/UPDATES.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Hosted coding runtime releases
|
|
2
|
-
|
|
3
|
-
One Bridge release owns the complete coding runtime: Codex CLI, Claude Agent SDK
|
|
4
|
-
(including its native Claude Code binary), both ACP adapters, Bridge and its
|
|
5
|
-
runner. Versions are pinned in package.json and package-lock.json. The image no
|
|
6
|
-
longer installs separate global CLIs. `codex`, `claude`, login, version probes,
|
|
7
|
-
coding turns and the supervised Codex process all resolve this bundle.
|
|
8
|
-
|
|
9
|
-
## Routine updates
|
|
10
|
-
|
|
11
|
-
Dependabot checks npm dependencies daily and groups the coding runtime changes.
|
|
12
|
-
The `Coding runtime compatibility` workflow runs the full Bridge tests, native
|
|
13
|
-
CLI versions, Codex app-server and both ACP initialization checks, package checks
|
|
14
|
-
and the Linux image build. Vendor CLI updates are disabled for the managed
|
|
15
|
-
executables so they cannot mutate a tested bundle underneath active sessions.
|
|
16
|
-
|
|
17
|
-
After review and internal qualification, publish a new Bridge version and promote
|
|
18
|
-
that exact version to the npm `hosted-stable` dist-tag. Promotion is a release
|
|
19
|
-
operation, never something a customer VM decides from upstream `latest` versions.
|
|
20
|
-
The channel is intentionally separate from `latest`, which connected devices use.
|
|
21
|
-
No tag is published or promoted merely by adding this workflow.
|
|
22
|
-
|
|
23
|
-
`npm pack`/`npm publish` generate a published npm-shrinkwrap.json from the committed
|
|
24
|
-
lockfile. Consumers therefore receive the dependency tree that was tested. The
|
|
25
|
-
temporary shrinkwrap is removed after packing. Always bump the Bridge version and
|
|
26
|
-
refresh its lockfile for a new release; npm versions are immutable.
|
|
27
|
-
|
|
28
|
-
Each hosted machine checks `hosted-stable` on a normal start, at most once per day.
|
|
29
|
-
It does not wake a stopped machine to check. Updates only happen **before
|
|
30
|
-
supervisord starts**, under a file lock; running coding, validation, previews,
|
|
31
|
-
maintenance and login processes are never hot-replaced. A machine that remains
|
|
32
|
-
running applies updates on its next normal stop/start.
|
|
33
|
-
|
|
34
|
-
The updater downloads an exact release, checks protocol compatibility, installs
|
|
35
|
-
the shrinkwrapped dependencies with lifecycle scripts disabled, and runs native
|
|
36
|
-
startup checks with an empty temporary home. It selects the new bundle atomically
|
|
37
|
-
only after those checks pass. Registry failures, invalid releases, failed checks
|
|
38
|
-
and interrupted installs preserve the previous selection. Failed versions are
|
|
39
|
-
quarantined; the next distinct approved version can still update.
|
|
40
|
-
|
|
41
|
-
Runtime files are stored under `/opt/kai-runtime`. User repositories, worktrees,
|
|
42
|
-
home directories and authentication remain on `/data` and are never copied or
|
|
43
|
-
overwritten by updates. The current and one previous installed bundle are kept.
|
|
44
|
-
This does not rebuild the VM, reset its usage allowance, or require a new login.
|
|
45
|
-
Setup/update running time still counts against the slot allowance.
|
|
46
|
-
|
|
47
|
-
## Rollback and support
|
|
48
|
-
|
|
49
|
-
Inspect `/opt/kai-runtime/state.json` for the active version, previous version,
|
|
50
|
-
last check, failed version and update error. Bridge's normal version/harness
|
|
51
|
-
reports show the selected executable versions in Gleap. Updates can be disabled
|
|
52
|
-
with the machine environment `KAI_HOSTED_RUNTIME_UPDATES=0`.
|
|
53
|
-
|
|
54
|
-
If a runtime passes startup checks but later proves incompatible, schedule a
|
|
55
|
-
rollback through maintenance SSH:
|
|
56
|
-
|
|
57
|
-
```sh
|
|
58
|
-
sudo node /opt/kai-bridge/fly/runtime-update.mjs --rollback-next-start
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Then stop and start the machine through Gleap. This selects the retained previous
|
|
62
|
-
bundle (or the image's original bundle), preserves credentials/files and
|
|
63
|
-
quarantines the rejected release. The command does not interrupt current work.
|
|
64
|
-
Do not run `npm update` inside active release directories.
|
|
65
|
-
|
|
66
|
-
The startup checks exercise real binaries and protocol initialization without
|
|
67
|
-
account credentials or billable prompts. They do not replace authenticated
|
|
68
|
-
two-session coding/refresh/verification tests on the internal cohort before
|
|
69
|
-
promoting `hosted-stable`. A startup pass cannot guarantee compatibility with
|
|
70
|
-
every provider-side behavior. Keep the previous runtime available for rollback.
|
|
71
|
-
|
|
72
|
-
Changing the bootstrap protocol, OS packages, browser revision or persistent data formats requires
|
|
73
|
-
an explicit image/migration release; the boot updater rejects unsupported
|
|
74
|
-
protocol versions and Playwright revisions that differ from the image's installed
|
|
75
|
-
browser. System modifications survive normal starts as configured by
|
|
76
|
-
Fly root-filesystem persistence.
|
package/fly/codex-auth.mjs
DELETED
package/fly/entrypoint.sh
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
mkdir -p /data/home/kai/.codex /data/home/kai/.claude /data/repos /data/logs
|
|
4
|
-
chown kai:kai /data/home/kai /data/home/kai/.codex /data/home/kai/.claude /data/repos /data/logs
|
|
5
|
-
touch /run/kai-runtime-update.lock
|
|
6
|
-
chown root:kai /run/kai-runtime-update.lock
|
|
7
|
-
chmod 660 /run/kai-runtime-update.lock
|
|
8
|
-
# The lock and boot boundary keep installations away from coding/login/preview
|
|
9
|
-
# processes. Failed or interrupted updates keep the previous validated bundle.
|
|
10
|
-
KAI_RUNTIME_ROOT="$(flock /run/kai-runtime-update.lock node /opt/kai-bridge/fly/runtime-update.mjs)"
|
|
11
|
-
export KAI_RUNTIME_ROOT
|
|
12
|
-
exec /usr/bin/supervisord -n -c /opt/kai-bridge/fly/supervisord.conf
|
package/fly/login-codex.sh
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
# Pause the single refresh owner while native device authentication updates it.
|
|
4
|
-
exec 9>/data/codex-login.lock
|
|
5
|
-
flock -n 9 || { echo 'Another Codex sign-in is already in progress.'; exit 1; }
|
|
6
|
-
control='/opt/kai-bridge/fly/supervisord.conf'
|
|
7
|
-
supervisorctl -c "$control" stop codex-auth
|
|
8
|
-
trap 'supervisorctl -c "$control" start codex-auth' EXIT
|
|
9
|
-
sudo -iu kai codex login --device-auth
|
package/fly/runtime-cli.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { runtimeRoot } from './runtime-update.mjs';
|
|
6
|
-
|
|
7
|
-
const root = process.env.KAI_RUNTIME_ROOT || runtimeRoot();
|
|
8
|
-
const name = process.env.KAI_RUNTIME_CLI_NAME || basename(process.argv[1]).replace(/\.mjs$/, '');
|
|
9
|
-
const { harnessBinary } = await import(pathToFileURL(join(root, 'src/harnesses.mjs')));
|
|
10
|
-
const command = name === 'git' ? process.execPath : harnessBinary(name);
|
|
11
|
-
if (!command) { process.stderr.write('The selected Kai runtime is missing this executable.\n'); process.exit(1); }
|
|
12
|
-
const args = name === 'git' ? [join(root, 'src/hosted-git.mjs'), ...process.argv.slice(2)] : process.argv.slice(2);
|
|
13
|
-
const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, KAI_RUNTIME_ROOT: root, DISABLE_AUTOUPDATER: '1' } });
|
|
14
|
-
child.on('error', () => process.exit(1));
|
|
15
|
-
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });
|
package/fly/runtime-cli.sh
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
KAI_RUNTIME_CLI_NAME="${0##*/}"
|
|
4
|
-
export KAI_RUNTIME_CLI_NAME
|
|
5
|
-
# Startup validation has an isolated HOME and already holds the exclusive lock.
|
|
6
|
-
if [ "${KAI_RUNTIME_UPDATE_CHECK:-0}" = '1' ]; then
|
|
7
|
-
exec node /opt/kai-bridge/fly/runtime-cli.mjs "$@"
|
|
8
|
-
fi
|
|
9
|
-
# A native CLI opened through maintenance SSH waits for an in-progress update.
|
|
10
|
-
exec flock --shared /run/kai-runtime-update.lock node /opt/kai-bridge/fly/runtime-cli.mjs "$@"
|
package/fly/runtime-launch.mjs
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
|
-
import { runtimeRoot } from './runtime-update.mjs';
|
|
4
|
-
const entry = { bridge: 'start.mjs', 'codex-auth': 'codex-auth.mjs' }[process.argv[2]];
|
|
5
|
-
if (!entry) throw new Error('Unknown hosted runtime service.');
|
|
6
|
-
const root = process.env.KAI_RUNTIME_ROOT || runtimeRoot();
|
|
7
|
-
process.env.KAI_RUNTIME_ROOT = root;
|
|
8
|
-
await import(pathToFileURL(join(root, 'fly', entry)));
|
package/fly/runtime-update.mjs
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
import { isNewer } from '../src/selfupdate.mjs';
|
|
7
|
-
|
|
8
|
-
const exec = promisify(execFile);
|
|
9
|
-
export const BASE_RUNTIME = '/opt/kai-bridge';
|
|
10
|
-
export const RUNTIME_STORE = '/opt/kai-runtime';
|
|
11
|
-
const CHANNEL = 'https://registry.npmjs.org/@gleapai/kai-bridge/hosted-stable';
|
|
12
|
-
const DAY = 86400_000;
|
|
13
|
-
const version = value => typeof value === 'string' && /^\d+\.\d+\.\d+$/.test(value);
|
|
14
|
-
const read = path => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; } };
|
|
15
|
-
const packageAt = root => read(join(root, 'package.json'));
|
|
16
|
-
export function browserVersion(root) {
|
|
17
|
-
const lock = read(join(root, existsSync(join(root, 'npm-shrinkwrap.json')) ? 'npm-shrinkwrap.json' : 'package-lock.json'));
|
|
18
|
-
return lock.packages?.['node_modules/playwright-core']?.version;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function assertImageCompatibility(candidate, base) {
|
|
22
|
-
// Chromium is installed by the image, outside the replaceable runtime. Never
|
|
23
|
-
// activate a new Playwright revision against the old browser executable.
|
|
24
|
-
if (!browserVersion(candidate) || browserVersion(candidate) !== browserVersion(base)) {
|
|
25
|
-
throw Object.assign(new Error('This release needs a browser image update.'), { code: 'image_required' });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function runtimeRoot(store = RUNTIME_STORE, base = BASE_RUNTIME) {
|
|
30
|
-
const state = read(join(store, 'state.json'));
|
|
31
|
-
const candidate = version(state.active) ? join(store, 'releases', state.active) : base;
|
|
32
|
-
return existsSync(join(candidate, 'package.json')) ? candidate : base;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function save(store, state) {
|
|
36
|
-
mkdirSync(store, { recursive: true });
|
|
37
|
-
const temporary = join(store, `state.${process.pid}.tmp`);
|
|
38
|
-
writeFileSync(temporary, JSON.stringify(state, null, 2) + '\n', { mode: 0o644 });
|
|
39
|
-
renameSync(temporary, join(store, 'state.json'));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function supervisorRunning() {
|
|
43
|
-
const pid = Number(readFileText('/run/supervisord.pid'));
|
|
44
|
-
if (!Number.isSafeInteger(pid) || pid <= 1) return false;
|
|
45
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
46
|
-
}
|
|
47
|
-
const readFileText = path => { try { return readFileSync(path, 'utf8').trim(); } catch { return ''; } };
|
|
48
|
-
|
|
49
|
-
export function requestRuntimeRollback(store = RUNTIME_STORE) {
|
|
50
|
-
const state = read(join(store, 'state.json'));
|
|
51
|
-
if (!state.active || !state.previousVersion) throw new Error('No previous runtime is available.');
|
|
52
|
-
writeFileSync(join(store, 'rollback-next-start'), '1\n', { mode: 0o600 });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function fetchHostedRelease() {
|
|
56
|
-
const response = await fetch(CHANNEL, { signal: AbortSignal.timeout(8000), headers: { accept: 'application/json' } });
|
|
57
|
-
if (response.status === 404) return null; // Channel has not been promoted yet.
|
|
58
|
-
if (!response.ok) throw new Error(`Release registry unavailable (${response.status}).`);
|
|
59
|
-
return response.json();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function installHostedRelease(release, destination, store, base = BASE_RUNTIME) {
|
|
63
|
-
mkdirSync(destination, { recursive: true });
|
|
64
|
-
const home = join(destination, '.install-home'); mkdirSync(home);
|
|
65
|
-
// No customer HOME, OAuth profiles, bootstrap token or npm login is inherited.
|
|
66
|
-
const env = { PATH: process.env.PATH, HOME: home, CI: '1', KAI_RUNTIME_UPDATE_CHECK: '1', npm_config_registry: 'https://registry.npmjs.org', npm_config_cache: join(store, 'cache') };
|
|
67
|
-
try {
|
|
68
|
-
const packed = await exec('npm', ['pack', `@gleapai/kai-bridge@${release.version}`, '--ignore-scripts', '--json', '--pack-destination', destination], { env, timeout: 30_000, maxBuffer: 1024 * 1024 });
|
|
69
|
-
const archive = JSON.parse(packed.stdout)?.[0]?.filename;
|
|
70
|
-
if (archive !== `gleapai-kai-bridge-${release.version}.tgz`) throw new Error('Unexpected runtime archive.');
|
|
71
|
-
await exec('tar', ['-xzf', join(destination, archive), '--strip-components=1', '-C', destination], { timeout: 10_000 });
|
|
72
|
-
const pkg = packageAt(destination), lock = read(join(destination, 'npm-shrinkwrap.json'));
|
|
73
|
-
if (pkg.name !== '@gleapai/kai-bridge' || pkg.version !== release.version || pkg.kaiHostedRuntime?.protocol !== 1 || lock.version !== pkg.version) throw new Error('Release lacks a matching locked runtime.');
|
|
74
|
-
assertImageCompatibility(destination, base);
|
|
75
|
-
await exec('npm', ['ci', '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund'], { cwd: destination, env, timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
76
|
-
await exec(process.execPath, [join(destination, 'scripts/runtime-smoke.mjs')], { cwd: destination, env, timeout: 45_000, maxBuffer: 1024 * 1024 });
|
|
77
|
-
rmSync(join(destination, archive), { force: true });
|
|
78
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Called under flock BEFORE supervisord starts. Running VMs never hot-swap
|
|
82
|
-
* runtimes. Install/check failures and interrupted installs retain the old root. */
|
|
83
|
-
export async function updateHostedRuntime({ store = RUNTIME_STORE, base = BASE_RUNTIME, now = Date.now(), busy = supervisorRunning(), enabled = true,
|
|
84
|
-
fetchRelease = fetchHostedRelease, install = installHostedRelease, log = message => process.stderr.write(`${message}\n`) } = {}) {
|
|
85
|
-
const current = runtimeRoot(store, base);
|
|
86
|
-
if (busy) return current;
|
|
87
|
-
const state = read(join(store, 'state.json'));
|
|
88
|
-
if (existsSync(join(store, 'rollback-next-start')) && state.active && state.previousVersion) {
|
|
89
|
-
save(store, { checkedAt: now, active: state.previous || null, failed: state.active, rolledBackAt: now });
|
|
90
|
-
rmSync(join(store, 'rollback-next-start'), { force: true });
|
|
91
|
-
log('Previous hosted runtime selected; the rejected release is quarantined.');
|
|
92
|
-
return runtimeRoot(store, base);
|
|
93
|
-
}
|
|
94
|
-
if (!enabled) return current;
|
|
95
|
-
if (state.checkedAt && now - state.checkedAt < DAY) return current;
|
|
96
|
-
state.checkedAt = now; save(store, state);
|
|
97
|
-
let release, staging;
|
|
98
|
-
try {
|
|
99
|
-
release = await fetchRelease();
|
|
100
|
-
if (!release || release.name !== '@gleapai/kai-bridge' || release.kaiHostedRuntime?.protocol !== 1 || !version(release.version)) return current;
|
|
101
|
-
if (!isNewer(release.version, packageAt(current).version) || state.failed === release.version) return current;
|
|
102
|
-
staging = join(store, 'releases', `.staging-${release.version}`);
|
|
103
|
-
rmSync(staging, { recursive: true, force: true });
|
|
104
|
-
await install(release, staging, store, base);
|
|
105
|
-
// Recheck admission boundary before activation, including manual invocation.
|
|
106
|
-
if (supervisorRunning()) throw new Error('Machine became busy during the update.');
|
|
107
|
-
const destination = join(store, 'releases', release.version);
|
|
108
|
-
rmSync(destination, { recursive: true, force: true });
|
|
109
|
-
renameSync(staging, destination);
|
|
110
|
-
save(store, { checkedAt: now, active: release.version, previous: state.active || null, previousVersion: packageAt(current).version, activatedAt: now });
|
|
111
|
-
// Keep one rollback bundle. User repositories and auth are outside this store.
|
|
112
|
-
try {
|
|
113
|
-
for (const entry of readdirSync(join(store, 'releases'))) {
|
|
114
|
-
if (version(entry) && entry !== release.version && entry !== state.active) rmSync(join(store, 'releases', entry), { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
rmSync(join(store, 'cache'), { recursive: true, force: true });
|
|
117
|
-
} catch { /* Cleanup cannot invalidate an already-validated activation. */ }
|
|
118
|
-
log(`Hosted runtime ${release.version} validated and selected for this start.`);
|
|
119
|
-
return destination;
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (staging) rmSync(staging, { recursive: true, force: true });
|
|
122
|
-
save(store, { ...state, ...(release?.version && version(release.version) ? { failed: release.version } : {}), error: error.code === 'image_required' ? 'This release needs a browser image update; previous runtime retained.' : 'Runtime update failed; previous release retained.' });
|
|
123
|
-
log('Hosted runtime update failed; starting the previous release.');
|
|
124
|
-
return current;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
129
|
-
if (process.argv.includes('--rollback-next-start')) {
|
|
130
|
-
requestRuntimeRollback(); process.stdout.write('Rollback scheduled. Stop and start the machine from Gleap to apply it.\n');
|
|
131
|
-
} else {
|
|
132
|
-
// stdout is consumed by the entrypoint: emit only the selected absolute path.
|
|
133
|
-
updateHostedRuntime({ enabled: process.env.KAI_HOSTED_RUNTIME_UPDATES !== '0' })
|
|
134
|
-
.then(root => process.stdout.write(`${root}\n`))
|
|
135
|
-
.catch(() => process.stdout.write(`${runtimeRoot()}\n`));
|
|
136
|
-
}
|
|
137
|
-
}
|
package/fly/start.mjs
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { bootstrapHosted } from '../src/hosted.mjs';
|
|
2
|
-
import { BridgeDaemon } from '../src/daemon.mjs';
|
|
3
|
-
await bootstrapHosted();
|
|
4
|
-
const daemon = new BridgeDaemon();
|
|
5
|
-
process.on('SIGTERM', () => { void daemon.stop(); });
|
|
6
|
-
process.on('SIGINT', () => { void daemon.stop(); });
|
|
7
|
-
await daemon.start();
|
package/fly/supervisord.conf
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
[unix_http_server]
|
|
2
|
-
file=/run/supervisor.sock
|
|
3
|
-
chmod=0700
|
|
4
|
-
[supervisord]
|
|
5
|
-
nodaemon=true
|
|
6
|
-
logfile=/data/logs/supervisor.log
|
|
7
|
-
pidfile=/run/supervisord.pid
|
|
8
|
-
[rpcinterface:supervisor]
|
|
9
|
-
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
|
|
10
|
-
[supervisorctl]
|
|
11
|
-
serverurl=unix:///run/supervisor.sock
|
|
12
|
-
[program:kai-bridge]
|
|
13
|
-
command=node /opt/kai-bridge/fly/runtime-launch.mjs bridge
|
|
14
|
-
user=kai
|
|
15
|
-
environment=HOME="/data/home/kai",KAI_HOME="/data/home/kai/.kai",DISPLAY=":99"
|
|
16
|
-
autorestart=unexpected
|
|
17
|
-
stopasgroup=true
|
|
18
|
-
killasgroup=true
|
|
19
|
-
stdout_logfile=/data/logs/bridge.log
|
|
20
|
-
stderr_logfile=/data/logs/bridge-error.log
|
|
21
|
-
[program:codex-auth]
|
|
22
|
-
command=node /opt/kai-bridge/fly/runtime-launch.mjs codex-auth
|
|
23
|
-
user=kai
|
|
24
|
-
environment=HOME="/data/home/kai",CODEX_HOME="/data/home/kai/.codex"
|
|
25
|
-
autorestart=true
|
|
26
|
-
stopasgroup=true
|
|
27
|
-
stdout_logfile=/data/logs/codex-auth.log
|
|
28
|
-
stderr_logfile=/data/logs/codex-auth-error.log
|
|
29
|
-
[program:display]
|
|
30
|
-
command=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -nolisten tcp
|
|
31
|
-
user=kai
|
|
32
|
-
autorestart=true
|
|
33
|
-
[program:desktop]
|
|
34
|
-
command=/usr/bin/fluxbox
|
|
35
|
-
user=kai
|
|
36
|
-
environment=DISPLAY=":99",HOME="/data/home/kai"
|
|
37
|
-
autorestart=true
|
|
38
|
-
[program:vnc]
|
|
39
|
-
command=/usr/bin/x11vnc -display :99 -localhost -forever -shared -nopw -rfbport 5900
|
|
40
|
-
user=kai
|
|
41
|
-
autorestart=true
|
|
42
|
-
[program:browser-login]
|
|
43
|
-
command=/usr/bin/websockify --web=/usr/share/novnc 127.0.0.1:6080 127.0.0.1:5900
|
|
44
|
-
user=kai
|
|
45
|
-
autorestart=true
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { connect } from 'node:net';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { harnessBinary } from './harnesses.mjs';
|
|
5
|
-
if (process.argv[2] !== 'app-server') {
|
|
6
|
-
// Version/login commands remain native. Refresh for coding sessions is owned
|
|
7
|
-
// exclusively by the supervised app-server.
|
|
8
|
-
const child = spawn(harnessBinary('codex'), process.argv.slice(2), { stdio: 'inherit' });
|
|
9
|
-
child.on('error', () => process.exit(1));
|
|
10
|
-
child.on('exit', code => process.exit(code || 0));
|
|
11
|
-
} else {
|
|
12
|
-
const socket = connect('/data/home/kai/.kai/codex.sock');
|
|
13
|
-
process.stdin.pipe(socket); socket.pipe(process.stdout);
|
|
14
|
-
socket.on('error', () => { process.stderr.write('The Codex authentication service is unavailable. Restart it using maintenance SSH.\n'); process.exit(1); });
|
|
15
|
-
socket.on('close', () => process.exit(0));
|
|
16
|
-
}
|
package/src/codex-broker.mjs
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// One native Codex app-server (and therefore one refresh-token owner) per VM.
|
|
2
|
-
// ACP adapters retain independent session configuration and talk JSON-RPC
|
|
3
|
-
// through private Unix sockets. No authentication files are copied per turn.
|
|
4
|
-
import { createServer } from 'node:net';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { createInterface } from 'node:readline';
|
|
7
|
-
import { chmodSync, rmSync, mkdirSync } from 'node:fs';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
9
|
-
import { boundHostedProcess } from './hosted-resources.mjs';
|
|
10
|
-
import { harnessBinary } from './harnesses.mjs';
|
|
11
|
-
|
|
12
|
-
export function startCodexBroker({ socketPath = '/data/home/kai/.kai/codex.sock', command = harnessBinary('codex'), spawnImpl = spawn } = {}) {
|
|
13
|
-
if (!command) throw new Error('The bundled Codex executable is missing.');
|
|
14
|
-
mkdirSync(dirname(socketPath), { recursive: true }); rmSync(socketPath, { force: true });
|
|
15
|
-
const processHandle = spawnImpl(command, ['app-server'], { env: { ...process.env, CODEX_HOME: '/data/home/kai/.codex' }, detached: process.env.KAI_HOSTED === '1', stdio: ['pipe', 'pipe', 'inherit'] });
|
|
16
|
-
boundHostedProcess(processHandle, { limitMb: 1536 });
|
|
17
|
-
const clients = new Set(), pending = new Map(), threadOwners = new Map(), reverse = new Map(), activeTurns = new Map();
|
|
18
|
-
let sequence = 0, initialized, initializeRequest, initializeWaiters = [], notified = false;
|
|
19
|
-
const send = (socket, msg) => { if (!socket.destroyed) socket.write(JSON.stringify(msg) + '\n'); };
|
|
20
|
-
const upstream = msg => processHandle.stdin.write(JSON.stringify(msg) + '\n');
|
|
21
|
-
const server = createServer(socket => {
|
|
22
|
-
clients.add(socket);
|
|
23
|
-
const lines = createInterface({ input: socket });
|
|
24
|
-
lines.on('line', line => {
|
|
25
|
-
let msg; try { msg = JSON.parse(line); } catch { socket.destroy(); return; }
|
|
26
|
-
if (msg.method === 'initialize') {
|
|
27
|
-
if (initialized) { send(socket, { id: msg.id, result: initialized }); return; }
|
|
28
|
-
initializeWaiters.push({ socket, id: msg.id });
|
|
29
|
-
if (!initializeRequest) { initializeRequest = ++sequence; upstream({ ...msg, id: initializeRequest }); }
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (msg.method === 'initialized') { if (!notified) { notified = true; upstream(msg); } return; }
|
|
33
|
-
if (!msg.method && reverse.has(String(msg.id))) {
|
|
34
|
-
const req = reverse.get(String(msg.id));
|
|
35
|
-
if (req.socket !== socket) return;
|
|
36
|
-
reverse.delete(String(msg.id)); upstream({ ...msg, id: req.id }); return;
|
|
37
|
-
}
|
|
38
|
-
const thread = msg.params?.threadId;
|
|
39
|
-
if (thread && threadOwners.has(thread) && threadOwners.get(thread) !== socket && !threadOwners.get(thread).destroyed) {
|
|
40
|
-
if (msg.id != null) send(socket, { id: msg.id, error: { code: -32000, message: 'This thread is active in another session.' } });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (thread) threadOwners.set(thread, socket);
|
|
44
|
-
if (msg.id != null) { const id = ++sequence; pending.set(id, { socket, id: msg.id }); upstream({ ...msg, id }); }
|
|
45
|
-
else upstream(msg);
|
|
46
|
-
});
|
|
47
|
-
socket.once('close', () => {
|
|
48
|
-
clients.delete(socket); lines.close();
|
|
49
|
-
initializeWaiters = initializeWaiters.filter(w => w.socket !== socket);
|
|
50
|
-
for (const [threadId, owner] of threadOwners) if (owner === socket && activeTurns.has(threadId))
|
|
51
|
-
upstream({ id: ++sequence, method: 'turn/interrupt', params: { threadId, turnId: activeTurns.get(threadId) } });
|
|
52
|
-
for (const [key, request] of reverse) if (request.socket === socket) { reverse.delete(key); upstream({ id: request.id, error: { code: -32000, message: 'Session owner disconnected.' } }); }
|
|
53
|
-
});
|
|
54
|
-
socket.on('error', () => socket.destroy());
|
|
55
|
-
});
|
|
56
|
-
createInterface({ input: processHandle.stdout }).on('line', line => {
|
|
57
|
-
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
58
|
-
if (msg.id === initializeRequest && !msg.method) {
|
|
59
|
-
initialized = msg.result;
|
|
60
|
-
for (const waiter of initializeWaiters) send(waiter.socket, { ...msg, id: waiter.id });
|
|
61
|
-
initializeWaiters = []; if (msg.error) initializeRequest = undefined; return;
|
|
62
|
-
}
|
|
63
|
-
if (msg.id != null && !msg.method) {
|
|
64
|
-
const req = pending.get(msg.id); if (!req) return;
|
|
65
|
-
pending.delete(msg.id);
|
|
66
|
-
const thread = msg.result?.thread?.id;
|
|
67
|
-
if (thread) threadOwners.set(thread, req.socket);
|
|
68
|
-
send(req.socket, { ...msg, id: req.id }); return;
|
|
69
|
-
}
|
|
70
|
-
const thread = msg.params?.threadId || msg.params?.thread_id || msg.params?.thread?.id;
|
|
71
|
-
if (thread && msg.method === 'turn/started' && msg.params?.turn?.id) activeTurns.set(thread, msg.params.turn.id);
|
|
72
|
-
if (thread && msg.method === 'turn/completed') activeTurns.delete(thread);
|
|
73
|
-
const socket = thread && threadOwners.get(thread);
|
|
74
|
-
if (socket && !socket.destroyed) {
|
|
75
|
-
if (msg.id != null) { const id = `up-${++sequence}`; reverse.set(id, { socket, id: msg.id }); send(socket, { ...msg, id }); }
|
|
76
|
-
else send(socket, msg);
|
|
77
|
-
} else if (!thread && msg.id == null && /^(account|model|serverStatus)/.test(msg.method || '')) {
|
|
78
|
-
for (const client of clients) send(client, msg);
|
|
79
|
-
} else if (msg.id != null) upstream({ id: msg.id, error: { code: -32000, message: 'Session owner disconnected.' } });
|
|
80
|
-
});
|
|
81
|
-
processHandle.once('exit', () => { for (const client of clients) client.destroy(); server.close(); });
|
|
82
|
-
processHandle.on('error', () => { for (const client of clients) client.destroy(); server.close(); });
|
|
83
|
-
server.listen(socketPath, () => chmodSync(socketPath, 0o600));
|
|
84
|
-
return { close() { for (const client of clients) client.destroy(); server.close(); processHandle.kill('SIGTERM'); rmSync(socketPath, { force: true }); } };
|
|
85
|
-
}
|