remote-codex 0.11.2 → 0.11.3
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/apps/relay-server/dist/index.d.ts +2 -0
- package/apps/relay-server/dist/index.js +1221 -0
- package/apps/supervisor-api/dist/index.js +610 -66
- package/apps/supervisor-web/dist/assets/index-CBIze1VS.css +1 -0
- package/apps/supervisor-web/dist/assets/index-YpGAPjED.js +4 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-CDk3ExRH.js → thread-ui-CF80LEEN.js} +126 -29
- package/apps/supervisor-web/dist/assets/{ui-vendor-CgOZX1B8.js → ui-vendor-CW6egZBG.js} +83 -78
- package/apps/supervisor-web/dist/index.html +4 -4
- package/bin/remote-codex.mjs +73 -16
- package/package.json +2 -1
- package/packages/shared/src/index.ts +154 -0
- package/apps/supervisor-web/dist/assets/index-CbDzXN9T.css +0 -1
- package/apps/supervisor-web/dist/assets/index-DQpHiQXN.js +0 -4
|
@@ -80,6 +80,7 @@ function truncateAutoThreadTitle(value) {
|
|
|
80
80
|
// ../../packages/config/src/index.ts
|
|
81
81
|
var envSchema = z.object({
|
|
82
82
|
NODE_ENV: z.enum(["development", "test", "production"]).optional(),
|
|
83
|
+
REMOTE_CODEX_MODE: z.enum(["local", "server", "relay"]).optional(),
|
|
83
84
|
HOST: z.string().min(1).optional(),
|
|
84
85
|
PORT: z.coerce.number().int().positive().optional(),
|
|
85
86
|
LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error", "fatal"]).optional(),
|
|
@@ -88,6 +89,12 @@ var envSchema = z.object({
|
|
|
88
89
|
APP_VERSION: z.string().min(1).optional(),
|
|
89
90
|
WORKSPACE_ROOT: z.string().optional(),
|
|
90
91
|
DATABASE_URL: z.string().optional(),
|
|
92
|
+
REMOTE_CODEX_ADMIN_USERNAME: z.string().min(1).optional(),
|
|
93
|
+
REMOTE_CODEX_ADMIN_PASSWORD: z.string().min(1).optional(),
|
|
94
|
+
REMOTE_CODEX_SESSION_SECRET: z.string().min(16).optional(),
|
|
95
|
+
REMOTE_CODEX_SESSION_TTL_SECONDS: z.coerce.number().int().positive().optional(),
|
|
96
|
+
REMOTE_CODEX_RELAY_SERVER_URL: z.string().url().optional(),
|
|
97
|
+
REMOTE_CODEX_RELAY_AGENT_TOKEN: z.string().min(1).optional(),
|
|
91
98
|
CODEX_HOME: z.string().optional(),
|
|
92
99
|
CODEX_COMMAND: z.string().min(1).optional(),
|
|
93
100
|
CODEX_APP_SERVER_START_TIMEOUT_MS: z.coerce.number().int().positive().optional(),
|
|
@@ -97,6 +104,31 @@ var envSchema = z.object({
|
|
|
97
104
|
OPENCODE_COMMAND: z.string().min(1).optional(),
|
|
98
105
|
REMOTE_CODEX_ENABLED_AGENT_PROVIDERS: z.string().optional()
|
|
99
106
|
});
|
|
107
|
+
function optionalNonEmpty(value) {
|
|
108
|
+
const normalized = value?.trim();
|
|
109
|
+
return normalized ? normalized : void 0;
|
|
110
|
+
}
|
|
111
|
+
function normalizeOptionalEnv(env) {
|
|
112
|
+
return {
|
|
113
|
+
...env,
|
|
114
|
+
WORKSPACE_ROOT: optionalNonEmpty(env.WORKSPACE_ROOT),
|
|
115
|
+
DATABASE_URL: optionalNonEmpty(env.DATABASE_URL),
|
|
116
|
+
REMOTE_CODEX_ADMIN_USERNAME: optionalNonEmpty(env.REMOTE_CODEX_ADMIN_USERNAME),
|
|
117
|
+
REMOTE_CODEX_ADMIN_PASSWORD: optionalNonEmpty(env.REMOTE_CODEX_ADMIN_PASSWORD),
|
|
118
|
+
REMOTE_CODEX_SESSION_SECRET: optionalNonEmpty(env.REMOTE_CODEX_SESSION_SECRET),
|
|
119
|
+
REMOTE_CODEX_RELAY_SERVER_URL: optionalNonEmpty(env.REMOTE_CODEX_RELAY_SERVER_URL),
|
|
120
|
+
REMOTE_CODEX_RELAY_AGENT_TOKEN: optionalNonEmpty(env.REMOTE_CODEX_RELAY_AGENT_TOKEN),
|
|
121
|
+
CODEX_HOME: optionalNonEmpty(env.CODEX_HOME),
|
|
122
|
+
CODEX_COMMAND: optionalNonEmpty(env.CODEX_COMMAND),
|
|
123
|
+
CLAUDE_HOME: optionalNonEmpty(env.CLAUDE_HOME),
|
|
124
|
+
CLAUDE_COMMAND: optionalNonEmpty(env.CLAUDE_COMMAND),
|
|
125
|
+
OPENCODE_HOME: optionalNonEmpty(env.OPENCODE_HOME),
|
|
126
|
+
OPENCODE_COMMAND: optionalNonEmpty(env.OPENCODE_COMMAND),
|
|
127
|
+
REMOTE_CODEX_ENABLED_AGENT_PROVIDERS: optionalNonEmpty(
|
|
128
|
+
env.REMOTE_CODEX_ENABLED_AGENT_PROVIDERS
|
|
129
|
+
)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
100
132
|
function resolveDatabaseUrl(nodeEnv, value) {
|
|
101
133
|
if (value && value.trim()) {
|
|
102
134
|
return path.resolve(value);
|
|
@@ -107,8 +139,9 @@ function resolveDatabaseUrl(nodeEnv, value) {
|
|
|
107
139
|
return path.resolve(".local", "supervisor-dev.sqlite");
|
|
108
140
|
}
|
|
109
141
|
function loadRuntimeConfig(env = process.env) {
|
|
110
|
-
const parsed = envSchema.parse(env);
|
|
142
|
+
const parsed = envSchema.parse(normalizeOptionalEnv(env));
|
|
111
143
|
const nodeEnv = parsed.NODE_ENV ?? "development";
|
|
144
|
+
const mode = parsed.REMOTE_CODEX_MODE ?? "local";
|
|
112
145
|
const workspaceRoot = parsed.WORKSPACE_ROOT?.trim() ? path.resolve(parsed.WORKSPACE_ROOT) : os.homedir();
|
|
113
146
|
const disableRequestLogging = parsed.DISABLE_REQUEST_LOGGING === void 0 ? nodeEnv === "production" : ["1", "true", "yes", "on"].includes(parsed.DISABLE_REQUEST_LOGGING.toLowerCase());
|
|
114
147
|
const enabledProviders = new Set(
|
|
@@ -119,6 +152,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
119
152
|
const opencodeHome = parsed.OPENCODE_HOME?.trim() ? path.resolve(parsed.OPENCODE_HOME) : path.join(os.homedir(), agentBackendMetadata.opencode.defaultHomeDir);
|
|
120
153
|
return {
|
|
121
154
|
nodeEnv,
|
|
155
|
+
mode,
|
|
122
156
|
host: parsed.HOST ?? "127.0.0.1",
|
|
123
157
|
port: parsed.PORT ?? 8787,
|
|
124
158
|
logLevel: parsed.LOG_LEVEL ?? (nodeEnv === "production" ? "warn" : "info"),
|
|
@@ -127,6 +161,16 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
127
161
|
appVersion: parsed.APP_VERSION ?? "0.1.0",
|
|
128
162
|
workspaceRoot,
|
|
129
163
|
databaseUrl: resolveDatabaseUrl(nodeEnv, parsed.DATABASE_URL),
|
|
164
|
+
auth: {
|
|
165
|
+
adminUsername: parsed.REMOTE_CODEX_ADMIN_USERNAME ?? null,
|
|
166
|
+
adminPassword: parsed.REMOTE_CODEX_ADMIN_PASSWORD ?? null,
|
|
167
|
+
sessionSecret: parsed.REMOTE_CODEX_SESSION_SECRET ?? null,
|
|
168
|
+
sessionTtlSeconds: parsed.REMOTE_CODEX_SESSION_TTL_SECONDS ?? 60 * 60 * 24 * 7
|
|
169
|
+
},
|
|
170
|
+
relay: {
|
|
171
|
+
serverUrl: parsed.REMOTE_CODEX_RELAY_SERVER_URL ?? null,
|
|
172
|
+
agentToken: parsed.REMOTE_CODEX_RELAY_AGENT_TOKEN ?? null
|
|
173
|
+
},
|
|
130
174
|
agentProviders: {
|
|
131
175
|
codex: {
|
|
132
176
|
provider: "codex",
|
|
@@ -21207,6 +21251,7 @@ async function registerSystemRoutes(app2) {
|
|
|
21207
21251
|
return {
|
|
21208
21252
|
appName: app2.services.config.appName,
|
|
21209
21253
|
appVersion: app2.services.config.appVersion,
|
|
21254
|
+
mode: app2.services.config.mode,
|
|
21210
21255
|
host: app2.services.config.host,
|
|
21211
21256
|
port: app2.services.config.port,
|
|
21212
21257
|
workspaceRoot: app2.services.config.workspaceRoot,
|
|
@@ -22450,6 +22495,46 @@ async function registerPluginRoutes(app2) {
|
|
|
22450
22495
|
});
|
|
22451
22496
|
}
|
|
22452
22497
|
|
|
22498
|
+
// src/routes/auth.ts
|
|
22499
|
+
import { z as z8 } from "zod";
|
|
22500
|
+
var loginSchema = z8.object({
|
|
22501
|
+
username: z8.string().min(1),
|
|
22502
|
+
password: z8.string().min(1)
|
|
22503
|
+
});
|
|
22504
|
+
async function registerAuthRoutes(app2) {
|
|
22505
|
+
app2.get("/api/auth/session", async (request) => {
|
|
22506
|
+
return app2.services.authService.verifyRequest(request);
|
|
22507
|
+
});
|
|
22508
|
+
app2.post("/api/auth/login", async (request, reply) => {
|
|
22509
|
+
const body = loginSchema.parse(request.body ?? {});
|
|
22510
|
+
const login = app2.services.authService.login(body);
|
|
22511
|
+
if (!login) {
|
|
22512
|
+
reply.status(401).send({
|
|
22513
|
+
code: "unauthorized",
|
|
22514
|
+
message: "Invalid username or password."
|
|
22515
|
+
});
|
|
22516
|
+
return;
|
|
22517
|
+
}
|
|
22518
|
+
if (login.token) {
|
|
22519
|
+
app2.services.authService.attachSessionCookie(reply, login.token);
|
|
22520
|
+
}
|
|
22521
|
+
return {
|
|
22522
|
+
token: login.token,
|
|
22523
|
+
session: login.session
|
|
22524
|
+
};
|
|
22525
|
+
});
|
|
22526
|
+
app2.post("/api/auth/logout", async (_request, reply) => {
|
|
22527
|
+
app2.services.authService.clearSessionCookie(reply);
|
|
22528
|
+
return {
|
|
22529
|
+
authenticated: false,
|
|
22530
|
+
username: null,
|
|
22531
|
+
expiresAt: null,
|
|
22532
|
+
mode: app2.services.authService.mode,
|
|
22533
|
+
authRequired: app2.services.authService.required
|
|
22534
|
+
};
|
|
22535
|
+
});
|
|
22536
|
+
}
|
|
22537
|
+
|
|
22453
22538
|
// src/provider-host-config-service.ts
|
|
22454
22539
|
import fs18 from "fs/promises";
|
|
22455
22540
|
import path18 from "path";
|
|
@@ -24638,17 +24723,17 @@ var TmuxShellBackend = class {
|
|
|
24638
24723
|
};
|
|
24639
24724
|
|
|
24640
24725
|
// src/routes/shells.ts
|
|
24641
|
-
import { z as
|
|
24726
|
+
import { z as z9 } from "zod";
|
|
24642
24727
|
async function registerShellRoutes(app2, options = {}) {
|
|
24643
|
-
const threadIdParams =
|
|
24644
|
-
const shellIdParams =
|
|
24645
|
-
const createShellSchema =
|
|
24646
|
-
cols:
|
|
24647
|
-
rows:
|
|
24648
|
-
label:
|
|
24728
|
+
const threadIdParams = z9.object({ id: z9.string().uuid() });
|
|
24729
|
+
const shellIdParams = z9.object({ id: z9.string().uuid() });
|
|
24730
|
+
const createShellSchema = z9.object({
|
|
24731
|
+
cols: z9.number().int().positive().optional(),
|
|
24732
|
+
rows: z9.number().int().positive().optional(),
|
|
24733
|
+
label: z9.string().trim().min(1).max(80).optional()
|
|
24649
24734
|
});
|
|
24650
|
-
const updateShellSchema =
|
|
24651
|
-
label:
|
|
24735
|
+
const updateShellSchema = z9.object({
|
|
24736
|
+
label: z9.string().trim().min(1).max(80).nullable().optional()
|
|
24652
24737
|
});
|
|
24653
24738
|
const routeOptions = options.preHandler ? { preHandler: options.preHandler } : {};
|
|
24654
24739
|
app2.get("/api/threads/:id/shell", routeOptions, async (request) => {
|
|
@@ -24865,9 +24950,362 @@ function makeShellErrorEnvelope(shellId, error) {
|
|
|
24865
24950
|
};
|
|
24866
24951
|
}
|
|
24867
24952
|
|
|
24953
|
+
// src/auth.ts
|
|
24954
|
+
import crypto2 from "crypto";
|
|
24955
|
+
var AUTH_COOKIE_NAME = "remote_codex_session";
|
|
24956
|
+
var AuthService = class {
|
|
24957
|
+
required;
|
|
24958
|
+
mode;
|
|
24959
|
+
username;
|
|
24960
|
+
password;
|
|
24961
|
+
secret;
|
|
24962
|
+
sessionTtlSeconds;
|
|
24963
|
+
constructor(config) {
|
|
24964
|
+
this.mode = config.mode;
|
|
24965
|
+
this.required = config.mode === "server" || config.mode === "relay";
|
|
24966
|
+
this.username = config.auth.adminUsername;
|
|
24967
|
+
this.password = config.auth.adminPassword;
|
|
24968
|
+
this.secret = config.auth.sessionSecret;
|
|
24969
|
+
this.sessionTtlSeconds = config.auth.sessionTtlSeconds;
|
|
24970
|
+
if (this.required) {
|
|
24971
|
+
const missing = [
|
|
24972
|
+
this.username ? null : "REMOTE_CODEX_ADMIN_USERNAME",
|
|
24973
|
+
this.password ? null : "REMOTE_CODEX_ADMIN_PASSWORD",
|
|
24974
|
+
this.secret ? null : "REMOTE_CODEX_SESSION_SECRET"
|
|
24975
|
+
].filter(Boolean);
|
|
24976
|
+
if (missing.length > 0) {
|
|
24977
|
+
throw new Error(
|
|
24978
|
+
`${config.mode} mode requires auth configuration: ${missing.join(", ")}.`
|
|
24979
|
+
);
|
|
24980
|
+
}
|
|
24981
|
+
}
|
|
24982
|
+
}
|
|
24983
|
+
login(input) {
|
|
24984
|
+
if (!this.required) {
|
|
24985
|
+
return {
|
|
24986
|
+
token: null,
|
|
24987
|
+
session: {
|
|
24988
|
+
authenticated: true,
|
|
24989
|
+
username: null,
|
|
24990
|
+
expiresAt: null,
|
|
24991
|
+
mode: this.mode,
|
|
24992
|
+
authRequired: false
|
|
24993
|
+
}
|
|
24994
|
+
};
|
|
24995
|
+
}
|
|
24996
|
+
if (!this.username || !this.password || !this.secret) {
|
|
24997
|
+
return null;
|
|
24998
|
+
}
|
|
24999
|
+
if (!constantTimeEqual(input.username, this.username) || !constantTimeEqual(input.password, this.password)) {
|
|
25000
|
+
return null;
|
|
25001
|
+
}
|
|
25002
|
+
return this.createSession(this.username);
|
|
25003
|
+
}
|
|
25004
|
+
verifyRequest(request) {
|
|
25005
|
+
if (!this.required) {
|
|
25006
|
+
return {
|
|
25007
|
+
authenticated: true,
|
|
25008
|
+
username: null,
|
|
25009
|
+
expiresAt: null,
|
|
25010
|
+
mode: this.mode,
|
|
25011
|
+
authRequired: false
|
|
25012
|
+
};
|
|
25013
|
+
}
|
|
25014
|
+
const token = readBearerToken(request) ?? readQueryToken(request) ?? readCookieToken(request);
|
|
25015
|
+
if (!token || !this.secret) {
|
|
25016
|
+
return unauthenticatedSession(this.mode);
|
|
25017
|
+
}
|
|
25018
|
+
return this.verifyToken(token);
|
|
25019
|
+
}
|
|
25020
|
+
attachSessionCookie(reply, token) {
|
|
25021
|
+
reply.header(
|
|
25022
|
+
"set-cookie",
|
|
25023
|
+
`${AUTH_COOKIE_NAME}=${encodeURIComponent(token)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${this.sessionTtlSeconds}`
|
|
25024
|
+
);
|
|
25025
|
+
}
|
|
25026
|
+
clearSessionCookie(reply) {
|
|
25027
|
+
reply.header(
|
|
25028
|
+
"set-cookie",
|
|
25029
|
+
`${AUTH_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`
|
|
25030
|
+
);
|
|
25031
|
+
}
|
|
25032
|
+
createSession(username) {
|
|
25033
|
+
const expiresAtMs = Date.now() + this.sessionTtlSeconds * 1e3;
|
|
25034
|
+
const payload = {
|
|
25035
|
+
username,
|
|
25036
|
+
expiresAt: expiresAtMs,
|
|
25037
|
+
nonce: crypto2.randomBytes(16).toString("base64url")
|
|
25038
|
+
};
|
|
25039
|
+
const payloadText = Buffer.from(JSON.stringify(payload), "utf8").toString(
|
|
25040
|
+
"base64url"
|
|
25041
|
+
);
|
|
25042
|
+
const signature = this.sign(payloadText);
|
|
25043
|
+
return {
|
|
25044
|
+
token: `${payloadText}.${signature}`,
|
|
25045
|
+
session: {
|
|
25046
|
+
authenticated: true,
|
|
25047
|
+
username,
|
|
25048
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
25049
|
+
mode: this.mode,
|
|
25050
|
+
authRequired: this.required
|
|
25051
|
+
}
|
|
25052
|
+
};
|
|
25053
|
+
}
|
|
25054
|
+
verifyToken(token) {
|
|
25055
|
+
const [payloadText, signature, extra] = token.split(".");
|
|
25056
|
+
if (!payloadText || !signature || extra !== void 0) {
|
|
25057
|
+
return unauthenticatedSession(this.mode);
|
|
25058
|
+
}
|
|
25059
|
+
if (!constantTimeEqual(signature, this.sign(payloadText))) {
|
|
25060
|
+
return unauthenticatedSession(this.mode);
|
|
25061
|
+
}
|
|
25062
|
+
let payload;
|
|
25063
|
+
try {
|
|
25064
|
+
payload = JSON.parse(
|
|
25065
|
+
Buffer.from(payloadText, "base64url").toString("utf8")
|
|
25066
|
+
);
|
|
25067
|
+
} catch {
|
|
25068
|
+
return unauthenticatedSession(this.mode);
|
|
25069
|
+
}
|
|
25070
|
+
if (!isSessionPayload(payload)) {
|
|
25071
|
+
return unauthenticatedSession(this.mode);
|
|
25072
|
+
}
|
|
25073
|
+
if (payload.expiresAt <= Date.now()) {
|
|
25074
|
+
return unauthenticatedSession(this.mode);
|
|
25075
|
+
}
|
|
25076
|
+
return {
|
|
25077
|
+
authenticated: true,
|
|
25078
|
+
username: payload.username,
|
|
25079
|
+
expiresAt: new Date(payload.expiresAt).toISOString(),
|
|
25080
|
+
mode: this.mode,
|
|
25081
|
+
authRequired: this.required
|
|
25082
|
+
};
|
|
25083
|
+
}
|
|
25084
|
+
sign(payloadText) {
|
|
25085
|
+
return crypto2.createHmac("sha256", this.secret ?? "").update(payloadText).digest("base64url");
|
|
25086
|
+
}
|
|
25087
|
+
};
|
|
25088
|
+
function unauthorizedPayload() {
|
|
25089
|
+
return {
|
|
25090
|
+
code: "unauthorized",
|
|
25091
|
+
message: "Authentication is required."
|
|
25092
|
+
};
|
|
25093
|
+
}
|
|
25094
|
+
function readBearerToken(request) {
|
|
25095
|
+
const authorization = request.headers.authorization;
|
|
25096
|
+
if (!authorization) {
|
|
25097
|
+
return null;
|
|
25098
|
+
}
|
|
25099
|
+
const match = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
25100
|
+
return match ? match[1].trim() : null;
|
|
25101
|
+
}
|
|
25102
|
+
function readCookieToken(request) {
|
|
25103
|
+
const cookie = request.headers.cookie;
|
|
25104
|
+
if (!cookie) {
|
|
25105
|
+
return null;
|
|
25106
|
+
}
|
|
25107
|
+
const entries = cookie.split(";");
|
|
25108
|
+
for (const entry of entries) {
|
|
25109
|
+
const [name, ...valueParts] = entry.trim().split("=");
|
|
25110
|
+
if (name === AUTH_COOKIE_NAME) {
|
|
25111
|
+
return decodeURIComponent(valueParts.join("="));
|
|
25112
|
+
}
|
|
25113
|
+
}
|
|
25114
|
+
return null;
|
|
25115
|
+
}
|
|
25116
|
+
function readQueryToken(request) {
|
|
25117
|
+
const query = request.query;
|
|
25118
|
+
if (!query || typeof query !== "object" || !("token" in query)) {
|
|
25119
|
+
return null;
|
|
25120
|
+
}
|
|
25121
|
+
const token = query.token;
|
|
25122
|
+
return typeof token === "string" && token.trim() ? token.trim() : null;
|
|
25123
|
+
}
|
|
25124
|
+
function unauthenticatedSession(mode) {
|
|
25125
|
+
return {
|
|
25126
|
+
authenticated: false,
|
|
25127
|
+
username: null,
|
|
25128
|
+
expiresAt: null,
|
|
25129
|
+
mode,
|
|
25130
|
+
authRequired: mode === "server" || mode === "relay"
|
|
25131
|
+
};
|
|
25132
|
+
}
|
|
25133
|
+
function isSessionPayload(value) {
|
|
25134
|
+
return typeof value === "object" && value !== null && "username" in value && typeof value.username === "string" && "expiresAt" in value && typeof value.expiresAt === "number" && "nonce" in value && typeof value.nonce === "string";
|
|
25135
|
+
}
|
|
25136
|
+
function constantTimeEqual(left, right) {
|
|
25137
|
+
const leftBuffer = Buffer.from(left);
|
|
25138
|
+
const rightBuffer = Buffer.from(right);
|
|
25139
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
25140
|
+
return false;
|
|
25141
|
+
}
|
|
25142
|
+
return crypto2.timingSafeEqual(leftBuffer, rightBuffer);
|
|
25143
|
+
}
|
|
25144
|
+
|
|
25145
|
+
// src/relay-tunnel-client.ts
|
|
25146
|
+
var RELAY_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
25147
|
+
var RELAY_RECONNECT_INITIAL_DELAY_MS = 1e3;
|
|
25148
|
+
var RELAY_RECONNECT_MAX_DELAY_MS = 3e4;
|
|
25149
|
+
var RelayTunnelClient = class {
|
|
25150
|
+
constructor(config, handleRequest, handleClientConnected, handleClientMessage) {
|
|
25151
|
+
this.config = config;
|
|
25152
|
+
this.handleRequest = handleRequest;
|
|
25153
|
+
this.handleClientConnected = handleClientConnected;
|
|
25154
|
+
this.handleClientMessage = handleClientMessage;
|
|
25155
|
+
}
|
|
25156
|
+
config;
|
|
25157
|
+
handleRequest;
|
|
25158
|
+
handleClientConnected;
|
|
25159
|
+
handleClientMessage;
|
|
25160
|
+
socket = null;
|
|
25161
|
+
heartbeatHandle = null;
|
|
25162
|
+
reconnectHandle = null;
|
|
25163
|
+
reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
|
|
25164
|
+
stopped = false;
|
|
25165
|
+
relayClientCleanup = /* @__PURE__ */ new Map();
|
|
25166
|
+
validateConfig() {
|
|
25167
|
+
if (!this.config.serverUrl || !this.config.agentToken) {
|
|
25168
|
+
throw new Error(
|
|
25169
|
+
"Relay mode requires REMOTE_CODEX_RELAY_SERVER_URL and REMOTE_CODEX_RELAY_AGENT_TOKEN."
|
|
25170
|
+
);
|
|
25171
|
+
}
|
|
25172
|
+
}
|
|
25173
|
+
start() {
|
|
25174
|
+
this.validateConfig();
|
|
25175
|
+
this.stopped = false;
|
|
25176
|
+
this.clearReconnect();
|
|
25177
|
+
if (this.socket) {
|
|
25178
|
+
return;
|
|
25179
|
+
}
|
|
25180
|
+
const url = new URL("/supervisor/tunnel", this.config.serverUrl ?? void 0);
|
|
25181
|
+
url.searchParams.set("token", this.config.agentToken ?? "");
|
|
25182
|
+
url.searchParams.set("deviceToken", this.config.agentToken ?? "");
|
|
25183
|
+
this.socket = new WebSocket(url);
|
|
25184
|
+
this.socket.addEventListener("open", () => {
|
|
25185
|
+
this.reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
|
|
25186
|
+
this.sendHeartbeat();
|
|
25187
|
+
this.heartbeatHandle = setInterval(() => {
|
|
25188
|
+
this.sendHeartbeat();
|
|
25189
|
+
}, RELAY_HEARTBEAT_INTERVAL_MS);
|
|
25190
|
+
});
|
|
25191
|
+
this.socket.addEventListener("close", () => {
|
|
25192
|
+
this.clearHeartbeat();
|
|
25193
|
+
this.cleanupRelayClients();
|
|
25194
|
+
this.socket = null;
|
|
25195
|
+
this.scheduleReconnect();
|
|
25196
|
+
});
|
|
25197
|
+
this.socket.addEventListener("message", (event) => {
|
|
25198
|
+
void this.handleMessage(String(event.data));
|
|
25199
|
+
});
|
|
25200
|
+
}
|
|
25201
|
+
stop() {
|
|
25202
|
+
this.stopped = true;
|
|
25203
|
+
this.clearHeartbeat();
|
|
25204
|
+
this.clearReconnect();
|
|
25205
|
+
this.cleanupRelayClients();
|
|
25206
|
+
this.socket?.close();
|
|
25207
|
+
this.socket = null;
|
|
25208
|
+
}
|
|
25209
|
+
sendHeartbeat() {
|
|
25210
|
+
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
25211
|
+
return;
|
|
25212
|
+
}
|
|
25213
|
+
this.socket.send(
|
|
25214
|
+
JSON.stringify({
|
|
25215
|
+
type: "relay.heartbeat",
|
|
25216
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
25217
|
+
})
|
|
25218
|
+
);
|
|
25219
|
+
}
|
|
25220
|
+
async handleMessage(rawMessage) {
|
|
25221
|
+
let parsed;
|
|
25222
|
+
try {
|
|
25223
|
+
parsed = JSON.parse(rawMessage);
|
|
25224
|
+
} catch {
|
|
25225
|
+
return;
|
|
25226
|
+
}
|
|
25227
|
+
if (parsed.type !== "relay.request") {
|
|
25228
|
+
if (parsed.type === "relay.client.connected") {
|
|
25229
|
+
const cleanup = this.handleClientConnected(parsed.clientId, (message) => {
|
|
25230
|
+
this.sendClientMessage(parsed.clientId, message);
|
|
25231
|
+
});
|
|
25232
|
+
this.relayClientCleanup.set(parsed.clientId, cleanup);
|
|
25233
|
+
return;
|
|
25234
|
+
}
|
|
25235
|
+
if (parsed.type === "relay.client.disconnected") {
|
|
25236
|
+
this.relayClientCleanup.get(parsed.clientId)?.();
|
|
25237
|
+
this.relayClientCleanup.delete(parsed.clientId);
|
|
25238
|
+
return;
|
|
25239
|
+
}
|
|
25240
|
+
if (parsed.type === "relay.client.message") {
|
|
25241
|
+
await this.handleClientMessage(parsed.clientId, parsed.payload, (message) => {
|
|
25242
|
+
this.sendClientMessage(parsed.clientId, message);
|
|
25243
|
+
});
|
|
25244
|
+
return;
|
|
25245
|
+
}
|
|
25246
|
+
return;
|
|
25247
|
+
}
|
|
25248
|
+
const response = await this.handleRequest(parsed.payload);
|
|
25249
|
+
this.socket?.send(
|
|
25250
|
+
JSON.stringify({
|
|
25251
|
+
type: "relay.response",
|
|
25252
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25253
|
+
requestId: parsed.requestId,
|
|
25254
|
+
payload: response
|
|
25255
|
+
})
|
|
25256
|
+
);
|
|
25257
|
+
}
|
|
25258
|
+
sendClientMessage(clientId, message) {
|
|
25259
|
+
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
25260
|
+
return;
|
|
25261
|
+
}
|
|
25262
|
+
this.socket.send(
|
|
25263
|
+
JSON.stringify({
|
|
25264
|
+
type: "relay.server.message",
|
|
25265
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25266
|
+
clientId,
|
|
25267
|
+
payload: message
|
|
25268
|
+
})
|
|
25269
|
+
);
|
|
25270
|
+
}
|
|
25271
|
+
clearHeartbeat() {
|
|
25272
|
+
if (this.heartbeatHandle) {
|
|
25273
|
+
clearInterval(this.heartbeatHandle);
|
|
25274
|
+
this.heartbeatHandle = null;
|
|
25275
|
+
}
|
|
25276
|
+
}
|
|
25277
|
+
scheduleReconnect() {
|
|
25278
|
+
if (this.stopped || this.reconnectHandle) {
|
|
25279
|
+
return;
|
|
25280
|
+
}
|
|
25281
|
+
const delayMs = this.reconnectDelayMs;
|
|
25282
|
+
this.reconnectDelayMs = Math.min(
|
|
25283
|
+
this.reconnectDelayMs * 2,
|
|
25284
|
+
RELAY_RECONNECT_MAX_DELAY_MS
|
|
25285
|
+
);
|
|
25286
|
+
this.reconnectHandle = setTimeout(() => {
|
|
25287
|
+
this.reconnectHandle = null;
|
|
25288
|
+
this.start();
|
|
25289
|
+
}, delayMs);
|
|
25290
|
+
}
|
|
25291
|
+
clearReconnect() {
|
|
25292
|
+
if (this.reconnectHandle) {
|
|
25293
|
+
clearTimeout(this.reconnectHandle);
|
|
25294
|
+
this.reconnectHandle = null;
|
|
25295
|
+
}
|
|
25296
|
+
}
|
|
25297
|
+
cleanupRelayClients() {
|
|
25298
|
+
for (const [clientId, cleanup] of this.relayClientCleanup) {
|
|
25299
|
+
cleanup();
|
|
25300
|
+
this.relayClientCleanup.delete(clientId);
|
|
25301
|
+
}
|
|
25302
|
+
}
|
|
25303
|
+
};
|
|
25304
|
+
|
|
24868
25305
|
// src/app.ts
|
|
24869
25306
|
var MAX_PROMPT_ATTACHMENTS2 = 10;
|
|
24870
25307
|
var MAX_PROMPT_ATTACHMENT_BYTES2 = 25 * 1024 * 1024;
|
|
25308
|
+
var RELAY_FORWARD_HEADER = "x-remote-codex-relay-forwarded";
|
|
24871
25309
|
var HttpError = class extends Error {
|
|
24872
25310
|
constructor(statusCode, payload) {
|
|
24873
25311
|
super(payload.message);
|
|
@@ -24927,6 +25365,7 @@ function buildApp(options = {}) {
|
|
|
24927
25365
|
const pluginRegistry = new PluginRegistry(builtinPlugins);
|
|
24928
25366
|
const pluginSettingsStore = new PluginSettingsStore(database.db);
|
|
24929
25367
|
const pluginService = new PluginService(pluginRegistry, pluginSettingsStore);
|
|
25368
|
+
const authService = new AuthService(config);
|
|
24930
25369
|
const runtimeBootstrap = options.runtimeBootstrap ?? createAgentRuntimeBootstrap(config);
|
|
24931
25370
|
const repoRoot = findRepoRoot();
|
|
24932
25371
|
const agentRuntimes = options.agentRuntimes ?? runtimeBootstrap.agentRuntimes;
|
|
@@ -24960,6 +25399,16 @@ function buildApp(options = {}) {
|
|
|
24960
25399
|
fileSize: MAX_PROMPT_ATTACHMENT_BYTES2
|
|
24961
25400
|
}
|
|
24962
25401
|
});
|
|
25402
|
+
const backendPluginHost = new BackendPluginHost(app2);
|
|
25403
|
+
backendPluginHost.register(createTerminalPluginBackendContribution());
|
|
25404
|
+
const relaySocketBridge = createRelaySocketBridge(app2, eventBus, backendPluginHost);
|
|
25405
|
+
const relayTunnelClient = config.mode === "relay" ? options.relayTunnelClient ?? new RelayTunnelClient(
|
|
25406
|
+
config.relay,
|
|
25407
|
+
createRelayRequestHandler(app2),
|
|
25408
|
+
relaySocketBridge.handleConnected,
|
|
25409
|
+
relaySocketBridge.handleMessage
|
|
25410
|
+
) : null;
|
|
25411
|
+
relayTunnelClient?.validateConfig();
|
|
24963
25412
|
app2.decorate("services", {
|
|
24964
25413
|
config,
|
|
24965
25414
|
database,
|
|
@@ -24971,10 +25420,29 @@ function buildApp(options = {}) {
|
|
|
24971
25420
|
providerHostConfigService,
|
|
24972
25421
|
pluginRegistry,
|
|
24973
25422
|
pluginService,
|
|
25423
|
+
authService,
|
|
25424
|
+
relayTunnelClient,
|
|
24974
25425
|
repoRoot
|
|
24975
25426
|
});
|
|
24976
|
-
|
|
24977
|
-
|
|
25427
|
+
app2.addHook("onRequest", async (request, reply) => {
|
|
25428
|
+
if (!authService.required) {
|
|
25429
|
+
return;
|
|
25430
|
+
}
|
|
25431
|
+
const requestPath = new URL(request.url, "http://localhost").pathname;
|
|
25432
|
+
if (!requestPath.startsWith("/api/")) {
|
|
25433
|
+
return;
|
|
25434
|
+
}
|
|
25435
|
+
if (requestPath === "/api/auth/login" || requestPath === "/api/auth/logout" || requestPath === "/api/auth/session") {
|
|
25436
|
+
return;
|
|
25437
|
+
}
|
|
25438
|
+
if (config.mode === "relay" && request.headers[RELAY_FORWARD_HEADER] === "1") {
|
|
25439
|
+
return;
|
|
25440
|
+
}
|
|
25441
|
+
const session = authService.verifyRequest(request);
|
|
25442
|
+
if (!session.authenticated) {
|
|
25443
|
+
return reply.status(401).send(unauthorizedPayload());
|
|
25444
|
+
}
|
|
25445
|
+
});
|
|
24978
25446
|
app2.register(async (realtimeApp) => {
|
|
24979
25447
|
await realtimeApp.register(websocket);
|
|
24980
25448
|
realtimeApp.route({
|
|
@@ -24986,69 +25454,32 @@ function buildApp(options = {}) {
|
|
|
24986
25454
|
message: "Upgrade to websocket is required."
|
|
24987
25455
|
});
|
|
24988
25456
|
},
|
|
24989
|
-
wsHandler: (socket) => {
|
|
24990
|
-
const
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
};
|
|
24995
|
-
function send(message) {
|
|
24996
|
-
if (socket.readyState === 1) {
|
|
24997
|
-
socket.send(JSON.stringify(message));
|
|
24998
|
-
}
|
|
25457
|
+
wsHandler: (socket, request) => {
|
|
25458
|
+
const session = authService.verifyRequest(request);
|
|
25459
|
+
if (!session.authenticated) {
|
|
25460
|
+
socket.close(1008, "Authentication is required.");
|
|
25461
|
+
return;
|
|
24999
25462
|
}
|
|
25000
|
-
|
|
25001
|
-
|
|
25002
|
-
|
|
25003
|
-
|
|
25004
|
-
|
|
25005
|
-
|
|
25006
|
-
|
|
25007
|
-
const unsubscribeShell = eventBus.onShellEvent((event) => {
|
|
25008
|
-
send(event);
|
|
25009
|
-
});
|
|
25010
|
-
socket.on("message", async (rawMessage) => {
|
|
25011
|
-
let parsed;
|
|
25012
|
-
try {
|
|
25013
|
-
parsed = JSON.parse(rawMessage.toString());
|
|
25014
|
-
} catch {
|
|
25015
|
-
return;
|
|
25016
|
-
}
|
|
25017
|
-
try {
|
|
25018
|
-
if (parsed.type === "supervisor.ping") {
|
|
25019
|
-
send({
|
|
25020
|
-
type: "supervisor.pong",
|
|
25021
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25022
|
-
payload: {
|
|
25023
|
-
requestTimestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : null
|
|
25024
|
-
}
|
|
25025
|
-
});
|
|
25026
|
-
return;
|
|
25463
|
+
const supervisorSession = createSupervisorSocketSession({
|
|
25464
|
+
app: app2,
|
|
25465
|
+
eventBus,
|
|
25466
|
+
backendPluginHost,
|
|
25467
|
+
send(message) {
|
|
25468
|
+
if (socket.readyState === 1) {
|
|
25469
|
+
socket.send(JSON.stringify(message));
|
|
25027
25470
|
}
|
|
25028
|
-
const handled = await backendPluginHost.handleSocketMessage({
|
|
25029
|
-
app: app2,
|
|
25030
|
-
send,
|
|
25031
|
-
onClose,
|
|
25032
|
-
state: socketState,
|
|
25033
|
-
message: parsed
|
|
25034
|
-
});
|
|
25035
|
-
if (!handled) {
|
|
25036
|
-
return;
|
|
25037
|
-
}
|
|
25038
|
-
} catch {
|
|
25039
|
-
return;
|
|
25040
25471
|
}
|
|
25041
25472
|
});
|
|
25473
|
+
socket.on("message", async (rawMessage) => {
|
|
25474
|
+
await supervisorSession.handleMessage(rawMessage.toString());
|
|
25475
|
+
});
|
|
25042
25476
|
socket.on("close", () => {
|
|
25043
|
-
|
|
25044
|
-
handler();
|
|
25045
|
-
}
|
|
25046
|
-
unsubscribe();
|
|
25047
|
-
unsubscribeShell();
|
|
25477
|
+
supervisorSession.close();
|
|
25048
25478
|
});
|
|
25049
25479
|
}
|
|
25050
25480
|
});
|
|
25051
25481
|
});
|
|
25482
|
+
app2.register(registerAuthRoutes);
|
|
25052
25483
|
app2.register(registerSystemRoutes);
|
|
25053
25484
|
app2.register(registerAgentRuntimeRoutes);
|
|
25054
25485
|
app2.register(registerPluginRoutes);
|
|
@@ -25157,6 +25588,7 @@ function buildApp(options = {}) {
|
|
|
25157
25588
|
});
|
|
25158
25589
|
app2.addHook("onClose", async () => {
|
|
25159
25590
|
await shellService.stop();
|
|
25591
|
+
relayTunnelClient?.stop();
|
|
25160
25592
|
await Promise.all(agentRuntimes.all().map((runtime) => runtime.stop()));
|
|
25161
25593
|
database.sqlite.close();
|
|
25162
25594
|
});
|
|
@@ -25166,6 +25598,7 @@ function buildApp(options = {}) {
|
|
|
25166
25598
|
codexHome: runtimeBootstrap.providerHostHomes.codex ?? null,
|
|
25167
25599
|
repoRoot
|
|
25168
25600
|
});
|
|
25601
|
+
relayTunnelClient?.start();
|
|
25169
25602
|
await Promise.all(agentRuntimes.all().map((runtime) => runtime.start()));
|
|
25170
25603
|
await shellService.syncShellStateOnStartup();
|
|
25171
25604
|
} catch (error) {
|
|
@@ -25181,6 +25614,117 @@ function requestLog(app2, error) {
|
|
|
25181
25614
|
}
|
|
25182
25615
|
app2.log.error({ error }, "Non-error value reached Fastify error handler.");
|
|
25183
25616
|
}
|
|
25617
|
+
function createRelayRequestHandler(app2) {
|
|
25618
|
+
return async function handleRelayRequest(request) {
|
|
25619
|
+
const response = await app2.inject({
|
|
25620
|
+
method: request.method,
|
|
25621
|
+
url: request.path,
|
|
25622
|
+
headers: {
|
|
25623
|
+
...request.headers,
|
|
25624
|
+
[RELAY_FORWARD_HEADER]: "1"
|
|
25625
|
+
},
|
|
25626
|
+
...request.body !== null ? { payload: request.body } : {}
|
|
25627
|
+
});
|
|
25628
|
+
return {
|
|
25629
|
+
statusCode: response.statusCode,
|
|
25630
|
+
headers: relayResponseHeaders(response.headers),
|
|
25631
|
+
body: response.body
|
|
25632
|
+
};
|
|
25633
|
+
};
|
|
25634
|
+
}
|
|
25635
|
+
function createSupervisorSocketSession(input) {
|
|
25636
|
+
const closeHandlers = [];
|
|
25637
|
+
const socketState = /* @__PURE__ */ new Map();
|
|
25638
|
+
const onClose = (handler) => {
|
|
25639
|
+
closeHandlers.push(handler);
|
|
25640
|
+
};
|
|
25641
|
+
input.send({
|
|
25642
|
+
type: "supervisor.connected",
|
|
25643
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
25644
|
+
});
|
|
25645
|
+
const unsubscribeThread = input.eventBus.onThreadEvent((event) => {
|
|
25646
|
+
input.send(event);
|
|
25647
|
+
});
|
|
25648
|
+
const unsubscribeShell = input.eventBus.onShellEvent((event) => {
|
|
25649
|
+
input.send(event);
|
|
25650
|
+
});
|
|
25651
|
+
return {
|
|
25652
|
+
async handleMessage(rawMessage) {
|
|
25653
|
+
let parsed;
|
|
25654
|
+
try {
|
|
25655
|
+
parsed = JSON.parse(rawMessage);
|
|
25656
|
+
} catch {
|
|
25657
|
+
return;
|
|
25658
|
+
}
|
|
25659
|
+
try {
|
|
25660
|
+
if (parsed.type === "supervisor.ping") {
|
|
25661
|
+
input.send({
|
|
25662
|
+
type: "supervisor.pong",
|
|
25663
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25664
|
+
payload: {
|
|
25665
|
+
requestTimestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : null
|
|
25666
|
+
}
|
|
25667
|
+
});
|
|
25668
|
+
return;
|
|
25669
|
+
}
|
|
25670
|
+
await input.backendPluginHost.handleSocketMessage({
|
|
25671
|
+
app: input.app,
|
|
25672
|
+
send: input.send,
|
|
25673
|
+
onClose,
|
|
25674
|
+
state: socketState,
|
|
25675
|
+
message: parsed
|
|
25676
|
+
});
|
|
25677
|
+
} catch {
|
|
25678
|
+
return;
|
|
25679
|
+
}
|
|
25680
|
+
},
|
|
25681
|
+
close() {
|
|
25682
|
+
for (const handler of closeHandlers.splice(0)) {
|
|
25683
|
+
handler();
|
|
25684
|
+
}
|
|
25685
|
+
unsubscribeThread();
|
|
25686
|
+
unsubscribeShell();
|
|
25687
|
+
}
|
|
25688
|
+
};
|
|
25689
|
+
}
|
|
25690
|
+
function createRelaySocketBridge(app2, eventBus, backendPluginHost) {
|
|
25691
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
25692
|
+
return {
|
|
25693
|
+
handleConnected(clientId, send) {
|
|
25694
|
+
const existing = sessions.get(clientId);
|
|
25695
|
+
existing?.close();
|
|
25696
|
+
const session = createSupervisorSocketSession({
|
|
25697
|
+
app: app2,
|
|
25698
|
+
eventBus,
|
|
25699
|
+
backendPluginHost,
|
|
25700
|
+
send
|
|
25701
|
+
});
|
|
25702
|
+
sessions.set(clientId, session);
|
|
25703
|
+
return () => {
|
|
25704
|
+
session.close();
|
|
25705
|
+
sessions.delete(clientId);
|
|
25706
|
+
};
|
|
25707
|
+
},
|
|
25708
|
+
async handleMessage(clientId, message) {
|
|
25709
|
+
const session = sessions.get(clientId);
|
|
25710
|
+
if (!session) {
|
|
25711
|
+
return;
|
|
25712
|
+
}
|
|
25713
|
+
await session.handleMessage(JSON.stringify(message));
|
|
25714
|
+
}
|
|
25715
|
+
};
|
|
25716
|
+
}
|
|
25717
|
+
function relayResponseHeaders(headers) {
|
|
25718
|
+
const output = {};
|
|
25719
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
25720
|
+
if (Array.isArray(value)) {
|
|
25721
|
+
output[name] = value.join(", ");
|
|
25722
|
+
} else if (value !== void 0) {
|
|
25723
|
+
output[name] = String(value);
|
|
25724
|
+
}
|
|
25725
|
+
}
|
|
25726
|
+
return output;
|
|
25727
|
+
}
|
|
25184
25728
|
|
|
25185
25729
|
// src/index.ts
|
|
25186
25730
|
if (fs25.existsSync(".env")) {
|