remote-codex 0.11.31 → 0.11.33
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 +12 -4
- package/apps/relay-server/dist/index.js +2956 -164
- package/apps/supervisor-api/dist/index.js +497 -54
- package/apps/supervisor-web/dist/assets/index-CdjTdnJt.css +1 -0
- package/apps/supervisor-web/dist/assets/index-DIi8wTEY.js +21 -0
- package/apps/supervisor-web/dist/assets/thread-ui-B9eC2H4u.js +3677 -0
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +6 -3
- package/package.json +1 -1
- package/packages/agent-runtime/src/types.ts +13 -0
- package/packages/claude/src/runtimeAdapter.test.ts +121 -1
- package/packages/claude/src/runtimeAdapter.ts +131 -0
- package/packages/codex/src/appServerManager.ts +16 -0
- package/packages/codex/src/runtimeAdapter.test.ts +44 -0
- package/packages/codex/src/runtimeAdapter.ts +66 -0
- package/packages/db/migrations/0029_thread_turn_delivery.sql +19 -0
- package/packages/db/src/repositories.ts +96 -1
- package/packages/db/src/schema.ts +20 -0
- package/packages/shared/src/index.ts +199 -6
- package/scripts/run-web-service.mjs +2 -2
- package/scripts/service-manager.mjs +2 -2
- package/apps/supervisor-web/dist/assets/index-BkwJP0KB.js +0 -6
- package/apps/supervisor-web/dist/assets/index-CJFMmjP5.css +0 -1
- package/apps/supervisor-web/dist/assets/thread-ui-BOqbjoiB.js +0 -3677
|
@@ -9,6 +9,7 @@ import fs2 from "fs";
|
|
|
9
9
|
import fsp from "fs/promises";
|
|
10
10
|
import path2 from "path";
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
12
|
+
import crypto3 from "crypto";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
|
|
14
15
|
// src/request-broker.ts
|
|
@@ -59,6 +60,282 @@ var RelayRequestBroker = class {
|
|
|
59
60
|
}
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
// src/hosted-sandbox-provider.ts
|
|
64
|
+
var HostedSandboxProviderError = class extends Error {
|
|
65
|
+
constructor(code, message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.code = code;
|
|
68
|
+
}
|
|
69
|
+
code;
|
|
70
|
+
};
|
|
71
|
+
var DisabledHostedSandboxProvider = class {
|
|
72
|
+
async capability() {
|
|
73
|
+
return {
|
|
74
|
+
provider: "disabled",
|
|
75
|
+
configured: false,
|
|
76
|
+
reachable: false,
|
|
77
|
+
available: false,
|
|
78
|
+
reasonCode: "hosted_sandbox_disabled",
|
|
79
|
+
reason: "Hosted supervisor VMs are not configured on this relay.",
|
|
80
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
inventory() {
|
|
84
|
+
return this.disabled();
|
|
85
|
+
}
|
|
86
|
+
createCredential(_openaiApiKey, _idempotencyKey) {
|
|
87
|
+
return this.disabled();
|
|
88
|
+
}
|
|
89
|
+
createCodexCredential(_files, _idempotencyKey) {
|
|
90
|
+
return this.disabled();
|
|
91
|
+
}
|
|
92
|
+
deleteCredential(_credentialRef, _idempotencyKey) {
|
|
93
|
+
return this.disabled();
|
|
94
|
+
}
|
|
95
|
+
create(_input, _idempotencyKey) {
|
|
96
|
+
return this.disabled();
|
|
97
|
+
}
|
|
98
|
+
status(_id) {
|
|
99
|
+
return this.disabled();
|
|
100
|
+
}
|
|
101
|
+
start(_id, _idempotencyKey) {
|
|
102
|
+
return this.disabled();
|
|
103
|
+
}
|
|
104
|
+
stop(_id, _idempotencyKey) {
|
|
105
|
+
return this.disabled();
|
|
106
|
+
}
|
|
107
|
+
snapshot(_id, _name, _idempotencyKey) {
|
|
108
|
+
return this.disabled();
|
|
109
|
+
}
|
|
110
|
+
delete(_id, _idempotencyKey) {
|
|
111
|
+
return this.disabled();
|
|
112
|
+
}
|
|
113
|
+
provision(_input, _idempotencyKey) {
|
|
114
|
+
return this.disabled();
|
|
115
|
+
}
|
|
116
|
+
readCodexFiles(_id) {
|
|
117
|
+
return this.disabled();
|
|
118
|
+
}
|
|
119
|
+
writeCodexFiles(_id, _files, _idempotencyKey) {
|
|
120
|
+
return this.disabled();
|
|
121
|
+
}
|
|
122
|
+
disabled() {
|
|
123
|
+
return Promise.reject(new Error("Hosted sandbox provider is disabled."));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var IncusHostedSandboxProvider = class {
|
|
127
|
+
constructor(config2) {
|
|
128
|
+
this.config = config2;
|
|
129
|
+
}
|
|
130
|
+
config;
|
|
131
|
+
async capability(signal) {
|
|
132
|
+
const result = await this.request("/v1/capability", signal ? { signal } : {});
|
|
133
|
+
const available = result.available && result.credentialStoreReady === true;
|
|
134
|
+
return {
|
|
135
|
+
provider: "incus",
|
|
136
|
+
configured: true,
|
|
137
|
+
reachable: true,
|
|
138
|
+
available,
|
|
139
|
+
reasonCode: available ? null : "incus_host_agent_not_ready",
|
|
140
|
+
reason: available ? null : "Incus or encrypted credential storage is not ready.",
|
|
141
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
142
|
+
...result.limits ? { limits: result.limits } : {},
|
|
143
|
+
...result.capacity ? { capacity: result.capacity } : {},
|
|
144
|
+
...result.metrics ? { metrics: result.metrics } : {},
|
|
145
|
+
...result.alerts ? { alerts: result.alerts } : {}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
inventory() {
|
|
149
|
+
return this.request("/v1/inventory");
|
|
150
|
+
}
|
|
151
|
+
async createCredential(openaiApiKey, idempotencyKey) {
|
|
152
|
+
const result = await this.request(
|
|
153
|
+
"/v1/credentials",
|
|
154
|
+
{ method: "POST", idempotencyKey, body: { openaiApiKey } }
|
|
155
|
+
);
|
|
156
|
+
return result.credentialRef;
|
|
157
|
+
}
|
|
158
|
+
async createCodexCredential(files, idempotencyKey) {
|
|
159
|
+
const result = await this.request(
|
|
160
|
+
"/v1/credentials",
|
|
161
|
+
{ method: "POST", idempotencyKey, body: { codexFiles: files } }
|
|
162
|
+
);
|
|
163
|
+
return result.credentialRef;
|
|
164
|
+
}
|
|
165
|
+
async deleteCredential(credentialRef, idempotencyKey) {
|
|
166
|
+
await this.request(`/v1/credentials/${encodeURIComponent(credentialRef)}`, {
|
|
167
|
+
method: "DELETE",
|
|
168
|
+
idempotencyKey
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
create(input, idempotencyKey) {
|
|
172
|
+
return this.request("/v1/instances", {
|
|
173
|
+
method: "POST",
|
|
174
|
+
idempotencyKey,
|
|
175
|
+
body: input
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
status(id) {
|
|
179
|
+
return this.request(
|
|
180
|
+
`/v1/instances/${encodeURIComponent(id)}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
start(id, idempotencyKey) {
|
|
184
|
+
return this.instanceMutation(id, "start", idempotencyKey);
|
|
185
|
+
}
|
|
186
|
+
stop(id, idempotencyKey) {
|
|
187
|
+
return this.instanceMutation(id, "stop", idempotencyKey);
|
|
188
|
+
}
|
|
189
|
+
async snapshot(id, name, idempotencyKey) {
|
|
190
|
+
await this.request(`/v1/instances/${encodeURIComponent(id)}/snapshots`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
idempotencyKey,
|
|
193
|
+
body: { name }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async delete(id, idempotencyKey) {
|
|
197
|
+
await this.request(`/v1/instances/${encodeURIComponent(id)}`, {
|
|
198
|
+
method: "DELETE",
|
|
199
|
+
idempotencyKey
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async provision(input, idempotencyKey) {
|
|
203
|
+
await this.request(
|
|
204
|
+
`/v1/instances/${encodeURIComponent(input.id)}/provision`,
|
|
205
|
+
{
|
|
206
|
+
method: "POST",
|
|
207
|
+
idempotencyKey,
|
|
208
|
+
body: {
|
|
209
|
+
relayServerUrl: input.relayServerUrl,
|
|
210
|
+
relayAgentToken: input.relayAgentToken,
|
|
211
|
+
credentialRef: input.credentialRef,
|
|
212
|
+
codexConfig: input.codexConfig,
|
|
213
|
+
localAdminUsername: input.localAdminUsername ?? "admin"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
readCodexFiles(id) {
|
|
219
|
+
return this.request(
|
|
220
|
+
`/v1/instances/${encodeURIComponent(id)}/backends/codex/files`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
async writeCodexFiles(id, files, idempotencyKey) {
|
|
224
|
+
await this.request(
|
|
225
|
+
`/v1/instances/${encodeURIComponent(id)}/backends/codex/files`,
|
|
226
|
+
{ method: "PUT", idempotencyKey, body: files }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
instanceMutation(id, action, idempotencyKey) {
|
|
230
|
+
return this.request(
|
|
231
|
+
`/v1/instances/${encodeURIComponent(id)}/${action}`,
|
|
232
|
+
{ method: "POST", idempotencyKey }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
async request(pathname, options = {}) {
|
|
236
|
+
if (!this.config.agentUrl || !this.config.agentToken) {
|
|
237
|
+
throw new Error("Incus host-agent is not configured.");
|
|
238
|
+
}
|
|
239
|
+
const headers = {
|
|
240
|
+
authorization: `Bearer ${this.config.agentToken}`
|
|
241
|
+
};
|
|
242
|
+
if (options.idempotencyKey) {
|
|
243
|
+
headers["idempotency-key"] = options.idempotencyKey;
|
|
244
|
+
}
|
|
245
|
+
if (options.body !== void 0) {
|
|
246
|
+
headers["content-type"] = "application/json";
|
|
247
|
+
}
|
|
248
|
+
const request = {
|
|
249
|
+
method: options.method ?? "GET",
|
|
250
|
+
headers,
|
|
251
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
252
|
+
...options.signal ? { signal: options.signal } : {}
|
|
253
|
+
};
|
|
254
|
+
const response = await fetch(
|
|
255
|
+
`${this.config.agentUrl.replace(/\/$/, "")}${pathname}`,
|
|
256
|
+
request
|
|
257
|
+
);
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const payload = await response.json().catch(() => null);
|
|
260
|
+
const detail = typeof payload?.message === "string" && payload.message.trim() ? payload.message.trim() : `Incus host-agent request failed with ${response.status}.`;
|
|
261
|
+
throw new HostedSandboxProviderError(
|
|
262
|
+
typeof payload?.code === "string" ? payload.code : "provider_request_failed",
|
|
263
|
+
detail
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return await response.json();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
function createHostedSandboxProvider(config2) {
|
|
270
|
+
return config2.provider === "incus" ? new IncusHostedSandboxProvider(config2) : new DisabledHostedSandboxProvider();
|
|
271
|
+
}
|
|
272
|
+
var HostedSandboxCapabilityService = class {
|
|
273
|
+
constructor(provider, options) {
|
|
274
|
+
this.provider = provider;
|
|
275
|
+
this.options = options;
|
|
276
|
+
}
|
|
277
|
+
provider;
|
|
278
|
+
options;
|
|
279
|
+
consecutiveFailures = 0;
|
|
280
|
+
circuitOpenedAt = null;
|
|
281
|
+
async read() {
|
|
282
|
+
const now = this.options.now ?? Date.now;
|
|
283
|
+
const failureThreshold = this.options.failureThreshold ?? 2;
|
|
284
|
+
const circuitResetMs = this.options.circuitResetMs ?? 3e4;
|
|
285
|
+
if (this.circuitOpenedAt !== null && now() - this.circuitOpenedAt < circuitResetMs) {
|
|
286
|
+
return unavailableCapability(
|
|
287
|
+
"hosted_provider_circuit_open",
|
|
288
|
+
"Hosted supervisor VM operations are temporarily unavailable after repeated provider failures."
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (this.circuitOpenedAt !== null) {
|
|
292
|
+
this.circuitOpenedAt = null;
|
|
293
|
+
this.consecutiveFailures = 0;
|
|
294
|
+
}
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
let timeout = null;
|
|
297
|
+
try {
|
|
298
|
+
const capability = await Promise.race([
|
|
299
|
+
this.provider.capability(controller.signal),
|
|
300
|
+
new Promise((_resolve, reject) => {
|
|
301
|
+
timeout = setTimeout(() => {
|
|
302
|
+
controller.abort();
|
|
303
|
+
reject(new Error("Hosted sandbox provider request timed out."));
|
|
304
|
+
}, this.options.timeoutMs);
|
|
305
|
+
})
|
|
306
|
+
]);
|
|
307
|
+
this.consecutiveFailures = 0;
|
|
308
|
+
this.circuitOpenedAt = null;
|
|
309
|
+
return capability;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
this.consecutiveFailures += 1;
|
|
312
|
+
if (this.consecutiveFailures >= failureThreshold) {
|
|
313
|
+
this.circuitOpenedAt = now();
|
|
314
|
+
}
|
|
315
|
+
const timedOut = controller.signal.aborted;
|
|
316
|
+
return unavailableCapability(
|
|
317
|
+
timedOut ? "hosted_provider_timeout" : "hosted_provider_unreachable",
|
|
318
|
+
timedOut ? "The hosted supervisor VM provider did not respond in time." : "The hosted supervisor VM provider could not be reached."
|
|
319
|
+
);
|
|
320
|
+
} finally {
|
|
321
|
+
if (timeout) {
|
|
322
|
+
clearTimeout(timeout);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
function unavailableCapability(reasonCode, reason) {
|
|
328
|
+
return {
|
|
329
|
+
provider: "incus",
|
|
330
|
+
configured: true,
|
|
331
|
+
reachable: false,
|
|
332
|
+
available: false,
|
|
333
|
+
reasonCode,
|
|
334
|
+
reason,
|
|
335
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
62
339
|
// src/relay-store.ts
|
|
63
340
|
import crypto from "crypto";
|
|
64
341
|
import fs from "fs";
|
|
@@ -75,6 +352,7 @@ var RelayStore = class _RelayStore {
|
|
|
75
352
|
this.sqlite.pragma("foreign_keys = ON");
|
|
76
353
|
this.migrate();
|
|
77
354
|
this.importLegacyJson(legacyJsonPath);
|
|
355
|
+
this.migrateHostedSandboxMembers();
|
|
78
356
|
this.ensureRegistrationSetting(registrationEnabled);
|
|
79
357
|
}
|
|
80
358
|
databasePath;
|
|
@@ -105,7 +383,11 @@ var RelayStore = class _RelayStore {
|
|
|
105
383
|
}
|
|
106
384
|
register(input) {
|
|
107
385
|
if (!this.registrationEnabled()) {
|
|
108
|
-
throw new RelayStoreError(
|
|
386
|
+
throw new RelayStoreError(
|
|
387
|
+
403,
|
|
388
|
+
"forbidden",
|
|
389
|
+
"Registration is currently disabled."
|
|
390
|
+
);
|
|
109
391
|
}
|
|
110
392
|
const user = this.createStoredUser({
|
|
111
393
|
email: input.email,
|
|
@@ -118,21 +400,41 @@ var RelayStore = class _RelayStore {
|
|
|
118
400
|
}
|
|
119
401
|
requestRegistrationApproval(input) {
|
|
120
402
|
if (!this.registrationEnabled()) {
|
|
121
|
-
throw new RelayStoreError(
|
|
403
|
+
throw new RelayStoreError(
|
|
404
|
+
403,
|
|
405
|
+
"forbidden",
|
|
406
|
+
"Registration is currently disabled."
|
|
407
|
+
);
|
|
122
408
|
}
|
|
123
409
|
const email = input.email.trim().toLowerCase();
|
|
124
410
|
const username = normalizeUsername(input.username);
|
|
125
411
|
if (!email.includes("@")) {
|
|
126
|
-
throw new RelayStoreError(
|
|
412
|
+
throw new RelayStoreError(
|
|
413
|
+
400,
|
|
414
|
+
"bad_request",
|
|
415
|
+
"A valid email address is required."
|
|
416
|
+
);
|
|
127
417
|
}
|
|
128
418
|
if (username.length < 3) {
|
|
129
|
-
throw new RelayStoreError(
|
|
419
|
+
throw new RelayStoreError(
|
|
420
|
+
400,
|
|
421
|
+
"bad_request",
|
|
422
|
+
"Username must be at least 3 characters."
|
|
423
|
+
);
|
|
130
424
|
}
|
|
131
425
|
if (input.password.length < 8) {
|
|
132
|
-
throw new RelayStoreError(
|
|
426
|
+
throw new RelayStoreError(
|
|
427
|
+
400,
|
|
428
|
+
"bad_request",
|
|
429
|
+
"Password must be at least 8 characters."
|
|
430
|
+
);
|
|
133
431
|
}
|
|
134
432
|
if (this.getUserByIdentifier(email) || this.getUserByUsername(username)) {
|
|
135
|
-
throw new RelayStoreError(
|
|
433
|
+
throw new RelayStoreError(
|
|
434
|
+
409,
|
|
435
|
+
"conflict",
|
|
436
|
+
"A user with that email or username already exists."
|
|
437
|
+
);
|
|
136
438
|
}
|
|
137
439
|
const existing = this.rowToPendingRegistration(
|
|
138
440
|
this.sqlite.prepare(
|
|
@@ -158,19 +460,64 @@ var RelayStore = class _RelayStore {
|
|
|
158
460
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
461
|
status: "pending",
|
|
160
462
|
reviewedAt: null,
|
|
161
|
-
reviewedByUserId: null
|
|
463
|
+
reviewedByUserId: null,
|
|
464
|
+
provider: input.provider ?? "password",
|
|
465
|
+
providerSubject: input.providerSubject ?? null
|
|
162
466
|
};
|
|
163
467
|
this.insertPendingRegistration(record);
|
|
164
468
|
return this.publicPendingRegistration(record);
|
|
165
469
|
}
|
|
470
|
+
authenticateExternalIdentity(identity, approvalRequired) {
|
|
471
|
+
const linked = this.sqlite.prepare(
|
|
472
|
+
"SELECT user_id FROM relay_user_identities WHERE provider = ? AND provider_subject = ?"
|
|
473
|
+
).get(identity.provider, identity.subject);
|
|
474
|
+
if (linked) {
|
|
475
|
+
const user2 = this.requireUser(linked.user_id);
|
|
476
|
+
if (!user2.enabled) throw new RelayStoreError(401, "unauthorized", "This relay account is disabled.");
|
|
477
|
+
return { kind: "login", result: this.createLoginResult(user2) };
|
|
478
|
+
}
|
|
479
|
+
if (!this.registrationEnabled()) {
|
|
480
|
+
throw new RelayStoreError(403, "forbidden", "Registration is currently disabled.");
|
|
481
|
+
}
|
|
482
|
+
if (this.getUserByIdentifier(identity.email)) {
|
|
483
|
+
throw new RelayStoreError(409, "conflict", "An account with this email already exists. Sign in with its current method before linking OAuth.");
|
|
484
|
+
}
|
|
485
|
+
const username = this.availableUsername(identity.username);
|
|
486
|
+
const password = crypto.randomBytes(32).toString("base64url");
|
|
487
|
+
if (approvalRequired) {
|
|
488
|
+
const request = this.requestRegistrationApproval({
|
|
489
|
+
email: identity.email,
|
|
490
|
+
username,
|
|
491
|
+
password,
|
|
492
|
+
provider: identity.provider,
|
|
493
|
+
providerSubject: identity.subject
|
|
494
|
+
});
|
|
495
|
+
return { kind: "pending", request };
|
|
496
|
+
}
|
|
497
|
+
const user = this.createStoredUser({ email: identity.email, username, password, role: "user" });
|
|
498
|
+
const create = this.sqlite.transaction(() => {
|
|
499
|
+
this.insertUser(user);
|
|
500
|
+
this.insertIdentity(user.id, identity.provider, identity.subject, identity.email);
|
|
501
|
+
});
|
|
502
|
+
create();
|
|
503
|
+
return { kind: "login", result: this.createLoginResult(user) };
|
|
504
|
+
}
|
|
166
505
|
login(input) {
|
|
167
506
|
const normalizedIdentifier = input.identifier.trim().toLowerCase();
|
|
168
507
|
const user = this.getUserByIdentifier(normalizedIdentifier);
|
|
169
508
|
if (!user || !user.enabled) {
|
|
170
|
-
throw new RelayStoreError(
|
|
509
|
+
throw new RelayStoreError(
|
|
510
|
+
401,
|
|
511
|
+
"unauthorized",
|
|
512
|
+
"Invalid username or password."
|
|
513
|
+
);
|
|
171
514
|
}
|
|
172
515
|
if (!verifySecret(input.password, user.passwordSalt, user.passwordHash)) {
|
|
173
|
-
throw new RelayStoreError(
|
|
516
|
+
throw new RelayStoreError(
|
|
517
|
+
401,
|
|
518
|
+
"unauthorized",
|
|
519
|
+
"Invalid username or password."
|
|
520
|
+
);
|
|
174
521
|
}
|
|
175
522
|
return this.createLoginResult(user);
|
|
176
523
|
}
|
|
@@ -210,12 +557,446 @@ var RelayStore = class _RelayStore {
|
|
|
210
557
|
token
|
|
211
558
|
};
|
|
212
559
|
}
|
|
560
|
+
createHostedSandboxRequested(input) {
|
|
561
|
+
const admin = this.requireUser(input.createdByAdminUserId);
|
|
562
|
+
const assignedUsers = this.requireHostedSandboxMembers(
|
|
563
|
+
input.assignedUserIds
|
|
564
|
+
);
|
|
565
|
+
const sandboxId = crypto.randomUUID();
|
|
566
|
+
const operationId = crypto.randomUUID();
|
|
567
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
568
|
+
let deviceResult = null;
|
|
569
|
+
const create = this.sqlite.transaction(() => {
|
|
570
|
+
deviceResult = this.createDevice(admin.id, {
|
|
571
|
+
name: input.deviceName
|
|
572
|
+
});
|
|
573
|
+
this.sqlite.prepare(
|
|
574
|
+
`
|
|
575
|
+
INSERT INTO relay_hosted_sandboxes (
|
|
576
|
+
id, device_id, assigned_user_id, created_by_admin_user_id,
|
|
577
|
+
provider, provider_instance_id, image_version,
|
|
578
|
+
cpu_count, memory_mib, disk_gib, status, credential_ref,
|
|
579
|
+
codex_config_json,
|
|
580
|
+
last_error_code, last_error_message, created_at, updated_at
|
|
581
|
+
) VALUES (?, ?, ?, ?, 'incus', NULL, ?, ?, ?, ?, 'requested', ?, ?, NULL, NULL, ?, ?)
|
|
582
|
+
`
|
|
583
|
+
).run(
|
|
584
|
+
sandboxId,
|
|
585
|
+
deviceResult.device.id,
|
|
586
|
+
admin.id,
|
|
587
|
+
input.createdByAdminUserId,
|
|
588
|
+
input.imageVersion,
|
|
589
|
+
input.resources.cpuCount,
|
|
590
|
+
input.resources.memoryMiB,
|
|
591
|
+
input.resources.diskGiB,
|
|
592
|
+
input.credentialRef,
|
|
593
|
+
JSON.stringify(input.codexConfig ?? parseHostedCodexConfig(null)),
|
|
594
|
+
now,
|
|
595
|
+
now
|
|
596
|
+
);
|
|
597
|
+
const insertMember = this.sqlite.prepare(
|
|
598
|
+
`INSERT INTO relay_hosted_sandbox_members
|
|
599
|
+
(sandbox_id, user_id, position, created_at) VALUES (?, ?, ?, ?)`
|
|
600
|
+
);
|
|
601
|
+
assignedUsers.forEach(
|
|
602
|
+
(user, position) => insertMember.run(sandboxId, user.id, position, now)
|
|
603
|
+
);
|
|
604
|
+
this.insertHostedOperation({
|
|
605
|
+
id: operationId,
|
|
606
|
+
sandboxId,
|
|
607
|
+
action: "create",
|
|
608
|
+
status: "pending",
|
|
609
|
+
errorCode: null,
|
|
610
|
+
errorMessage: null,
|
|
611
|
+
createdAt: now,
|
|
612
|
+
updatedAt: now
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
create();
|
|
616
|
+
const context = this.getHostedProvisionContext(sandboxId);
|
|
617
|
+
if (!context || !deviceResult) {
|
|
618
|
+
throw new Error("Hosted sandbox transaction did not persist.");
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
sandbox: this.getHostedSandboxDetail(sandboxId),
|
|
622
|
+
operation: this.getHostedOperation(operationId),
|
|
623
|
+
deviceToken: context.deviceToken
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
listHostedSandboxes() {
|
|
627
|
+
return this.sqlite.prepare(
|
|
628
|
+
"SELECT * FROM relay_hosted_sandboxes ORDER BY created_at DESC"
|
|
629
|
+
).all().map((row) => this.rowToHostedSandbox(row));
|
|
630
|
+
}
|
|
631
|
+
setHostedSandboxMembers(id, userIds) {
|
|
632
|
+
if (!this.getHostedSandboxDetail(id)) {
|
|
633
|
+
throw new RelayStoreError(404, "not_found", "Hosted VM was not found.");
|
|
634
|
+
}
|
|
635
|
+
const users = this.requireHostedSandboxMembers(userIds);
|
|
636
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
637
|
+
this.sqlite.transaction(() => {
|
|
638
|
+
this.sqlite.prepare(
|
|
639
|
+
"DELETE FROM relay_hosted_sandbox_members WHERE sandbox_id = ?"
|
|
640
|
+
).run(id);
|
|
641
|
+
const insert = this.sqlite.prepare(
|
|
642
|
+
`INSERT INTO relay_hosted_sandbox_members
|
|
643
|
+
(sandbox_id, user_id, position, created_at) VALUES (?, ?, ?, ?)`
|
|
644
|
+
);
|
|
645
|
+
users.forEach((user, position) => insert.run(id, user.id, position, now));
|
|
646
|
+
this.sqlite.prepare(
|
|
647
|
+
"UPDATE relay_hosted_sandboxes SET updated_at = ? WHERE id = ?"
|
|
648
|
+
).run(now, id);
|
|
649
|
+
})();
|
|
650
|
+
return this.getHostedSandboxDetail(id);
|
|
651
|
+
}
|
|
652
|
+
setHostedWorkspaceIsolation(id, enabled) {
|
|
653
|
+
const result = this.sqlite.prepare(
|
|
654
|
+
`UPDATE relay_hosted_sandboxes
|
|
655
|
+
SET workspace_isolation_enabled = ?, updated_at = ? WHERE id = ?`
|
|
656
|
+
).run(enabled ? 1 : 0, (/* @__PURE__ */ new Date()).toISOString(), id);
|
|
657
|
+
if (result.changes === 0) {
|
|
658
|
+
throw new RelayStoreError(404, "not_found", "Hosted VM was not found.");
|
|
659
|
+
}
|
|
660
|
+
return this.getHostedSandboxDetail(id);
|
|
661
|
+
}
|
|
662
|
+
hostedWorkspaceIsolation(deviceId) {
|
|
663
|
+
const row = this.sqlite.prepare(
|
|
664
|
+
`SELECT id, workspace_isolation_enabled
|
|
665
|
+
FROM relay_hosted_sandboxes WHERE device_id = ?`
|
|
666
|
+
).get(deviceId);
|
|
667
|
+
return row ? { sandboxId: row.id, enabled: Boolean(row.workspace_isolation_enabled) } : null;
|
|
668
|
+
}
|
|
669
|
+
hostedWorkspaceIsolationForUser(deviceId, userId) {
|
|
670
|
+
const row = this.sqlite.prepare(
|
|
671
|
+
`SELECT hs.id, hs.workspace_isolation_enabled
|
|
672
|
+
FROM relay_hosted_sandboxes hs
|
|
673
|
+
JOIN relay_hosted_sandbox_members m ON m.sandbox_id = hs.id
|
|
674
|
+
WHERE hs.device_id = ? AND m.user_id = ?`
|
|
675
|
+
).get(deviceId, userId);
|
|
676
|
+
return row ? { sandboxId: row.id, enabled: Boolean(row.workspace_isolation_enabled) } : null;
|
|
677
|
+
}
|
|
678
|
+
hostedUserWorkspaceIds(sandboxId, userId) {
|
|
679
|
+
return this.sqlite.prepare(
|
|
680
|
+
`SELECT workspace_id FROM relay_hosted_user_workspaces
|
|
681
|
+
WHERE sandbox_id = ? AND user_id = ? ORDER BY created_at`
|
|
682
|
+
).all(sandboxId, userId).map((row) => row.workspace_id);
|
|
683
|
+
}
|
|
684
|
+
recordHostedUserWorkspace(sandboxId, userId, workspaceId, initial = false) {
|
|
685
|
+
this.sqlite.prepare(
|
|
686
|
+
`INSERT OR IGNORE INTO relay_hosted_user_workspaces
|
|
687
|
+
(sandbox_id, user_id, workspace_id, initial_workspace, created_at)
|
|
688
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
689
|
+
).run(sandboxId, userId, workspaceId, initial ? 1 : 0, (/* @__PURE__ */ new Date()).toISOString());
|
|
690
|
+
}
|
|
691
|
+
ownsHostedWorkspace(sandboxId, userId, workspaceId) {
|
|
692
|
+
return Boolean(
|
|
693
|
+
this.sqlite.prepare(
|
|
694
|
+
`SELECT 1 FROM relay_hosted_user_workspaces
|
|
695
|
+
WHERE sandbox_id = ? AND user_id = ? AND workspace_id = ?`
|
|
696
|
+
).get(sandboxId, userId, workspaceId)
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
recordHostedUserThread(sandboxId, userId, threadId, workspaceId) {
|
|
700
|
+
this.sqlite.prepare(
|
|
701
|
+
`INSERT OR IGNORE INTO relay_hosted_user_threads
|
|
702
|
+
(sandbox_id, user_id, thread_id, workspace_id, created_at)
|
|
703
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
704
|
+
).run(sandboxId, userId, threadId, workspaceId, (/* @__PURE__ */ new Date()).toISOString());
|
|
705
|
+
}
|
|
706
|
+
ownsHostedThread(sandboxId, userId, threadId) {
|
|
707
|
+
return Boolean(
|
|
708
|
+
this.sqlite.prepare(
|
|
709
|
+
`SELECT 1 FROM relay_hosted_user_threads
|
|
710
|
+
WHERE sandbox_id = ? AND user_id = ? AND thread_id = ?`
|
|
711
|
+
).get(sandboxId, userId, threadId)
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
listHostedProviderRecords() {
|
|
715
|
+
return this.sqlite.prepare(
|
|
716
|
+
"SELECT id, credential_ref FROM relay_hosted_sandboxes ORDER BY id"
|
|
717
|
+
).all().map((row) => ({ id: row.id, credentialRef: row.credential_ref }));
|
|
718
|
+
}
|
|
719
|
+
getHostedSandboxDetail(id) {
|
|
720
|
+
const row = this.sqlite.prepare("SELECT * FROM relay_hosted_sandboxes WHERE id = ?").get(id);
|
|
721
|
+
if (!row) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
...this.rowToHostedSandbox(row),
|
|
726
|
+
operations: this.getHostedOperations(id)
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
getHostedProvisionContext(id) {
|
|
730
|
+
const row = this.sqlite.prepare(
|
|
731
|
+
`
|
|
732
|
+
SELECT hs.*, d.token AS device_token
|
|
733
|
+
FROM relay_hosted_sandboxes hs
|
|
734
|
+
JOIN relay_devices d ON d.id = hs.device_id
|
|
735
|
+
WHERE hs.id = ?
|
|
736
|
+
`
|
|
737
|
+
).get(id);
|
|
738
|
+
if (!row || !row.device_token) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
sandbox: this.rowToHostedSandbox(row),
|
|
743
|
+
deviceToken: row.device_token,
|
|
744
|
+
credentialRef: row.credential_ref,
|
|
745
|
+
codexConfig: parseHostedCodexConfig(row.codex_config_json)
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
listHostedSandboxesNeedingReconciliation() {
|
|
749
|
+
return this.sqlite.prepare(
|
|
750
|
+
`
|
|
751
|
+
SELECT id FROM relay_hosted_sandboxes
|
|
752
|
+
WHERE status IN ('requested', 'creating', 'starting', 'provisioning')
|
|
753
|
+
ORDER BY created_at ASC
|
|
754
|
+
`
|
|
755
|
+
).all().map((row) => row.id);
|
|
756
|
+
}
|
|
757
|
+
updateHostedSandboxStatus(id, status, options = {}) {
|
|
758
|
+
const result = this.sqlite.prepare(
|
|
759
|
+
`
|
|
760
|
+
UPDATE relay_hosted_sandboxes
|
|
761
|
+
SET status = ?,
|
|
762
|
+
provider_instance_id = COALESCE(?, provider_instance_id),
|
|
763
|
+
last_error_code = ?,
|
|
764
|
+
last_error_message = ?,
|
|
765
|
+
running_since = CASE
|
|
766
|
+
WHEN ? = 'stopped' THEN NULL
|
|
767
|
+
WHEN ? = 'starting' AND running_since IS NULL THEN ?
|
|
768
|
+
ELSE running_since
|
|
769
|
+
END,
|
|
770
|
+
updated_at = ?
|
|
771
|
+
WHERE id = ?
|
|
772
|
+
`
|
|
773
|
+
).run(
|
|
774
|
+
status,
|
|
775
|
+
options.providerInstanceId ?? null,
|
|
776
|
+
options.errorCode ?? null,
|
|
777
|
+
options.errorMessage ?? null,
|
|
778
|
+
status,
|
|
779
|
+
status,
|
|
780
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
781
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
782
|
+
id
|
|
783
|
+
);
|
|
784
|
+
if (result.changes < 1) {
|
|
785
|
+
throw new RelayStoreError(
|
|
786
|
+
404,
|
|
787
|
+
"not_found",
|
|
788
|
+
"Hosted sandbox was not found."
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
updateHostedOperation(id, status, error) {
|
|
793
|
+
const result = this.sqlite.prepare(
|
|
794
|
+
`
|
|
795
|
+
UPDATE relay_hosted_operations
|
|
796
|
+
SET status = ?, error_code = ?, error_message = ?, updated_at = ?
|
|
797
|
+
WHERE id = ?
|
|
798
|
+
`
|
|
799
|
+
).run(
|
|
800
|
+
status,
|
|
801
|
+
error?.code ?? null,
|
|
802
|
+
error?.message ?? null,
|
|
803
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
804
|
+
id
|
|
805
|
+
);
|
|
806
|
+
if (result.changes < 1) {
|
|
807
|
+
throw new RelayStoreError(
|
|
808
|
+
404,
|
|
809
|
+
"not_found",
|
|
810
|
+
"Hosted operation was not found."
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
createHostedOperation(sandboxId, action) {
|
|
815
|
+
if (!this.getHostedSandboxDetail(sandboxId)) {
|
|
816
|
+
throw new RelayStoreError(
|
|
817
|
+
404,
|
|
818
|
+
"not_found",
|
|
819
|
+
"Hosted sandbox was not found."
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
823
|
+
const operation = {
|
|
824
|
+
id: crypto.randomUUID(),
|
|
825
|
+
sandboxId,
|
|
826
|
+
action,
|
|
827
|
+
status: "pending",
|
|
828
|
+
errorCode: null,
|
|
829
|
+
errorMessage: null,
|
|
830
|
+
createdAt: now,
|
|
831
|
+
updatedAt: now
|
|
832
|
+
};
|
|
833
|
+
this.insertHostedOperation(operation);
|
|
834
|
+
return operation;
|
|
835
|
+
}
|
|
836
|
+
markHostedDeviceOnline(deviceId) {
|
|
837
|
+
this.sqlite.prepare(
|
|
838
|
+
`
|
|
839
|
+
UPDATE relay_hosted_sandboxes
|
|
840
|
+
SET status = 'online', last_error_code = NULL,
|
|
841
|
+
last_error_message = NULL,
|
|
842
|
+
running_since = COALESCE(running_since, ?), updated_at = ?
|
|
843
|
+
WHERE device_id = ? AND status != 'deleting'
|
|
844
|
+
`
|
|
845
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), (/* @__PURE__ */ new Date()).toISOString(), deviceId);
|
|
846
|
+
}
|
|
847
|
+
getHostedSandboxByDeviceId(deviceId) {
|
|
848
|
+
const row = this.sqlite.prepare("SELECT * FROM relay_hosted_sandboxes WHERE device_id = ?").get(deviceId);
|
|
849
|
+
return row ? this.rowToHostedSandbox(row) : null;
|
|
850
|
+
}
|
|
851
|
+
recordHostedUserActivity(deviceId, idleTimeoutMs) {
|
|
852
|
+
const row = this.sqlite.prepare("SELECT * FROM relay_hosted_sandboxes WHERE device_id = ?").get(deviceId);
|
|
853
|
+
if (!row) return null;
|
|
854
|
+
const now = /* @__PURE__ */ new Date();
|
|
855
|
+
const deadline = row.active_turn_count === 0 ? new Date(now.getTime() + idleTimeoutMs).toISOString() : null;
|
|
856
|
+
this.sqlite.prepare(
|
|
857
|
+
`
|
|
858
|
+
UPDATE relay_hosted_sandboxes
|
|
859
|
+
SET last_user_activity_at = ?, idle_deadline_at = ?,
|
|
860
|
+
lifecycle_generation = lifecycle_generation + 1, updated_at = ?
|
|
861
|
+
WHERE id = ?
|
|
862
|
+
`
|
|
863
|
+
).run(now.toISOString(), deadline, now.toISOString(), row.id);
|
|
864
|
+
return this.getHostedSandboxDetail(row.id);
|
|
865
|
+
}
|
|
866
|
+
recordHostedTurnActivity(input) {
|
|
867
|
+
const row = this.sqlite.prepare("SELECT * FROM relay_hosted_sandboxes WHERE device_id = ?").get(input.deviceId);
|
|
868
|
+
if (!row) return null;
|
|
869
|
+
const update = this.sqlite.transaction(() => {
|
|
870
|
+
if (input.kind === "turn_started") {
|
|
871
|
+
this.sqlite.prepare(
|
|
872
|
+
`
|
|
873
|
+
INSERT OR IGNORE INTO relay_hosted_active_turns (
|
|
874
|
+
sandbox_id, thread_id, turn_id, started_at
|
|
875
|
+
) VALUES (?, ?, ?, ?)
|
|
876
|
+
`
|
|
877
|
+
).run(row.id, input.threadId, input.turnId, (/* @__PURE__ */ new Date()).toISOString());
|
|
878
|
+
} else {
|
|
879
|
+
this.sqlite.prepare(
|
|
880
|
+
`
|
|
881
|
+
DELETE FROM relay_hosted_active_turns
|
|
882
|
+
WHERE sandbox_id = ? AND thread_id = ? AND turn_id = ?
|
|
883
|
+
`
|
|
884
|
+
).run(row.id, input.threadId, input.turnId);
|
|
885
|
+
}
|
|
886
|
+
const active = this.sqlite.prepare(
|
|
887
|
+
"SELECT COUNT(*) AS count FROM relay_hosted_active_turns WHERE sandbox_id = ?"
|
|
888
|
+
).get(row.id);
|
|
889
|
+
const now = /* @__PURE__ */ new Date();
|
|
890
|
+
const deadline = active.count === 0 ? new Date(now.getTime() + input.idleTimeoutMs).toISOString() : null;
|
|
891
|
+
this.sqlite.prepare(
|
|
892
|
+
`
|
|
893
|
+
UPDATE relay_hosted_sandboxes
|
|
894
|
+
SET active_turn_count = ?, idle_deadline_at = ?,
|
|
895
|
+
lifecycle_generation = lifecycle_generation + 1, updated_at = ?
|
|
896
|
+
WHERE id = ?
|
|
897
|
+
`
|
|
898
|
+
).run(active.count, deadline, now.toISOString(), row.id);
|
|
899
|
+
});
|
|
900
|
+
update();
|
|
901
|
+
return this.getHostedSandboxDetail(row.id);
|
|
902
|
+
}
|
|
903
|
+
armHostedIdleDeadline(id, idleTimeoutMs) {
|
|
904
|
+
const now = /* @__PURE__ */ new Date();
|
|
905
|
+
const current = this.getHostedSandboxDetail(id);
|
|
906
|
+
if (!current || current.activeTurnCount > 0) {
|
|
907
|
+
return current;
|
|
908
|
+
}
|
|
909
|
+
const activityDeadline = current.lastUserActivityAt ? new Date(
|
|
910
|
+
new Date(current.lastUserActivityAt).getTime() + idleTimeoutMs
|
|
911
|
+
).toISOString() : null;
|
|
912
|
+
const deadline = activityDeadline ? current.idleDeadlineAt && current.idleDeadlineAt < activityDeadline ? current.idleDeadlineAt : activityDeadline : current.idleDeadlineAt ?? new Date(now.getTime() + idleTimeoutMs).toISOString();
|
|
913
|
+
if (deadline === current.idleDeadlineAt) {
|
|
914
|
+
return current;
|
|
915
|
+
}
|
|
916
|
+
this.sqlite.prepare(
|
|
917
|
+
`
|
|
918
|
+
UPDATE relay_hosted_sandboxes
|
|
919
|
+
SET idle_deadline_at = ?, lifecycle_generation = lifecycle_generation + 1,
|
|
920
|
+
updated_at = ?
|
|
921
|
+
WHERE id = ? AND active_turn_count = 0
|
|
922
|
+
`
|
|
923
|
+
).run(deadline, now.toISOString(), id);
|
|
924
|
+
return this.getHostedSandboxDetail(id);
|
|
925
|
+
}
|
|
926
|
+
listHostedIdleDeadlines() {
|
|
927
|
+
return this.sqlite.prepare(
|
|
928
|
+
`
|
|
929
|
+
SELECT id, idle_deadline_at, lifecycle_generation
|
|
930
|
+
FROM relay_hosted_sandboxes
|
|
931
|
+
WHERE idle_deadline_at IS NOT NULL AND active_turn_count = 0
|
|
932
|
+
AND status = 'online'
|
|
933
|
+
`
|
|
934
|
+
).all().map((row) => ({
|
|
935
|
+
id: row.id,
|
|
936
|
+
deadlineAt: row.idle_deadline_at,
|
|
937
|
+
generation: row.lifecycle_generation
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
claimHostedIdleStop(id, generation, now) {
|
|
941
|
+
const result = this.sqlite.prepare(
|
|
942
|
+
`
|
|
943
|
+
UPDATE relay_hosted_sandboxes
|
|
944
|
+
SET status = 'stopping', idle_deadline_at = NULL,
|
|
945
|
+
lifecycle_generation = lifecycle_generation + 1, updated_at = ?
|
|
946
|
+
WHERE id = ? AND lifecycle_generation = ? AND active_turn_count = 0
|
|
947
|
+
AND status = 'online' AND idle_deadline_at <= ?
|
|
948
|
+
`
|
|
949
|
+
).run(now.toISOString(), id, generation, now.toISOString());
|
|
950
|
+
return result.changes === 1;
|
|
951
|
+
}
|
|
213
952
|
deleteDevice(userId, deviceId) {
|
|
953
|
+
const hosted = this.sqlite.prepare("SELECT id FROM relay_hosted_sandboxes WHERE device_id = ?").get(deviceId);
|
|
954
|
+
if (hosted) {
|
|
955
|
+
throw new RelayStoreError(
|
|
956
|
+
409,
|
|
957
|
+
"conflict",
|
|
958
|
+
"Hosted devices must be deleted from the Hosted supervisor VM admin panel."
|
|
959
|
+
);
|
|
960
|
+
}
|
|
214
961
|
const result = this.sqlite.prepare("DELETE FROM relay_devices WHERE id = ? AND owner_user_id = ?").run(deviceId, userId);
|
|
215
962
|
if (result.changes < 1) {
|
|
216
963
|
throw new RelayStoreError(404, "not_found", "Device was not found.");
|
|
217
964
|
}
|
|
218
965
|
}
|
|
966
|
+
deleteHostedSandboxRecord(id) {
|
|
967
|
+
const context = this.getHostedProvisionContext(id);
|
|
968
|
+
if (!context) {
|
|
969
|
+
throw new RelayStoreError(
|
|
970
|
+
404,
|
|
971
|
+
"not_found",
|
|
972
|
+
"Hosted sandbox was not found."
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const remove = this.sqlite.transaction(() => {
|
|
976
|
+
this.sqlite.prepare("DELETE FROM relay_hosted_sandboxes WHERE id = ?").run(id);
|
|
977
|
+
this.sqlite.prepare("DELETE FROM relay_devices WHERE id = ?").run(context.sandbox.deviceId);
|
|
978
|
+
});
|
|
979
|
+
remove();
|
|
980
|
+
return {
|
|
981
|
+
id,
|
|
982
|
+
deviceId: context.sandbox.deviceId,
|
|
983
|
+
credentialRef: context.credentialRef
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
replaceHostedCredentialRef(id, credentialRef) {
|
|
987
|
+
const context = this.getHostedProvisionContext(id);
|
|
988
|
+
if (!context) {
|
|
989
|
+
throw new RelayStoreError(
|
|
990
|
+
404,
|
|
991
|
+
"not_found",
|
|
992
|
+
"Hosted sandbox was not found."
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
this.sqlite.prepare(
|
|
996
|
+
"UPDATE relay_hosted_sandboxes SET credential_ref = ?, updated_at = ? WHERE id = ?"
|
|
997
|
+
).run(credentialRef, (/* @__PURE__ */ new Date()).toISOString(), id);
|
|
998
|
+
return context.credentialRef;
|
|
999
|
+
}
|
|
219
1000
|
verifyDeviceToken(token) {
|
|
220
1001
|
if (!token) {
|
|
221
1002
|
return null;
|
|
@@ -236,7 +1017,11 @@ var RelayStore = class _RelayStore {
|
|
|
236
1017
|
throw new RelayStoreError(404, "not_found", "Target user was not found.");
|
|
237
1018
|
}
|
|
238
1019
|
if (target.id === ownerUserId) {
|
|
239
|
-
throw new RelayStoreError(
|
|
1020
|
+
throw new RelayStoreError(
|
|
1021
|
+
400,
|
|
1022
|
+
"bad_request",
|
|
1023
|
+
"You cannot share a session with yourself."
|
|
1024
|
+
);
|
|
240
1025
|
}
|
|
241
1026
|
const existing = this.rowToShare(
|
|
242
1027
|
this.sqlite.prepare(
|
|
@@ -295,16 +1080,28 @@ var RelayStore = class _RelayStore {
|
|
|
295
1080
|
throw new RelayStoreError(404, "not_found", "Target user was not found.");
|
|
296
1081
|
}
|
|
297
1082
|
if (target.id === ownerUserId) {
|
|
298
|
-
throw new RelayStoreError(
|
|
1083
|
+
throw new RelayStoreError(
|
|
1084
|
+
400,
|
|
1085
|
+
"bad_request",
|
|
1086
|
+
"You cannot share access with yourself."
|
|
1087
|
+
);
|
|
299
1088
|
}
|
|
300
1089
|
const scope = normalizeShareScope(input.scope);
|
|
301
1090
|
const threadId = scope === "thread" ? input.threadId?.trim() || null : null;
|
|
302
1091
|
const workspaceId = scope === "workspace" ? input.workspaceId?.trim() || null : null;
|
|
303
1092
|
if (scope === "thread" && !threadId) {
|
|
304
|
-
throw new RelayStoreError(
|
|
1093
|
+
throw new RelayStoreError(
|
|
1094
|
+
400,
|
|
1095
|
+
"bad_request",
|
|
1096
|
+
"threadId is required for thread grants."
|
|
1097
|
+
);
|
|
305
1098
|
}
|
|
306
1099
|
if (scope === "workspace" && !workspaceId) {
|
|
307
|
-
throw new RelayStoreError(
|
|
1100
|
+
throw new RelayStoreError(
|
|
1101
|
+
400,
|
|
1102
|
+
"bad_request",
|
|
1103
|
+
"workspaceId is required for workspace grants."
|
|
1104
|
+
);
|
|
308
1105
|
}
|
|
309
1106
|
const existing = this.rowToGrant(
|
|
310
1107
|
this.sqlite.prepare(
|
|
@@ -318,7 +1115,14 @@ var RelayStore = class _RelayStore {
|
|
|
318
1115
|
AND COALESCE(workspace_id, '') = COALESCE(?, '')
|
|
319
1116
|
AND revoked_at IS NULL
|
|
320
1117
|
`
|
|
321
|
-
).get(
|
|
1118
|
+
).get(
|
|
1119
|
+
ownerUserId,
|
|
1120
|
+
target.id,
|
|
1121
|
+
input.deviceId,
|
|
1122
|
+
scope,
|
|
1123
|
+
threadId,
|
|
1124
|
+
workspaceId
|
|
1125
|
+
)
|
|
322
1126
|
);
|
|
323
1127
|
if (existing) {
|
|
324
1128
|
return this.updateGrantRecord(existing.id, {
|
|
@@ -362,7 +1166,9 @@ var RelayStore = class _RelayStore {
|
|
|
362
1166
|
}
|
|
363
1167
|
updateShare(userId, shareId, input) {
|
|
364
1168
|
const share = this.rowToShare(
|
|
365
|
-
this.sqlite.prepare(
|
|
1169
|
+
this.sqlite.prepare(
|
|
1170
|
+
"SELECT * FROM relay_shares WHERE id = ? AND owner_user_id = ? AND revoked_at IS NULL"
|
|
1171
|
+
).get(shareId, userId)
|
|
366
1172
|
);
|
|
367
1173
|
if (!share) {
|
|
368
1174
|
throw new RelayStoreError(404, "not_found", "Share was not found.");
|
|
@@ -379,7 +1185,9 @@ var RelayStore = class _RelayStore {
|
|
|
379
1185
|
}
|
|
380
1186
|
revokeShare(userId, shareId) {
|
|
381
1187
|
const share = this.rowToShare(
|
|
382
|
-
this.sqlite.prepare(
|
|
1188
|
+
this.sqlite.prepare(
|
|
1189
|
+
"SELECT * FROM relay_shares WHERE id = ? AND owner_user_id = ?"
|
|
1190
|
+
).get(shareId, userId)
|
|
383
1191
|
);
|
|
384
1192
|
if (!share) {
|
|
385
1193
|
throw new RelayStoreError(404, "not_found", "Share was not found.");
|
|
@@ -390,7 +1198,9 @@ var RelayStore = class _RelayStore {
|
|
|
390
1198
|
}
|
|
391
1199
|
updateGrant(userId, grantId, input) {
|
|
392
1200
|
const grant = this.rowToGrant(
|
|
393
|
-
this.sqlite.prepare(
|
|
1201
|
+
this.sqlite.prepare(
|
|
1202
|
+
"SELECT * FROM relay_access_grants WHERE id = ? AND owner_user_id = ? AND revoked_at IS NULL"
|
|
1203
|
+
).get(grantId, userId)
|
|
394
1204
|
);
|
|
395
1205
|
if (!grant) {
|
|
396
1206
|
throw new RelayStoreError(404, "not_found", "Grant was not found.");
|
|
@@ -409,7 +1219,9 @@ var RelayStore = class _RelayStore {
|
|
|
409
1219
|
}
|
|
410
1220
|
revokeGrant(userId, grantId) {
|
|
411
1221
|
const grant = this.rowToGrant(
|
|
412
|
-
this.sqlite.prepare(
|
|
1222
|
+
this.sqlite.prepare(
|
|
1223
|
+
"SELECT * FROM relay_access_grants WHERE id = ? AND owner_user_id = ?"
|
|
1224
|
+
).get(grantId, userId)
|
|
413
1225
|
);
|
|
414
1226
|
if (!grant) {
|
|
415
1227
|
throw new RelayStoreError(404, "not_found", "Grant was not found.");
|
|
@@ -433,6 +1245,25 @@ var RelayStore = class _RelayStore {
|
|
|
433
1245
|
canCreateThreads: true
|
|
434
1246
|
};
|
|
435
1247
|
}
|
|
1248
|
+
const hostedMember = this.sqlite.prepare(
|
|
1249
|
+
`SELECT 1
|
|
1250
|
+
FROM relay_hosted_sandbox_members m
|
|
1251
|
+
JOIN relay_hosted_sandboxes hs ON hs.id = m.sandbox_id
|
|
1252
|
+
WHERE m.user_id = ? AND hs.device_id = ?`
|
|
1253
|
+
).get(userId, deviceId);
|
|
1254
|
+
if (hostedMember) {
|
|
1255
|
+
return {
|
|
1256
|
+
kind: "owner",
|
|
1257
|
+
share: null,
|
|
1258
|
+
grant: null,
|
|
1259
|
+
scope: "owner",
|
|
1260
|
+
threadAccess: "control",
|
|
1261
|
+
workspaceAccess: "write",
|
|
1262
|
+
workspaceId: null,
|
|
1263
|
+
workspaceScope: null,
|
|
1264
|
+
canCreateThreads: true
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
436
1267
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
437
1268
|
if (scope.threadId) {
|
|
438
1269
|
const share = this.rowToShare(
|
|
@@ -515,23 +1346,34 @@ var RelayStore = class _RelayStore {
|
|
|
515
1346
|
}
|
|
516
1347
|
portalSummary(userId, connectedDevices) {
|
|
517
1348
|
const user = this.requireUser(userId);
|
|
518
|
-
const devices =
|
|
1349
|
+
const devices = [
|
|
1350
|
+
...this.getDevicesByOwner(userId),
|
|
1351
|
+
...this.getHostedDevicesByMember(userId)
|
|
1352
|
+
].filter(
|
|
1353
|
+
(device, index, all) => all.findIndex((item) => item.id === device.id) === index
|
|
1354
|
+
);
|
|
519
1355
|
const sharedWithMe = this.getSharesByTarget(userId);
|
|
520
1356
|
const sharedByMe = this.getSharesByOwner(userId);
|
|
521
1357
|
const grantsWithMe = this.getGrantsByTarget(userId);
|
|
522
1358
|
const grantsByMe = this.getGrantsByOwner(userId);
|
|
523
1359
|
return {
|
|
524
1360
|
user: this.publicUser(user),
|
|
525
|
-
devices: devices.map(
|
|
1361
|
+
devices: devices.map(
|
|
1362
|
+
(device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)
|
|
1363
|
+
),
|
|
526
1364
|
sharedWithMe: sharedWithMe.map((share) => this.publicShare(share)),
|
|
527
1365
|
sharedByMe: sharedByMe.map((share) => this.publicShare(share)),
|
|
528
1366
|
sharedDevicesWithMe: grantsWithMe.filter((grant) => grant.scope === "device").map((grant) => this.publicGrant(grant)),
|
|
529
1367
|
sharedThreadsWithMe: [
|
|
530
|
-
...sharedWithMe.map(
|
|
1368
|
+
...sharedWithMe.map(
|
|
1369
|
+
(share) => this.publicGrant(this.grantFromShare(share))
|
|
1370
|
+
),
|
|
531
1371
|
...grantsWithMe.filter((grant) => grant.scope !== "device").map((grant) => this.publicGrant(grant))
|
|
532
1372
|
],
|
|
533
1373
|
grantsByMe: [
|
|
534
|
-
...sharedByMe.map(
|
|
1374
|
+
...sharedByMe.map(
|
|
1375
|
+
(share) => this.publicGrant(this.grantFromShare(share))
|
|
1376
|
+
),
|
|
535
1377
|
...grantsByMe.map((grant) => this.publicGrant(grant))
|
|
536
1378
|
]
|
|
537
1379
|
};
|
|
@@ -540,17 +1382,39 @@ var RelayStore = class _RelayStore {
|
|
|
540
1382
|
return this.getSharesByTarget(userId).filter((share) => share.deviceId === deviceId).map((share) => this.publicShare(share));
|
|
541
1383
|
}
|
|
542
1384
|
adminSummary(connectedDevices, options = {}) {
|
|
543
|
-
const conversationWindowDays = normalizeConversationWindowDays(
|
|
1385
|
+
const conversationWindowDays = normalizeConversationWindowDays(
|
|
1386
|
+
options.conversationWindowDays
|
|
1387
|
+
);
|
|
544
1388
|
const users = this.getUsers();
|
|
545
1389
|
const devices = this.getDevices();
|
|
546
1390
|
const deviceCounts = /* @__PURE__ */ new Map();
|
|
1391
|
+
const hostedDeviceIds = new Set(
|
|
1392
|
+
this.sqlite.prepare("SELECT device_id FROM relay_hosted_sandboxes").all().map((row) => row.device_id)
|
|
1393
|
+
);
|
|
547
1394
|
for (const device of devices) {
|
|
548
|
-
|
|
1395
|
+
if (hostedDeviceIds.has(device.id)) continue;
|
|
1396
|
+
deviceCounts.set(
|
|
1397
|
+
device.ownerUserId,
|
|
1398
|
+
(deviceCounts.get(device.ownerUserId) ?? 0) + 1
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
const hostedMemberships = this.sqlite.prepare("SELECT user_id FROM relay_hosted_sandbox_members").all();
|
|
1402
|
+
for (const membership of hostedMemberships) {
|
|
1403
|
+
deviceCounts.set(
|
|
1404
|
+
membership.user_id,
|
|
1405
|
+
(deviceCounts.get(membership.user_id) ?? 0) + 1
|
|
1406
|
+
);
|
|
549
1407
|
}
|
|
550
|
-
const conversationCounts = this.conversationCountsByUser(
|
|
1408
|
+
const conversationCounts = this.conversationCountsByUser(
|
|
1409
|
+
conversationWindowDays
|
|
1410
|
+
);
|
|
551
1411
|
return {
|
|
552
1412
|
users: users.map(
|
|
553
|
-
(user) => this.publicAdminUser(
|
|
1413
|
+
(user) => this.publicAdminUser(
|
|
1414
|
+
user,
|
|
1415
|
+
deviceCounts.get(user.id) ?? 0,
|
|
1416
|
+
conversationCounts.get(user.id) ?? 0
|
|
1417
|
+
)
|
|
554
1418
|
),
|
|
555
1419
|
devices: devices.map((device) => {
|
|
556
1420
|
const owner = users.find((user) => user.id === device.ownerUserId);
|
|
@@ -561,7 +1425,9 @@ var RelayStore = class _RelayStore {
|
|
|
561
1425
|
options.metadata
|
|
562
1426
|
);
|
|
563
1427
|
}),
|
|
564
|
-
shares: this.getShares({ includeRevoked: true }).map(
|
|
1428
|
+
shares: this.getShares({ includeRevoked: true }).map(
|
|
1429
|
+
(share) => this.publicShare(share)
|
|
1430
|
+
),
|
|
565
1431
|
pendingRegistrations: this.pendingRegistrations(),
|
|
566
1432
|
settings: this.registrationSettings(),
|
|
567
1433
|
conversationWindowDays,
|
|
@@ -576,7 +1442,13 @@ var RelayStore = class _RelayStore {
|
|
|
576
1442
|
return {
|
|
577
1443
|
enabled: this.registrationEnabled(),
|
|
578
1444
|
registrationPassword: this.getSetting("registrationPassword"),
|
|
579
|
-
approvalRequired: this.getSetting("registrationApprovalRequired") === "true"
|
|
1445
|
+
approvalRequired: this.getSetting("registrationApprovalRequired") === "true",
|
|
1446
|
+
googleAuthEnabled: this.getSetting("googleAuthEnabled") === "true",
|
|
1447
|
+
githubAuthEnabled: this.getSetting("githubAuthEnabled") === "true",
|
|
1448
|
+
emailVerificationEnabled: this.getSetting("emailVerificationEnabled") === "true",
|
|
1449
|
+
googleAuthAvailable: false,
|
|
1450
|
+
githubAuthAvailable: false,
|
|
1451
|
+
emailVerificationAvailable: false
|
|
580
1452
|
};
|
|
581
1453
|
}
|
|
582
1454
|
updateRegistrationSettings(input) {
|
|
@@ -586,7 +1458,11 @@ var RelayStore = class _RelayStore {
|
|
|
586
1458
|
if (input.registrationPassword !== void 0) {
|
|
587
1459
|
const password = input.registrationPassword?.trim() || null;
|
|
588
1460
|
if (password !== null && password.length < 8) {
|
|
589
|
-
throw new RelayStoreError(
|
|
1461
|
+
throw new RelayStoreError(
|
|
1462
|
+
400,
|
|
1463
|
+
"bad_request",
|
|
1464
|
+
"Registration password must be at least 8 characters."
|
|
1465
|
+
);
|
|
590
1466
|
}
|
|
591
1467
|
if (password === null) {
|
|
592
1468
|
this.deleteSetting("registrationPassword");
|
|
@@ -595,10 +1471,21 @@ var RelayStore = class _RelayStore {
|
|
|
595
1471
|
}
|
|
596
1472
|
}
|
|
597
1473
|
if (input.approvalRequired !== void 0) {
|
|
598
|
-
this.setSetting(
|
|
1474
|
+
this.setSetting(
|
|
1475
|
+
"registrationApprovalRequired",
|
|
1476
|
+
input.approvalRequired ? "true" : "false"
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
for (const key of ["googleAuthEnabled", "githubAuthEnabled", "emailVerificationEnabled"]) {
|
|
1480
|
+
if (input[key] !== void 0) this.setSetting(key, input[key] ? "true" : "false");
|
|
599
1481
|
}
|
|
600
1482
|
return this.registrationSettings();
|
|
601
1483
|
}
|
|
1484
|
+
ensureAuthSettings(input) {
|
|
1485
|
+
if (this.getSetting("googleAuthEnabled") === null) this.setSetting("googleAuthEnabled", input.google ? "true" : "false");
|
|
1486
|
+
if (this.getSetting("githubAuthEnabled") === null) this.setSetting("githubAuthEnabled", input.github ? "true" : "false");
|
|
1487
|
+
if (this.getSetting("emailVerificationEnabled") === null) this.setSetting("emailVerificationEnabled", "false");
|
|
1488
|
+
}
|
|
602
1489
|
ensureRegistrationPassword(password) {
|
|
603
1490
|
if (!password || this.getSetting("registrationPassword") !== null) {
|
|
604
1491
|
return;
|
|
@@ -620,6 +1507,9 @@ var RelayStore = class _RelayStore {
|
|
|
620
1507
|
};
|
|
621
1508
|
const approve = this.sqlite.transaction(() => {
|
|
622
1509
|
this.insertUser(user);
|
|
1510
|
+
if (record.providerSubject && record.provider !== "password") {
|
|
1511
|
+
this.insertIdentity(user.id, record.provider, record.providerSubject, record.email);
|
|
1512
|
+
}
|
|
623
1513
|
this.sqlite.prepare(
|
|
624
1514
|
`
|
|
625
1515
|
UPDATE relay_pending_registrations
|
|
@@ -664,7 +1554,11 @@ var RelayStore = class _RelayStore {
|
|
|
664
1554
|
setUserEnabled(userId, enabled) {
|
|
665
1555
|
const user = this.requireUser(userId);
|
|
666
1556
|
if (user.role === "admin" && !enabled) {
|
|
667
|
-
throw new RelayStoreError(
|
|
1557
|
+
throw new RelayStoreError(
|
|
1558
|
+
400,
|
|
1559
|
+
"bad_request",
|
|
1560
|
+
"The admin user cannot be disabled."
|
|
1561
|
+
);
|
|
668
1562
|
}
|
|
669
1563
|
this.sqlite.prepare("UPDATE relay_users SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, userId);
|
|
670
1564
|
return this.publicUser({ ...user, enabled });
|
|
@@ -672,21 +1566,51 @@ var RelayStore = class _RelayStore {
|
|
|
672
1566
|
deleteUser(userId) {
|
|
673
1567
|
const user = this.requireUser(userId);
|
|
674
1568
|
if (user.role === "admin") {
|
|
675
|
-
throw new RelayStoreError(
|
|
1569
|
+
throw new RelayStoreError(
|
|
1570
|
+
400,
|
|
1571
|
+
"bad_request",
|
|
1572
|
+
"The admin user cannot be deleted."
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
const soleMembership = this.sqlite.prepare(
|
|
1576
|
+
`SELECT hs.id
|
|
1577
|
+
FROM relay_hosted_sandbox_members m
|
|
1578
|
+
JOIN relay_hosted_sandboxes hs ON hs.id = m.sandbox_id
|
|
1579
|
+
WHERE m.user_id = ?
|
|
1580
|
+
AND (SELECT COUNT(*) FROM relay_hosted_sandbox_members all_members
|
|
1581
|
+
WHERE all_members.sandbox_id = m.sandbox_id) = 1
|
|
1582
|
+
LIMIT 1`
|
|
1583
|
+
).get(userId);
|
|
1584
|
+
if (soleMembership) {
|
|
1585
|
+
throw new RelayStoreError(
|
|
1586
|
+
409,
|
|
1587
|
+
"conflict",
|
|
1588
|
+
"Reassign or delete the user's hosted VM before deleting this account."
|
|
1589
|
+
);
|
|
676
1590
|
}
|
|
677
1591
|
this.sqlite.prepare("DELETE FROM relay_users WHERE id = ?").run(userId);
|
|
678
1592
|
}
|
|
679
1593
|
adminResetUserPassword(userId, password) {
|
|
680
1594
|
const user = this.requireUser(userId);
|
|
681
1595
|
if (user.role === "admin") {
|
|
682
|
-
throw new RelayStoreError(
|
|
1596
|
+
throw new RelayStoreError(
|
|
1597
|
+
400,
|
|
1598
|
+
"bad_request",
|
|
1599
|
+
"The admin user password cannot be reset here."
|
|
1600
|
+
);
|
|
683
1601
|
}
|
|
684
1602
|
if (password.length < 8) {
|
|
685
|
-
throw new RelayStoreError(
|
|
1603
|
+
throw new RelayStoreError(
|
|
1604
|
+
400,
|
|
1605
|
+
"bad_request",
|
|
1606
|
+
"Password must be at least 8 characters."
|
|
1607
|
+
);
|
|
686
1608
|
}
|
|
687
1609
|
const passwordSalt = crypto.randomBytes(16).toString("base64url");
|
|
688
1610
|
const passwordHash = hashSecret(password, passwordSalt);
|
|
689
|
-
this.sqlite.prepare(
|
|
1611
|
+
this.sqlite.prepare(
|
|
1612
|
+
"UPDATE relay_users SET password_salt = ?, password_hash = ? WHERE id = ?"
|
|
1613
|
+
).run(passwordSalt, passwordHash, user.id);
|
|
690
1614
|
return this.publicUser({
|
|
691
1615
|
...user,
|
|
692
1616
|
passwordSalt,
|
|
@@ -697,11 +1621,19 @@ var RelayStore = class _RelayStore {
|
|
|
697
1621
|
const user = this.requireUser(userId);
|
|
698
1622
|
const username = input.username !== void 0 ? normalizeUsername(input.username) : user.username;
|
|
699
1623
|
if (username.length < 3) {
|
|
700
|
-
throw new RelayStoreError(
|
|
1624
|
+
throw new RelayStoreError(
|
|
1625
|
+
400,
|
|
1626
|
+
"bad_request",
|
|
1627
|
+
"Username must be at least 3 characters."
|
|
1628
|
+
);
|
|
701
1629
|
}
|
|
702
1630
|
const existingUsername = this.getUserByUsername(username);
|
|
703
1631
|
if (existingUsername && existingUsername.id !== user.id) {
|
|
704
|
-
throw new RelayStoreError(
|
|
1632
|
+
throw new RelayStoreError(
|
|
1633
|
+
409,
|
|
1634
|
+
"conflict",
|
|
1635
|
+
"A user with that username already exists."
|
|
1636
|
+
);
|
|
705
1637
|
}
|
|
706
1638
|
this.sqlite.prepare("UPDATE relay_users SET username = ? WHERE id = ?").run(username, user.id);
|
|
707
1639
|
return this.publicUser({ ...user, username });
|
|
@@ -709,14 +1641,24 @@ var RelayStore = class _RelayStore {
|
|
|
709
1641
|
updatePassword(userId, input) {
|
|
710
1642
|
const user = this.requireUser(userId);
|
|
711
1643
|
if (!verifySecret(input.currentPassword, user.passwordSalt, user.passwordHash)) {
|
|
712
|
-
throw new RelayStoreError(
|
|
1644
|
+
throw new RelayStoreError(
|
|
1645
|
+
403,
|
|
1646
|
+
"forbidden",
|
|
1647
|
+
"Current password is incorrect."
|
|
1648
|
+
);
|
|
713
1649
|
}
|
|
714
1650
|
if (input.newPassword.length < 8) {
|
|
715
|
-
throw new RelayStoreError(
|
|
1651
|
+
throw new RelayStoreError(
|
|
1652
|
+
400,
|
|
1653
|
+
"bad_request",
|
|
1654
|
+
"Password must be at least 8 characters."
|
|
1655
|
+
);
|
|
716
1656
|
}
|
|
717
1657
|
const passwordSalt = crypto.randomBytes(16).toString("base64url");
|
|
718
1658
|
const passwordHash = hashSecret(input.newPassword, passwordSalt);
|
|
719
|
-
this.sqlite.prepare(
|
|
1659
|
+
this.sqlite.prepare(
|
|
1660
|
+
"UPDATE relay_users SET password_salt = ?, password_hash = ? WHERE id = ?"
|
|
1661
|
+
).run(passwordSalt, passwordHash, user.id);
|
|
720
1662
|
return this.publicUser({
|
|
721
1663
|
...user,
|
|
722
1664
|
passwordSalt,
|
|
@@ -731,16 +1673,25 @@ var RelayStore = class _RelayStore {
|
|
|
731
1673
|
};
|
|
732
1674
|
}
|
|
733
1675
|
publicDevice(device, status) {
|
|
1676
|
+
const hosted = this.sqlite.prepare(
|
|
1677
|
+
`
|
|
1678
|
+
SELECT status, active_turn_count, idle_deadline_at
|
|
1679
|
+
FROM relay_hosted_sandboxes WHERE device_id = ?
|
|
1680
|
+
`
|
|
1681
|
+
).get(device.id);
|
|
734
1682
|
return {
|
|
735
1683
|
id: device.id,
|
|
736
1684
|
ownerUserId: device.ownerUserId,
|
|
737
1685
|
name: device.name,
|
|
738
|
-
token: device.token,
|
|
1686
|
+
token: hosted ? null : device.token,
|
|
739
1687
|
tokenPreview: device.tokenPreview,
|
|
740
1688
|
connected: Boolean(status?.connected),
|
|
741
1689
|
connectedAt: status?.connectedAt ?? null,
|
|
742
1690
|
lastHeartbeatAt: status?.lastHeartbeatAt ?? null,
|
|
743
|
-
createdAt: device.createdAt
|
|
1691
|
+
createdAt: device.createdAt,
|
|
1692
|
+
hostedStatus: hosted?.status ?? null,
|
|
1693
|
+
hostedActiveTurnCount: hosted?.active_turn_count ?? 0,
|
|
1694
|
+
hostedIdleDeadlineAt: hosted?.idle_deadline_at ?? null
|
|
744
1695
|
};
|
|
745
1696
|
}
|
|
746
1697
|
publicAdminUser(user, deviceCount, conversationCount) {
|
|
@@ -772,7 +1723,14 @@ var RelayStore = class _RelayStore {
|
|
|
772
1723
|
id, share_id, user_id, username, kind, accessed_at
|
|
773
1724
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
774
1725
|
`
|
|
775
|
-
).run(
|
|
1726
|
+
).run(
|
|
1727
|
+
crypto.randomUUID(),
|
|
1728
|
+
share.id,
|
|
1729
|
+
user.id,
|
|
1730
|
+
user.username,
|
|
1731
|
+
kind,
|
|
1732
|
+
accessedAt
|
|
1733
|
+
);
|
|
776
1734
|
}
|
|
777
1735
|
recordGrantAccess(grant, user, kind = "access") {
|
|
778
1736
|
if (grant.revokedAt || grant.expiresAt && grant.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
|
|
@@ -785,7 +1743,14 @@ var RelayStore = class _RelayStore {
|
|
|
785
1743
|
id, grant_id, user_id, username, kind, accessed_at
|
|
786
1744
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
787
1745
|
`
|
|
788
|
-
).run(
|
|
1746
|
+
).run(
|
|
1747
|
+
crypto.randomUUID(),
|
|
1748
|
+
grant.id,
|
|
1749
|
+
user.id,
|
|
1750
|
+
user.username,
|
|
1751
|
+
kind,
|
|
1752
|
+
accessedAt
|
|
1753
|
+
);
|
|
789
1754
|
}
|
|
790
1755
|
publicShare(share) {
|
|
791
1756
|
const owner = this.getUser(share.ownerUserId);
|
|
@@ -850,6 +1815,97 @@ var RelayStore = class _RelayStore {
|
|
|
850
1815
|
|
|
851
1816
|
CREATE INDEX IF NOT EXISTS relay_devices_owner_idx ON relay_devices(owner_user_id);
|
|
852
1817
|
|
|
1818
|
+
CREATE TABLE IF NOT EXISTS relay_hosted_sandboxes (
|
|
1819
|
+
id TEXT PRIMARY KEY,
|
|
1820
|
+
device_id TEXT NOT NULL UNIQUE REFERENCES relay_devices(id) ON DELETE CASCADE,
|
|
1821
|
+
assigned_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE RESTRICT,
|
|
1822
|
+
created_by_admin_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE RESTRICT,
|
|
1823
|
+
provider TEXT NOT NULL CHECK (provider IN ('incus')),
|
|
1824
|
+
provider_instance_id TEXT,
|
|
1825
|
+
image_version TEXT NOT NULL,
|
|
1826
|
+
cpu_count INTEGER NOT NULL,
|
|
1827
|
+
memory_mib INTEGER NOT NULL,
|
|
1828
|
+
disk_gib INTEGER NOT NULL,
|
|
1829
|
+
status TEXT NOT NULL CHECK (status IN (
|
|
1830
|
+
'requested', 'creating', 'starting', 'provisioning', 'stopped',
|
|
1831
|
+
'online', 'stopping', 'error', 'deleting'
|
|
1832
|
+
)),
|
|
1833
|
+
credential_ref TEXT NOT NULL,
|
|
1834
|
+
codex_config_json TEXT,
|
|
1835
|
+
last_error_code TEXT,
|
|
1836
|
+
last_error_message TEXT,
|
|
1837
|
+
active_turn_count INTEGER NOT NULL DEFAULT 0,
|
|
1838
|
+
last_user_activity_at TEXT,
|
|
1839
|
+
idle_deadline_at TEXT,
|
|
1840
|
+
lifecycle_generation INTEGER NOT NULL DEFAULT 0,
|
|
1841
|
+
created_at TEXT NOT NULL,
|
|
1842
|
+
updated_at TEXT NOT NULL
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
CREATE INDEX IF NOT EXISTS relay_hosted_sandboxes_assigned_idx
|
|
1846
|
+
ON relay_hosted_sandboxes(assigned_user_id, created_at DESC);
|
|
1847
|
+
CREATE INDEX IF NOT EXISTS relay_hosted_sandboxes_status_idx
|
|
1848
|
+
ON relay_hosted_sandboxes(status, updated_at);
|
|
1849
|
+
|
|
1850
|
+
CREATE TABLE IF NOT EXISTS relay_hosted_sandbox_members (
|
|
1851
|
+
sandbox_id TEXT NOT NULL REFERENCES relay_hosted_sandboxes(id) ON DELETE CASCADE,
|
|
1852
|
+
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
1853
|
+
position INTEGER NOT NULL,
|
|
1854
|
+
created_at TEXT NOT NULL,
|
|
1855
|
+
PRIMARY KEY (sandbox_id, user_id)
|
|
1856
|
+
);
|
|
1857
|
+
|
|
1858
|
+
CREATE INDEX IF NOT EXISTS relay_hosted_sandbox_members_user_idx
|
|
1859
|
+
ON relay_hosted_sandbox_members(user_id, sandbox_id);
|
|
1860
|
+
|
|
1861
|
+
CREATE TABLE IF NOT EXISTS relay_hosted_user_workspaces (
|
|
1862
|
+
sandbox_id TEXT NOT NULL REFERENCES relay_hosted_sandboxes(id) ON DELETE CASCADE,
|
|
1863
|
+
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
1864
|
+
workspace_id TEXT NOT NULL,
|
|
1865
|
+
initial_workspace INTEGER NOT NULL DEFAULT 0,
|
|
1866
|
+
created_at TEXT NOT NULL,
|
|
1867
|
+
PRIMARY KEY (sandbox_id, workspace_id)
|
|
1868
|
+
);
|
|
1869
|
+
|
|
1870
|
+
CREATE INDEX IF NOT EXISTS relay_hosted_user_workspaces_user_idx
|
|
1871
|
+
ON relay_hosted_user_workspaces(sandbox_id, user_id, created_at);
|
|
1872
|
+
|
|
1873
|
+
CREATE TABLE IF NOT EXISTS relay_hosted_user_threads (
|
|
1874
|
+
sandbox_id TEXT NOT NULL REFERENCES relay_hosted_sandboxes(id) ON DELETE CASCADE,
|
|
1875
|
+
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
1876
|
+
thread_id TEXT NOT NULL,
|
|
1877
|
+
workspace_id TEXT NOT NULL,
|
|
1878
|
+
created_at TEXT NOT NULL,
|
|
1879
|
+
PRIMARY KEY (sandbox_id, thread_id)
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
CREATE INDEX IF NOT EXISTS relay_hosted_user_threads_user_idx
|
|
1883
|
+
ON relay_hosted_user_threads(sandbox_id, user_id, created_at);
|
|
1884
|
+
|
|
1885
|
+
CREATE TABLE IF NOT EXISTS relay_hosted_operations (
|
|
1886
|
+
id TEXT PRIMARY KEY,
|
|
1887
|
+
sandbox_id TEXT NOT NULL REFERENCES relay_hosted_sandboxes(id) ON DELETE CASCADE,
|
|
1888
|
+
action TEXT NOT NULL CHECK (action IN (
|
|
1889
|
+
'create', 'start', 'stop', 'snapshot', 'delete', 'rotate_credential'
|
|
1890
|
+
)),
|
|
1891
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'succeeded', 'failed')),
|
|
1892
|
+
error_code TEXT,
|
|
1893
|
+
error_message TEXT,
|
|
1894
|
+
created_at TEXT NOT NULL,
|
|
1895
|
+
updated_at TEXT NOT NULL
|
|
1896
|
+
);
|
|
1897
|
+
|
|
1898
|
+
CREATE INDEX IF NOT EXISTS relay_hosted_operations_sandbox_idx
|
|
1899
|
+
ON relay_hosted_operations(sandbox_id, created_at DESC);
|
|
1900
|
+
|
|
1901
|
+
CREATE TABLE IF NOT EXISTS relay_hosted_active_turns (
|
|
1902
|
+
sandbox_id TEXT NOT NULL REFERENCES relay_hosted_sandboxes(id) ON DELETE CASCADE,
|
|
1903
|
+
thread_id TEXT NOT NULL,
|
|
1904
|
+
turn_id TEXT NOT NULL,
|
|
1905
|
+
started_at TEXT NOT NULL,
|
|
1906
|
+
PRIMARY KEY (sandbox_id, thread_id, turn_id)
|
|
1907
|
+
);
|
|
1908
|
+
|
|
853
1909
|
CREATE TABLE IF NOT EXISTS relay_shares (
|
|
854
1910
|
id TEXT PRIMARY KEY,
|
|
855
1911
|
owner_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
@@ -947,21 +2003,84 @@ var RelayStore = class _RelayStore {
|
|
|
947
2003
|
reviewed_by_user_id TEXT
|
|
948
2004
|
);
|
|
949
2005
|
|
|
2006
|
+
CREATE TABLE IF NOT EXISTS relay_user_identities (
|
|
2007
|
+
id TEXT PRIMARY KEY,
|
|
2008
|
+
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
2009
|
+
provider TEXT NOT NULL CHECK (provider IN ('google', 'github')),
|
|
2010
|
+
provider_subject TEXT NOT NULL,
|
|
2011
|
+
provider_email TEXT NOT NULL,
|
|
2012
|
+
created_at TEXT NOT NULL,
|
|
2013
|
+
UNIQUE(provider, provider_subject)
|
|
2014
|
+
);
|
|
2015
|
+
|
|
950
2016
|
CREATE INDEX IF NOT EXISTS relay_pending_registrations_status_idx ON relay_pending_registrations(status, created_at DESC);
|
|
951
2017
|
`);
|
|
952
2018
|
this.ensureColumn("relay_users", "last_seen_at", "TEXT");
|
|
2019
|
+
this.ensureColumn("relay_pending_registrations", "provider", "TEXT NOT NULL DEFAULT 'password'");
|
|
2020
|
+
this.ensureColumn("relay_pending_registrations", "provider_subject", "TEXT");
|
|
953
2021
|
this.ensureColumn("relay_devices", "token", "TEXT");
|
|
2022
|
+
this.ensureColumn(
|
|
2023
|
+
"relay_hosted_sandboxes",
|
|
2024
|
+
"active_turn_count",
|
|
2025
|
+
"INTEGER NOT NULL DEFAULT 0"
|
|
2026
|
+
);
|
|
2027
|
+
this.ensureColumn(
|
|
2028
|
+
"relay_hosted_sandboxes",
|
|
2029
|
+
"last_user_activity_at",
|
|
2030
|
+
"TEXT"
|
|
2031
|
+
);
|
|
2032
|
+
this.ensureColumn("relay_hosted_sandboxes", "idle_deadline_at", "TEXT");
|
|
2033
|
+
this.ensureColumn(
|
|
2034
|
+
"relay_hosted_sandboxes",
|
|
2035
|
+
"lifecycle_generation",
|
|
2036
|
+
"INTEGER NOT NULL DEFAULT 0"
|
|
2037
|
+
);
|
|
2038
|
+
this.ensureColumn("relay_hosted_sandboxes", "codex_config_json", "TEXT");
|
|
2039
|
+
this.ensureColumn(
|
|
2040
|
+
"relay_hosted_sandboxes",
|
|
2041
|
+
"workspace_isolation_enabled",
|
|
2042
|
+
"INTEGER NOT NULL DEFAULT 0"
|
|
2043
|
+
);
|
|
2044
|
+
this.ensureColumn("relay_hosted_sandboxes", "running_since", "TEXT");
|
|
954
2045
|
this.ensureColumn("relay_shares", "thread_title", "TEXT");
|
|
955
2046
|
this.ensureColumn("relay_shares", "workspace_id", "TEXT");
|
|
956
2047
|
this.ensureColumn("relay_shares", "workspace_label", "TEXT");
|
|
957
|
-
this.ensureColumn(
|
|
958
|
-
|
|
2048
|
+
this.ensureColumn(
|
|
2049
|
+
"relay_shares",
|
|
2050
|
+
"thread_access",
|
|
2051
|
+
"TEXT NOT NULL DEFAULT 'control'"
|
|
2052
|
+
);
|
|
2053
|
+
this.ensureColumn(
|
|
2054
|
+
"relay_shares",
|
|
2055
|
+
"workspace_access",
|
|
2056
|
+
"TEXT NOT NULL DEFAULT 'none'"
|
|
2057
|
+
);
|
|
959
2058
|
this.ensureColumn("relay_shares", "expires_at", "TEXT");
|
|
960
|
-
this.ensureColumn(
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
2059
|
+
this.ensureColumn(
|
|
2060
|
+
"relay_access_grants",
|
|
2061
|
+
"workspace_scope",
|
|
2062
|
+
"TEXT NOT NULL DEFAULT 'all'"
|
|
2063
|
+
);
|
|
2064
|
+
this.ensureColumn(
|
|
2065
|
+
"relay_access_grants",
|
|
2066
|
+
"workspace_ids",
|
|
2067
|
+
"TEXT NOT NULL DEFAULT '[]'"
|
|
2068
|
+
);
|
|
2069
|
+
this.ensureColumn(
|
|
2070
|
+
"relay_access_grants",
|
|
2071
|
+
"can_create_threads",
|
|
2072
|
+
"INTEGER NOT NULL DEFAULT 0"
|
|
2073
|
+
);
|
|
2074
|
+
this.ensureColumn(
|
|
2075
|
+
"relay_share_access_events",
|
|
2076
|
+
"kind",
|
|
2077
|
+
"TEXT NOT NULL DEFAULT 'access'"
|
|
2078
|
+
);
|
|
2079
|
+
this.ensureColumn(
|
|
2080
|
+
"relay_access_grant_events",
|
|
2081
|
+
"kind",
|
|
2082
|
+
"TEXT NOT NULL DEFAULT 'access'"
|
|
2083
|
+
);
|
|
965
2084
|
}
|
|
966
2085
|
ensureColumn(table, column, definition) {
|
|
967
2086
|
const columns = this.sqlite.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -970,6 +2089,59 @@ var RelayStore = class _RelayStore {
|
|
|
970
2089
|
}
|
|
971
2090
|
this.sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
972
2091
|
}
|
|
2092
|
+
migrateHostedSandboxMembers() {
|
|
2093
|
+
this.sqlite.transaction(() => {
|
|
2094
|
+
const legacyRows = this.sqlite.prepare(
|
|
2095
|
+
`SELECT id, device_id, assigned_user_id, created_by_admin_user_id, created_at
|
|
2096
|
+
FROM relay_hosted_sandboxes`
|
|
2097
|
+
).all();
|
|
2098
|
+
const insertMember = this.sqlite.prepare(
|
|
2099
|
+
`INSERT OR IGNORE INTO relay_hosted_sandbox_members
|
|
2100
|
+
(sandbox_id, user_id, position, created_at) VALUES (?, ?, 0, ?)`
|
|
2101
|
+
);
|
|
2102
|
+
const updateSandbox = this.sqlite.prepare(
|
|
2103
|
+
`UPDATE relay_hosted_sandboxes SET assigned_user_id = ? WHERE id = ?`
|
|
2104
|
+
);
|
|
2105
|
+
const updateDevice = this.sqlite.prepare(
|
|
2106
|
+
`UPDATE relay_devices SET owner_user_id = ? WHERE id = ?`
|
|
2107
|
+
);
|
|
2108
|
+
for (const row of legacyRows) {
|
|
2109
|
+
if (row.assigned_user_id !== row.created_by_admin_user_id) {
|
|
2110
|
+
insertMember.run(row.id, row.assigned_user_id, row.created_at);
|
|
2111
|
+
}
|
|
2112
|
+
updateSandbox.run(row.created_by_admin_user_id, row.id);
|
|
2113
|
+
updateDevice.run(row.created_by_admin_user_id, row.device_id);
|
|
2114
|
+
}
|
|
2115
|
+
})();
|
|
2116
|
+
}
|
|
2117
|
+
requireHostedSandboxMembers(userIds) {
|
|
2118
|
+
const uniqueIds = [...new Set(userIds)];
|
|
2119
|
+
if (uniqueIds.length < 1 || uniqueIds.length > 20) {
|
|
2120
|
+
throw new RelayStoreError(
|
|
2121
|
+
400,
|
|
2122
|
+
"bad_request",
|
|
2123
|
+
"A hosted VM requires between 1 and 20 assigned users."
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
if (uniqueIds.length !== userIds.length) {
|
|
2127
|
+
throw new RelayStoreError(
|
|
2128
|
+
400,
|
|
2129
|
+
"bad_request",
|
|
2130
|
+
"Assigned users must be unique."
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
return uniqueIds.map((userId) => {
|
|
2134
|
+
const user = this.requireUser(userId);
|
|
2135
|
+
if (user.role !== "user" || !user.enabled) {
|
|
2136
|
+
throw new RelayStoreError(
|
|
2137
|
+
400,
|
|
2138
|
+
"bad_request",
|
|
2139
|
+
"Hosted VMs can only be assigned to enabled user accounts."
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
return user;
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
973
2145
|
importLegacyJson(legacyJsonPath) {
|
|
974
2146
|
if (!legacyJsonPath || !fs.existsSync(legacyJsonPath)) {
|
|
975
2147
|
return;
|
|
@@ -979,7 +2151,9 @@ var RelayStore = class _RelayStore {
|
|
|
979
2151
|
if (existingUsers.count > 0 || imported === legacyJsonPath) {
|
|
980
2152
|
return;
|
|
981
2153
|
}
|
|
982
|
-
const parsed = JSON.parse(
|
|
2154
|
+
const parsed = JSON.parse(
|
|
2155
|
+
fs.readFileSync(legacyJsonPath, "utf8")
|
|
2156
|
+
);
|
|
983
2157
|
const data = {
|
|
984
2158
|
registrationEnabled: typeof parsed.registrationEnabled === "boolean" ? parsed.registrationEnabled : true,
|
|
985
2159
|
users: Array.isArray(parsed.users) ? parsed.users : [],
|
|
@@ -987,7 +2161,10 @@ var RelayStore = class _RelayStore {
|
|
|
987
2161
|
shares: Array.isArray(parsed.shares) ? parsed.shares : []
|
|
988
2162
|
};
|
|
989
2163
|
const importData = this.sqlite.transaction(() => {
|
|
990
|
-
this.setSetting(
|
|
2164
|
+
this.setSetting(
|
|
2165
|
+
"registrationEnabled",
|
|
2166
|
+
data.registrationEnabled ? "true" : "false"
|
|
2167
|
+
);
|
|
991
2168
|
for (const user of data.users) {
|
|
992
2169
|
this.insertUser(user);
|
|
993
2170
|
}
|
|
@@ -1003,7 +2180,10 @@ var RelayStore = class _RelayStore {
|
|
|
1003
2180
|
}
|
|
1004
2181
|
ensureRegistrationSetting(registrationEnabled) {
|
|
1005
2182
|
if (this.getSetting("registrationEnabled") === null) {
|
|
1006
|
-
this.setSetting(
|
|
2183
|
+
this.setSetting(
|
|
2184
|
+
"registrationEnabled",
|
|
2185
|
+
registrationEnabled ? "true" : "false"
|
|
2186
|
+
);
|
|
1007
2187
|
}
|
|
1008
2188
|
}
|
|
1009
2189
|
registrationEnabled() {
|
|
@@ -1014,7 +2194,9 @@ var RelayStore = class _RelayStore {
|
|
|
1014
2194
|
return row?.value ?? null;
|
|
1015
2195
|
}
|
|
1016
2196
|
setSetting(key, value) {
|
|
1017
|
-
this.sqlite.prepare(
|
|
2197
|
+
this.sqlite.prepare(
|
|
2198
|
+
"INSERT INTO relay_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
|
2199
|
+
).run(key, value);
|
|
1018
2200
|
}
|
|
1019
2201
|
deleteSetting(key) {
|
|
1020
2202
|
this.sqlite.prepare("DELETE FROM relay_settings WHERE key = ?").run(key);
|
|
@@ -1023,16 +2205,32 @@ var RelayStore = class _RelayStore {
|
|
|
1023
2205
|
const email = input.email.trim().toLowerCase();
|
|
1024
2206
|
const username = normalizeUsername(input.username);
|
|
1025
2207
|
if (!email.includes("@")) {
|
|
1026
|
-
throw new RelayStoreError(
|
|
2208
|
+
throw new RelayStoreError(
|
|
2209
|
+
400,
|
|
2210
|
+
"bad_request",
|
|
2211
|
+
"A valid email address is required."
|
|
2212
|
+
);
|
|
1027
2213
|
}
|
|
1028
2214
|
if (username.length < 3) {
|
|
1029
|
-
throw new RelayStoreError(
|
|
2215
|
+
throw new RelayStoreError(
|
|
2216
|
+
400,
|
|
2217
|
+
"bad_request",
|
|
2218
|
+
"Username must be at least 3 characters."
|
|
2219
|
+
);
|
|
1030
2220
|
}
|
|
1031
2221
|
if (input.password.length < 8) {
|
|
1032
|
-
throw new RelayStoreError(
|
|
2222
|
+
throw new RelayStoreError(
|
|
2223
|
+
400,
|
|
2224
|
+
"bad_request",
|
|
2225
|
+
"Password must be at least 8 characters."
|
|
2226
|
+
);
|
|
1033
2227
|
}
|
|
1034
2228
|
if (this.getUserByIdentifier(email) || this.getUserByUsername(username)) {
|
|
1035
|
-
throw new RelayStoreError(
|
|
2229
|
+
throw new RelayStoreError(
|
|
2230
|
+
409,
|
|
2231
|
+
"conflict",
|
|
2232
|
+
"A user with that email or username already exists."
|
|
2233
|
+
);
|
|
1036
2234
|
}
|
|
1037
2235
|
const passwordSalt = crypto.randomBytes(16).toString("base64url");
|
|
1038
2236
|
return {
|
|
@@ -1064,7 +2262,9 @@ var RelayStore = class _RelayStore {
|
|
|
1064
2262
|
expiresAt: Date.now() + SESSION_TTL_MS,
|
|
1065
2263
|
nonce: crypto.randomBytes(16).toString("base64url")
|
|
1066
2264
|
};
|
|
1067
|
-
const payloadText = Buffer.from(JSON.stringify(payload), "utf8").toString(
|
|
2265
|
+
const payloadText = Buffer.from(JSON.stringify(payload), "utf8").toString(
|
|
2266
|
+
"base64url"
|
|
2267
|
+
);
|
|
1068
2268
|
const signature = crypto.createHmac("sha256", this.sessionSecret).update(payloadText).digest("base64url");
|
|
1069
2269
|
return `${payloadText}.${signature}`;
|
|
1070
2270
|
}
|
|
@@ -1078,7 +2278,9 @@ var RelayStore = class _RelayStore {
|
|
|
1078
2278
|
return null;
|
|
1079
2279
|
}
|
|
1080
2280
|
try {
|
|
1081
|
-
const payload = JSON.parse(
|
|
2281
|
+
const payload = JSON.parse(
|
|
2282
|
+
Buffer.from(payloadText, "base64url").toString("utf8")
|
|
2283
|
+
);
|
|
1082
2284
|
if (typeof payload?.userId !== "string" || typeof payload?.expiresAt !== "number" || typeof payload?.nonce !== "string" || payload.expiresAt <= Date.now()) {
|
|
1083
2285
|
return null;
|
|
1084
2286
|
}
|
|
@@ -1140,6 +2342,34 @@ var RelayStore = class _RelayStore {
|
|
|
1140
2342
|
device.createdAt
|
|
1141
2343
|
);
|
|
1142
2344
|
}
|
|
2345
|
+
insertHostedOperation(operation) {
|
|
2346
|
+
this.sqlite.prepare(
|
|
2347
|
+
`
|
|
2348
|
+
INSERT INTO relay_hosted_operations (
|
|
2349
|
+
id, sandbox_id, action, status, error_code, error_message,
|
|
2350
|
+
created_at, updated_at
|
|
2351
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2352
|
+
`
|
|
2353
|
+
).run(
|
|
2354
|
+
operation.id,
|
|
2355
|
+
operation.sandboxId,
|
|
2356
|
+
operation.action,
|
|
2357
|
+
operation.status,
|
|
2358
|
+
operation.errorCode,
|
|
2359
|
+
operation.errorMessage,
|
|
2360
|
+
operation.createdAt,
|
|
2361
|
+
operation.updatedAt
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
getHostedOperation(id) {
|
|
2365
|
+
const row = this.sqlite.prepare("SELECT * FROM relay_hosted_operations WHERE id = ?").get(id);
|
|
2366
|
+
return row ? this.rowToHostedOperation(row) : null;
|
|
2367
|
+
}
|
|
2368
|
+
getHostedOperations(sandboxId) {
|
|
2369
|
+
return this.sqlite.prepare(
|
|
2370
|
+
"SELECT * FROM relay_hosted_operations WHERE sandbox_id = ? ORDER BY created_at DESC"
|
|
2371
|
+
).all(sandboxId).map((row) => this.rowToHostedOperation(row));
|
|
2372
|
+
}
|
|
1143
2373
|
insertShare(share) {
|
|
1144
2374
|
this.sqlite.prepare(
|
|
1145
2375
|
`
|
|
@@ -1308,7 +2538,9 @@ var RelayStore = class _RelayStore {
|
|
|
1308
2538
|
);
|
|
1309
2539
|
}
|
|
1310
2540
|
getUser(id) {
|
|
1311
|
-
return this.rowToUser(
|
|
2541
|
+
return this.rowToUser(
|
|
2542
|
+
this.sqlite.prepare("SELECT * FROM relay_users WHERE id = ?").get(id)
|
|
2543
|
+
);
|
|
1312
2544
|
}
|
|
1313
2545
|
getUserByIdentifier(identifier) {
|
|
1314
2546
|
return this.rowToUser(
|
|
@@ -1324,19 +2556,37 @@ var RelayStore = class _RelayStore {
|
|
|
1324
2556
|
return this.sqlite.prepare("SELECT * FROM relay_users ORDER BY created_at ASC").all().map((row) => this.rowToUser(row)).filter((user) => Boolean(user));
|
|
1325
2557
|
}
|
|
1326
2558
|
getDevice(id) {
|
|
1327
|
-
return this.rowToDevice(
|
|
2559
|
+
return this.rowToDevice(
|
|
2560
|
+
this.sqlite.prepare("SELECT * FROM relay_devices WHERE id = ?").get(id)
|
|
2561
|
+
);
|
|
1328
2562
|
}
|
|
1329
2563
|
getDevices() {
|
|
1330
2564
|
return this.sqlite.prepare("SELECT * FROM relay_devices ORDER BY created_at ASC").all().map((row) => this.rowToDevice(row)).filter((device) => Boolean(device));
|
|
1331
2565
|
}
|
|
1332
2566
|
getDevicesByOwner(ownerUserId) {
|
|
1333
|
-
return this.sqlite.prepare(
|
|
2567
|
+
return this.sqlite.prepare(
|
|
2568
|
+
"SELECT * FROM relay_devices WHERE owner_user_id = ? ORDER BY created_at ASC"
|
|
2569
|
+
).all(ownerUserId).map((row) => this.rowToDevice(row)).filter((device) => Boolean(device));
|
|
2570
|
+
}
|
|
2571
|
+
getHostedDevicesByMember(userId) {
|
|
2572
|
+
return this.sqlite.prepare(
|
|
2573
|
+
`SELECT d.*
|
|
2574
|
+
FROM relay_hosted_sandbox_members m
|
|
2575
|
+
JOIN relay_hosted_sandboxes hs ON hs.id = m.sandbox_id
|
|
2576
|
+
JOIN relay_devices d ON d.id = hs.device_id
|
|
2577
|
+
WHERE m.user_id = ?
|
|
2578
|
+
ORDER BY d.created_at ASC`
|
|
2579
|
+
).all(userId).map((row) => this.rowToDevice(row)).filter((device) => Boolean(device));
|
|
1334
2580
|
}
|
|
1335
2581
|
getSharesByOwner(ownerUserId) {
|
|
1336
|
-
return this.sqlite.prepare(
|
|
2582
|
+
return this.sqlite.prepare(
|
|
2583
|
+
"SELECT * FROM relay_shares WHERE owner_user_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC"
|
|
2584
|
+
).all(ownerUserId, (/* @__PURE__ */ new Date()).toISOString()).map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
|
|
1337
2585
|
}
|
|
1338
2586
|
getSharesByTarget(targetUserId) {
|
|
1339
|
-
return this.sqlite.prepare(
|
|
2587
|
+
return this.sqlite.prepare(
|
|
2588
|
+
"SELECT * FROM relay_shares WHERE target_user_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC"
|
|
2589
|
+
).all(targetUserId, (/* @__PURE__ */ new Date()).toISOString()).map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
|
|
1340
2590
|
}
|
|
1341
2591
|
getShares(options = {}) {
|
|
1342
2592
|
const sql = options.includeRevoked ? "SELECT * FROM relay_shares ORDER BY created_at DESC" : "SELECT * FROM relay_shares WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC";
|
|
@@ -1344,10 +2594,14 @@ var RelayStore = class _RelayStore {
|
|
|
1344
2594
|
return rows.map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
|
|
1345
2595
|
}
|
|
1346
2596
|
getGrantsByOwner(ownerUserId) {
|
|
1347
|
-
return this.sqlite.prepare(
|
|
2597
|
+
return this.sqlite.prepare(
|
|
2598
|
+
"SELECT * FROM relay_access_grants WHERE owner_user_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC"
|
|
2599
|
+
).all(ownerUserId, (/* @__PURE__ */ new Date()).toISOString()).map((row) => this.rowToGrant(row)).filter((grant) => Boolean(grant));
|
|
1348
2600
|
}
|
|
1349
2601
|
getGrantsByTarget(targetUserId) {
|
|
1350
|
-
return this.sqlite.prepare(
|
|
2602
|
+
return this.sqlite.prepare(
|
|
2603
|
+
"SELECT * FROM relay_access_grants WHERE target_user_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC"
|
|
2604
|
+
).all(targetUserId, (/* @__PURE__ */ new Date()).toISOString()).map((row) => this.rowToGrant(row)).filter((grant) => Boolean(grant));
|
|
1351
2605
|
}
|
|
1352
2606
|
findBestGrant(userId, deviceId, scope) {
|
|
1353
2607
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1372,7 +2626,9 @@ var RelayStore = class _RelayStore {
|
|
|
1372
2626
|
`
|
|
1373
2627
|
).all(userId, deviceId, now);
|
|
1374
2628
|
const grants = rows.map((row) => this.rowToGrant(row)).filter((grant) => Boolean(grant));
|
|
1375
|
-
const matchingGrants = grants.filter(
|
|
2629
|
+
const matchingGrants = grants.filter(
|
|
2630
|
+
(grant) => this.grantMatchesScope(grant, scope)
|
|
2631
|
+
);
|
|
1376
2632
|
return matchingGrants.length > 0 ? mergeMatchingGrants(matchingGrants, scope) : null;
|
|
1377
2633
|
}
|
|
1378
2634
|
grantMatchesScope(grant, scope) {
|
|
@@ -1445,7 +2701,9 @@ var RelayStore = class _RelayStore {
|
|
|
1445
2701
|
};
|
|
1446
2702
|
}
|
|
1447
2703
|
conversationCountsByUser(days) {
|
|
1448
|
-
const since = new Date(
|
|
2704
|
+
const since = new Date(
|
|
2705
|
+
Date.now() - days * 24 * 60 * 60 * 1e3
|
|
2706
|
+
).toISOString();
|
|
1449
2707
|
const rows = this.sqlite.prepare(
|
|
1450
2708
|
`
|
|
1451
2709
|
SELECT user_id, COUNT(*) AS count
|
|
@@ -1467,13 +2725,23 @@ var RelayStore = class _RelayStore {
|
|
|
1467
2725
|
}
|
|
1468
2726
|
requirePendingRegistration(id) {
|
|
1469
2727
|
const record = this.rowToPendingRegistration(
|
|
1470
|
-
this.sqlite.prepare(
|
|
2728
|
+
this.sqlite.prepare(
|
|
2729
|
+
"SELECT * FROM relay_pending_registrations WHERE id = ? AND status = 'pending'"
|
|
2730
|
+
).get(id)
|
|
1471
2731
|
);
|
|
1472
2732
|
if (!record) {
|
|
1473
|
-
throw new RelayStoreError(
|
|
2733
|
+
throw new RelayStoreError(
|
|
2734
|
+
404,
|
|
2735
|
+
"not_found",
|
|
2736
|
+
"Pending registration was not found."
|
|
2737
|
+
);
|
|
1474
2738
|
}
|
|
1475
2739
|
if (this.getUserByIdentifier(record.email) || this.getUserByUsername(record.username)) {
|
|
1476
|
-
throw new RelayStoreError(
|
|
2740
|
+
throw new RelayStoreError(
|
|
2741
|
+
409,
|
|
2742
|
+
"conflict",
|
|
2743
|
+
"A user with that email or username already exists."
|
|
2744
|
+
);
|
|
1477
2745
|
}
|
|
1478
2746
|
return record;
|
|
1479
2747
|
}
|
|
@@ -1482,7 +2750,8 @@ var RelayStore = class _RelayStore {
|
|
|
1482
2750
|
id: record.id,
|
|
1483
2751
|
email: record.email,
|
|
1484
2752
|
username: record.username,
|
|
1485
|
-
createdAt: record.createdAt
|
|
2753
|
+
createdAt: record.createdAt,
|
|
2754
|
+
provider: record.provider
|
|
1486
2755
|
};
|
|
1487
2756
|
}
|
|
1488
2757
|
insertPendingRegistration(record) {
|
|
@@ -1490,8 +2759,8 @@ var RelayStore = class _RelayStore {
|
|
|
1490
2759
|
`
|
|
1491
2760
|
INSERT INTO relay_pending_registrations (
|
|
1492
2761
|
id, email, username, password_salt, password_hash,
|
|
1493
|
-
created_at, status, reviewed_at, reviewed_by_user_id
|
|
1494
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2762
|
+
created_at, status, reviewed_at, reviewed_by_user_id, provider, provider_subject
|
|
2763
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1495
2764
|
`
|
|
1496
2765
|
).run(
|
|
1497
2766
|
record.id,
|
|
@@ -1502,9 +2771,32 @@ var RelayStore = class _RelayStore {
|
|
|
1502
2771
|
record.createdAt,
|
|
1503
2772
|
record.status,
|
|
1504
2773
|
record.reviewedAt,
|
|
1505
|
-
record.reviewedByUserId
|
|
2774
|
+
record.reviewedByUserId,
|
|
2775
|
+
record.provider,
|
|
2776
|
+
record.providerSubject
|
|
1506
2777
|
);
|
|
1507
2778
|
}
|
|
2779
|
+
insertIdentity(userId, provider, subject, email) {
|
|
2780
|
+
this.sqlite.prepare(`
|
|
2781
|
+
INSERT INTO relay_user_identities (
|
|
2782
|
+
id, user_id, provider, provider_subject, provider_email, created_at
|
|
2783
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
2784
|
+
`).run(crypto.randomUUID(), userId, provider, subject, email, (/* @__PURE__ */ new Date()).toISOString());
|
|
2785
|
+
}
|
|
2786
|
+
availableUsername(input) {
|
|
2787
|
+
const base = normalizeUsername(input).slice(0, 48) || "user";
|
|
2788
|
+
let candidate = base.length >= 3 ? base : `${base}user`;
|
|
2789
|
+
let suffix = 1;
|
|
2790
|
+
const isTaken = (username) => Boolean(
|
|
2791
|
+
this.getUserByUsername(username) || this.sqlite.prepare(
|
|
2792
|
+
"SELECT 1 FROM relay_pending_registrations WHERE username = ? AND status = 'pending' LIMIT 1"
|
|
2793
|
+
).get(username)
|
|
2794
|
+
);
|
|
2795
|
+
while (isTaken(candidate)) {
|
|
2796
|
+
candidate = `${base.slice(0, 42)}-${suffix++}`;
|
|
2797
|
+
}
|
|
2798
|
+
return candidate;
|
|
2799
|
+
}
|
|
1508
2800
|
rowToUser(row) {
|
|
1509
2801
|
if (!row) return null;
|
|
1510
2802
|
return {
|
|
@@ -1531,6 +2823,60 @@ var RelayStore = class _RelayStore {
|
|
|
1531
2823
|
createdAt: row.created_at
|
|
1532
2824
|
};
|
|
1533
2825
|
}
|
|
2826
|
+
rowToHostedSandbox(row) {
|
|
2827
|
+
const assignedUsers = this.sqlite.prepare(
|
|
2828
|
+
`SELECT u.*
|
|
2829
|
+
FROM relay_hosted_sandbox_members m
|
|
2830
|
+
JOIN relay_users u ON u.id = m.user_id
|
|
2831
|
+
WHERE m.sandbox_id = ?
|
|
2832
|
+
ORDER BY m.position ASC, m.created_at ASC`
|
|
2833
|
+
).all(row.id).map((userRow) => this.rowToUser(userRow)).filter((user) => Boolean(user)).map((user) => ({
|
|
2834
|
+
userId: user.id,
|
|
2835
|
+
username: user.username,
|
|
2836
|
+
email: user.email
|
|
2837
|
+
}));
|
|
2838
|
+
const primaryUser = assignedUsers[0];
|
|
2839
|
+
const device = this.getDevice(row.device_id);
|
|
2840
|
+
return {
|
|
2841
|
+
id: row.id,
|
|
2842
|
+
deviceId: row.device_id,
|
|
2843
|
+
deviceName: device?.name ?? "Hosted supervisor VM",
|
|
2844
|
+
assignedUserId: primaryUser?.userId ?? row.assigned_user_id,
|
|
2845
|
+
assignedUsername: primaryUser?.username ?? "unknown",
|
|
2846
|
+
assignedUsers,
|
|
2847
|
+
workspaceIsolationEnabled: Boolean(row.workspace_isolation_enabled),
|
|
2848
|
+
createdByAdminUserId: row.created_by_admin_user_id,
|
|
2849
|
+
provider: "incus",
|
|
2850
|
+
providerInstanceId: row.provider_instance_id,
|
|
2851
|
+
imageVersion: row.image_version,
|
|
2852
|
+
resources: {
|
|
2853
|
+
cpuCount: row.cpu_count,
|
|
2854
|
+
memoryMiB: row.memory_mib,
|
|
2855
|
+
diskGiB: row.disk_gib
|
|
2856
|
+
},
|
|
2857
|
+
status: row.status,
|
|
2858
|
+
lastErrorCode: row.last_error_code,
|
|
2859
|
+
lastErrorMessage: row.last_error_message,
|
|
2860
|
+
activeTurnCount: row.active_turn_count,
|
|
2861
|
+
lastUserActivityAt: row.last_user_activity_at,
|
|
2862
|
+
idleDeadlineAt: row.idle_deadline_at,
|
|
2863
|
+
runningSince: row.running_since,
|
|
2864
|
+
createdAt: row.created_at,
|
|
2865
|
+
updatedAt: row.updated_at
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
rowToHostedOperation(row) {
|
|
2869
|
+
return {
|
|
2870
|
+
id: row.id,
|
|
2871
|
+
sandboxId: row.sandbox_id,
|
|
2872
|
+
action: row.action,
|
|
2873
|
+
status: row.status,
|
|
2874
|
+
errorCode: row.error_code,
|
|
2875
|
+
errorMessage: row.error_message,
|
|
2876
|
+
createdAt: row.created_at,
|
|
2877
|
+
updatedAt: row.updated_at
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
1534
2880
|
rowToShare(row) {
|
|
1535
2881
|
if (!row) return null;
|
|
1536
2882
|
return {
|
|
@@ -1596,10 +2942,32 @@ var RelayStore = class _RelayStore {
|
|
|
1596
2942
|
createdAt: row.created_at,
|
|
1597
2943
|
status: row.status,
|
|
1598
2944
|
reviewedAt: row.reviewed_at ?? null,
|
|
1599
|
-
reviewedByUserId: row.reviewed_by_user_id ?? null
|
|
2945
|
+
reviewedByUserId: row.reviewed_by_user_id ?? null,
|
|
2946
|
+
provider: row.provider ?? "password",
|
|
2947
|
+
providerSubject: row.provider_subject ?? null
|
|
1600
2948
|
};
|
|
1601
2949
|
}
|
|
1602
2950
|
};
|
|
2951
|
+
function parseHostedCodexConfig(value) {
|
|
2952
|
+
const fallback = {
|
|
2953
|
+
modelProvider: "OpenAI",
|
|
2954
|
+
model: "gpt-5.4",
|
|
2955
|
+
reviewModel: "gpt-5.4",
|
|
2956
|
+
reasoningEffort: "medium",
|
|
2957
|
+
baseUrl: "https://api.openai.com/v1",
|
|
2958
|
+
wireApi: "responses",
|
|
2959
|
+
requiresOpenaiAuth: true,
|
|
2960
|
+
disableResponseStorage: true,
|
|
2961
|
+
networkAccess: "enabled",
|
|
2962
|
+
goals: true
|
|
2963
|
+
};
|
|
2964
|
+
if (!value) return fallback;
|
|
2965
|
+
try {
|
|
2966
|
+
return { ...fallback, ...JSON.parse(value) };
|
|
2967
|
+
} catch {
|
|
2968
|
+
return fallback;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
1603
2971
|
var RelayStoreError = class extends Error {
|
|
1604
2972
|
constructor(statusCode, code, message) {
|
|
1605
2973
|
super(message);
|
|
@@ -1634,7 +3002,9 @@ function normalizeWorkspaceIds(values) {
|
|
|
1634
3002
|
if (!Array.isArray(values)) {
|
|
1635
3003
|
return [];
|
|
1636
3004
|
}
|
|
1637
|
-
return Array.from(
|
|
3005
|
+
return Array.from(
|
|
3006
|
+
new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
3007
|
+
).sort();
|
|
1638
3008
|
}
|
|
1639
3009
|
function mergeMatchingGrants(grants, scope) {
|
|
1640
3010
|
const sorted = [...grants].sort(compareGrantCapability);
|
|
@@ -1710,7 +3080,9 @@ function parseWorkspaceIds(value) {
|
|
|
1710
3080
|
}
|
|
1711
3081
|
try {
|
|
1712
3082
|
const parsed = JSON.parse(value);
|
|
1713
|
-
return normalizeWorkspaceIds(
|
|
3083
|
+
return normalizeWorkspaceIds(
|
|
3084
|
+
Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []
|
|
3085
|
+
);
|
|
1714
3086
|
} catch {
|
|
1715
3087
|
return [];
|
|
1716
3088
|
}
|
|
@@ -1755,10 +3127,554 @@ function safeEqual(left, right) {
|
|
|
1755
3127
|
return crypto.timingSafeEqual(leftBuffer, rightBuffer);
|
|
1756
3128
|
}
|
|
1757
3129
|
|
|
3130
|
+
// src/hosted-sandbox-reconciler.ts
|
|
3131
|
+
var HostedSandboxReconciler = class {
|
|
3132
|
+
constructor(store, provider, config2) {
|
|
3133
|
+
this.store = store;
|
|
3134
|
+
this.provider = provider;
|
|
3135
|
+
this.config = config2;
|
|
3136
|
+
}
|
|
3137
|
+
store;
|
|
3138
|
+
provider;
|
|
3139
|
+
config;
|
|
3140
|
+
latest = emptyReport();
|
|
3141
|
+
timer = null;
|
|
3142
|
+
inFlight = null;
|
|
3143
|
+
start() {
|
|
3144
|
+
if (this.timer || this.config.provider === "disabled") return;
|
|
3145
|
+
void this.run();
|
|
3146
|
+
this.timer = setInterval(
|
|
3147
|
+
() => void this.run(),
|
|
3148
|
+
this.config.reconcileIntervalMs
|
|
3149
|
+
);
|
|
3150
|
+
this.timer.unref?.();
|
|
3151
|
+
}
|
|
3152
|
+
close() {
|
|
3153
|
+
if (this.timer) clearInterval(this.timer);
|
|
3154
|
+
this.timer = null;
|
|
3155
|
+
}
|
|
3156
|
+
read() {
|
|
3157
|
+
return this.latest;
|
|
3158
|
+
}
|
|
3159
|
+
run() {
|
|
3160
|
+
if (this.inFlight) return this.inFlight;
|
|
3161
|
+
this.inFlight = this.performAudit().finally(() => {
|
|
3162
|
+
this.inFlight = null;
|
|
3163
|
+
});
|
|
3164
|
+
return this.inFlight;
|
|
3165
|
+
}
|
|
3166
|
+
async deleteOrphanInstance(id) {
|
|
3167
|
+
const report = await this.run();
|
|
3168
|
+
if (!report.orphanInstances.some((instance) => instance.id === id)) {
|
|
3169
|
+
throw new RelayStoreError(
|
|
3170
|
+
409,
|
|
3171
|
+
"conflict",
|
|
3172
|
+
"The instance is no longer an orphan."
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
await this.provider.delete(id, `relay-orphan-instance-delete-${id}`);
|
|
3176
|
+
return this.performAudit();
|
|
3177
|
+
}
|
|
3178
|
+
async deleteOrphanCredential(credentialRef) {
|
|
3179
|
+
const report = await this.run();
|
|
3180
|
+
if (!report.orphanCredentials.some(
|
|
3181
|
+
(credential) => credential.credentialRef === credentialRef
|
|
3182
|
+
)) {
|
|
3183
|
+
throw new RelayStoreError(
|
|
3184
|
+
409,
|
|
3185
|
+
"conflict",
|
|
3186
|
+
"The credential is no longer an orphan."
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
await this.provider.deleteCredential(
|
|
3190
|
+
credentialRef,
|
|
3191
|
+
`relay-orphan-credential-delete-${credentialRef}`
|
|
3192
|
+
);
|
|
3193
|
+
return this.performAudit();
|
|
3194
|
+
}
|
|
3195
|
+
async performAudit() {
|
|
3196
|
+
try {
|
|
3197
|
+
const expected = this.store.listHostedProviderRecords();
|
|
3198
|
+
const inventory = await this.provider.inventory();
|
|
3199
|
+
const expectedIds = new Set(expected.map((record) => record.id));
|
|
3200
|
+
const expectedCredentials = new Set(
|
|
3201
|
+
expected.map((record) => record.credentialRef)
|
|
3202
|
+
);
|
|
3203
|
+
const instanceIds = new Set(
|
|
3204
|
+
inventory.instances.map((instance) => instance.id)
|
|
3205
|
+
);
|
|
3206
|
+
const credentialRefs = new Set(
|
|
3207
|
+
inventory.credentials.map((credential) => credential.credentialRef)
|
|
3208
|
+
);
|
|
3209
|
+
const orphanInstances = inventory.instances.filter(
|
|
3210
|
+
(instance) => !expectedIds.has(instance.id)
|
|
3211
|
+
);
|
|
3212
|
+
const orphanCredentials = inventory.credentials.filter(
|
|
3213
|
+
(credential) => !expectedCredentials.has(credential.credentialRef)
|
|
3214
|
+
);
|
|
3215
|
+
const missingInstanceSandboxIds = expected.filter((record) => !instanceIds.has(record.id)).map((record) => record.id);
|
|
3216
|
+
const missingCredentialSandboxIds = expected.filter((record) => !credentialRefs.has(record.credentialRef)).map((record) => record.id);
|
|
3217
|
+
const hasIssues = orphanInstances.length > 0 || orphanCredentials.length > 0 || missingInstanceSandboxIds.length > 0 || missingCredentialSandboxIds.length > 0;
|
|
3218
|
+
this.latest = {
|
|
3219
|
+
status: hasIssues ? "issues" : "healthy",
|
|
3220
|
+
checkedAt: inventory.checkedAt,
|
|
3221
|
+
errorCode: null,
|
|
3222
|
+
missingInstanceSandboxIds,
|
|
3223
|
+
missingCredentialSandboxIds,
|
|
3224
|
+
orphanInstances,
|
|
3225
|
+
orphanCredentials,
|
|
3226
|
+
orphanSnapshotCount: orphanInstances.reduce(
|
|
3227
|
+
(count, instance) => count + instance.snapshots.length,
|
|
3228
|
+
0
|
|
3229
|
+
)
|
|
3230
|
+
};
|
|
3231
|
+
} catch {
|
|
3232
|
+
this.latest = {
|
|
3233
|
+
...emptyReport(),
|
|
3234
|
+
status: "unavailable",
|
|
3235
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3236
|
+
errorCode: "hosted_inventory_unavailable"
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
return this.latest;
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
function emptyReport() {
|
|
3243
|
+
return {
|
|
3244
|
+
status: "never_run",
|
|
3245
|
+
checkedAt: null,
|
|
3246
|
+
errorCode: null,
|
|
3247
|
+
missingInstanceSandboxIds: [],
|
|
3248
|
+
missingCredentialSandboxIds: [],
|
|
3249
|
+
orphanInstances: [],
|
|
3250
|
+
orphanCredentials: [],
|
|
3251
|
+
orphanSnapshotCount: 0
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// src/hosted-sandbox-service.ts
|
|
3256
|
+
import crypto2 from "crypto";
|
|
3257
|
+
var HostedSandboxService = class {
|
|
3258
|
+
constructor(store, provider, config2) {
|
|
3259
|
+
this.store = store;
|
|
3260
|
+
this.provider = provider;
|
|
3261
|
+
this.config = config2;
|
|
3262
|
+
}
|
|
3263
|
+
store;
|
|
3264
|
+
provider;
|
|
3265
|
+
config;
|
|
3266
|
+
running = /* @__PURE__ */ new Map();
|
|
3267
|
+
idleTimers = /* @__PURE__ */ new Map();
|
|
3268
|
+
list() {
|
|
3269
|
+
return this.store.listHostedSandboxes();
|
|
3270
|
+
}
|
|
3271
|
+
detail(id) {
|
|
3272
|
+
const detail = this.store.getHostedSandboxDetail(id);
|
|
3273
|
+
if (!detail) {
|
|
3274
|
+
throw new RelayStoreError(
|
|
3275
|
+
404,
|
|
3276
|
+
"not_found",
|
|
3277
|
+
"Hosted sandbox was not found."
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
return detail;
|
|
3281
|
+
}
|
|
3282
|
+
async create(input) {
|
|
3283
|
+
this.requireConfiguredRelayUrl();
|
|
3284
|
+
const requestId = crypto2.randomUUID();
|
|
3285
|
+
const credentialRef = await this.provider.createCodexCredential(
|
|
3286
|
+
input.codexFiles,
|
|
3287
|
+
`relay-credential-${requestId}`
|
|
3288
|
+
);
|
|
3289
|
+
try {
|
|
3290
|
+
const created = this.store.createHostedSandboxRequested({
|
|
3291
|
+
createdByAdminUserId: input.createdByAdminUserId,
|
|
3292
|
+
assignedUserIds: input.assignedUserIds,
|
|
3293
|
+
deviceName: input.deviceName,
|
|
3294
|
+
imageVersion: input.imageVersion,
|
|
3295
|
+
resources: input.resources,
|
|
3296
|
+
credentialRef
|
|
3297
|
+
});
|
|
3298
|
+
this.schedule(created.sandbox.id, created.operation.id);
|
|
3299
|
+
return {
|
|
3300
|
+
sandbox: created.sandbox,
|
|
3301
|
+
operation: created.operation
|
|
3302
|
+
};
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
await this.provider.deleteCredential(
|
|
3305
|
+
credentialRef,
|
|
3306
|
+
`relay-credential-compensate-${requestId}`
|
|
3307
|
+
).catch(() => void 0);
|
|
3308
|
+
throw error;
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
updateMembers(id, userIds) {
|
|
3312
|
+
return this.store.setHostedSandboxMembers(id, userIds);
|
|
3313
|
+
}
|
|
3314
|
+
readCodexFiles(id) {
|
|
3315
|
+
this.detail(id);
|
|
3316
|
+
return this.provider.readCodexFiles(id);
|
|
3317
|
+
}
|
|
3318
|
+
async writeCodexFiles(id, files) {
|
|
3319
|
+
this.detail(id);
|
|
3320
|
+
await this.provider.writeCodexFiles(
|
|
3321
|
+
id,
|
|
3322
|
+
files,
|
|
3323
|
+
`relay-codex-files-${crypto2.randomUUID()}`
|
|
3324
|
+
);
|
|
3325
|
+
return { updated: true };
|
|
3326
|
+
}
|
|
3327
|
+
retry(id) {
|
|
3328
|
+
this.detail(id);
|
|
3329
|
+
const operation = this.store.createHostedOperation(id, "create");
|
|
3330
|
+
this.store.updateHostedSandboxStatus(id, "requested");
|
|
3331
|
+
this.schedule(id, operation.id);
|
|
3332
|
+
return operation;
|
|
3333
|
+
}
|
|
3334
|
+
start(id) {
|
|
3335
|
+
const detail = this.detail(id);
|
|
3336
|
+
this.store.recordHostedUserActivity(
|
|
3337
|
+
detail.deviceId,
|
|
3338
|
+
this.config.idleTimeoutMs
|
|
3339
|
+
);
|
|
3340
|
+
const operation = this.store.createHostedOperation(id, "start");
|
|
3341
|
+
this.scheduleLifecycle(id, operation, async () => {
|
|
3342
|
+
this.store.updateHostedSandboxStatus(id, "starting");
|
|
3343
|
+
await this.provider.start(
|
|
3344
|
+
id,
|
|
3345
|
+
`relay-sandbox-start-action-${operation.id}`
|
|
3346
|
+
);
|
|
3347
|
+
this.store.updateHostedSandboxStatus(id, "starting");
|
|
3348
|
+
});
|
|
3349
|
+
return operation;
|
|
3350
|
+
}
|
|
3351
|
+
stop(id) {
|
|
3352
|
+
this.detail(id);
|
|
3353
|
+
const operation = this.store.createHostedOperation(id, "stop");
|
|
3354
|
+
this.scheduleLifecycle(id, operation, async () => {
|
|
3355
|
+
this.store.updateHostedSandboxStatus(id, "stopping");
|
|
3356
|
+
await this.provider.stop(id, `relay-sandbox-stop-action-${operation.id}`);
|
|
3357
|
+
this.store.updateHostedSandboxStatus(id, "stopped");
|
|
3358
|
+
});
|
|
3359
|
+
return operation;
|
|
3360
|
+
}
|
|
3361
|
+
snapshot(id, name) {
|
|
3362
|
+
this.detail(id);
|
|
3363
|
+
const operation = this.store.createHostedOperation(id, "snapshot");
|
|
3364
|
+
this.scheduleLifecycle(
|
|
3365
|
+
id,
|
|
3366
|
+
operation,
|
|
3367
|
+
() => this.provider.snapshot(
|
|
3368
|
+
id,
|
|
3369
|
+
name,
|
|
3370
|
+
`relay-sandbox-snapshot-${operation.id}`
|
|
3371
|
+
)
|
|
3372
|
+
);
|
|
3373
|
+
return operation;
|
|
3374
|
+
}
|
|
3375
|
+
delete(id) {
|
|
3376
|
+
const context = this.store.getHostedProvisionContext(id);
|
|
3377
|
+
if (!context) {
|
|
3378
|
+
throw new RelayStoreError(
|
|
3379
|
+
404,
|
|
3380
|
+
"not_found",
|
|
3381
|
+
"Hosted sandbox was not found."
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
const operation = this.store.createHostedOperation(id, "delete");
|
|
3385
|
+
this.scheduleLifecycle(
|
|
3386
|
+
id,
|
|
3387
|
+
operation,
|
|
3388
|
+
async () => {
|
|
3389
|
+
this.store.updateHostedSandboxStatus(id, "deleting");
|
|
3390
|
+
await this.provider.delete(id, `relay-sandbox-delete-${operation.id}`);
|
|
3391
|
+
await this.provider.deleteCredential(
|
|
3392
|
+
context.credentialRef,
|
|
3393
|
+
`relay-credential-delete-${operation.id}`
|
|
3394
|
+
);
|
|
3395
|
+
this.store.updateHostedOperation(operation.id, "succeeded");
|
|
3396
|
+
this.store.deleteHostedSandboxRecord(id);
|
|
3397
|
+
},
|
|
3398
|
+
{ operationCompletesInside: true }
|
|
3399
|
+
);
|
|
3400
|
+
return operation;
|
|
3401
|
+
}
|
|
3402
|
+
async rotateCredential(id, openaiApiKey) {
|
|
3403
|
+
const context = this.store.getHostedProvisionContext(id);
|
|
3404
|
+
if (!context) {
|
|
3405
|
+
throw new RelayStoreError(
|
|
3406
|
+
404,
|
|
3407
|
+
"not_found",
|
|
3408
|
+
"Hosted sandbox was not found."
|
|
3409
|
+
);
|
|
3410
|
+
}
|
|
3411
|
+
if (this.running.has(id)) {
|
|
3412
|
+
throw new RelayStoreError(
|
|
3413
|
+
409,
|
|
3414
|
+
"conflict",
|
|
3415
|
+
"Another Hosted supervisor VM operation is already running."
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
const relayServerUrl = this.requireConfiguredRelayUrl();
|
|
3419
|
+
const operation = this.store.createHostedOperation(id, "rotate_credential");
|
|
3420
|
+
let credentialRef;
|
|
3421
|
+
try {
|
|
3422
|
+
credentialRef = await this.provider.createCredential(
|
|
3423
|
+
openaiApiKey,
|
|
3424
|
+
`relay-credential-rotate-${operation.id}`
|
|
3425
|
+
);
|
|
3426
|
+
} catch {
|
|
3427
|
+
this.store.updateHostedOperation(operation.id, "failed", {
|
|
3428
|
+
code: "hosted_sandbox_rotate_credential_failed",
|
|
3429
|
+
message: "Hosted supervisor VM credential rotation failed."
|
|
3430
|
+
});
|
|
3431
|
+
throw new RelayStoreError(
|
|
3432
|
+
502,
|
|
3433
|
+
"service_unavailable",
|
|
3434
|
+
"Hosted supervisor VM credential rotation failed."
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3437
|
+
this.scheduleLifecycle(id, operation, async () => {
|
|
3438
|
+
await this.provider.provision(
|
|
3439
|
+
{
|
|
3440
|
+
id,
|
|
3441
|
+
relayServerUrl,
|
|
3442
|
+
relayAgentToken: context.deviceToken,
|
|
3443
|
+
credentialRef,
|
|
3444
|
+
codexConfig: context.codexConfig
|
|
3445
|
+
},
|
|
3446
|
+
`relay-sandbox-reprovision-${operation.id}`
|
|
3447
|
+
);
|
|
3448
|
+
const previousRef = this.store.replaceHostedCredentialRef(
|
|
3449
|
+
id,
|
|
3450
|
+
credentialRef
|
|
3451
|
+
);
|
|
3452
|
+
await this.provider.deleteCredential(
|
|
3453
|
+
previousRef,
|
|
3454
|
+
`relay-credential-retire-${operation.id}`
|
|
3455
|
+
);
|
|
3456
|
+
});
|
|
3457
|
+
return operation;
|
|
3458
|
+
}
|
|
3459
|
+
reconcilePending() {
|
|
3460
|
+
for (const id of this.store.listHostedSandboxesNeedingReconciliation()) {
|
|
3461
|
+
const detail = this.store.getHostedSandboxDetail(id);
|
|
3462
|
+
const operation = detail?.operations.find((candidate) => candidate.action === "create") ?? this.store.createHostedOperation(id, "create");
|
|
3463
|
+
this.schedule(id, operation.id);
|
|
3464
|
+
}
|
|
3465
|
+
this.restoreIdleTimers();
|
|
3466
|
+
}
|
|
3467
|
+
close() {
|
|
3468
|
+
for (const timer of this.idleTimers.values()) {
|
|
3469
|
+
clearTimeout(timer);
|
|
3470
|
+
}
|
|
3471
|
+
this.idleTimers.clear();
|
|
3472
|
+
}
|
|
3473
|
+
markOnline(deviceId) {
|
|
3474
|
+
this.store.markHostedDeviceOnline(deviceId);
|
|
3475
|
+
const sandbox = this.store.getHostedSandboxByDeviceId(deviceId);
|
|
3476
|
+
if (!sandbox) return;
|
|
3477
|
+
const armed = this.store.armHostedIdleDeadline(
|
|
3478
|
+
sandbox.id,
|
|
3479
|
+
this.config.idleTimeoutMs
|
|
3480
|
+
);
|
|
3481
|
+
if (armed?.idleDeadlineAt) {
|
|
3482
|
+
this.scheduleIdleTimer(armed.id, armed.idleDeadlineAt);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
recordUserActivity(deviceId) {
|
|
3486
|
+
const sandbox = this.store.recordHostedUserActivity(
|
|
3487
|
+
deviceId,
|
|
3488
|
+
this.config.idleTimeoutMs
|
|
3489
|
+
);
|
|
3490
|
+
if (!sandbox) return { hosted: false, waking: false };
|
|
3491
|
+
if (sandbox.idleDeadlineAt && sandbox.status === "online") {
|
|
3492
|
+
this.scheduleIdleTimer(sandbox.id, sandbox.idleDeadlineAt);
|
|
3493
|
+
}
|
|
3494
|
+
if (sandbox.status === "stopped") {
|
|
3495
|
+
this.start(sandbox.id);
|
|
3496
|
+
return { hosted: true, waking: true };
|
|
3497
|
+
}
|
|
3498
|
+
return {
|
|
3499
|
+
hosted: true,
|
|
3500
|
+
waking: ["requested", "creating", "starting", "provisioning"].includes(
|
|
3501
|
+
sandbox.status
|
|
3502
|
+
)
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
wakeIfStopped(deviceId) {
|
|
3506
|
+
const sandbox = this.store.getHostedSandboxByDeviceId(deviceId);
|
|
3507
|
+
if (!sandbox) return { hosted: false, waking: false };
|
|
3508
|
+
if (sandbox.status === "stopped") {
|
|
3509
|
+
this.start(sandbox.id);
|
|
3510
|
+
return { hosted: true, waking: true };
|
|
3511
|
+
}
|
|
3512
|
+
return {
|
|
3513
|
+
hosted: true,
|
|
3514
|
+
waking: ["requested", "creating", "starting", "provisioning"].includes(
|
|
3515
|
+
sandbox.status
|
|
3516
|
+
)
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
recordTurnActivity(input) {
|
|
3520
|
+
const sandbox = this.store.recordHostedTurnActivity({
|
|
3521
|
+
...input,
|
|
3522
|
+
idleTimeoutMs: this.config.idleTimeoutMs
|
|
3523
|
+
});
|
|
3524
|
+
if (!sandbox) return;
|
|
3525
|
+
if (sandbox.activeTurnCount > 0) {
|
|
3526
|
+
this.clearIdleTimer(sandbox.id);
|
|
3527
|
+
} else if (sandbox.idleDeadlineAt && sandbox.status === "online") {
|
|
3528
|
+
this.scheduleIdleTimer(sandbox.id, sandbox.idleDeadlineAt);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
schedule(sandboxId, operationId) {
|
|
3532
|
+
if (this.running.has(sandboxId)) {
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const promise = this.runCreateSaga(sandboxId, operationId).finally(() => {
|
|
3536
|
+
this.running.delete(sandboxId);
|
|
3537
|
+
});
|
|
3538
|
+
this.running.set(sandboxId, promise);
|
|
3539
|
+
}
|
|
3540
|
+
scheduleLifecycle(sandboxId, operation, action, options = {}) {
|
|
3541
|
+
if (this.running.has(sandboxId)) {
|
|
3542
|
+
throw new RelayStoreError(
|
|
3543
|
+
409,
|
|
3544
|
+
"conflict",
|
|
3545
|
+
"Another Hosted supervisor VM operation is already running."
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
const promise = (async () => {
|
|
3549
|
+
try {
|
|
3550
|
+
this.store.updateHostedOperation(operation.id, "running");
|
|
3551
|
+
await action();
|
|
3552
|
+
if (!options.operationCompletesInside) {
|
|
3553
|
+
this.store.updateHostedOperation(operation.id, "succeeded");
|
|
3554
|
+
}
|
|
3555
|
+
} catch {
|
|
3556
|
+
const error = {
|
|
3557
|
+
code: `hosted_sandbox_${operation.action}_failed`,
|
|
3558
|
+
message: `Hosted supervisor VM ${operation.action} failed.`
|
|
3559
|
+
};
|
|
3560
|
+
this.store.updateHostedOperation(operation.id, "failed", error);
|
|
3561
|
+
this.store.updateHostedSandboxStatus(sandboxId, "error", {
|
|
3562
|
+
errorCode: error.code,
|
|
3563
|
+
errorMessage: error.message
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
})().finally(() => this.running.delete(sandboxId));
|
|
3567
|
+
this.running.set(sandboxId, promise);
|
|
3568
|
+
}
|
|
3569
|
+
restoreIdleTimers() {
|
|
3570
|
+
for (const candidate of this.store.listHostedIdleDeadlines()) {
|
|
3571
|
+
const normalized = this.store.armHostedIdleDeadline(
|
|
3572
|
+
candidate.id,
|
|
3573
|
+
this.config.idleTimeoutMs
|
|
3574
|
+
);
|
|
3575
|
+
if (normalized?.idleDeadlineAt && normalized.status === "online") {
|
|
3576
|
+
this.scheduleIdleTimer(normalized.id, normalized.idleDeadlineAt);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
scheduleIdleTimer(sandboxId, deadlineAt) {
|
|
3581
|
+
this.clearIdleTimer(sandboxId);
|
|
3582
|
+
const delay = Math.max(0, new Date(deadlineAt).getTime() - Date.now());
|
|
3583
|
+
const timer = setTimeout(() => {
|
|
3584
|
+
this.idleTimers.delete(sandboxId);
|
|
3585
|
+
void this.handleIdleDeadline(sandboxId);
|
|
3586
|
+
}, delay);
|
|
3587
|
+
timer.unref?.();
|
|
3588
|
+
this.idleTimers.set(sandboxId, timer);
|
|
3589
|
+
}
|
|
3590
|
+
clearIdleTimer(sandboxId) {
|
|
3591
|
+
const timer = this.idleTimers.get(sandboxId);
|
|
3592
|
+
if (timer) clearTimeout(timer);
|
|
3593
|
+
this.idleTimers.delete(sandboxId);
|
|
3594
|
+
}
|
|
3595
|
+
async handleIdleDeadline(sandboxId) {
|
|
3596
|
+
if (this.running.has(sandboxId)) return;
|
|
3597
|
+
const candidate = this.store.listHostedIdleDeadlines().find((entry) => entry.id === sandboxId);
|
|
3598
|
+
if (!candidate || !this.store.claimHostedIdleStop(
|
|
3599
|
+
sandboxId,
|
|
3600
|
+
candidate.generation,
|
|
3601
|
+
/* @__PURE__ */ new Date()
|
|
3602
|
+
)) {
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
const operation = this.store.createHostedOperation(sandboxId, "stop");
|
|
3606
|
+
this.scheduleLifecycle(sandboxId, operation, async () => {
|
|
3607
|
+
await this.provider.stop(
|
|
3608
|
+
sandboxId,
|
|
3609
|
+
`relay-sandbox-idle-stop-${operation.id}`
|
|
3610
|
+
);
|
|
3611
|
+
this.store.updateHostedSandboxStatus(sandboxId, "stopped");
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
async runCreateSaga(sandboxId, operationId) {
|
|
3615
|
+
try {
|
|
3616
|
+
const context = this.store.getHostedProvisionContext(sandboxId);
|
|
3617
|
+
if (!context) {
|
|
3618
|
+
throw new Error("Hosted sandbox provision context is unavailable.");
|
|
3619
|
+
}
|
|
3620
|
+
const relayServerUrl = this.requireConfiguredRelayUrl();
|
|
3621
|
+
this.store.updateHostedOperation(operationId, "running");
|
|
3622
|
+
this.store.updateHostedSandboxStatus(sandboxId, "creating");
|
|
3623
|
+
const instance = await this.provider.create(
|
|
3624
|
+
{
|
|
3625
|
+
id: sandboxId,
|
|
3626
|
+
imageVersion: context.sandbox.imageVersion,
|
|
3627
|
+
resources: context.sandbox.resources
|
|
3628
|
+
},
|
|
3629
|
+
`relay-sandbox-create-${sandboxId}`
|
|
3630
|
+
);
|
|
3631
|
+
this.store.updateHostedSandboxStatus(sandboxId, "starting", {
|
|
3632
|
+
providerInstanceId: instance.name
|
|
3633
|
+
});
|
|
3634
|
+
await this.provider.start(sandboxId, `relay-sandbox-start-${sandboxId}`);
|
|
3635
|
+
this.store.updateHostedSandboxStatus(sandboxId, "provisioning");
|
|
3636
|
+
await this.provider.provision(
|
|
3637
|
+
{
|
|
3638
|
+
id: sandboxId,
|
|
3639
|
+
relayServerUrl,
|
|
3640
|
+
relayAgentToken: context.deviceToken,
|
|
3641
|
+
credentialRef: context.credentialRef,
|
|
3642
|
+
codexConfig: context.codexConfig
|
|
3643
|
+
},
|
|
3644
|
+
`relay-sandbox-provision-${sandboxId}`
|
|
3645
|
+
);
|
|
3646
|
+
this.store.updateHostedOperation(operationId, "succeeded");
|
|
3647
|
+
this.store.updateHostedSandboxStatus(sandboxId, "starting");
|
|
3648
|
+
} catch (caught) {
|
|
3649
|
+
const detail = caught instanceof HostedSandboxProviderError && caught.code === "running_instance_limit_reached" ? caught.message : null;
|
|
3650
|
+
const error = {
|
|
3651
|
+
code: "hosted_sandbox_create_failed",
|
|
3652
|
+
message: detail ? `Hosted supervisor VM creation failed: ${detail}` : "Hosted supervisor VM creation failed."
|
|
3653
|
+
};
|
|
3654
|
+
this.store.updateHostedOperation(operationId, "failed", error);
|
|
3655
|
+
this.store.updateHostedSandboxStatus(sandboxId, "error", {
|
|
3656
|
+
errorCode: error.code,
|
|
3657
|
+
errorMessage: error.message
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
requireConfiguredRelayUrl() {
|
|
3662
|
+
if (!this.config.relayServerUrl) {
|
|
3663
|
+
throw new RelayStoreError(
|
|
3664
|
+
503,
|
|
3665
|
+
"service_unavailable",
|
|
3666
|
+
"Hosted supervisor VM relay URL is not configured."
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
return this.config.relayServerUrl;
|
|
3670
|
+
}
|
|
3671
|
+
};
|
|
3672
|
+
|
|
1758
3673
|
// src/app.ts
|
|
1759
3674
|
var RELAY_REQUEST_TIMEOUT_MS = 3e4;
|
|
1760
3675
|
var RELAY_PORTAL_METADATA_TIMEOUT_MS = 900;
|
|
1761
3676
|
var WEBSOCKET_OPEN = 1;
|
|
3677
|
+
var hostedBootstrapPromises = /* @__PURE__ */ new Map();
|
|
1762
3678
|
var RELAY_COOKIE_NAME = "remote_codex_relay_session";
|
|
1763
3679
|
var threadAccessSchema = z.enum(["read", "control"]);
|
|
1764
3680
|
var workspaceAccessSchema = z.enum(["none", "read", "write"]);
|
|
@@ -1777,6 +3693,62 @@ var registerSchema = z.object({
|
|
|
1777
3693
|
var createDeviceSchema = z.object({
|
|
1778
3694
|
name: z.string().trim().min(1).max(120)
|
|
1779
3695
|
});
|
|
3696
|
+
var hostedCodexConfigSchema = z.object({
|
|
3697
|
+
modelProvider: z.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,31}$/),
|
|
3698
|
+
model: z.string().trim().min(1).max(120),
|
|
3699
|
+
reviewModel: z.string().trim().min(1).max(120),
|
|
3700
|
+
reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]),
|
|
3701
|
+
baseUrl: z.string().url().refine((value) => value.startsWith("https://"), "HTTPS is required."),
|
|
3702
|
+
wireApi: z.literal("responses"),
|
|
3703
|
+
requiresOpenaiAuth: z.boolean(),
|
|
3704
|
+
disableResponseStorage: z.boolean(),
|
|
3705
|
+
networkAccess: z.enum(["enabled", "disabled"]),
|
|
3706
|
+
goals: z.boolean()
|
|
3707
|
+
}).strict().default({
|
|
3708
|
+
modelProvider: "OpenAI",
|
|
3709
|
+
model: "gpt-5.4",
|
|
3710
|
+
reviewModel: "gpt-5.4",
|
|
3711
|
+
reasoningEffort: "medium",
|
|
3712
|
+
baseUrl: "https://api.openai.com/v1",
|
|
3713
|
+
wireApi: "responses",
|
|
3714
|
+
requiresOpenaiAuth: true,
|
|
3715
|
+
disableResponseStorage: true,
|
|
3716
|
+
networkAccess: "enabled",
|
|
3717
|
+
goals: true
|
|
3718
|
+
});
|
|
3719
|
+
var createHostedSandboxSchema = z.object({
|
|
3720
|
+
assignedUserIds: z.array(z.string().uuid()).min(1).max(20),
|
|
3721
|
+
deviceName: z.string().trim().min(1).max(120),
|
|
3722
|
+
imageVersion: z.enum([
|
|
3723
|
+
"ubuntu-24.04-v1",
|
|
3724
|
+
"ubuntu-24.04-v2",
|
|
3725
|
+
"ubuntu-24.04-v3",
|
|
3726
|
+
"ubuntu-24.04-v4",
|
|
3727
|
+
"ubuntu-24.04-v5"
|
|
3728
|
+
]),
|
|
3729
|
+
resources: z.object({
|
|
3730
|
+
cpuCount: z.number().int().min(1).max(2),
|
|
3731
|
+
memoryMiB: z.number().int().min(1024).max(2048),
|
|
3732
|
+
diskGiB: z.number().int().min(10).max(12)
|
|
3733
|
+
}),
|
|
3734
|
+
backends: z.array(z.literal("codex")).length(1),
|
|
3735
|
+
codexFiles: z.object({
|
|
3736
|
+
configToml: z.string().min(1).max(128 * 1024),
|
|
3737
|
+
authJson: z.string().min(2).max(128 * 1024)
|
|
3738
|
+
})
|
|
3739
|
+
}).strict();
|
|
3740
|
+
var updateHostedSandboxMembersSchema = z.object({
|
|
3741
|
+
assignedUserIds: z.array(z.string().uuid()).min(1).max(20)
|
|
3742
|
+
}).strict();
|
|
3743
|
+
var updateHostedSandboxSettingsSchema = z.object({ workspaceIsolationEnabled: z.boolean() }).strict();
|
|
3744
|
+
var hostedSnapshotSchema = z.object({
|
|
3745
|
+
name: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/)
|
|
3746
|
+
});
|
|
3747
|
+
var rotateHostedCredentialSchema = z.object({ openaiApiKey: z.string().min(20).max(512) }).strict();
|
|
3748
|
+
var hostedCodexFilesSchema = z.object({
|
|
3749
|
+
configToml: z.string().min(1).max(128 * 1024),
|
|
3750
|
+
authJson: z.string().min(2).max(128 * 1024)
|
|
3751
|
+
}).strict();
|
|
1780
3752
|
var createShareSchema = z.object({
|
|
1781
3753
|
targetIdentifier: z.string().trim().min(1).optional(),
|
|
1782
3754
|
targetUsername: z.string().trim().min(3).optional(),
|
|
@@ -1818,10 +3790,13 @@ var createGrantSchema = z.object({
|
|
|
1818
3790
|
}).refine((input) => input.scope !== "thread" || Boolean(input.threadId), {
|
|
1819
3791
|
message: "threadId is required for thread grants.",
|
|
1820
3792
|
path: ["threadId"]
|
|
1821
|
-
}).refine(
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
3793
|
+
}).refine(
|
|
3794
|
+
(input) => input.scope !== "workspace" || Boolean(input.workspaceId),
|
|
3795
|
+
{
|
|
3796
|
+
message: "workspaceId is required for workspace grants.",
|
|
3797
|
+
path: ["workspaceId"]
|
|
3798
|
+
}
|
|
3799
|
+
);
|
|
1825
3800
|
var updateGrantSchema = z.object({
|
|
1826
3801
|
workspaceId: z.string().uuid().nullable().optional(),
|
|
1827
3802
|
workspaceScope: workspaceScopeSchema.optional(),
|
|
@@ -1844,7 +3819,10 @@ var adminQuerySchema = z.object({
|
|
|
1844
3819
|
var updateRegistrationSettingsSchema = z.object({
|
|
1845
3820
|
enabled: z.boolean().optional(),
|
|
1846
3821
|
registrationPassword: z.string().nullable().optional(),
|
|
1847
|
-
approvalRequired: z.boolean().optional()
|
|
3822
|
+
approvalRequired: z.boolean().optional(),
|
|
3823
|
+
googleAuthEnabled: z.boolean().optional(),
|
|
3824
|
+
githubAuthEnabled: z.boolean().optional(),
|
|
3825
|
+
emailVerificationEnabled: z.boolean().optional()
|
|
1848
3826
|
});
|
|
1849
3827
|
var updateAccountSchema = z.object({
|
|
1850
3828
|
username: z.string().trim().min(3).optional()
|
|
@@ -1865,10 +3843,7 @@ var DEFAULT_WEBVIEW_CORS_ORIGINS = /* @__PURE__ */ new Set([
|
|
|
1865
3843
|
"http://localhost",
|
|
1866
3844
|
"https://localhost"
|
|
1867
3845
|
]);
|
|
1868
|
-
var WEBVIEW_CORS_ALLOW_HEADERS = [
|
|
1869
|
-
"authorization",
|
|
1870
|
-
"content-type"
|
|
1871
|
-
].join(", ");
|
|
3846
|
+
var WEBVIEW_CORS_ALLOW_HEADERS = ["authorization", "content-type"].join(", ");
|
|
1872
3847
|
var WEBVIEW_CORS_ALLOW_METHODS = [
|
|
1873
3848
|
"GET",
|
|
1874
3849
|
"POST",
|
|
@@ -1931,9 +3906,13 @@ var RELAY_RESPONSE_HEADER_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
|
1931
3906
|
]);
|
|
1932
3907
|
function buildRelayServer(config2, options = {}) {
|
|
1933
3908
|
const app2 = Fastify({ logger: false });
|
|
1934
|
-
app2.addContentTypeParser(
|
|
1935
|
-
|
|
1936
|
-
|
|
3909
|
+
app2.addContentTypeParser(
|
|
3910
|
+
"*",
|
|
3911
|
+
{ parseAs: "buffer" },
|
|
3912
|
+
(_request, body, done) => {
|
|
3913
|
+
done(null, body);
|
|
3914
|
+
}
|
|
3915
|
+
);
|
|
1937
3916
|
const store = RelayStore.fromDataDir(
|
|
1938
3917
|
config2.dataDir,
|
|
1939
3918
|
config2.sessionSecret,
|
|
@@ -1943,6 +3922,12 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1943
3922
|
store.setRegistrationEnabled(config2.registrationEnabled);
|
|
1944
3923
|
}
|
|
1945
3924
|
store.ensureRegistrationPassword(config2.registrationPassword);
|
|
3925
|
+
const googleAvailable = Boolean(config2.googleOAuthClientId && config2.googleOAuthClientSecret);
|
|
3926
|
+
const githubAvailable = Boolean(config2.githubOAuthClientId && config2.githubOAuthClientSecret);
|
|
3927
|
+
store.ensureAuthSettings({
|
|
3928
|
+
google: googleAvailable && config2.googleOAuthEnabled !== false,
|
|
3929
|
+
github: githubAvailable && config2.githubOAuthEnabled !== false
|
|
3930
|
+
});
|
|
1946
3931
|
store.seedAdmin({
|
|
1947
3932
|
username: config2.adminUsername,
|
|
1948
3933
|
email: config2.adminEmail,
|
|
@@ -1951,7 +3936,28 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1951
3936
|
const state = {
|
|
1952
3937
|
supervisors: /* @__PURE__ */ new Map()
|
|
1953
3938
|
};
|
|
1954
|
-
const
|
|
3939
|
+
const hostedSandboxProvider = options.hostedSandboxProvider ?? createHostedSandboxProvider(config2.hostedSandbox);
|
|
3940
|
+
const hostedSandboxCapability = new HostedSandboxCapabilityService(
|
|
3941
|
+
hostedSandboxProvider,
|
|
3942
|
+
{ timeoutMs: config2.hostedSandbox.requestTimeoutMs }
|
|
3943
|
+
);
|
|
3944
|
+
const hostedSandboxService = new HostedSandboxService(
|
|
3945
|
+
store,
|
|
3946
|
+
hostedSandboxProvider,
|
|
3947
|
+
config2.hostedSandbox
|
|
3948
|
+
);
|
|
3949
|
+
const hostedSandboxReconciler = new HostedSandboxReconciler(
|
|
3950
|
+
store,
|
|
3951
|
+
hostedSandboxProvider,
|
|
3952
|
+
config2.hostedSandbox
|
|
3953
|
+
);
|
|
3954
|
+
const allowedWebViewCorsOrigins = webViewCorsOrigins(
|
|
3955
|
+
options.env ?? process.env
|
|
3956
|
+
);
|
|
3957
|
+
app2.addHook("onReady", () => {
|
|
3958
|
+
queueMicrotask(() => hostedSandboxService.reconcilePending());
|
|
3959
|
+
queueMicrotask(() => hostedSandboxReconciler.start());
|
|
3960
|
+
});
|
|
1955
3961
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1956
3962
|
if (!allowedWebViewCorsOrigins) {
|
|
1957
3963
|
return;
|
|
@@ -1975,8 +3981,55 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1975
3981
|
supervisorCount: state.supervisors.size
|
|
1976
3982
|
};
|
|
1977
3983
|
});
|
|
3984
|
+
const authSettings = () => ({
|
|
3985
|
+
...store.registrationSettings(),
|
|
3986
|
+
googleAuthAvailable: googleAvailable,
|
|
3987
|
+
githubAuthAvailable: githubAvailable,
|
|
3988
|
+
emailVerificationAvailable: config2.emailVerificationConfigured
|
|
3989
|
+
});
|
|
1978
3990
|
app2.get("/relay/auth/session", async (request) => {
|
|
1979
|
-
return
|
|
3991
|
+
return { ...verifyRelayRequestSession(request, store), registrationSettings: authSettings() };
|
|
3992
|
+
});
|
|
3993
|
+
app2.get("/relay/auth/oauth/:provider/start", async (request, reply) => {
|
|
3994
|
+
const { provider } = z.object({ provider: z.enum(["google", "github"]) }).parse(request.params);
|
|
3995
|
+
const settings = authSettings();
|
|
3996
|
+
if (provider === "google" ? !settings.googleAuthEnabled || !googleAvailable : !settings.githubAuthEnabled || !githubAvailable) {
|
|
3997
|
+
reply.status(403).send({ code: "forbidden", message: `${provider === "google" ? "Google" : "GitHub"} authentication is disabled.` });
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
const state2 = signOAuthState(provider, config2.sessionSecret);
|
|
4001
|
+
const callback = oauthCallbackUrl(request, config2, provider);
|
|
4002
|
+
const url = provider === "google" ? new URL("https://accounts.google.com/o/oauth2/v2/auth") : new URL("https://github.com/login/oauth/authorize");
|
|
4003
|
+
url.searchParams.set("client_id", provider === "google" ? config2.googleOAuthClientId : config2.githubOAuthClientId);
|
|
4004
|
+
url.searchParams.set("redirect_uri", callback);
|
|
4005
|
+
url.searchParams.set("response_type", "code");
|
|
4006
|
+
url.searchParams.set("state", state2);
|
|
4007
|
+
url.searchParams.set("scope", provider === "google" ? "openid email profile" : "read:user user:email");
|
|
4008
|
+
if (provider === "google") url.searchParams.set("prompt", "select_account");
|
|
4009
|
+
reply.redirect(url.toString());
|
|
4010
|
+
});
|
|
4011
|
+
app2.get("/relay/auth/oauth/:provider/callback", async (request, reply) => {
|
|
4012
|
+
const { provider } = z.object({ provider: z.enum(["google", "github"]) }).parse(request.params);
|
|
4013
|
+
const query = z.object({ code: z.string().min(1), state: z.string().min(1) }).parse(request.query);
|
|
4014
|
+
if (!verifyOAuthState(query.state, provider, config2.sessionSecret)) {
|
|
4015
|
+
reply.redirect("/relay-portal?oauthError=OAuth%20request%20expired%20or%20was%20invalid.");
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
try {
|
|
4019
|
+
const identity = provider === "google" ? await fetchGoogleIdentity(query.code, oauthCallbackUrl(request, config2, provider), config2) : await fetchGitHubIdentity(query.code, oauthCallbackUrl(request, config2, provider), config2);
|
|
4020
|
+
const outcome = store.authenticateExternalIdentity(identity, store.registrationSettings().approvalRequired);
|
|
4021
|
+
if (outcome.kind === "pending") {
|
|
4022
|
+
reply.redirect("/relay-portal?oauthPending=1");
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
store.recordUserSeen(outcome.result.session.user.id);
|
|
4026
|
+
attachRelayCookie(reply, outcome.result.token);
|
|
4027
|
+
reply.redirect("/relay-portal");
|
|
4028
|
+
} catch (error) {
|
|
4029
|
+
request.log.error(error);
|
|
4030
|
+
const message = error instanceof RelayStoreError ? error.message : "OAuth authentication failed.";
|
|
4031
|
+
reply.redirect(`/relay-portal?oauthError=${encodeURIComponent(message)}`);
|
|
4032
|
+
}
|
|
1980
4033
|
});
|
|
1981
4034
|
app2.post("/relay/auth/register", async (request, reply) => {
|
|
1982
4035
|
const body = registerSchema.parse(request.body ?? {});
|
|
@@ -2039,7 +4092,11 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2039
4092
|
if (!user) {
|
|
2040
4093
|
return;
|
|
2041
4094
|
}
|
|
2042
|
-
return enrichPortalSummary(
|
|
4095
|
+
return enrichPortalSummary(
|
|
4096
|
+
store.portalSummary(user.id, connectionStatus(state)),
|
|
4097
|
+
state,
|
|
4098
|
+
store
|
|
4099
|
+
);
|
|
2043
4100
|
});
|
|
2044
4101
|
app2.get("/relay/access", async (request, reply) => {
|
|
2045
4102
|
const user = requireRelayUser(request, reply, store);
|
|
@@ -2158,22 +4215,228 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2158
4215
|
const baseSummary = store.adminSummary(connectionStatus(state), {
|
|
2159
4216
|
...query.days !== void 0 ? { conversationWindowDays: query.days } : {}
|
|
2160
4217
|
});
|
|
2161
|
-
|
|
4218
|
+
const summary = await enrichAdminSummary(baseSummary, state, store, query.days);
|
|
4219
|
+
return { ...summary, settings: authSettings() };
|
|
4220
|
+
});
|
|
4221
|
+
app2.get(
|
|
4222
|
+
"/relay/admin/hosted-sandboxes/capability",
|
|
4223
|
+
async (request, reply) => {
|
|
4224
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4225
|
+
if (!user) {
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
return hostedSandboxCapability.read();
|
|
4229
|
+
}
|
|
4230
|
+
);
|
|
4231
|
+
app2.get("/relay/admin/hosted-sandboxes", async (request, reply) => {
|
|
4232
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4233
|
+
if (!user) {
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
return { sandboxes: hostedSandboxService.list() };
|
|
4237
|
+
});
|
|
4238
|
+
app2.get(
|
|
4239
|
+
"/relay/admin/hosted-sandboxes/reconciliation",
|
|
4240
|
+
async (request, reply) => {
|
|
4241
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4242
|
+
if (!user) return;
|
|
4243
|
+
return hostedSandboxReconciler.read();
|
|
4244
|
+
}
|
|
4245
|
+
);
|
|
4246
|
+
app2.post(
|
|
4247
|
+
"/relay/admin/hosted-sandboxes/reconciliation/run",
|
|
4248
|
+
async (request, reply) => {
|
|
4249
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4250
|
+
if (!user) return;
|
|
4251
|
+
return hostedSandboxReconciler.run();
|
|
4252
|
+
}
|
|
4253
|
+
);
|
|
4254
|
+
app2.delete(
|
|
4255
|
+
"/relay/admin/hosted-sandboxes/reconciliation/orphan-instances/:sandboxId",
|
|
4256
|
+
async (request, reply) => {
|
|
4257
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4258
|
+
if (!user) return;
|
|
4259
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4260
|
+
return hostedSandboxReconciler.deleteOrphanInstance(sandboxId);
|
|
4261
|
+
}
|
|
4262
|
+
);
|
|
4263
|
+
app2.delete(
|
|
4264
|
+
"/relay/admin/hosted-sandboxes/reconciliation/orphan-credentials/:credentialRef",
|
|
4265
|
+
async (request, reply) => {
|
|
4266
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4267
|
+
if (!user) return;
|
|
4268
|
+
const { credentialRef } = z.object({
|
|
4269
|
+
credentialRef: z.string().regex(/^rcc_[A-Za-z0-9_-]{32}$/)
|
|
4270
|
+
}).parse(request.params);
|
|
4271
|
+
return hostedSandboxReconciler.deleteOrphanCredential(credentialRef);
|
|
4272
|
+
}
|
|
4273
|
+
);
|
|
4274
|
+
app2.get(
|
|
4275
|
+
"/relay/admin/hosted-sandboxes/:sandboxId",
|
|
4276
|
+
async (request, reply) => {
|
|
4277
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4278
|
+
if (!user) {
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4282
|
+
return hostedSandboxService.detail(sandboxId);
|
|
4283
|
+
}
|
|
4284
|
+
);
|
|
4285
|
+
app2.post("/relay/admin/hosted-sandboxes", async (request, reply) => {
|
|
4286
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4287
|
+
if (!user) {
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
const body = createHostedSandboxSchema.parse(request.body ?? {});
|
|
4291
|
+
const result = await hostedSandboxService.create({
|
|
4292
|
+
createdByAdminUserId: user.id,
|
|
4293
|
+
...body
|
|
4294
|
+
});
|
|
4295
|
+
return reply.code(202).send(result);
|
|
2162
4296
|
});
|
|
4297
|
+
app2.put(
|
|
4298
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/members",
|
|
4299
|
+
async (request, reply) => {
|
|
4300
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4301
|
+
if (!user) {
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
4304
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4305
|
+
const body = updateHostedSandboxMembersSchema.parse(request.body ?? {});
|
|
4306
|
+
return hostedSandboxService.updateMembers(
|
|
4307
|
+
sandboxId,
|
|
4308
|
+
body.assignedUserIds
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
);
|
|
4312
|
+
app2.patch(
|
|
4313
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/settings",
|
|
4314
|
+
async (request, reply) => {
|
|
4315
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4316
|
+
if (!user) return;
|
|
4317
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4318
|
+
const body = updateHostedSandboxSettingsSchema.parse(request.body ?? {});
|
|
4319
|
+
return store.setHostedWorkspaceIsolation(
|
|
4320
|
+
sandboxId,
|
|
4321
|
+
body.workspaceIsolationEnabled
|
|
4322
|
+
);
|
|
4323
|
+
}
|
|
4324
|
+
);
|
|
4325
|
+
app2.post(
|
|
4326
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/retry",
|
|
4327
|
+
async (request, reply) => {
|
|
4328
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4329
|
+
if (!user) {
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4333
|
+
return reply.code(202).send({
|
|
4334
|
+
sandbox: hostedSandboxService.detail(sandboxId),
|
|
4335
|
+
operation: hostedSandboxService.retry(sandboxId)
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
);
|
|
4339
|
+
app2.post(
|
|
4340
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/start",
|
|
4341
|
+
async (request, reply) => {
|
|
4342
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4343
|
+
if (!user) return;
|
|
4344
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4345
|
+
return reply.code(202).send({
|
|
4346
|
+
operation: hostedSandboxService.start(sandboxId)
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
);
|
|
4350
|
+
app2.post(
|
|
4351
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/stop",
|
|
4352
|
+
async (request, reply) => {
|
|
4353
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4354
|
+
if (!user) return;
|
|
4355
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4356
|
+
return reply.code(202).send({
|
|
4357
|
+
operation: hostedSandboxService.stop(sandboxId)
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
);
|
|
4361
|
+
app2.post(
|
|
4362
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/snapshots",
|
|
4363
|
+
async (request, reply) => {
|
|
4364
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4365
|
+
if (!user) return;
|
|
4366
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4367
|
+
const { name } = hostedSnapshotSchema.parse(request.body ?? {});
|
|
4368
|
+
return reply.code(202).send({
|
|
4369
|
+
operation: hostedSandboxService.snapshot(sandboxId, name)
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
);
|
|
4373
|
+
app2.delete(
|
|
4374
|
+
"/relay/admin/hosted-sandboxes/:sandboxId",
|
|
4375
|
+
async (request, reply) => {
|
|
4376
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4377
|
+
if (!user) return;
|
|
4378
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4379
|
+
return reply.code(202).send({
|
|
4380
|
+
operation: hostedSandboxService.delete(sandboxId)
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
);
|
|
4384
|
+
app2.post(
|
|
4385
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/rotate-credential",
|
|
4386
|
+
async (request, reply) => {
|
|
4387
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4388
|
+
if (!user) return;
|
|
4389
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4390
|
+
const { openaiApiKey } = rotateHostedCredentialSchema.parse(
|
|
4391
|
+
request.body ?? {}
|
|
4392
|
+
);
|
|
4393
|
+
return reply.code(202).send({
|
|
4394
|
+
operation: await hostedSandboxService.rotateCredential(
|
|
4395
|
+
sandboxId,
|
|
4396
|
+
openaiApiKey
|
|
4397
|
+
)
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
);
|
|
4401
|
+
app2.get(
|
|
4402
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/backends/codex/files",
|
|
4403
|
+
async (request, reply) => {
|
|
4404
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4405
|
+
if (!user) return;
|
|
4406
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4407
|
+
return hostedSandboxService.readCodexFiles(sandboxId);
|
|
4408
|
+
}
|
|
4409
|
+
);
|
|
4410
|
+
app2.put(
|
|
4411
|
+
"/relay/admin/hosted-sandboxes/:sandboxId/backends/codex/files",
|
|
4412
|
+
async (request, reply) => {
|
|
4413
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4414
|
+
if (!user) return;
|
|
4415
|
+
const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
|
|
4416
|
+
const body = hostedCodexFilesSchema.parse(request.body ?? {});
|
|
4417
|
+
return hostedSandboxService.writeCodexFiles(sandboxId, body);
|
|
4418
|
+
}
|
|
4419
|
+
);
|
|
2163
4420
|
app2.patch("/relay/admin/settings/registration", async (request, reply) => {
|
|
2164
4421
|
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
2165
4422
|
if (!user) {
|
|
2166
4423
|
return;
|
|
2167
4424
|
}
|
|
2168
4425
|
const body = updateRegistrationSettingsSchema.parse(request.body ?? {});
|
|
4426
|
+
if (body.googleAuthEnabled && !googleAvailable) throw new RelayStoreError(400, "bad_request", "Google OAuth credentials are not configured.");
|
|
4427
|
+
if (body.githubAuthEnabled && !githubAvailable) throw new RelayStoreError(400, "bad_request", "GitHub OAuth credentials are not configured.");
|
|
4428
|
+
if (body.emailVerificationEnabled && !config2.emailVerificationConfigured) throw new RelayStoreError(400, "bad_request", "Email verification is not configured.");
|
|
2169
4429
|
const settings = store.updateRegistrationSettings({
|
|
2170
4430
|
...body.enabled !== void 0 ? { enabled: body.enabled } : {},
|
|
2171
4431
|
...body.registrationPassword !== void 0 ? { registrationPassword: body.registrationPassword } : {},
|
|
2172
|
-
...body.approvalRequired !== void 0 ? { approvalRequired: body.approvalRequired } : {}
|
|
4432
|
+
...body.approvalRequired !== void 0 ? { approvalRequired: body.approvalRequired } : {},
|
|
4433
|
+
...body.googleAuthEnabled !== void 0 ? { googleAuthEnabled: body.googleAuthEnabled } : {},
|
|
4434
|
+
...body.githubAuthEnabled !== void 0 ? { githubAuthEnabled: body.githubAuthEnabled } : {},
|
|
4435
|
+
...body.emailVerificationEnabled !== void 0 ? { emailVerificationEnabled: body.emailVerificationEnabled } : {}
|
|
2173
4436
|
});
|
|
2174
4437
|
return {
|
|
2175
4438
|
registrationEnabled: settings.enabled,
|
|
2176
|
-
settings
|
|
4439
|
+
settings: { ...settings, googleAuthAvailable: googleAvailable, githubAuthAvailable: githubAvailable, emailVerificationAvailable: config2.emailVerificationConfigured }
|
|
2177
4440
|
};
|
|
2178
4441
|
});
|
|
2179
4442
|
app2.patch("/relay/admin/users/:userId", async (request, reply) => {
|
|
@@ -2194,31 +4457,40 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2194
4457
|
store.deleteUser(userId);
|
|
2195
4458
|
return { id: userId };
|
|
2196
4459
|
});
|
|
2197
|
-
app2.post(
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
4460
|
+
app2.post(
|
|
4461
|
+
"/relay/admin/users/:userId/reset-password",
|
|
4462
|
+
async (request, reply) => {
|
|
4463
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4464
|
+
if (!user) {
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
const { userId } = z.object({ userId: z.string().uuid() }).parse(request.params);
|
|
4468
|
+
const body = adminResetPasswordSchema.parse(request.body ?? {});
|
|
4469
|
+
return store.adminResetUserPassword(userId, body.password);
|
|
2201
4470
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
4471
|
+
);
|
|
4472
|
+
app2.post(
|
|
4473
|
+
"/relay/admin/registrations/:requestId/approve",
|
|
4474
|
+
async (request, reply) => {
|
|
4475
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4476
|
+
if (!user) {
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
const { requestId } = z.object({ requestId: z.string().uuid() }).parse(request.params);
|
|
4480
|
+
return store.approvePendingRegistration(user.id, requestId);
|
|
2210
4481
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
4482
|
+
);
|
|
4483
|
+
app2.post(
|
|
4484
|
+
"/relay/admin/registrations/:requestId/reject",
|
|
4485
|
+
async (request, reply) => {
|
|
4486
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
4487
|
+
if (!user) {
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
const { requestId } = z.object({ requestId: z.string().uuid() }).parse(request.params);
|
|
4491
|
+
return store.rejectPendingRegistration(user.id, requestId);
|
|
2218
4492
|
}
|
|
2219
|
-
|
|
2220
|
-
return store.rejectPendingRegistration(user.id, requestId);
|
|
2221
|
-
});
|
|
4493
|
+
);
|
|
2222
4494
|
app2.all("/relay/devices/:deviceId/api/*", async (request, reply) => {
|
|
2223
4495
|
const user = requireRelayUser(request, reply, store);
|
|
2224
4496
|
if (!user) {
|
|
@@ -2231,6 +4503,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2231
4503
|
reply,
|
|
2232
4504
|
state,
|
|
2233
4505
|
store,
|
|
4506
|
+
hostedSandboxService,
|
|
2234
4507
|
user,
|
|
2235
4508
|
deviceId,
|
|
2236
4509
|
targetPath
|
|
@@ -2247,6 +4520,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2247
4520
|
reply,
|
|
2248
4521
|
state,
|
|
2249
4522
|
store,
|
|
4523
|
+
hostedSandboxService,
|
|
2250
4524
|
user,
|
|
2251
4525
|
deviceId,
|
|
2252
4526
|
targetPath: "/healthz"
|
|
@@ -2271,6 +4545,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2271
4545
|
reply,
|
|
2272
4546
|
state,
|
|
2273
4547
|
store,
|
|
4548
|
+
hostedSandboxService,
|
|
2274
4549
|
user,
|
|
2275
4550
|
deviceId,
|
|
2276
4551
|
targetPath
|
|
@@ -2298,12 +4573,16 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2298
4573
|
}
|
|
2299
4574
|
const connectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2300
4575
|
const existing = state.supervisors.get(deviceId);
|
|
2301
|
-
existing?.socket.send(
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
4576
|
+
existing?.socket.send(
|
|
4577
|
+
JSON.stringify({
|
|
4578
|
+
type: "relay.connected",
|
|
4579
|
+
timestamp: connectedAt,
|
|
4580
|
+
deviceId
|
|
4581
|
+
})
|
|
4582
|
+
);
|
|
4583
|
+
existing?.clientSockets.forEach(
|
|
4584
|
+
(clientConnection) => clientConnection.socket.close()
|
|
4585
|
+
);
|
|
2307
4586
|
const connection = {
|
|
2308
4587
|
deviceId,
|
|
2309
4588
|
socket,
|
|
@@ -2315,6 +4594,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2315
4594
|
ipAddress: relayClientIp(request)
|
|
2316
4595
|
};
|
|
2317
4596
|
state.supervisors.set(deviceId, connection);
|
|
4597
|
+
hostedSandboxService.markOnline(deviceId);
|
|
2318
4598
|
socket.send(
|
|
2319
4599
|
JSON.stringify({
|
|
2320
4600
|
type: "relay.connected",
|
|
@@ -2325,7 +4605,9 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2325
4605
|
socket.on("message", (rawMessage) => {
|
|
2326
4606
|
let parsed;
|
|
2327
4607
|
try {
|
|
2328
|
-
parsed = JSON.parse(
|
|
4608
|
+
parsed = JSON.parse(
|
|
4609
|
+
rawMessage.toString()
|
|
4610
|
+
);
|
|
2329
4611
|
} catch {
|
|
2330
4612
|
return;
|
|
2331
4613
|
}
|
|
@@ -2333,21 +4615,54 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2333
4615
|
connection.lastHeartbeatAt = parsed.timestamp;
|
|
2334
4616
|
return;
|
|
2335
4617
|
}
|
|
4618
|
+
if (parsed.type === "relay.activity") {
|
|
4619
|
+
hostedSandboxService.recordTurnActivity({
|
|
4620
|
+
deviceId,
|
|
4621
|
+
threadId: parsed.payload.threadId,
|
|
4622
|
+
turnId: parsed.payload.turnId,
|
|
4623
|
+
kind: parsed.payload.kind
|
|
4624
|
+
});
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
2336
4627
|
if (parsed.type === "relay.server.message") {
|
|
2337
|
-
const clientConnection = connection.clientSockets.get(
|
|
4628
|
+
const clientConnection = connection.clientSockets.get(
|
|
4629
|
+
parsed.clientId
|
|
4630
|
+
);
|
|
4631
|
+
const eventThreadId = threadIdFromSocketPayload(parsed.payload);
|
|
2338
4632
|
if (clientConnection) {
|
|
2339
|
-
const
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
4633
|
+
const freshAccess = store.effectiveAccess(
|
|
4634
|
+
clientConnection.user.id,
|
|
4635
|
+
clientConnection.deviceId,
|
|
4636
|
+
{
|
|
4637
|
+
threadId: clientConnection.threadId ?? eventThreadId
|
|
4638
|
+
}
|
|
4639
|
+
);
|
|
2343
4640
|
if (!freshAccess) {
|
|
2344
4641
|
connection.clientSockets.delete(parsed.clientId);
|
|
2345
|
-
clientConnection.socket.close(
|
|
4642
|
+
clientConnection.socket.close(
|
|
4643
|
+
1008,
|
|
4644
|
+
"Shared access is no longer allowed."
|
|
4645
|
+
);
|
|
2346
4646
|
return;
|
|
2347
4647
|
}
|
|
2348
4648
|
clientConnection.access = freshAccess;
|
|
4649
|
+
const isolation = store.hostedWorkspaceIsolationForUser(
|
|
4650
|
+
clientConnection.deviceId,
|
|
4651
|
+
clientConnection.user.id
|
|
4652
|
+
);
|
|
4653
|
+
const connectionControlEvent = parsed.payload.type === "supervisor.connected" || parsed.payload.type === "supervisor.pong";
|
|
4654
|
+
if (isolation?.enabled && !connectionControlEvent && (!eventThreadId || !store.ownsHostedThread(
|
|
4655
|
+
isolation.sandboxId,
|
|
4656
|
+
clientConnection.user.id,
|
|
4657
|
+
eventThreadId
|
|
4658
|
+
))) {
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
2349
4661
|
}
|
|
2350
|
-
if (clientConnection && clientConnection.socket.readyState === WEBSOCKET_OPEN && shouldForwardSocketEvent(
|
|
4662
|
+
if (clientConnection && clientConnection.socket.readyState === WEBSOCKET_OPEN && shouldForwardSocketEvent(
|
|
4663
|
+
parsed.payload,
|
|
4664
|
+
clientConnection.threadId
|
|
4665
|
+
)) {
|
|
2351
4666
|
clientConnection.socket.send(JSON.stringify(parsed.payload));
|
|
2352
4667
|
}
|
|
2353
4668
|
return;
|
|
@@ -2358,7 +4673,9 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2358
4673
|
if (state.supervisors.get(deviceId)?.socket === socket) {
|
|
2359
4674
|
state.supervisors.delete(deviceId);
|
|
2360
4675
|
}
|
|
2361
|
-
connection.requestBroker.rejectAll(
|
|
4676
|
+
connection.requestBroker.rejectAll(
|
|
4677
|
+
new Error("Supervisor tunnel closed.")
|
|
4678
|
+
);
|
|
2362
4679
|
for (const [clientId, clientConnection] of connection.clientSockets) {
|
|
2363
4680
|
connection.clientSockets.delete(clientId);
|
|
2364
4681
|
clientConnection.socket.close();
|
|
@@ -2376,27 +4693,55 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2376
4693
|
});
|
|
2377
4694
|
},
|
|
2378
4695
|
wsHandler: (socket, request) => {
|
|
2379
|
-
const session =
|
|
4696
|
+
const session = verifyRelayRequestSession(request, store);
|
|
2380
4697
|
const deviceId = pathParam(request.params, "deviceId");
|
|
2381
4698
|
const threadId = queryString(request.query, "threadId");
|
|
2382
4699
|
if (!session.authenticated || !session.user || !deviceId) {
|
|
2383
4700
|
socket.close(1008, "Relay login is required.");
|
|
2384
4701
|
return;
|
|
2385
4702
|
}
|
|
2386
|
-
const access = store.effectiveAccess(session.user.id, deviceId, {
|
|
4703
|
+
const access = store.effectiveAccess(session.user.id, deviceId, {
|
|
4704
|
+
threadId
|
|
4705
|
+
});
|
|
2387
4706
|
if (!access) {
|
|
2388
4707
|
socket.close(1008, "Device access is not allowed.");
|
|
2389
4708
|
return;
|
|
2390
4709
|
}
|
|
4710
|
+
const isolation = store.hostedWorkspaceIsolationForUser(
|
|
4711
|
+
deviceId,
|
|
4712
|
+
session.user.id
|
|
4713
|
+
);
|
|
4714
|
+
if (isolation?.enabled && threadId && !store.ownsHostedThread(isolation.sandboxId, session.user.id, threadId)) {
|
|
4715
|
+
socket.close(1008, "This thread belongs to another VM user.");
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
const lifecycle = hostedSandboxService.wakeIfStopped(deviceId);
|
|
4719
|
+
if (lifecycle.waking) {
|
|
4720
|
+
socket.close(1013, "Hosted supervisor VM is starting.");
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
2391
4723
|
const supervisor = state.supervisors.get(deviceId);
|
|
2392
4724
|
if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
2393
4725
|
socket.close(1013, "No supervisor is connected for this device.");
|
|
2394
4726
|
return;
|
|
2395
4727
|
}
|
|
2396
4728
|
if (access.kind === "shared") {
|
|
2397
|
-
recordRelayAccess(
|
|
4729
|
+
recordRelayAccess(
|
|
4730
|
+
store,
|
|
4731
|
+
access,
|
|
4732
|
+
session.user,
|
|
4733
|
+
threadId ? "open_thread" : "open_device"
|
|
4734
|
+
);
|
|
2398
4735
|
}
|
|
2399
|
-
connectRelayWebsocket(
|
|
4736
|
+
connectRelayWebsocket(
|
|
4737
|
+
supervisor,
|
|
4738
|
+
socket,
|
|
4739
|
+
store,
|
|
4740
|
+
session.user,
|
|
4741
|
+
deviceId,
|
|
4742
|
+
threadId,
|
|
4743
|
+
access
|
|
4744
|
+
);
|
|
2400
4745
|
}
|
|
2401
4746
|
});
|
|
2402
4747
|
realtimeApp.route({
|
|
@@ -2409,17 +4754,25 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2409
4754
|
});
|
|
2410
4755
|
},
|
|
2411
4756
|
wsHandler: (socket, request) => {
|
|
2412
|
-
const session =
|
|
4757
|
+
const session = verifyRelayRequestSession(request, store);
|
|
2413
4758
|
if (!session.authenticated || !session.user) {
|
|
2414
4759
|
socket.close(1008, "Relay login is required.");
|
|
2415
4760
|
return;
|
|
2416
4761
|
}
|
|
2417
4762
|
const threadId = queryString(request.query, "threadId");
|
|
2418
|
-
const deviceId = firstAccessibleConnectedDevice(
|
|
4763
|
+
const deviceId = firstAccessibleConnectedDevice(
|
|
4764
|
+
state,
|
|
4765
|
+
store,
|
|
4766
|
+
session.user.id,
|
|
4767
|
+
threadId
|
|
4768
|
+
);
|
|
2419
4769
|
const supervisor = deviceId ? state.supervisors.get(deviceId) : null;
|
|
2420
4770
|
const access = deviceId ? store.effectiveAccess(session.user.id, deviceId, { threadId }) : null;
|
|
2421
4771
|
if (!deviceId || !supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
2422
|
-
socket.close(
|
|
4772
|
+
socket.close(
|
|
4773
|
+
1013,
|
|
4774
|
+
"No accessible supervisor is connected to this relay."
|
|
4775
|
+
);
|
|
2423
4776
|
return;
|
|
2424
4777
|
}
|
|
2425
4778
|
if (!access) {
|
|
@@ -2427,15 +4780,32 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2427
4780
|
return;
|
|
2428
4781
|
}
|
|
2429
4782
|
if (access.kind === "shared") {
|
|
2430
|
-
recordRelayAccess(
|
|
4783
|
+
recordRelayAccess(
|
|
4784
|
+
store,
|
|
4785
|
+
access,
|
|
4786
|
+
session.user,
|
|
4787
|
+
threadId ? "open_thread" : "open_device"
|
|
4788
|
+
);
|
|
2431
4789
|
}
|
|
2432
|
-
connectRelayWebsocket(
|
|
4790
|
+
connectRelayWebsocket(
|
|
4791
|
+
supervisor,
|
|
4792
|
+
socket,
|
|
4793
|
+
store,
|
|
4794
|
+
session.user,
|
|
4795
|
+
deviceId,
|
|
4796
|
+
threadId,
|
|
4797
|
+
access
|
|
4798
|
+
);
|
|
2433
4799
|
}
|
|
2434
4800
|
});
|
|
2435
4801
|
});
|
|
2436
4802
|
if (config2.webDistDir) {
|
|
2437
4803
|
registerRelayWebApp(app2, config2.webDistDir);
|
|
2438
4804
|
}
|
|
4805
|
+
app2.addHook("onClose", () => {
|
|
4806
|
+
hostedSandboxService.close();
|
|
4807
|
+
hostedSandboxReconciler.close();
|
|
4808
|
+
});
|
|
2439
4809
|
app2.setErrorHandler((error, _request, reply) => {
|
|
2440
4810
|
if (error instanceof RelayStoreError) {
|
|
2441
4811
|
reply.status(error.statusCode).send({
|
|
@@ -2445,9 +4815,12 @@ function buildRelayServer(config2, options = {}) {
|
|
|
2445
4815
|
return;
|
|
2446
4816
|
}
|
|
2447
4817
|
if (error instanceof z.ZodError) {
|
|
4818
|
+
const firstIssue = error.issues[0];
|
|
4819
|
+
const field = firstIssue?.path[0];
|
|
4820
|
+
const message = field === "password" ? "Password must be at least 8 characters." : field === "username" ? "Username must be at least 3 characters." : field === "email" ? "Enter a valid email address." : "The request payload is invalid.";
|
|
2448
4821
|
reply.status(400).send({
|
|
2449
4822
|
code: "bad_request",
|
|
2450
|
-
message
|
|
4823
|
+
message,
|
|
2451
4824
|
details: {
|
|
2452
4825
|
issues: error.issues
|
|
2453
4826
|
}
|
|
@@ -2466,7 +4839,9 @@ function webViewCorsOrigins(env) {
|
|
|
2466
4839
|
return null;
|
|
2467
4840
|
}
|
|
2468
4841
|
const configured = env.REMOTE_CODEX_WEBVIEW_CORS_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
2469
|
-
return new Set(
|
|
4842
|
+
return new Set(
|
|
4843
|
+
configured?.length ? configured : DEFAULT_WEBVIEW_CORS_ORIGINS
|
|
4844
|
+
);
|
|
2470
4845
|
}
|
|
2471
4846
|
function applyWebViewCorsHeaders(reply, origin) {
|
|
2472
4847
|
reply.header("access-control-allow-origin", origin);
|
|
@@ -2549,6 +4924,18 @@ async function forwardRelayHttp(input) {
|
|
|
2549
4924
|
workspaceId: conversationEvent.workspaceId
|
|
2550
4925
|
});
|
|
2551
4926
|
}
|
|
4927
|
+
const lifecycle = isHostedUserActivityRequest(
|
|
4928
|
+
input.request.method,
|
|
4929
|
+
input.targetPath
|
|
4930
|
+
) ? input.hostedSandboxService.recordUserActivity(input.deviceId) : input.hostedSandboxService.wakeIfStopped(input.deviceId);
|
|
4931
|
+
if (lifecycle.waking) {
|
|
4932
|
+
input.reply.status(503).send({
|
|
4933
|
+
code: "service_unavailable",
|
|
4934
|
+
message: "Hosted supervisor VM is starting. Retry shortly.",
|
|
4935
|
+
details: { reason: "hosted_sandbox_starting" }
|
|
4936
|
+
});
|
|
4937
|
+
return;
|
|
4938
|
+
}
|
|
2552
4939
|
const supervisor = input.state.supervisors.get(input.deviceId);
|
|
2553
4940
|
if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
2554
4941
|
input.reply.status(503).send({
|
|
@@ -2558,6 +4945,64 @@ async function forwardRelayHttp(input) {
|
|
|
2558
4945
|
return;
|
|
2559
4946
|
}
|
|
2560
4947
|
try {
|
|
4948
|
+
const isolation = input.store.hostedWorkspaceIsolationForUser(
|
|
4949
|
+
input.deviceId,
|
|
4950
|
+
input.user.id
|
|
4951
|
+
);
|
|
4952
|
+
if (isolation?.enabled) {
|
|
4953
|
+
if (input.request.method.toUpperCase() === "GET" && (targetUrl.pathname === "/api/workspaces" || targetUrl.pathname === "/api/threads")) {
|
|
4954
|
+
await ensureHostedUserBootstrap({
|
|
4955
|
+
store: input.store,
|
|
4956
|
+
supervisor,
|
|
4957
|
+
deviceId: input.deviceId,
|
|
4958
|
+
sandboxId: isolation.sandboxId,
|
|
4959
|
+
user: input.user
|
|
4960
|
+
});
|
|
4961
|
+
}
|
|
4962
|
+
if (workspaceId && !input.store.ownsHostedWorkspace(
|
|
4963
|
+
isolation.sandboxId,
|
|
4964
|
+
input.user.id,
|
|
4965
|
+
workspaceId
|
|
4966
|
+
)) {
|
|
4967
|
+
input.reply.status(403).send({
|
|
4968
|
+
code: "forbidden",
|
|
4969
|
+
message: "This workspace belongs to another VM user."
|
|
4970
|
+
});
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
if (threadId && !input.store.ownsHostedThread(
|
|
4974
|
+
isolation.sandboxId,
|
|
4975
|
+
input.user.id,
|
|
4976
|
+
threadId
|
|
4977
|
+
)) {
|
|
4978
|
+
input.reply.status(403).send({
|
|
4979
|
+
code: "forbidden",
|
|
4980
|
+
message: "This thread belongs to another VM user."
|
|
4981
|
+
});
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
if (input.request.method.toUpperCase() === "POST" && targetUrl.pathname === "/api/threads/start") {
|
|
4985
|
+
const requestedWorkspaceId = isObject(input.request.body) && typeof input.request.body.workspaceId === "string" ? input.request.body.workspaceId : null;
|
|
4986
|
+
if (!requestedWorkspaceId || !input.store.ownsHostedWorkspace(
|
|
4987
|
+
isolation.sandboxId,
|
|
4988
|
+
input.user.id,
|
|
4989
|
+
requestedWorkspaceId
|
|
4990
|
+
)) {
|
|
4991
|
+
input.reply.status(403).send({
|
|
4992
|
+
code: "forbidden",
|
|
4993
|
+
message: "Create threads only in your own workspace."
|
|
4994
|
+
});
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
if (input.request.method.toUpperCase() === "POST" && targetUrl.pathname === "/api/threads/import") {
|
|
4999
|
+
input.reply.status(403).send({
|
|
5000
|
+
code: "forbidden",
|
|
5001
|
+
message: "Thread import is unavailable while user workspace isolation is enabled."
|
|
5002
|
+
});
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
2561
5006
|
const requestId = randomUUID();
|
|
2562
5007
|
const requestBody = relayRequestBody(input.request.body);
|
|
2563
5008
|
const response = await supervisor.requestBroker.forward(supervisor.socket, {
|
|
@@ -2578,6 +5023,18 @@ async function forwardRelayHttp(input) {
|
|
|
2578
5023
|
input.reply.header(name, value);
|
|
2579
5024
|
}
|
|
2580
5025
|
}
|
|
5026
|
+
if (isolation?.enabled && response.statusCode >= 200 && response.statusCode < 300) {
|
|
5027
|
+
const transformed = transformHostedIsolatedResponse({
|
|
5028
|
+
store: input.store,
|
|
5029
|
+
sandboxId: isolation.sandboxId,
|
|
5030
|
+
userId: input.user.id,
|
|
5031
|
+
method: input.request.method,
|
|
5032
|
+
pathname: targetUrl.pathname,
|
|
5033
|
+
response
|
|
5034
|
+
});
|
|
5035
|
+
input.reply.status(response.statusCode).send(transformed);
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
2581
5038
|
input.reply.status(response.statusCode).send(relayResponseBody(response));
|
|
2582
5039
|
} catch (error) {
|
|
2583
5040
|
const message = error instanceof Error ? error.message : "Relay request failed.";
|
|
@@ -2609,7 +5066,162 @@ async function forwardSharedThreadList(input) {
|
|
|
2609
5066
|
return isObject(thread) ? thread : null;
|
|
2610
5067
|
})
|
|
2611
5068
|
);
|
|
2612
|
-
input.reply.send(
|
|
5069
|
+
input.reply.send(
|
|
5070
|
+
threads.filter(
|
|
5071
|
+
(thread) => Boolean(thread)
|
|
5072
|
+
)
|
|
5073
|
+
);
|
|
5074
|
+
}
|
|
5075
|
+
async function ensureHostedUserBootstrap(input) {
|
|
5076
|
+
if (input.store.hostedUserWorkspaceIds(input.sandboxId, input.user.id).length) {
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
const key = `${input.sandboxId}:${input.user.id}`;
|
|
5080
|
+
const existing = hostedBootstrapPromises.get(key);
|
|
5081
|
+
if (existing) return existing;
|
|
5082
|
+
const pending = (async () => {
|
|
5083
|
+
const slug = input.user.username.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "user";
|
|
5084
|
+
const directory = `${slug}-${input.user.id.slice(0, 8)}`;
|
|
5085
|
+
const absoluteDirectory = `/home/remote-codex/workspaces/${directory}`;
|
|
5086
|
+
const label = `${input.user.username}'s workspace`;
|
|
5087
|
+
const current = await forwardSupervisorCommandJson(
|
|
5088
|
+
input.supervisor,
|
|
5089
|
+
input.deviceId,
|
|
5090
|
+
"GET",
|
|
5091
|
+
"/api/workspaces"
|
|
5092
|
+
);
|
|
5093
|
+
const currentWorkspaces = Array.isArray(current) ? current : [];
|
|
5094
|
+
let workspace = currentWorkspaces.find(
|
|
5095
|
+
(candidate) => isObject(candidate) && typeof candidate.absPath === "string" && candidate.absPath === absoluteDirectory
|
|
5096
|
+
);
|
|
5097
|
+
if (!workspace) {
|
|
5098
|
+
workspace = await forwardSupervisorCommandJson(
|
|
5099
|
+
input.supervisor,
|
|
5100
|
+
input.deviceId,
|
|
5101
|
+
"POST",
|
|
5102
|
+
"/api/workspaces",
|
|
5103
|
+
{ absPath: absoluteDirectory, label }
|
|
5104
|
+
);
|
|
5105
|
+
}
|
|
5106
|
+
const workspaceId = stringField(workspace, "id");
|
|
5107
|
+
if (!workspaceId) throw new Error("Initial workspace creation returned no id.");
|
|
5108
|
+
const thread = await forwardSupervisorCommandJson(
|
|
5109
|
+
input.supervisor,
|
|
5110
|
+
input.deviceId,
|
|
5111
|
+
"POST",
|
|
5112
|
+
"/api/threads/start",
|
|
5113
|
+
{
|
|
5114
|
+
workspaceId,
|
|
5115
|
+
title: "Getting started",
|
|
5116
|
+
provider: "codex",
|
|
5117
|
+
model: "gpt-5.6-sol",
|
|
5118
|
+
reasoningEffort: "low",
|
|
5119
|
+
approvalMode: "yolo"
|
|
5120
|
+
}
|
|
5121
|
+
);
|
|
5122
|
+
const threadId = stringField(thread, "id");
|
|
5123
|
+
if (!threadId) throw new Error("Initial thread creation returned no id.");
|
|
5124
|
+
input.store.recordHostedUserWorkspace(
|
|
5125
|
+
input.sandboxId,
|
|
5126
|
+
input.user.id,
|
|
5127
|
+
workspaceId,
|
|
5128
|
+
true
|
|
5129
|
+
);
|
|
5130
|
+
input.store.recordHostedUserThread(
|
|
5131
|
+
input.sandboxId,
|
|
5132
|
+
input.user.id,
|
|
5133
|
+
threadId,
|
|
5134
|
+
workspaceId
|
|
5135
|
+
);
|
|
5136
|
+
})().finally(() => hostedBootstrapPromises.delete(key));
|
|
5137
|
+
hostedBootstrapPromises.set(key, pending);
|
|
5138
|
+
return pending;
|
|
5139
|
+
}
|
|
5140
|
+
async function forwardSupervisorCommandJson(supervisor, deviceId, method, path4, body) {
|
|
5141
|
+
const response = await supervisor.requestBroker.forward(supervisor.socket, {
|
|
5142
|
+
type: "relay.request",
|
|
5143
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5144
|
+
requestId: randomUUID(),
|
|
5145
|
+
deviceId,
|
|
5146
|
+
payload: {
|
|
5147
|
+
method,
|
|
5148
|
+
path: path4,
|
|
5149
|
+
headers: body === void 0 ? {} : { "content-type": "application/json" },
|
|
5150
|
+
body: body === void 0 ? null : JSON.stringify(body)
|
|
5151
|
+
}
|
|
5152
|
+
});
|
|
5153
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
5154
|
+
let detail = relayJsonBody(response);
|
|
5155
|
+
try {
|
|
5156
|
+
const payload = JSON.parse(detail);
|
|
5157
|
+
detail = isObject(payload) && typeof payload.message === "string" ? payload.message : detail;
|
|
5158
|
+
} catch {
|
|
5159
|
+
}
|
|
5160
|
+
throw new Error(
|
|
5161
|
+
`Supervisor bootstrap request failed with ${response.statusCode}: ${detail.slice(0, 300)}`
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
5164
|
+
return JSON.parse(relayJsonBody(response));
|
|
5165
|
+
}
|
|
5166
|
+
function transformHostedIsolatedResponse(input) {
|
|
5167
|
+
const raw = relayJsonBody(input.response);
|
|
5168
|
+
let payload;
|
|
5169
|
+
try {
|
|
5170
|
+
payload = JSON.parse(raw);
|
|
5171
|
+
} catch {
|
|
5172
|
+
return relayResponseBody(input.response);
|
|
5173
|
+
}
|
|
5174
|
+
const method = input.method.toUpperCase();
|
|
5175
|
+
if (method === "GET" && input.pathname === "/api/workspaces" && Array.isArray(payload)) {
|
|
5176
|
+
const owned = new Set(
|
|
5177
|
+
input.store.hostedUserWorkspaceIds(input.sandboxId, input.userId)
|
|
5178
|
+
);
|
|
5179
|
+
return payload.filter(
|
|
5180
|
+
(workspace) => isObject(workspace) && owned.has(stringField(workspace, "id") ?? "")
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5183
|
+
if (method === "POST" && input.pathname === "/api/workspaces" && isObject(payload)) {
|
|
5184
|
+
const workspaceId = stringField(payload, "id");
|
|
5185
|
+
if (workspaceId) {
|
|
5186
|
+
input.store.recordHostedUserWorkspace(
|
|
5187
|
+
input.sandboxId,
|
|
5188
|
+
input.userId,
|
|
5189
|
+
workspaceId
|
|
5190
|
+
);
|
|
5191
|
+
}
|
|
5192
|
+
return payload;
|
|
5193
|
+
}
|
|
5194
|
+
if (method === "GET" && input.pathname === "/api/threads" && Array.isArray(payload)) {
|
|
5195
|
+
const ownedWorkspaces = new Set(
|
|
5196
|
+
input.store.hostedUserWorkspaceIds(input.sandboxId, input.userId)
|
|
5197
|
+
);
|
|
5198
|
+
return payload.filter((thread) => {
|
|
5199
|
+
if (!isObject(thread)) return false;
|
|
5200
|
+
const threadId = stringField(thread, "id");
|
|
5201
|
+
const workspaceId = stringField(thread, "workspaceId");
|
|
5202
|
+
if (!threadId || !workspaceId || !ownedWorkspaces.has(workspaceId)) return false;
|
|
5203
|
+
input.store.recordHostedUserThread(
|
|
5204
|
+
input.sandboxId,
|
|
5205
|
+
input.userId,
|
|
5206
|
+
threadId,
|
|
5207
|
+
workspaceId
|
|
5208
|
+
);
|
|
5209
|
+
return true;
|
|
5210
|
+
});
|
|
5211
|
+
}
|
|
5212
|
+
if (method === "POST" && input.pathname === "/api/threads/start" && isObject(payload)) {
|
|
5213
|
+
const threadId = stringField(payload, "id");
|
|
5214
|
+
const workspaceId = stringField(payload, "workspaceId");
|
|
5215
|
+
if (threadId && workspaceId) {
|
|
5216
|
+
input.store.recordHostedUserThread(
|
|
5217
|
+
input.sandboxId,
|
|
5218
|
+
input.userId,
|
|
5219
|
+
threadId,
|
|
5220
|
+
workspaceId
|
|
5221
|
+
);
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
return payload;
|
|
2613
5225
|
}
|
|
2614
5226
|
function relayResponseBody(response) {
|
|
2615
5227
|
if (response.bodyEncoding === "base64") {
|
|
@@ -2619,7 +5231,13 @@ function relayResponseBody(response) {
|
|
|
2619
5231
|
}
|
|
2620
5232
|
function connectRelayWebsocket(supervisor, socket, store, user, deviceId, threadId, access) {
|
|
2621
5233
|
const clientId = randomUUID();
|
|
2622
|
-
supervisor.clientSockets.set(clientId, {
|
|
5234
|
+
supervisor.clientSockets.set(clientId, {
|
|
5235
|
+
socket,
|
|
5236
|
+
threadId,
|
|
5237
|
+
deviceId,
|
|
5238
|
+
user,
|
|
5239
|
+
access
|
|
5240
|
+
});
|
|
2623
5241
|
sendToSupervisor(supervisor, {
|
|
2624
5242
|
type: "relay.client.connected",
|
|
2625
5243
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2711,7 +5329,9 @@ function registerRelayWebApp(app2, distDirInput) {
|
|
|
2711
5329
|
return reply.send(payload);
|
|
2712
5330
|
}
|
|
2713
5331
|
const stat = await fsp.stat(assetPath);
|
|
2714
|
-
reply.header("cache-control", "public, max-age=31536000, immutable").header("content-length", stat.size).type(
|
|
5332
|
+
reply.header("cache-control", "public, max-age=31536000, immutable").header("content-length", stat.size).type(
|
|
5333
|
+
mimeTypes.get(path2.extname(assetPath).toLowerCase()) ?? "application/octet-stream"
|
|
5334
|
+
);
|
|
2715
5335
|
return reply.send(fs2.createReadStream(assetPath));
|
|
2716
5336
|
});
|
|
2717
5337
|
}
|
|
@@ -2747,7 +5367,7 @@ async function resolveAssetPath(distDir, indexFile, pathname) {
|
|
|
2747
5367
|
return indexFile;
|
|
2748
5368
|
}
|
|
2749
5369
|
function requireRelayUser(request, reply, store, options = {}) {
|
|
2750
|
-
const session =
|
|
5370
|
+
const session = verifyRelayRequestSession(request, store);
|
|
2751
5371
|
if (!session.authenticated || !session.user) {
|
|
2752
5372
|
reply.status(401).send({
|
|
2753
5373
|
code: "unauthorized",
|
|
@@ -2773,8 +5393,19 @@ function requireRelayUser(request, reply, store, options = {}) {
|
|
|
2773
5393
|
request.relayUser = session.user;
|
|
2774
5394
|
return session.user;
|
|
2775
5395
|
}
|
|
2776
|
-
function
|
|
2777
|
-
|
|
5396
|
+
function verifyRelayRequestSession(request, store) {
|
|
5397
|
+
const candidates = [
|
|
5398
|
+
bearerToken(request.headers.authorization),
|
|
5399
|
+
queryToken(request.query, "relaySession"),
|
|
5400
|
+
queryToken(request.query, "token"),
|
|
5401
|
+
readCookie(request.headers.cookie, RELAY_COOKIE_NAME)
|
|
5402
|
+
];
|
|
5403
|
+
for (const token of candidates) {
|
|
5404
|
+
if (!token) continue;
|
|
5405
|
+
const session = store.verifySession(token);
|
|
5406
|
+
if (session.authenticated) return session;
|
|
5407
|
+
}
|
|
5408
|
+
return store.emptySession();
|
|
2778
5409
|
}
|
|
2779
5410
|
function attachRelayCookie(reply, token) {
|
|
2780
5411
|
reply.header(
|
|
@@ -2788,6 +5419,66 @@ function clearRelayCookie(reply) {
|
|
|
2788
5419
|
`${RELAY_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`
|
|
2789
5420
|
);
|
|
2790
5421
|
}
|
|
5422
|
+
function oauthCallbackUrl(request, config2, provider) {
|
|
5423
|
+
const configured = config2.publicBaseUrl?.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
|
|
5424
|
+
const forwardedProto = String(request.headers["x-forwarded-proto"] ?? "").split(",")[0]?.trim();
|
|
5425
|
+
const protocol = forwardedProto || request.protocol;
|
|
5426
|
+
const host = String(request.headers["x-forwarded-host"] ?? request.headers.host ?? "localhost:8788").split(",")[0]?.trim();
|
|
5427
|
+
const base = configured || `${protocol}://${host}`;
|
|
5428
|
+
return `${base}/relay/auth/oauth/${provider}/callback`;
|
|
5429
|
+
}
|
|
5430
|
+
function signOAuthState(provider, secret) {
|
|
5431
|
+
const payload = Buffer.from(JSON.stringify({ provider, expiresAt: Date.now() + 10 * 6e4, nonce: crypto3.randomBytes(18).toString("base64url") })).toString("base64url");
|
|
5432
|
+
const signature = crypto3.createHmac("sha256", secret).update(payload).digest("base64url");
|
|
5433
|
+
return `${payload}.${signature}`;
|
|
5434
|
+
}
|
|
5435
|
+
function verifyOAuthState(state, provider, secret) {
|
|
5436
|
+
const [payload, signature] = state.split(".");
|
|
5437
|
+
if (!payload || !signature) return false;
|
|
5438
|
+
const expected = crypto3.createHmac("sha256", secret).update(payload).digest();
|
|
5439
|
+
const actual = Buffer.from(signature, "base64url");
|
|
5440
|
+
if (actual.length !== expected.length || !crypto3.timingSafeEqual(actual, expected)) return false;
|
|
5441
|
+
try {
|
|
5442
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
5443
|
+
return parsed.provider === provider && typeof parsed.expiresAt === "number" && parsed.expiresAt > Date.now();
|
|
5444
|
+
} catch {
|
|
5445
|
+
return false;
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
async function fetchGoogleIdentity(code, redirectUri, config2) {
|
|
5449
|
+
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
5450
|
+
method: "POST",
|
|
5451
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
5452
|
+
body: new URLSearchParams({ code, client_id: config2.googleOAuthClientId, client_secret: config2.googleOAuthClientSecret, redirect_uri: redirectUri, grant_type: "authorization_code" })
|
|
5453
|
+
});
|
|
5454
|
+
if (!tokenResponse.ok) throw new Error("Google token exchange failed.");
|
|
5455
|
+
const tokens = await tokenResponse.json();
|
|
5456
|
+
const response = await fetch("https://openidconnect.googleapis.com/v1/userinfo", { headers: { authorization: `Bearer ${tokens.access_token}` } });
|
|
5457
|
+
if (!response.ok) throw new Error("Google profile lookup failed.");
|
|
5458
|
+
const profile = await response.json();
|
|
5459
|
+
if (!profile.sub || !profile.email || !profile.email_verified) throw new Error("Google did not provide a verified email address.");
|
|
5460
|
+
return { provider: "google", subject: profile.sub, email: profile.email.toLowerCase(), username: profile.email.split("@")[0] || profile.name || "google-user" };
|
|
5461
|
+
}
|
|
5462
|
+
async function fetchGitHubIdentity(code, redirectUri, config2) {
|
|
5463
|
+
const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
|
|
5464
|
+
method: "POST",
|
|
5465
|
+
headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
|
|
5466
|
+
body: new URLSearchParams({ code, client_id: config2.githubOAuthClientId, client_secret: config2.githubOAuthClientSecret, redirect_uri: redirectUri })
|
|
5467
|
+
});
|
|
5468
|
+
if (!tokenResponse.ok) throw new Error("GitHub token exchange failed.");
|
|
5469
|
+
const tokens = await tokenResponse.json();
|
|
5470
|
+
const headers = { accept: "application/vnd.github+json", authorization: `Bearer ${tokens.access_token}`, "user-agent": "remote-codex-relay" };
|
|
5471
|
+
const [userResponse, emailsResponse] = await Promise.all([
|
|
5472
|
+
fetch("https://api.github.com/user", { headers }),
|
|
5473
|
+
fetch("https://api.github.com/user/emails", { headers })
|
|
5474
|
+
]);
|
|
5475
|
+
if (!userResponse.ok || !emailsResponse.ok) throw new Error("GitHub profile lookup failed.");
|
|
5476
|
+
const user = await userResponse.json();
|
|
5477
|
+
const emails = await emailsResponse.json();
|
|
5478
|
+
const email = emails.find((item) => item.primary && item.verified)?.email ?? emails.find((item) => item.verified)?.email;
|
|
5479
|
+
if (!user.id || !user.login || !email) throw new Error("GitHub did not provide a verified email address.");
|
|
5480
|
+
return { provider: "github", subject: String(user.id), email: email.toLowerCase(), username: user.login };
|
|
5481
|
+
}
|
|
2791
5482
|
function connectionStatus(state) {
|
|
2792
5483
|
const statuses = /* @__PURE__ */ new Map();
|
|
2793
5484
|
for (const [deviceId, supervisor] of state.supervisors) {
|
|
@@ -2814,7 +5505,9 @@ async function enrichAdminSummary(summary, state, store, conversationWindowDays)
|
|
|
2814
5505
|
fetchRelayThreads(supervisor, device.id)
|
|
2815
5506
|
]);
|
|
2816
5507
|
workspacesByDeviceId.set(device.id, workspaces);
|
|
2817
|
-
const workspaceLabelById = new Map(
|
|
5508
|
+
const workspaceLabelById = new Map(
|
|
5509
|
+
workspaces.map((workspace) => [workspace.id, workspace.label])
|
|
5510
|
+
);
|
|
2818
5511
|
threadsByDeviceId.set(
|
|
2819
5512
|
device.id,
|
|
2820
5513
|
threads.map((thread) => ({
|
|
@@ -2845,7 +5538,11 @@ async function enrichAdminSummary(summary, state, store, conversationWindowDays)
|
|
|
2845
5538
|
};
|
|
2846
5539
|
}
|
|
2847
5540
|
async function fetchRelayWorkspaces(supervisor, deviceId) {
|
|
2848
|
-
const payload = await forwardSupervisorJson(
|
|
5541
|
+
const payload = await forwardSupervisorJson(
|
|
5542
|
+
supervisor,
|
|
5543
|
+
deviceId,
|
|
5544
|
+
"/api/workspaces"
|
|
5545
|
+
);
|
|
2849
5546
|
const rows = Array.isArray(payload) ? payload : [];
|
|
2850
5547
|
const workspaces = [];
|
|
2851
5548
|
for (const workspace of rows.filter(isObject)) {
|
|
@@ -2863,7 +5560,11 @@ async function fetchRelayWorkspaces(supervisor, deviceId) {
|
|
|
2863
5560
|
return workspaces.slice(0, 50);
|
|
2864
5561
|
}
|
|
2865
5562
|
async function fetchRelayThreads(supervisor, deviceId) {
|
|
2866
|
-
const payload = await forwardSupervisorJson(
|
|
5563
|
+
const payload = await forwardSupervisorJson(
|
|
5564
|
+
supervisor,
|
|
5565
|
+
deviceId,
|
|
5566
|
+
"/api/threads"
|
|
5567
|
+
);
|
|
2867
5568
|
const rows = Array.isArray(payload) ? payload : [];
|
|
2868
5569
|
const threads = [];
|
|
2869
5570
|
for (const thread of rows.filter(isObject)) {
|
|
@@ -2896,7 +5597,11 @@ async function enrichPortalSummary(portal, state, store) {
|
|
|
2896
5597
|
const threadCacheKey = `${share.deviceId}:${share.threadId}`;
|
|
2897
5598
|
let threadTitlePromise = threadCache.get(threadCacheKey);
|
|
2898
5599
|
if (!threadTitlePromise) {
|
|
2899
|
-
threadTitlePromise = fetchRelayThreadTitle(
|
|
5600
|
+
threadTitlePromise = fetchRelayThreadTitle(
|
|
5601
|
+
supervisor,
|
|
5602
|
+
share.deviceId,
|
|
5603
|
+
share.threadId
|
|
5604
|
+
);
|
|
2900
5605
|
threadCache.set(threadCacheKey, threadTitlePromise);
|
|
2901
5606
|
}
|
|
2902
5607
|
let workspaceLabelPromise = Promise.resolve(null);
|
|
@@ -2950,7 +5655,11 @@ async function fetchRelayThreadTitle(supervisor, deviceId, threadId) {
|
|
|
2950
5655
|
return stringField(thread, "title");
|
|
2951
5656
|
}
|
|
2952
5657
|
async function fetchRelayWorkspaceLabel(supervisor, deviceId, workspaceId) {
|
|
2953
|
-
const payload = await forwardSupervisorJson(
|
|
5658
|
+
const payload = await forwardSupervisorJson(
|
|
5659
|
+
supervisor,
|
|
5660
|
+
deviceId,
|
|
5661
|
+
`/api/workspaces/${encodeURIComponent(workspaceId)}`
|
|
5662
|
+
);
|
|
2954
5663
|
return stringField(payload, "label");
|
|
2955
5664
|
}
|
|
2956
5665
|
function stableShareThreadTitle(share) {
|
|
@@ -3049,7 +5758,7 @@ function isAllowedSharedRuntimeMetadataRequest(method, pathname) {
|
|
|
3049
5758
|
if (pathname === "/api/plugins") {
|
|
3050
5759
|
return true;
|
|
3051
5760
|
}
|
|
3052
|
-
return /^\/api\/agent-runtimes\/[^/]+\/(?:status|models)$/.test(pathname);
|
|
5761
|
+
return /^\/api\/agent-runtimes\/[^/]+\/(?:status|models|subscription-usage)$/.test(pathname);
|
|
3053
5762
|
}
|
|
3054
5763
|
function threadIdFromPath(pathValue) {
|
|
3055
5764
|
const pathname = new URL(pathValue, "http://relay.local").pathname;
|
|
@@ -3081,6 +5790,13 @@ function conversationEventFromRequest(method, pathValue, body) {
|
|
|
3081
5790
|
}
|
|
3082
5791
|
return null;
|
|
3083
5792
|
}
|
|
5793
|
+
function isHostedUserActivityRequest(method, pathValue) {
|
|
5794
|
+
const methodName = method.toUpperCase();
|
|
5795
|
+
if (["GET", "HEAD", "OPTIONS"].includes(methodName)) {
|
|
5796
|
+
return false;
|
|
5797
|
+
}
|
|
5798
|
+
return new URL(pathValue, "http://relay.local").pathname.startsWith("/api/");
|
|
5799
|
+
}
|
|
3084
5800
|
function relayAccessEventKindFromRequest(method, pathValue) {
|
|
3085
5801
|
const methodName = method.toUpperCase();
|
|
3086
5802
|
const pathname = new URL(pathValue, "http://relay.local").pathname;
|
|
@@ -3096,7 +5812,9 @@ function relayAccessEventKindFromRequest(method, pathValue) {
|
|
|
3096
5812
|
if (methodName === "GET" && /^\/api\/workspaces\/[^/]+$/.test(pathname)) {
|
|
3097
5813
|
return "open_device";
|
|
3098
5814
|
}
|
|
3099
|
-
if (methodName === "GET" && /^\/api\/workspaces\/[^/]+\/(?:files\/(?:tree|preview|raw|download)|artifacts(?:\/[^/]+(?:\/download)?)?)$/.test(
|
|
5815
|
+
if (methodName === "GET" && /^\/api\/workspaces\/[^/]+\/(?:files\/(?:tree|preview|raw|download)|artifacts(?:\/[^/]+(?:\/download)?)?)$/.test(
|
|
5816
|
+
pathname
|
|
5817
|
+
)) {
|
|
3100
5818
|
return "read_workspace_file";
|
|
3101
5819
|
}
|
|
3102
5820
|
if (["POST", "PUT", "PATCH", "DELETE"].includes(methodName) && /^\/api\/workspaces\/[^/]+\/files(?:\/(?:upload|move))?$/.test(pathname)) {
|
|
@@ -3135,7 +5853,12 @@ function isAllowedForRelayAccess(access, method, pathValue) {
|
|
|
3135
5853
|
}
|
|
3136
5854
|
const workspaceId = workspaceIdFromPath(pathValue);
|
|
3137
5855
|
if (workspaceId) {
|
|
3138
|
-
return isAllowedSharedWorkspacePath(
|
|
5856
|
+
return isAllowedSharedWorkspacePath(
|
|
5857
|
+
access,
|
|
5858
|
+
methodName,
|
|
5859
|
+
pathname,
|
|
5860
|
+
workspaceId
|
|
5861
|
+
);
|
|
3139
5862
|
}
|
|
3140
5863
|
if (access.scope === "device") {
|
|
3141
5864
|
if (methodName === "GET" && (pathname === "/api/threads" || pathname === "/api/workspaces")) {
|
|
@@ -3220,7 +5943,9 @@ function isAllowedSharedWorkspacePath(access, methodName, pathname, workspaceId)
|
|
|
3220
5943
|
new RegExp(`^/api/workspaces/${escapedWorkspaceId}/files/download$`),
|
|
3221
5944
|
new RegExp(`^/api/workspaces/${escapedWorkspaceId}/artifacts$`),
|
|
3222
5945
|
new RegExp(`^/api/workspaces/${escapedWorkspaceId}/artifacts/[^/]+$`),
|
|
3223
|
-
new RegExp(
|
|
5946
|
+
new RegExp(
|
|
5947
|
+
`^/api/workspaces/${escapedWorkspaceId}/artifacts/[^/]+/download$`
|
|
5948
|
+
)
|
|
3224
5949
|
];
|
|
3225
5950
|
if (methodName === "GET" && readPatterns.some((pattern) => pattern.test(pathname))) {
|
|
3226
5951
|
return true;
|
|
@@ -3349,7 +6074,23 @@ var envSchema = z2.object({
|
|
|
3349
6074
|
REMOTE_CODEX_RELAY_SESSION_SECRET: z2.string().min(16).optional(),
|
|
3350
6075
|
REMOTE_CODEX_RELAY_REGISTRATION_ENABLED: z2.string().optional(),
|
|
3351
6076
|
REMOTE_CODEX_RELAY_REGISTRATION_PASSWORD: z2.string().min(8).optional(),
|
|
3352
|
-
|
|
6077
|
+
REMOTE_CODEX_PUBLIC_BASE_URL: z2.string().url().optional(),
|
|
6078
|
+
REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_ID: z2.string().min(1).optional(),
|
|
6079
|
+
REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_SECRET: z2.string().min(1).optional(),
|
|
6080
|
+
REMOTE_CODEX_GOOGLE_OAUTH_ENABLED: z2.string().optional(),
|
|
6081
|
+
REMOTE_CODEX_GITHUB_OAUTH_CLIENT_ID: z2.string().min(1).optional(),
|
|
6082
|
+
REMOTE_CODEX_GITHUB_OAUTH_CLIENT_SECRET: z2.string().min(1).optional(),
|
|
6083
|
+
REMOTE_CODEX_GITHUB_OAUTH_ENABLED: z2.string().optional(),
|
|
6084
|
+
REMOTE_CODEX_EMAIL_VERIFICATION_SECRET: z2.string().min(16).optional(),
|
|
6085
|
+
REMOTE_CODEX_POSTMARK_SERVER_TOKEN: z2.string().min(1).optional(),
|
|
6086
|
+
REMOTE_CODEX_RELAY_WEB_DIST_DIR: z2.string().min(1).optional(),
|
|
6087
|
+
REMOTE_CODEX_HOSTED_SANDBOX_PROVIDER: z2.enum(["disabled", "incus"]).optional(),
|
|
6088
|
+
REMOTE_CODEX_INCUS_HOST_AGENT_URL: z2.string().url().optional(),
|
|
6089
|
+
REMOTE_CODEX_INCUS_HOST_AGENT_TOKEN: z2.string().min(16).optional(),
|
|
6090
|
+
REMOTE_CODEX_HOSTED_RELAY_SERVER_URL: z2.string().url().refine((value) => value.startsWith("ws://") || value.startsWith("wss://")).optional(),
|
|
6091
|
+
REMOTE_CODEX_INCUS_HOST_AGENT_TIMEOUT_MS: z2.coerce.number().int().positive().max(3e4).optional(),
|
|
6092
|
+
REMOTE_CODEX_HOSTED_IDLE_TIMEOUT_MS: z2.coerce.number().int().positive().max(24 * 60 * 6e4).optional(),
|
|
6093
|
+
REMOTE_CODEX_HOSTED_RECONCILE_INTERVAL_MS: z2.coerce.number().int().min(1e4).max(24 * 60 * 6e4).optional()
|
|
3353
6094
|
});
|
|
3354
6095
|
function optionalNonEmpty(value) {
|
|
3355
6096
|
const normalized = value?.trim();
|
|
@@ -3369,7 +6110,9 @@ function normalizeOptionalEnv(env) {
|
|
|
3369
6110
|
env.REMOTE_CODEX_RELAY_CLIENT_TOKEN
|
|
3370
6111
|
),
|
|
3371
6112
|
REMOTE_CODEX_ADMIN_EMAIL: optionalNonEmpty(env.REMOTE_CODEX_ADMIN_EMAIL),
|
|
3372
|
-
REMOTE_CODEX_RELAY_DATA_DIR: optionalNonEmpty(
|
|
6113
|
+
REMOTE_CODEX_RELAY_DATA_DIR: optionalNonEmpty(
|
|
6114
|
+
env.REMOTE_CODEX_RELAY_DATA_DIR
|
|
6115
|
+
),
|
|
3373
6116
|
REMOTE_CODEX_RELAY_SESSION_SECRET: optionalNonEmpty(
|
|
3374
6117
|
env.REMOTE_CODEX_RELAY_SESSION_SECRET
|
|
3375
6118
|
),
|
|
@@ -3379,8 +6122,38 @@ function normalizeOptionalEnv(env) {
|
|
|
3379
6122
|
REMOTE_CODEX_RELAY_REGISTRATION_PASSWORD: optionalNonEmpty(
|
|
3380
6123
|
env.REMOTE_CODEX_RELAY_REGISTRATION_PASSWORD
|
|
3381
6124
|
),
|
|
6125
|
+
REMOTE_CODEX_PUBLIC_BASE_URL: optionalNonEmpty(env.REMOTE_CODEX_PUBLIC_BASE_URL),
|
|
6126
|
+
REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_ID: optionalNonEmpty(env.REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_ID),
|
|
6127
|
+
REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_SECRET: optionalNonEmpty(env.REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_SECRET),
|
|
6128
|
+
REMOTE_CODEX_GOOGLE_OAUTH_ENABLED: optionalNonEmpty(env.REMOTE_CODEX_GOOGLE_OAUTH_ENABLED),
|
|
6129
|
+
REMOTE_CODEX_GITHUB_OAUTH_CLIENT_ID: optionalNonEmpty(env.REMOTE_CODEX_GITHUB_OAUTH_CLIENT_ID),
|
|
6130
|
+
REMOTE_CODEX_GITHUB_OAUTH_CLIENT_SECRET: optionalNonEmpty(env.REMOTE_CODEX_GITHUB_OAUTH_CLIENT_SECRET),
|
|
6131
|
+
REMOTE_CODEX_GITHUB_OAUTH_ENABLED: optionalNonEmpty(env.REMOTE_CODEX_GITHUB_OAUTH_ENABLED),
|
|
6132
|
+
REMOTE_CODEX_EMAIL_VERIFICATION_SECRET: optionalNonEmpty(env.REMOTE_CODEX_EMAIL_VERIFICATION_SECRET),
|
|
6133
|
+
REMOTE_CODEX_POSTMARK_SERVER_TOKEN: optionalNonEmpty(env.REMOTE_CODEX_POSTMARK_SERVER_TOKEN),
|
|
3382
6134
|
REMOTE_CODEX_RELAY_WEB_DIST_DIR: optionalNonEmpty(
|
|
3383
6135
|
env.REMOTE_CODEX_RELAY_WEB_DIST_DIR
|
|
6136
|
+
),
|
|
6137
|
+
REMOTE_CODEX_HOSTED_SANDBOX_PROVIDER: optionalNonEmpty(
|
|
6138
|
+
env.REMOTE_CODEX_HOSTED_SANDBOX_PROVIDER
|
|
6139
|
+
),
|
|
6140
|
+
REMOTE_CODEX_INCUS_HOST_AGENT_URL: optionalNonEmpty(
|
|
6141
|
+
env.REMOTE_CODEX_INCUS_HOST_AGENT_URL
|
|
6142
|
+
),
|
|
6143
|
+
REMOTE_CODEX_INCUS_HOST_AGENT_TOKEN: optionalNonEmpty(
|
|
6144
|
+
env.REMOTE_CODEX_INCUS_HOST_AGENT_TOKEN
|
|
6145
|
+
),
|
|
6146
|
+
REMOTE_CODEX_HOSTED_RELAY_SERVER_URL: optionalNonEmpty(
|
|
6147
|
+
env.REMOTE_CODEX_HOSTED_RELAY_SERVER_URL
|
|
6148
|
+
),
|
|
6149
|
+
REMOTE_CODEX_INCUS_HOST_AGENT_TIMEOUT_MS: optionalNonEmpty(
|
|
6150
|
+
env.REMOTE_CODEX_INCUS_HOST_AGENT_TIMEOUT_MS
|
|
6151
|
+
),
|
|
6152
|
+
REMOTE_CODEX_HOSTED_IDLE_TIMEOUT_MS: optionalNonEmpty(
|
|
6153
|
+
env.REMOTE_CODEX_HOSTED_IDLE_TIMEOUT_MS
|
|
6154
|
+
),
|
|
6155
|
+
REMOTE_CODEX_HOSTED_RECONCILE_INTERVAL_MS: optionalNonEmpty(
|
|
6156
|
+
env.REMOTE_CODEX_HOSTED_RECONCILE_INTERVAL_MS
|
|
3384
6157
|
)
|
|
3385
6158
|
};
|
|
3386
6159
|
}
|
|
@@ -3401,7 +6174,26 @@ function loadRelayServerConfig(env = process.env) {
|
|
|
3401
6174
|
),
|
|
3402
6175
|
registrationEnabledConfigured: parsed.REMOTE_CODEX_RELAY_REGISTRATION_ENABLED !== void 0,
|
|
3403
6176
|
registrationPassword: parsed.REMOTE_CODEX_RELAY_REGISTRATION_PASSWORD ?? null,
|
|
3404
|
-
|
|
6177
|
+
publicBaseUrl: parsed.REMOTE_CODEX_PUBLIC_BASE_URL?.replace(/\/$/, "") ?? null,
|
|
6178
|
+
googleOAuthClientId: parsed.REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_ID ?? null,
|
|
6179
|
+
googleOAuthClientSecret: parsed.REMOTE_CODEX_GOOGLE_OAUTH_CLIENT_SECRET ?? null,
|
|
6180
|
+
googleOAuthEnabled: parsed.REMOTE_CODEX_GOOGLE_OAUTH_ENABLED !== "false",
|
|
6181
|
+
githubOAuthClientId: parsed.REMOTE_CODEX_GITHUB_OAUTH_CLIENT_ID ?? null,
|
|
6182
|
+
githubOAuthClientSecret: parsed.REMOTE_CODEX_GITHUB_OAUTH_CLIENT_SECRET ?? null,
|
|
6183
|
+
githubOAuthEnabled: parsed.REMOTE_CODEX_GITHUB_OAUTH_ENABLED !== "false",
|
|
6184
|
+
emailVerificationConfigured: Boolean(
|
|
6185
|
+
parsed.REMOTE_CODEX_EMAIL_VERIFICATION_SECRET && parsed.REMOTE_CODEX_POSTMARK_SERVER_TOKEN
|
|
6186
|
+
),
|
|
6187
|
+
webDistDir: parsed.REMOTE_CODEX_RELAY_WEB_DIST_DIR ?? defaultRelayWebDistDir(),
|
|
6188
|
+
hostedSandbox: {
|
|
6189
|
+
provider: parsed.REMOTE_CODEX_HOSTED_SANDBOX_PROVIDER ?? "disabled",
|
|
6190
|
+
agentUrl: parsed.REMOTE_CODEX_INCUS_HOST_AGENT_URL ?? null,
|
|
6191
|
+
agentToken: parsed.REMOTE_CODEX_INCUS_HOST_AGENT_TOKEN ?? null,
|
|
6192
|
+
relayServerUrl: parsed.REMOTE_CODEX_HOSTED_RELAY_SERVER_URL ?? null,
|
|
6193
|
+
requestTimeoutMs: parsed.REMOTE_CODEX_INCUS_HOST_AGENT_TIMEOUT_MS ?? 1500,
|
|
6194
|
+
idleTimeoutMs: parsed.REMOTE_CODEX_HOSTED_IDLE_TIMEOUT_MS ?? 30 * 6e4,
|
|
6195
|
+
reconcileIntervalMs: parsed.REMOTE_CODEX_HOSTED_RECONCILE_INTERVAL_MS ?? 5 * 6e4
|
|
6196
|
+
}
|
|
3405
6197
|
};
|
|
3406
6198
|
}
|
|
3407
6199
|
function defaultRelayWebDistDir() {
|