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.
@@ -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 z8 } from "zod";
24726
+ import { z as z9 } from "zod";
24642
24727
  async function registerShellRoutes(app2, options = {}) {
24643
- const threadIdParams = z8.object({ id: z8.string().uuid() });
24644
- const shellIdParams = z8.object({ id: z8.string().uuid() });
24645
- const createShellSchema = z8.object({
24646
- cols: z8.number().int().positive().optional(),
24647
- rows: z8.number().int().positive().optional(),
24648
- label: z8.string().trim().min(1).max(80).optional()
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 = z8.object({
24651
- label: z8.string().trim().min(1).max(80).nullable().optional()
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
- const backendPluginHost = new BackendPluginHost(app2);
24977
- backendPluginHost.register(createTerminalPluginBackendContribution());
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 closeHandlers = [];
24991
- const socketState = /* @__PURE__ */ new Map();
24992
- const onClose = (handler) => {
24993
- closeHandlers.push(handler);
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
- send({
25001
- type: "supervisor.connected",
25002
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
25003
- });
25004
- const unsubscribe = eventBus.onThreadEvent((event) => {
25005
- send(event);
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
- for (const handler of closeHandlers.splice(0)) {
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")) {