@blue-soda/dsh-remote 0.4.29 → 0.4.30
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/dist/client-runtime.d.ts +137 -1
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +399 -22
- package/dist/client-runtime.js.map +1 -1
- package/dist/client-secure-transport.d.ts.map +1 -1
- package/dist/client-secure-transport.js +2 -1
- package/dist/client-secure-transport.js.map +1 -1
- package/dist/client-target-store.d.ts +40 -0
- package/dist/client-target-store.d.ts.map +1 -0
- package/dist/client-target-store.js +79 -0
- package/dist/client-target-store.js.map +1 -0
- package/dist/client.github.js +63 -15
- package/dist/client.js +63 -15
- package/dist/codex/domain.d.ts +9 -0
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +48 -7
- package/dist/codex/domain.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +662 -162
- package/dist/index.js.map +4 -4
- package/dist/remote-typert-gateway.d.ts.map +1 -1
- package/dist/remote-typert-gateway.js +5 -3
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/rpc-binary-attachments.d.ts +17 -0
- package/dist/rpc-binary-attachments.d.ts.map +1 -0
- package/dist/rpc-binary-attachments.js +89 -0
- package/dist/rpc-binary-attachments.js.map +1 -0
- package/dist/service.d.ts +10 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +17 -1
- package/dist/service.js.map +1 -1
- package/dist/tui-command.js +7 -5
- package/dist/tui-command.js.map +1 -1
- package/dist/typert-gateway-switch.d.ts.map +1 -1
- package/dist/typert-gateway-switch.js +56 -4
- package/dist/typert-gateway-switch.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dsh-plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14579,7 +14579,7 @@ var ClientSecureTransport = class {
|
|
|
14579
14579
|
}
|
|
14580
14580
|
requireNoise() {
|
|
14581
14581
|
if (this.noise === void 0 || !this.noise.complete || this.closed) {
|
|
14582
|
-
throw new Error("The authenticated Noise channel is not connected.");
|
|
14582
|
+
throw Object.assign(new Error("The authenticated Noise channel is not connected."), { code: "TRANSPORT_CLOSED" });
|
|
14583
14583
|
}
|
|
14584
14584
|
return this.noise;
|
|
14585
14585
|
}
|
|
@@ -15162,6 +15162,68 @@ function base64ToBytes2(value) {
|
|
|
15162
15162
|
return bytes;
|
|
15163
15163
|
}
|
|
15164
15164
|
|
|
15165
|
+
// src/rpc-binary-attachments.ts
|
|
15166
|
+
function hydrateRpcAttachments(result) {
|
|
15167
|
+
if (!isRecord3(result)) return result;
|
|
15168
|
+
const envelope = result;
|
|
15169
|
+
if (!Array.isArray(envelope.attachments) || envelope.attachments.length === 0) return result;
|
|
15170
|
+
const { attachments, ...rest } = result;
|
|
15171
|
+
const base = Object.hasOwn(rest, "value") ? rest.value : void 0;
|
|
15172
|
+
for (const attachment of attachments) {
|
|
15173
|
+
const entry = attachment;
|
|
15174
|
+
const path = Array.isArray(entry.path) ? entry.path : void 0;
|
|
15175
|
+
if (path === void 0 || path.length === 0) {
|
|
15176
|
+
throw new Error("The remote Host returned a byte attachment without a path.");
|
|
15177
|
+
}
|
|
15178
|
+
assignAtPath(base, path, decodeAttachmentBytes(entry.bytes));
|
|
15179
|
+
}
|
|
15180
|
+
return rest;
|
|
15181
|
+
}
|
|
15182
|
+
function assignAtPath(base, path, bytes) {
|
|
15183
|
+
if (base === null || typeof base !== "object") {
|
|
15184
|
+
throw new Error("The remote Host returned a byte attachment that its result cannot hold.");
|
|
15185
|
+
}
|
|
15186
|
+
let cursor2 = base;
|
|
15187
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
15188
|
+
const key = pathKey(path[index]);
|
|
15189
|
+
const next = cursor2[key];
|
|
15190
|
+
if (next === null || typeof next !== "object") {
|
|
15191
|
+
throw new Error("The remote Host returned a byte attachment at an unknown result path.");
|
|
15192
|
+
}
|
|
15193
|
+
cursor2 = next;
|
|
15194
|
+
}
|
|
15195
|
+
cursor2[pathKey(path[path.length - 1])] = bytes;
|
|
15196
|
+
}
|
|
15197
|
+
function pathKey(segment) {
|
|
15198
|
+
if (typeof segment === "string") return segment;
|
|
15199
|
+
if (typeof segment === "number" && Number.isInteger(segment) && segment >= 0) return String(segment);
|
|
15200
|
+
throw new Error("The remote Host returned a byte attachment with an invalid path segment.");
|
|
15201
|
+
}
|
|
15202
|
+
function decodeAttachmentBytes(value) {
|
|
15203
|
+
if (value instanceof Uint8Array) return value;
|
|
15204
|
+
if (typeof value === "string") return decodeBase64(value);
|
|
15205
|
+
if (Array.isArray(value)) return Uint8Array.from(value);
|
|
15206
|
+
if (!isRecord3(value)) throw new Error("The remote Host returned invalid byte attachment data.");
|
|
15207
|
+
if (Array.isArray(value.data)) {
|
|
15208
|
+
return Uint8Array.from(value.data);
|
|
15209
|
+
}
|
|
15210
|
+
const keys = Object.keys(value);
|
|
15211
|
+
if (keys.every((key) => /^\d+$/u.test(key))) {
|
|
15212
|
+
const ordered = keys.map(Number).sort((left, right) => left - right);
|
|
15213
|
+
return Uint8Array.from(ordered.map((key) => value[String(key)]));
|
|
15214
|
+
}
|
|
15215
|
+
throw new Error("The remote Host returned invalid byte attachment data.");
|
|
15216
|
+
}
|
|
15217
|
+
function decodeBase64(value) {
|
|
15218
|
+
const binary = atob(value);
|
|
15219
|
+
const bytes = new Uint8Array(binary.length);
|
|
15220
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
15221
|
+
return bytes;
|
|
15222
|
+
}
|
|
15223
|
+
function isRecord3(value) {
|
|
15224
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15225
|
+
}
|
|
15226
|
+
|
|
15165
15227
|
// src/session-format-compat.ts
|
|
15166
15228
|
function normalizeLegacySessionGatewayValue(endpoint, value) {
|
|
15167
15229
|
if (endpoint === "session/page") return normalizePage(value);
|
|
@@ -15169,7 +15231,7 @@ function normalizeLegacySessionGatewayValue(endpoint, value) {
|
|
|
15169
15231
|
return value;
|
|
15170
15232
|
}
|
|
15171
15233
|
function normalizeFollowFrame(value) {
|
|
15172
|
-
if (!
|
|
15234
|
+
if (!isRecord4(value)) return value;
|
|
15173
15235
|
if (value.type === "snapshot") {
|
|
15174
15236
|
return {
|
|
15175
15237
|
...value,
|
|
@@ -15181,7 +15243,7 @@ function normalizeFollowFrame(value) {
|
|
|
15181
15243
|
return normalizeEntry(value);
|
|
15182
15244
|
}
|
|
15183
15245
|
function normalizePage(value) {
|
|
15184
|
-
if (!
|
|
15246
|
+
if (!isRecord4(value)) return value;
|
|
15185
15247
|
return { ...value, records: normalizeRecords(value.records) };
|
|
15186
15248
|
}
|
|
15187
15249
|
function normalizeRecords(value) {
|
|
@@ -15226,10 +15288,10 @@ function eventTime(value) {
|
|
|
15226
15288
|
return typeof value.event.time === "number" && Number.isSafeInteger(value.event.time) ? value.event.time : 0;
|
|
15227
15289
|
}
|
|
15228
15290
|
function isEventEntry(value) {
|
|
15229
|
-
return
|
|
15291
|
+
return isRecord4(value) && value.type === "event" && isRecord4(value.event);
|
|
15230
15292
|
}
|
|
15231
15293
|
function normalizeHeader(value) {
|
|
15232
|
-
if (!
|
|
15294
|
+
if (!isRecord4(value)) return value;
|
|
15233
15295
|
const next = {
|
|
15234
15296
|
...value,
|
|
15235
15297
|
version: 3,
|
|
@@ -15238,7 +15300,7 @@ function normalizeHeader(value) {
|
|
|
15238
15300
|
return next.agentPreset === "code" ? { ...next, agentPreset: "ptc" } : next;
|
|
15239
15301
|
}
|
|
15240
15302
|
function normalizeEvent(value) {
|
|
15241
|
-
if (!
|
|
15303
|
+
if (!isRecord4(value)) return value;
|
|
15242
15304
|
let next = value;
|
|
15243
15305
|
const type = normalizeEventType(value.type);
|
|
15244
15306
|
if (type !== value.type) next = { ...next, type };
|
|
@@ -15258,7 +15320,7 @@ function normalizeEventType(value) {
|
|
|
15258
15320
|
return value;
|
|
15259
15321
|
}
|
|
15260
15322
|
function normalizeEventData(type, value) {
|
|
15261
|
-
if (!
|
|
15323
|
+
if (!isRecord4(value)) return value;
|
|
15262
15324
|
if (type === "request/header") return normalizeRequestHeaderData(value);
|
|
15263
15325
|
if (type === "agent-preset/selected" && value.agentPreset === "code") return { ...value, agentPreset: "ptc" };
|
|
15264
15326
|
if (type === "user/message") return normalizeMessage(value);
|
|
@@ -15271,7 +15333,7 @@ function normalizeEventData(type, value) {
|
|
|
15271
15333
|
return value;
|
|
15272
15334
|
}
|
|
15273
15335
|
function normalizeRequestHeaderData(value) {
|
|
15274
|
-
if (!
|
|
15336
|
+
if (!isRecord4(value.header)) return value;
|
|
15275
15337
|
const header = normalizeRequestHeader(value.header);
|
|
15276
15338
|
return header === value.header ? value : { ...value, header };
|
|
15277
15339
|
}
|
|
@@ -15287,7 +15349,7 @@ function normalizeRequestHeader(value) {
|
|
|
15287
15349
|
changed = true;
|
|
15288
15350
|
continue;
|
|
15289
15351
|
}
|
|
15290
|
-
if (key === "adapterDefaults" &&
|
|
15352
|
+
if (key === "adapterDefaults" && isRecord4(field) && Object.keys(field).length === 0) {
|
|
15291
15353
|
changed = true;
|
|
15292
15354
|
continue;
|
|
15293
15355
|
}
|
|
@@ -15296,12 +15358,12 @@ function normalizeRequestHeader(value) {
|
|
|
15296
15358
|
return changed ? next : value;
|
|
15297
15359
|
}
|
|
15298
15360
|
function normalizeMessage(value) {
|
|
15299
|
-
if (!
|
|
15361
|
+
if (!isRecord4(value) || !isRecord4(value.source)) return value;
|
|
15300
15362
|
if (value.source.kind !== "plugin" || value.source.plugin !== "tools-code-mode") return value;
|
|
15301
15363
|
return { ...value, source: { ...value.source, plugin: "tools-ptc" } };
|
|
15302
15364
|
}
|
|
15303
15365
|
function normalizeSurfaceOp(value) {
|
|
15304
|
-
if (!
|
|
15366
|
+
if (!isRecord4(value) || value.op !== "replace") return value;
|
|
15305
15367
|
const { start, end, ...rest } = value;
|
|
15306
15368
|
if (start === void 0 && end === void 0) return value;
|
|
15307
15369
|
return {
|
|
@@ -15311,7 +15373,7 @@ function normalizeSurfaceOp(value) {
|
|
|
15311
15373
|
...end === void 0 ? {} : { endSeq: end }
|
|
15312
15374
|
};
|
|
15313
15375
|
}
|
|
15314
|
-
function
|
|
15376
|
+
function isRecord4(value) {
|
|
15315
15377
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15316
15378
|
}
|
|
15317
15379
|
function isEventSeq(value) {
|
|
@@ -15352,7 +15414,7 @@ var RemoteTypertGateway2 = class {
|
|
|
15352
15414
|
response = await this.callTransferred(encoded, signal);
|
|
15353
15415
|
}
|
|
15354
15416
|
}
|
|
15355
|
-
const result = parseRpcResult(response);
|
|
15417
|
+
const result = parseRpcResult(hydrateRpcAttachments(response));
|
|
15356
15418
|
const settingsResult = this.normalizeLegacyWelcomeSettings(endpoint, payload, result);
|
|
15357
15419
|
if (settingsResult !== void 0) return settingsResult;
|
|
15358
15420
|
if (result.ok && this.compatibility === "legacy-to-v3") {
|
|
@@ -15365,11 +15427,11 @@ var RemoteTypertGateway2 = class {
|
|
|
15365
15427
|
/** DSH <=0.1.6 cannot persist the 0.1.7 welcome acknowledgement remotely. */
|
|
15366
15428
|
normalizeLegacyWelcomeSettings(endpoint, payload, result) {
|
|
15367
15429
|
if (!isLegacyRemoteHost2(this.harnessVersion)) return void 0;
|
|
15368
|
-
if (endpoint === "settings/describe" && result.ok &&
|
|
15430
|
+
if (endpoint === "settings/describe" && result.ok && isRecord5(result.value)) {
|
|
15369
15431
|
this.settingsDescribeValue = result.value;
|
|
15370
15432
|
return this.legacyWelcomeAcknowledged ? { ...result, value: patchWelcomeDescribe2(result.value) } : void 0;
|
|
15371
15433
|
}
|
|
15372
|
-
if (endpoint !== "settings/mutate" || !
|
|
15434
|
+
if (endpoint !== "settings/mutate" || !isRecord5(payload) || !isRecord5(payload.args)) return void 0;
|
|
15373
15435
|
const args = payload.args;
|
|
15374
15436
|
if (args.ns !== WELCOME_NOTICE_NAMESPACE2 || !isWelcomeNoticeOperation(args.ops)) return void 0;
|
|
15375
15437
|
const value = patchWelcomeDescribe2(this.settingsDescribeValue ?? { namespaces: [] });
|
|
@@ -15398,7 +15460,8 @@ var RemoteTypertGateway2 = class {
|
|
|
15398
15460
|
async *iterate(streamId, endpoint, queue, unsubscribe, unsubscribeClose, signal, onAbort) {
|
|
15399
15461
|
try {
|
|
15400
15462
|
for await (const value of queue) {
|
|
15401
|
-
|
|
15463
|
+
const hydrated = hydrateRpcAttachments(value);
|
|
15464
|
+
yield this.compatibility === "legacy-to-v3" ? normalizeLegacySessionGatewayValue(endpoint, hydrated) : hydrated;
|
|
15402
15465
|
}
|
|
15403
15466
|
} finally {
|
|
15404
15467
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -15533,16 +15596,16 @@ function routeStreamEvent2(event, streamId, queue) {
|
|
|
15533
15596
|
queue.fail(remoteFailure({
|
|
15534
15597
|
code: typeof failure2?.code === "string" ? failure2.code : "internal",
|
|
15535
15598
|
message: typeof failure2?.message === "string" ? failure2.message : "The remote Harness stream failed.",
|
|
15536
|
-
details:
|
|
15599
|
+
details: isRecord5(failure2?.details) ? failure2.details : {}
|
|
15537
15600
|
}));
|
|
15538
15601
|
} else {
|
|
15539
15602
|
queue.close();
|
|
15540
15603
|
}
|
|
15541
15604
|
}
|
|
15542
15605
|
function parseRpcResult(value) {
|
|
15543
|
-
if (!
|
|
15606
|
+
if (!isRecord5(value)) throw new Error("The remote Host returned an invalid Gateway result.");
|
|
15544
15607
|
if (value.ok === true) return Object.hasOwn(value, "value") ? { ok: true, value: value.value } : { ok: true };
|
|
15545
|
-
if (value.ok !== false || !
|
|
15608
|
+
if (value.ok !== false || !isRecord5(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !isRecord5(value.error.details)) {
|
|
15546
15609
|
throw new Error("The remote Host returned an invalid Gateway failure.");
|
|
15547
15610
|
}
|
|
15548
15611
|
return {
|
|
@@ -15576,7 +15639,7 @@ function base64ToBytes3(value) {
|
|
|
15576
15639
|
if (bytesToBase643(bytes) !== value) throw new Error("The remote Host returned non-canonical Harness Remote transfer data.");
|
|
15577
15640
|
return bytes;
|
|
15578
15641
|
}
|
|
15579
|
-
function
|
|
15642
|
+
function isRecord5(value) {
|
|
15580
15643
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15581
15644
|
}
|
|
15582
15645
|
function hasErrorCode(error, code) {
|
|
@@ -15589,13 +15652,13 @@ function isLegacyRemoteHost2(version) {
|
|
|
15589
15652
|
return Number(match[1]) === 0 && Number(match[2]) === 1 && Number(match[3]) < 7;
|
|
15590
15653
|
}
|
|
15591
15654
|
function isWelcomeNoticeOperation(value) {
|
|
15592
|
-
return Array.isArray(value) && value.some((operation) =>
|
|
15655
|
+
return Array.isArray(value) && value.some((operation) => isRecord5(operation) && operation.op === "set" && Array.isArray(operation.path) && operation.path.length === 1 && operation.path[0] === WELCOME_NOTICE_FIELD2 && operation.value === WELCOME_NOTICE_VERSION2);
|
|
15593
15656
|
}
|
|
15594
15657
|
function patchWelcomeDescribe2(value) {
|
|
15595
15658
|
if (!Array.isArray(value.namespaces)) return value;
|
|
15596
15659
|
const namespaces = value.namespaces.map((namespace) => {
|
|
15597
|
-
if (!
|
|
15598
|
-
const current =
|
|
15660
|
+
if (!isRecord5(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE2) return namespace;
|
|
15661
|
+
const current = isRecord5(namespace.value) ? namespace.value : {};
|
|
15599
15662
|
return { ...namespace, value: { ...current, [WELCOME_NOTICE_FIELD2]: WELCOME_NOTICE_VERSION2 } };
|
|
15600
15663
|
});
|
|
15601
15664
|
return { ...value, namespaces };
|
|
@@ -15757,11 +15820,11 @@ var DATA_IMAGE_URL2 = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/
|
|
|
15757
15820
|
function string(value) {
|
|
15758
15821
|
return typeof value === "string" ? value : void 0;
|
|
15759
15822
|
}
|
|
15760
|
-
function
|
|
15823
|
+
function isRecord6(value) {
|
|
15761
15824
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15762
15825
|
}
|
|
15763
15826
|
function record2(value) {
|
|
15764
|
-
return
|
|
15827
|
+
return isRecord6(value) ? value : {};
|
|
15765
15828
|
}
|
|
15766
15829
|
function toolOutputImageBlocks(item) {
|
|
15767
15830
|
const candidates = [
|
|
@@ -15851,7 +15914,7 @@ function collectImageBlocks(value, output = []) {
|
|
|
15851
15914
|
for (const item of value) collectImageBlocks(item, output);
|
|
15852
15915
|
return output;
|
|
15853
15916
|
}
|
|
15854
|
-
if (!
|
|
15917
|
+
if (!isRecord6(value)) return output;
|
|
15855
15918
|
if (value.type === "image") output.push(value);
|
|
15856
15919
|
for (const [key, child] of Object.entries(value)) {
|
|
15857
15920
|
if (key === "attachment") continue;
|
|
@@ -16932,7 +16995,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
16932
16995
|
this.broadcastRcHost({ type: "host/session-status", sessionId, running: args[1] });
|
|
16933
16996
|
} else if (event === "api-session/removed" && sessionId !== void 0) {
|
|
16934
16997
|
this.broadcastRcHost({ type: "host/session-removed", sessionId });
|
|
16935
|
-
} else if (event === "api-session/added" &&
|
|
16998
|
+
} else if (event === "api-session/added" && isRecord7(args[0])) {
|
|
16936
16999
|
const summary = args[0];
|
|
16937
17000
|
this.broadcastRcHost({
|
|
16938
17001
|
type: "host/session-added",
|
|
@@ -17322,7 +17385,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
17322
17385
|
}
|
|
17323
17386
|
createApiProxy() {
|
|
17324
17387
|
const call = (endpoint) => async (request, signal) => {
|
|
17325
|
-
const payload = endpoint === "session.prompt" &&
|
|
17388
|
+
const payload = endpoint === "session.prompt" && isRecord7(request.payload) ? { ...request.payload, requestId: String(request.rpcId) } : request.payload;
|
|
17326
17389
|
const result = await this.dispatch(rcEndpoint(endpoint), { args: { request: payload } }, signal ?? new AbortController().signal);
|
|
17327
17390
|
return {
|
|
17328
17391
|
rpcId: request.rpcId,
|
|
@@ -17977,7 +18040,7 @@ function codexPromptInput(content) {
|
|
|
17977
18040
|
if (content.length === 0 || content.length > MAX_CODEX_PROMPT_PARTS) return void 0;
|
|
17978
18041
|
const input2 = [];
|
|
17979
18042
|
for (const value of content) {
|
|
17980
|
-
if (!
|
|
18043
|
+
if (!isRecord7(value)) return void 0;
|
|
17981
18044
|
if (value.type === "text") {
|
|
17982
18045
|
if (typeof value.text !== "string" || value.text.length === 0 || value.text.length > MAX_CODEX_PROMPT_TEXT) return void 0;
|
|
17983
18046
|
input2.push({ type: "text", text: value.text });
|
|
@@ -18227,12 +18290,12 @@ function errorCode(error) {
|
|
|
18227
18290
|
return "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
18228
18291
|
}
|
|
18229
18292
|
function errorDetails(error) {
|
|
18230
|
-
return "details" in error &&
|
|
18293
|
+
return "details" in error && isRecord7(error.details) ? error.details : {};
|
|
18231
18294
|
}
|
|
18232
18295
|
function record3(value) {
|
|
18233
|
-
return
|
|
18296
|
+
return isRecord7(value) ? value : {};
|
|
18234
18297
|
}
|
|
18235
|
-
function
|
|
18298
|
+
function isRecord7(value) {
|
|
18236
18299
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18237
18300
|
}
|
|
18238
18301
|
function array(value) {
|
|
@@ -18629,7 +18692,7 @@ function normalizeServerUrl(value) {
|
|
|
18629
18692
|
}
|
|
18630
18693
|
|
|
18631
18694
|
// src/version.ts
|
|
18632
|
-
var PLUGIN_VERSION = "0.4.
|
|
18695
|
+
var PLUGIN_VERSION = "0.4.30";
|
|
18633
18696
|
|
|
18634
18697
|
// src/server-api.ts
|
|
18635
18698
|
var ENABLED_QR_PROVIDERS = ["github"];
|
|
@@ -19104,6 +19167,17 @@ function requireRecord(value, name2) {
|
|
|
19104
19167
|
// src/typert-gateway-switch.ts
|
|
19105
19168
|
var REMOTE_COMMAND_METHODS = ["execute", "list"];
|
|
19106
19169
|
var LOCAL_ONLY_NAMESPACES = /* @__PURE__ */ new Set(["dynamicCordisRunner"]);
|
|
19170
|
+
var LOCAL_FALLBACK_NAMESPACES = /* @__PURE__ */ new Set(["$events", "settings", "credentials", "dynamicCordisRunner"]);
|
|
19171
|
+
var PEER_GONE_CODES = /* @__PURE__ */ new Set([
|
|
19172
|
+
"TRANSPORT_CLOSED",
|
|
19173
|
+
"CLIENT_CLOSED",
|
|
19174
|
+
"RPC_TIMEOUT",
|
|
19175
|
+
"CONNECTION_FAILED",
|
|
19176
|
+
"CONNECTION_REPLACED",
|
|
19177
|
+
"NOT_CONNECTED",
|
|
19178
|
+
"UNAVAILABLE",
|
|
19179
|
+
"internal"
|
|
19180
|
+
]);
|
|
19107
19181
|
var ALL_REMOTE_COMMANDS = { execute: true, list: true };
|
|
19108
19182
|
var TypertGatewaySwitch = class {
|
|
19109
19183
|
runtime;
|
|
@@ -19255,7 +19329,7 @@ var TypertGatewaySwitch = class {
|
|
|
19255
19329
|
if (isLocalOnlyEndpoint(endpointOf(request))) return this.localInvoke(request);
|
|
19256
19330
|
if (this.remoteTarget !== void 0 && this.remoteAvailability()) {
|
|
19257
19331
|
return this.remoteTarget.invoke(request).catch((error) => {
|
|
19258
|
-
if (!
|
|
19332
|
+
if (!localFallbackAllowed(endpointOf(request), error)) throw error;
|
|
19259
19333
|
console.warn(`[dsh-remote] serving ${endpointOf(request)} locally: the peer did not answer it`, error);
|
|
19260
19334
|
return this.localInvoke(request);
|
|
19261
19335
|
});
|
|
@@ -19282,7 +19356,7 @@ var TypertGatewaySwitch = class {
|
|
|
19282
19356
|
*/
|
|
19283
19357
|
withLocalFallback(endpoint, remote, local) {
|
|
19284
19358
|
return remote().catch((error) => {
|
|
19285
|
-
if (!
|
|
19359
|
+
if (!localFallbackAllowed(endpoint, error)) throw error;
|
|
19286
19360
|
console.warn(`[dsh-remote] serving ${endpoint} locally: the peer did not answer it`, error);
|
|
19287
19361
|
return local();
|
|
19288
19362
|
});
|
|
@@ -19292,7 +19366,7 @@ var TypertGatewaySwitch = class {
|
|
|
19292
19366
|
if (normalized !== void 0) return normalized;
|
|
19293
19367
|
const source = error instanceof Error ? error : new Error("The Harness Gateway rejected the request.");
|
|
19294
19368
|
const code = "code" in source && typeof source.code === "string" ? source.code : "internal";
|
|
19295
|
-
const details = "details" in source &&
|
|
19369
|
+
const details = "details" in source && isRecord8(source.details) ? source.details : {};
|
|
19296
19370
|
return { code, message: source.message, details };
|
|
19297
19371
|
}
|
|
19298
19372
|
};
|
|
@@ -19332,7 +19406,7 @@ function requestFromCarrier(endpoint, payload, signal) {
|
|
|
19332
19406
|
if (segments.length !== 2 || segments.some((segment) => segment.length === 0)) {
|
|
19333
19407
|
throw new Error("The Harness Gateway endpoint is invalid.");
|
|
19334
19408
|
}
|
|
19335
|
-
if (!
|
|
19409
|
+
if (!isRecord8(payload) || !isRecord8(payload.args)) {
|
|
19336
19410
|
throw new Error("The Harness Gateway payload is invalid.");
|
|
19337
19411
|
}
|
|
19338
19412
|
return { namespace: segments[0], method: segments[1], args: payload.args, signal };
|
|
@@ -19340,15 +19414,27 @@ function requestFromCarrier(endpoint, payload, signal) {
|
|
|
19340
19414
|
function endpointOf(request) {
|
|
19341
19415
|
return `${request.namespace}/${request.method}`;
|
|
19342
19416
|
}
|
|
19343
|
-
function
|
|
19417
|
+
function namespaceOf(endpoint) {
|
|
19344
19418
|
const separator = endpoint.indexOf("/");
|
|
19345
|
-
return separator > 0
|
|
19419
|
+
return separator > 0 ? endpoint.slice(0, separator) : void 0;
|
|
19420
|
+
}
|
|
19421
|
+
function isLocalOnlyEndpoint(endpoint) {
|
|
19422
|
+
const namespace = namespaceOf(endpoint);
|
|
19423
|
+
return namespace !== void 0 && LOCAL_ONLY_NAMESPACES.has(namespace);
|
|
19424
|
+
}
|
|
19425
|
+
function localFallbackAllowed(endpoint, error) {
|
|
19426
|
+
if (!isUnansweredByPeer(error)) return false;
|
|
19427
|
+
if (!isRecord8(error)) return true;
|
|
19428
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
19429
|
+
if (!PEER_GONE_CODES.has(code)) return true;
|
|
19430
|
+
const namespace = namespaceOf(endpoint);
|
|
19431
|
+
return namespace !== void 0 && LOCAL_FALLBACK_NAMESPACES.has(namespace);
|
|
19346
19432
|
}
|
|
19347
19433
|
function isRemoteCommandMethod(method) {
|
|
19348
19434
|
return REMOTE_COMMAND_METHODS.includes(method);
|
|
19349
19435
|
}
|
|
19350
19436
|
function isUnansweredByPeer(error) {
|
|
19351
|
-
if (!
|
|
19437
|
+
if (!isRecord8(error)) return error instanceof Error;
|
|
19352
19438
|
const code = typeof error.code === "string" ? error.code : "";
|
|
19353
19439
|
return code === "" || UNANSWERED_BY_PEER_CODES.has(code);
|
|
19354
19440
|
}
|
|
@@ -19361,9 +19447,15 @@ var UNANSWERED_BY_PEER_CODES = /* @__PURE__ */ new Set([
|
|
|
19361
19447
|
"CONNECTION_REPLACED",
|
|
19362
19448
|
"NOT_CONNECTED",
|
|
19363
19449
|
"UNAVAILABLE",
|
|
19450
|
+
// The client's own transport codes: the peer cannot answer a call it never received, so these are
|
|
19451
|
+
// "unanswered" rather than a refusal - and PEER_GONE_CODES above is what decides whether the local
|
|
19452
|
+
// shell may answer for it.
|
|
19453
|
+
"TRANSPORT_CLOSED",
|
|
19454
|
+
"CLIENT_CLOSED",
|
|
19455
|
+
"RPC_TIMEOUT",
|
|
19364
19456
|
"internal"
|
|
19365
19457
|
]);
|
|
19366
|
-
function
|
|
19458
|
+
function isRecord8(value) {
|
|
19367
19459
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19368
19460
|
}
|
|
19369
19461
|
|
|
@@ -20442,14 +20534,34 @@ var HOST_AUTHORIZATION_ERRORS = /* @__PURE__ */ new Set([
|
|
|
20442
20534
|
"DEVICE_REVOKED",
|
|
20443
20535
|
"TOKEN_EXPIRED"
|
|
20444
20536
|
]);
|
|
20537
|
+
var CREDENTIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
20538
|
+
"AUTH_REQUIRED",
|
|
20539
|
+
"ACCOUNT_AUTH_REQUIRED",
|
|
20540
|
+
"AUTH_INVALID",
|
|
20541
|
+
"TOKEN_EXPIRED",
|
|
20542
|
+
"DEVICE_NOT_FOUND",
|
|
20543
|
+
"DEVICE_REVOKED",
|
|
20544
|
+
"MEMBERSHIP_REQUIRED"
|
|
20545
|
+
]);
|
|
20546
|
+
var LIVENESS_INTERVAL_MS = 3e4;
|
|
20547
|
+
var FAST_RECONNECT_INTERVAL_MS = 5e3;
|
|
20548
|
+
var LIVENESS_TIMEOUT_MS = 4e3;
|
|
20549
|
+
var LIVENESS_TOLERATED_FAILURES = 2;
|
|
20550
|
+
var NO_ANSWER_CODES = /* @__PURE__ */ new Set(["RPC_TIMEOUT", "CLIENT_CLOSED", "TRANSPORT_CLOSED", "RPC_ABORTED"]);
|
|
20551
|
+
function livenessProbeLost(error) {
|
|
20552
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
20553
|
+
if (typeof code !== "string") return true;
|
|
20554
|
+
return NO_ANSWER_CODES.has(code);
|
|
20555
|
+
}
|
|
20445
20556
|
var ClientModeRuntime = class {
|
|
20446
|
-
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
|
|
20557
|
+
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory, targetStore) {
|
|
20447
20558
|
this.config = config;
|
|
20448
20559
|
this.identities = identities;
|
|
20449
20560
|
this.server = server;
|
|
20450
20561
|
this.logger = logger;
|
|
20451
20562
|
this.host = host;
|
|
20452
20563
|
this.rtcFactoryProvider = rtcFactoryProvider;
|
|
20564
|
+
this.targetStore = targetStore;
|
|
20453
20565
|
this.proxySwitch = apiProxy === void 0 ? void 0 : new ApiProxySwitch(apiProxy);
|
|
20454
20566
|
this.gatewaySwitch = new TypertGatewaySwitch(typertGateway);
|
|
20455
20567
|
this.gatewaySwitch.setRemoteAvailability(() => this.connected !== void 0);
|
|
@@ -20474,12 +20586,56 @@ var ClientModeRuntime = class {
|
|
|
20474
20586
|
*/
|
|
20475
20587
|
remoteReconnectRun = 0;
|
|
20476
20588
|
pendingWorkspaceSelection;
|
|
20589
|
+
/**
|
|
20590
|
+
* Transport a running fast reconnect is replacing.
|
|
20591
|
+
*
|
|
20592
|
+
* The rebuild opens its own control connection and the Server closes the older one for the same
|
|
20593
|
+
* device, so that close belongs to our own replacement. Reading it as a lost peer is what made the
|
|
20594
|
+
* fast level escalate itself into the fallback before it could ever succeed.
|
|
20595
|
+
*/
|
|
20596
|
+
supersededClient;
|
|
20597
|
+
/**
|
|
20598
|
+
* True while a fast reconnect is rebuilding the link.
|
|
20599
|
+
*
|
|
20600
|
+
* The probe must not judge the link in the middle of its own replacement: the Server closes the
|
|
20601
|
+
* older connection for the device, so a check that ran then would read our replacement's side effect
|
|
20602
|
+
* as a second miss and escalate the level that is busy recovering. The rebuild's own outcome decides
|
|
20603
|
+
* first; only once it has settled does the cadence resume judging.
|
|
20604
|
+
*/
|
|
20605
|
+
fastRebuildInFlight = false;
|
|
20606
|
+
/**
|
|
20607
|
+
* The last workspace the user opened for a Host.
|
|
20608
|
+
*
|
|
20609
|
+
* Kept so a reconnect can republish it: re-selecting the workspace is what makes the native UI
|
|
20610
|
+
* re-read its session list, and without it a list poisoned by the outage stays wrong even after
|
|
20611
|
+
* the link is back.
|
|
20612
|
+
*/
|
|
20613
|
+
lastWorkspaceSelection;
|
|
20477
20614
|
codexVirtual;
|
|
20478
20615
|
proxySwitch;
|
|
20479
20616
|
gatewaySwitch;
|
|
20480
20617
|
codexStreams = /* @__PURE__ */ new Map();
|
|
20481
20618
|
connectionProgress;
|
|
20482
20619
|
connectionProgressRun = 0;
|
|
20620
|
+
/**
|
|
20621
|
+
* Host a boot is trying to restore, reported to the UI while the retry loop runs.
|
|
20622
|
+
*
|
|
20623
|
+
* Nothing is connected yet, so without this the window would show the local shell while the
|
|
20624
|
+
* user is still looking at the remote workspace they left.
|
|
20625
|
+
*/
|
|
20626
|
+
/**
|
|
20627
|
+
* Recovery in progress, and which kind.
|
|
20628
|
+
*
|
|
20629
|
+
* `restore` is a start that is reconnecting to the recorded target, `fast` keeps the session on
|
|
20630
|
+
* screen while its link is rebuilt, and `fallback` is the last resort that returns the user to
|
|
20631
|
+
* the local shell. The UI shows all three, so a reconnect is never invisible, and the retry loop
|
|
20632
|
+
* stops as soon as the user asks for local.
|
|
20633
|
+
*/
|
|
20634
|
+
reconnecting;
|
|
20635
|
+
/** Periodic proof of life for the live remote session; absent while nothing is connected. */
|
|
20636
|
+
livenessTimer;
|
|
20637
|
+
livenessInFlight = false;
|
|
20638
|
+
livenessFailures = 0;
|
|
20483
20639
|
closed = false;
|
|
20484
20640
|
async start() {
|
|
20485
20641
|
if (this.closed) throw new Error("client remote-mode runtime is closed");
|
|
@@ -20540,6 +20696,7 @@ var ClientModeRuntime = class {
|
|
|
20540
20696
|
...targetStatus,
|
|
20541
20697
|
connected: this.connected !== void 0,
|
|
20542
20698
|
fellBackToLocal: this.fellBackToLocal,
|
|
20699
|
+
...this.reconnecting === void 0 ? {} : { reconnecting: { ...this.reconnecting } },
|
|
20543
20700
|
transport: this.connected?.client.getStats().mode ?? "Disconnected",
|
|
20544
20701
|
connectedTargetDeviceId: this.connected?.target.deviceId,
|
|
20545
20702
|
preferredTransports: this.config.forceRelay ? ["relay"] : ["lan", "p2p", "turn", "relay"],
|
|
@@ -20686,10 +20843,14 @@ var ClientModeRuntime = class {
|
|
|
20686
20843
|
this.connected = void 0;
|
|
20687
20844
|
this.connectionProgress = void 0;
|
|
20688
20845
|
this.pendingWorkspaceSelection = void 0;
|
|
20846
|
+
this.lastWorkspaceSelection = void 0;
|
|
20689
20847
|
await this.closeCodexStreams(previous2?.client);
|
|
20690
20848
|
await previous2?.client.close().catch(() => void 0);
|
|
20691
20849
|
this.fellBackToLocal = false;
|
|
20850
|
+
this.reconnecting = void 0;
|
|
20851
|
+
this.stopLivenessWatch();
|
|
20692
20852
|
this.remoteReconnectRun += 1;
|
|
20853
|
+
await this.rememberTarget({ mode: "local" });
|
|
20693
20854
|
this.logger.info("Harness target switched", { mode: "local" });
|
|
20694
20855
|
return this.status();
|
|
20695
20856
|
}
|
|
@@ -20712,11 +20873,191 @@ var ClientModeRuntime = class {
|
|
|
20712
20873
|
await this.closeCodexVirtual();
|
|
20713
20874
|
this.selectRemoteTarget(next);
|
|
20714
20875
|
this.fellBackToLocal = false;
|
|
20876
|
+
this.reconnecting = void 0;
|
|
20877
|
+
await this.rememberTarget({ mode: "remote", hostDeviceId: next.target.deviceId, hostName: next.target.name });
|
|
20715
20878
|
await this.closeCodexStreams(previous?.client);
|
|
20716
20879
|
await previous?.client.close().catch(() => void 0);
|
|
20717
20880
|
this.logger.info("Harness target switched", { mode: "remote", targetDeviceId: shortId(next.target.deviceId) });
|
|
20718
20881
|
return this.status();
|
|
20719
20882
|
}
|
|
20883
|
+
/**
|
|
20884
|
+
* Prove the live remote link still answers, and act on the result.
|
|
20885
|
+
*
|
|
20886
|
+
* Only a missing answer counts as loss: the peer's own error (a refusal, an unknown method) came
|
|
20887
|
+
* back over the same link and therefore proves it is there.
|
|
20888
|
+
*
|
|
20889
|
+
* The two recovery levels differ in what the user keeps. The first miss rebuilds the link in
|
|
20890
|
+
* place, so the session, its workspace selection and the remote carriers all stay; the second
|
|
20891
|
+
* gives up and returns to the local shell.
|
|
20892
|
+
*/
|
|
20893
|
+
async verifyRemoteLiveness() {
|
|
20894
|
+
const connected = this.connected;
|
|
20895
|
+
if (connected === void 0 || this.livenessInFlight || this.fastRebuildInFlight) return;
|
|
20896
|
+
this.livenessInFlight = true;
|
|
20897
|
+
try {
|
|
20898
|
+
await connected.client.rpc("harness.transport.describe", {}, void 0, { timeoutMs: LIVENESS_TIMEOUT_MS });
|
|
20899
|
+
this.livenessFailures = 0;
|
|
20900
|
+
if (this.reconnecting?.phase === "fast") this.finishReconnect("the link answered again");
|
|
20901
|
+
} catch (error) {
|
|
20902
|
+
if (!livenessProbeLost(error)) {
|
|
20903
|
+
this.livenessFailures = 0;
|
|
20904
|
+
return;
|
|
20905
|
+
}
|
|
20906
|
+
this.livenessFailures += 1;
|
|
20907
|
+
this.logger.warn("remote Harness liveness check found no answer", {
|
|
20908
|
+
targetDeviceId: shortId(connected.target.deviceId),
|
|
20909
|
+
attempt: this.livenessFailures
|
|
20910
|
+
});
|
|
20911
|
+
if (this.livenessFailures === 1) {
|
|
20912
|
+
await this.enterFastReconnect(connected.target.deviceId, connected.target.name);
|
|
20913
|
+
return;
|
|
20914
|
+
}
|
|
20915
|
+
if (this.livenessFailures >= LIVENESS_TOLERATED_FAILURES) {
|
|
20916
|
+
this.handleRemoteTransportLost(connected.client, connected.target.deviceId, "unanswered-twice");
|
|
20917
|
+
}
|
|
20918
|
+
} finally {
|
|
20919
|
+
this.livenessInFlight = false;
|
|
20920
|
+
}
|
|
20921
|
+
}
|
|
20922
|
+
/**
|
|
20923
|
+
* Rebuild the link while the session stays on screen.
|
|
20924
|
+
*
|
|
20925
|
+
* This is what separates the two recovery levels: nothing is handed back to the local shell, so a
|
|
20926
|
+
* link that recovers does not cost the user the view they were working in.
|
|
20927
|
+
* @param targetDeviceId - the Host to rebuild the link to.
|
|
20928
|
+
*/
|
|
20929
|
+
async enterFastReconnect(targetDeviceId, targetName) {
|
|
20930
|
+
if (this.reconnecting !== void 0) return;
|
|
20931
|
+
this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fast" };
|
|
20932
|
+
this.logger.warn("remote Harness link stopped answering; reconnecting in place", {
|
|
20933
|
+
targetDeviceId: shortId(targetDeviceId)
|
|
20934
|
+
});
|
|
20935
|
+
this.armLivenessWatch(FAST_RECONNECT_INTERVAL_MS);
|
|
20936
|
+
await this.reestablish(targetDeviceId);
|
|
20937
|
+
}
|
|
20938
|
+
/**
|
|
20939
|
+
* Build a new transport for the session already on screen.
|
|
20940
|
+
*
|
|
20941
|
+
* The carriers hold the previous client, so they are rebound to the new one before the old client
|
|
20942
|
+
* is closed; leaving it open would keep pointing remote calls at a dead transport.
|
|
20943
|
+
* @param targetDeviceId - the Host to reconnect to.
|
|
20944
|
+
* @returns true when a new session is in place.
|
|
20945
|
+
*/
|
|
20946
|
+
async reestablish(targetDeviceId) {
|
|
20947
|
+
const previous = this.connected;
|
|
20948
|
+
this.supersededClient = previous?.client;
|
|
20949
|
+
this.fastRebuildInFlight = true;
|
|
20950
|
+
try {
|
|
20951
|
+
const next = await this.connect(targetDeviceId);
|
|
20952
|
+
if (previous === void 0) {
|
|
20953
|
+
this.supersededClient = void 0;
|
|
20954
|
+
await next.client.close().catch(() => void 0);
|
|
20955
|
+
return false;
|
|
20956
|
+
}
|
|
20957
|
+
await this.closePreview();
|
|
20958
|
+
this.connected = next;
|
|
20959
|
+
this.clearConnectionProgress(next.progressRunId);
|
|
20960
|
+
this.selectRemoteTarget(next);
|
|
20961
|
+
await this.closeCodexStreams(previous.client);
|
|
20962
|
+
await previous.client.close().catch(() => void 0);
|
|
20963
|
+
this.supersededClient = void 0;
|
|
20964
|
+
this.finishReconnect("link re-established");
|
|
20965
|
+
return true;
|
|
20966
|
+
} catch (error) {
|
|
20967
|
+
this.supersededClient = void 0;
|
|
20968
|
+
this.logger.warn("fast reconnect attempt failed", {
|
|
20969
|
+
targetDeviceId: shortId(targetDeviceId),
|
|
20970
|
+
code: safeErrorCode(error)
|
|
20971
|
+
});
|
|
20972
|
+
return false;
|
|
20973
|
+
} finally {
|
|
20974
|
+
this.fastRebuildInFlight = false;
|
|
20975
|
+
}
|
|
20976
|
+
}
|
|
20977
|
+
rememberWorkspaceSelection(selection) {
|
|
20978
|
+
this.pendingWorkspaceSelection = selection;
|
|
20979
|
+
this.lastWorkspaceSelection = { ...selection };
|
|
20980
|
+
}
|
|
20981
|
+
/**
|
|
20982
|
+
* Republish the workspace selection so the native UI re-reads its remote session list.
|
|
20983
|
+
*
|
|
20984
|
+
* The client half consumes status.workspaceSelection and reconnects that workspace; that refresh is
|
|
20985
|
+
* what replaces a list the outage had filled with local answers.
|
|
20986
|
+
* @param targetDeviceId - the Host the reconnect finished against.
|
|
20987
|
+
*/
|
|
20988
|
+
restoreWorkspaceSelection(targetDeviceId) {
|
|
20989
|
+
const selection = this.lastWorkspaceSelection;
|
|
20990
|
+
if (selection === void 0 || selection.targetDeviceId !== targetDeviceId) return;
|
|
20991
|
+
this.pendingWorkspaceSelection = { ...selection };
|
|
20992
|
+
this.logger.info("republishing the workspace selection to re-read the remote session list", {
|
|
20993
|
+
targetDeviceId: shortId(targetDeviceId),
|
|
20994
|
+
workspaceId: selection.workspaceId
|
|
20995
|
+
});
|
|
20996
|
+
}
|
|
20997
|
+
finishReconnect(reason) {
|
|
20998
|
+
const target2 = this.reconnecting?.targetDeviceId;
|
|
20999
|
+
if (target2 === void 0) return;
|
|
21000
|
+
this.reconnecting = void 0;
|
|
21001
|
+
this.livenessFailures = 0;
|
|
21002
|
+
this.armLivenessWatch(LIVENESS_INTERVAL_MS);
|
|
21003
|
+
this.restoreWorkspaceSelection(target2);
|
|
21004
|
+
this.logger.info("remote Harness reconnect finished", { targetDeviceId: shortId(target2), reason });
|
|
21005
|
+
}
|
|
21006
|
+
/**
|
|
21007
|
+
* Check the live session now, outside the cadence.
|
|
21008
|
+
*
|
|
21009
|
+
* Used when the page becomes visible again, which is when a suspended client is most likely to be
|
|
21010
|
+
* holding a link that already ended.
|
|
21011
|
+
* @returns the status after the check.
|
|
21012
|
+
*/
|
|
21013
|
+
async verifyRemoteConnection() {
|
|
21014
|
+
await this.verifyRemoteLiveness();
|
|
21015
|
+
return this.status();
|
|
21016
|
+
}
|
|
21017
|
+
armLivenessWatch(intervalMs) {
|
|
21018
|
+
if (this.livenessTimer !== void 0) clearInterval(this.livenessTimer);
|
|
21019
|
+
const timer = setInterval(() => {
|
|
21020
|
+
void this.verifyRemoteLiveness();
|
|
21021
|
+
}, intervalMs);
|
|
21022
|
+
timer.unref?.();
|
|
21023
|
+
this.livenessTimer = timer;
|
|
21024
|
+
}
|
|
21025
|
+
stopLivenessWatch() {
|
|
21026
|
+
if (this.livenessTimer !== void 0) clearInterval(this.livenessTimer);
|
|
21027
|
+
this.livenessTimer = void 0;
|
|
21028
|
+
this.livenessFailures = 0;
|
|
21029
|
+
this.livenessInFlight = false;
|
|
21030
|
+
}
|
|
21031
|
+
/**
|
|
21032
|
+
* Shared cleanup for a session whose transport is gone.
|
|
21033
|
+
*
|
|
21034
|
+
* A close event and an exhausted liveness check must leave exactly the same state behind, so both
|
|
21035
|
+
* paths run this. The session is gone for good here, which is why the phase becomes 'fallback' and
|
|
21036
|
+
* the retry loop keeps the UI saying that it is reconnecting.
|
|
21037
|
+
* @param client - the client that was connected.
|
|
21038
|
+
* @param targetDeviceId - the Host it was bound to.
|
|
21039
|
+
*/
|
|
21040
|
+
handleRemoteTransportLost(client, targetDeviceId, reason) {
|
|
21041
|
+
if (this.connected?.client !== client) return;
|
|
21042
|
+
this.supersededClient = void 0;
|
|
21043
|
+
const targetName = this.connected.target.name;
|
|
21044
|
+
this.stopLivenessWatch();
|
|
21045
|
+
void this.closePreview();
|
|
21046
|
+
this.connected = void 0;
|
|
21047
|
+
this.connectionProgress = void 0;
|
|
21048
|
+
this.pendingWorkspaceSelection = void 0;
|
|
21049
|
+
void this.closeCodexVirtual();
|
|
21050
|
+
this.proxySwitch?.selectLocal();
|
|
21051
|
+
this.gatewaySwitch.selectLocal();
|
|
21052
|
+
this.fellBackToLocal = true;
|
|
21053
|
+
this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fallback" };
|
|
21054
|
+
void client.close().catch(() => void 0);
|
|
21055
|
+
this.logger.warn("remote Harness transport lost; reconnecting", {
|
|
21056
|
+
targetDeviceId: shortId(targetDeviceId),
|
|
21057
|
+
reason
|
|
21058
|
+
});
|
|
21059
|
+
void this.reconnectRemoteSession(targetDeviceId);
|
|
21060
|
+
}
|
|
20720
21061
|
/**
|
|
20721
21062
|
* Re-establish a remote session whose transport closed.
|
|
20722
21063
|
*
|
|
@@ -20740,20 +21081,79 @@ var ClientModeRuntime = class {
|
|
|
20740
21081
|
if (this.connected !== void 0) return;
|
|
20741
21082
|
try {
|
|
20742
21083
|
await this.setMode("remote", targetDeviceId);
|
|
21084
|
+
this.restoreWorkspaceSelection(targetDeviceId);
|
|
20743
21085
|
this.logger.info("remote Harness session reconnected", { targetDeviceId: shortId(targetDeviceId) });
|
|
20744
21086
|
return;
|
|
20745
21087
|
} catch (error) {
|
|
21088
|
+
const code = safeErrorCode(error);
|
|
21089
|
+
if (code !== void 0 && CREDENTIAL_FAILURE_CODES.has(code)) {
|
|
21090
|
+
this.logger.warn("remote Harness reconnect stopped: authorization is required", {
|
|
21091
|
+
targetDeviceId: shortId(targetDeviceId),
|
|
21092
|
+
code
|
|
21093
|
+
});
|
|
21094
|
+
this.reconnecting = void 0;
|
|
21095
|
+
return;
|
|
21096
|
+
}
|
|
20746
21097
|
if (attempt < 3 || attempt % 10 === 0) {
|
|
20747
21098
|
this.logger.warn("remote Harness reconnect attempt failed", {
|
|
20748
21099
|
targetDeviceId: shortId(targetDeviceId),
|
|
20749
21100
|
attempt,
|
|
20750
|
-
code
|
|
21101
|
+
code
|
|
20751
21102
|
});
|
|
20752
21103
|
}
|
|
20753
21104
|
}
|
|
20754
21105
|
attempt += 1;
|
|
20755
21106
|
}
|
|
20756
21107
|
}
|
|
21108
|
+
/**
|
|
21109
|
+
* Persist the target a later boot may have to restore.
|
|
21110
|
+
*
|
|
21111
|
+
* A failure here must not fail a connect: the only cost is that a killed process comes back to
|
|
21112
|
+
* the local shell, which is where it would have been without this record.
|
|
21113
|
+
* @param target - the target to record.
|
|
21114
|
+
*/
|
|
21115
|
+
async rememberTarget(target2) {
|
|
21116
|
+
if (this.targetStore === void 0) return;
|
|
21117
|
+
try {
|
|
21118
|
+
await this.targetStore.save({
|
|
21119
|
+
...target2,
|
|
21120
|
+
...this.config.serverUrl === void 0 ? {} : { serverUrl: this.config.serverUrl }
|
|
21121
|
+
});
|
|
21122
|
+
} catch (error) {
|
|
21123
|
+
this.logger.warn("could not record the remote target", { code: safeErrorCode(error) });
|
|
21124
|
+
}
|
|
21125
|
+
}
|
|
21126
|
+
/**
|
|
21127
|
+
* Reconnect to the target this device was last using, with the usual backoff.
|
|
21128
|
+
*
|
|
21129
|
+
* A resumed app can miss the transport close entirely - Android may reclaim the process - so
|
|
21130
|
+
* nothing would start the retry loop and the user would face a local shell behind a remote
|
|
21131
|
+
* workspace view. Restoring the recorded target gives that case the same loop a dropped
|
|
21132
|
+
* transport gets. A target recorded for another Server is left alone: it is not reachable
|
|
21133
|
+
* through the configured one.
|
|
21134
|
+
* @returns true when a retry loop was started.
|
|
21135
|
+
*/
|
|
21136
|
+
async restoreLastTarget() {
|
|
21137
|
+
if (this.closed || this.connected !== void 0) return false;
|
|
21138
|
+
const record7 = await this.targetStore?.load();
|
|
21139
|
+
if (record7 === void 0 || record7.mode !== "remote" || record7.hostDeviceId === void 0) return false;
|
|
21140
|
+
if (record7.serverUrl !== void 0 && record7.serverUrl !== this.config.serverUrl) {
|
|
21141
|
+
this.logger.info("recorded remote target belongs to another Server; not restoring", {
|
|
21142
|
+
targetDeviceId: shortId(record7.hostDeviceId)
|
|
21143
|
+
});
|
|
21144
|
+
return false;
|
|
21145
|
+
}
|
|
21146
|
+
this.reconnecting = {
|
|
21147
|
+
targetDeviceId: record7.hostDeviceId,
|
|
21148
|
+
...record7.hostName === void 0 ? {} : { targetName: record7.hostName },
|
|
21149
|
+
phase: "restore"
|
|
21150
|
+
};
|
|
21151
|
+
this.logger.info("restoring the remote target of the previous run", {
|
|
21152
|
+
targetDeviceId: shortId(record7.hostDeviceId)
|
|
21153
|
+
});
|
|
21154
|
+
void this.reconnectRemoteSession(record7.hostDeviceId);
|
|
21155
|
+
return true;
|
|
21156
|
+
}
|
|
20757
21157
|
async listRemoteDirectory(targetDeviceId, path, signal) {
|
|
20758
21158
|
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
20759
21159
|
if (remote.features.remoteGateway) {
|
|
@@ -20808,7 +21208,7 @@ var ClientModeRuntime = class {
|
|
|
20808
21208
|
await this.closeCodexVirtual();
|
|
20809
21209
|
this.selectRemoteTarget(remote, transport);
|
|
20810
21210
|
const workspaceId = workspaceRecordId(workspace.workspace);
|
|
20811
|
-
this.
|
|
21211
|
+
this.rememberWorkspaceSelection({ targetDeviceId: remote.target.deviceId, workspaceId });
|
|
20812
21212
|
this.logger.info("Remote workspace opened", { targetDeviceId: shortId(remote.target.deviceId) });
|
|
20813
21213
|
return { ...this.status(), workspace };
|
|
20814
21214
|
}
|
|
@@ -20842,12 +21242,12 @@ var ClientModeRuntime = class {
|
|
|
20842
21242
|
this.codexVirtual = virtual;
|
|
20843
21243
|
this.selectCodexTarget(virtual, remote);
|
|
20844
21244
|
const preferredSessionId = await virtual.preferredSessionId(signal);
|
|
20845
|
-
this.
|
|
21245
|
+
this.rememberWorkspaceSelection({
|
|
20846
21246
|
targetDeviceId: remote.target.deviceId,
|
|
20847
21247
|
workspaceId,
|
|
20848
21248
|
backend: "codex",
|
|
20849
21249
|
...preferredSessionId === void 0 ? {} : { sessionId: preferredSessionId }
|
|
20850
|
-
};
|
|
21250
|
+
});
|
|
20851
21251
|
this.logger.info("CodeX virtual workspace opened", { targetDeviceId: shortId(remote.target.deviceId) });
|
|
20852
21252
|
return { ...this.status(), workspace };
|
|
20853
21253
|
}
|
|
@@ -20872,6 +21272,7 @@ var ClientModeRuntime = class {
|
|
|
20872
21272
|
return this.openCodexWorkspace(targetDeviceId, codexProjectWorkspaceId(project.id), signal);
|
|
20873
21273
|
}
|
|
20874
21274
|
consumeWorkspaceSelection(selection) {
|
|
21275
|
+
this.lastWorkspaceSelection = { ...selection };
|
|
20875
21276
|
const pending = this.pendingWorkspaceSelection;
|
|
20876
21277
|
if (pending?.targetDeviceId === selection.targetDeviceId && pending.workspaceId === selection.workspaceId && (pending.backend ?? "harness") === (selection.backend ?? "harness")) {
|
|
20877
21278
|
this.pendingWorkspaceSelection = void 0;
|
|
@@ -20882,6 +21283,7 @@ var ClientModeRuntime = class {
|
|
|
20882
21283
|
await this.closePreview();
|
|
20883
21284
|
if (this.closed) return;
|
|
20884
21285
|
this.closed = true;
|
|
21286
|
+
this.stopLivenessWatch();
|
|
20885
21287
|
this.proxySwitch?.selectLocal();
|
|
20886
21288
|
await this.closePreview();
|
|
20887
21289
|
this.gatewaySwitch.selectLocal();
|
|
@@ -20949,10 +21351,10 @@ var ClientModeRuntime = class {
|
|
|
20949
21351
|
}
|
|
20950
21352
|
}
|
|
20951
21353
|
stream.unsubscribe = remote.client.onEvent((event) => {
|
|
20952
|
-
if (event.event === "codex.app.frame" &&
|
|
21354
|
+
if (event.event === "codex.app.frame" && isRecord9(event.data) && event.data.streamId === value.streamId) {
|
|
20953
21355
|
this.appendCodexFrame(stream, event.data);
|
|
20954
21356
|
}
|
|
20955
|
-
if (event.event === "codex.app.stream.closed" &&
|
|
21357
|
+
if (event.event === "codex.app.stream.closed" && isRecord9(event.data) && event.data.streamId === value.streamId) {
|
|
20956
21358
|
stream.closed = typeof event.data.reason === "string" ? event.data.reason : "closed";
|
|
20957
21359
|
stream.wake();
|
|
20958
21360
|
}
|
|
@@ -20979,7 +21381,7 @@ var ClientModeRuntime = class {
|
|
|
20979
21381
|
stream.wake();
|
|
20980
21382
|
};
|
|
20981
21383
|
appendCodexFrame(stream, data2) {
|
|
20982
|
-
if (!
|
|
21384
|
+
if (!isRecord9(data2) || !isRecord9(data2.frame) || typeof data2.frame.method !== "string") return;
|
|
20983
21385
|
if (stream.frames.length >= 256) {
|
|
20984
21386
|
stream.closed = "overflow";
|
|
20985
21387
|
} else {
|
|
@@ -21185,21 +21587,15 @@ var ClientModeRuntime = class {
|
|
|
21185
21587
|
connectedPreference === void 0 ? void 0 : [connectedPreference]
|
|
21186
21588
|
);
|
|
21187
21589
|
connectedClient.onClose(() => {
|
|
21188
|
-
if (this.
|
|
21189
|
-
|
|
21190
|
-
|
|
21191
|
-
|
|
21192
|
-
|
|
21193
|
-
|
|
21194
|
-
this.
|
|
21195
|
-
this.gatewaySwitch.selectLocal();
|
|
21196
|
-
this.fellBackToLocal = true;
|
|
21197
|
-
void connectedClient.close().catch(() => void 0);
|
|
21198
|
-
this.logger.warn("remote Harness transport closed; reconnecting", {
|
|
21199
|
-
targetDeviceId: shortId(target2.deviceId)
|
|
21200
|
-
});
|
|
21201
|
-
void this.reconnectRemoteSession(target2.deviceId);
|
|
21590
|
+
if (this.supersededClient === connectedClient) {
|
|
21591
|
+
this.logger.info("ignoring the close of the transport a reconnect is replacing", {
|
|
21592
|
+
targetDeviceId: shortId(target2.deviceId)
|
|
21593
|
+
});
|
|
21594
|
+
return;
|
|
21595
|
+
}
|
|
21596
|
+
this.handleRemoteTransportLost(connectedClient, target2.deviceId, "transport-closed");
|
|
21202
21597
|
});
|
|
21598
|
+
this.armLivenessWatch(LIVENESS_INTERVAL_MS);
|
|
21203
21599
|
const connectionDetails = await connectedTransport.connectionDetails().catch(() => void 0);
|
|
21204
21600
|
this.logger.info("remote Harness transport ready", {
|
|
21205
21601
|
targetDeviceId: shortId(target2.deviceId),
|
|
@@ -21270,6 +21666,7 @@ var ClientModeRuntime = class {
|
|
|
21270
21666
|
async handleControl(endpoint, payload, signal) {
|
|
21271
21667
|
try {
|
|
21272
21668
|
if (endpoint === "status") return ok2(await this.detailedStatus());
|
|
21669
|
+
if (endpoint === "client.connection.verify") return ok2(await this.verifyRemoteConnection());
|
|
21273
21670
|
if (endpoint === "devices") return ok2(await this.devices());
|
|
21274
21671
|
if (endpoint === "client.account.login") {
|
|
21275
21672
|
const value = record4(payload);
|
|
@@ -21483,7 +21880,7 @@ function record4(value) {
|
|
|
21483
21880
|
}
|
|
21484
21881
|
return value;
|
|
21485
21882
|
}
|
|
21486
|
-
function
|
|
21883
|
+
function isRecord9(value) {
|
|
21487
21884
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21488
21885
|
}
|
|
21489
21886
|
function ok2(value) {
|
|
@@ -21508,11 +21905,11 @@ async function readRemoteWorkspaceBaseline(gateway, signal) {
|
|
|
21508
21905
|
const iterator = source[Symbol.asyncIterator]();
|
|
21509
21906
|
try {
|
|
21510
21907
|
const first = await iterator.next();
|
|
21511
|
-
if (first.done || !
|
|
21908
|
+
if (first.done || !isRecord9(first.value) || first.value.type !== "baseline" || !isRecord9(first.value.value) || !Array.isArray(first.value.value.items)) {
|
|
21512
21909
|
throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned an invalid Workspace baseline.");
|
|
21513
21910
|
}
|
|
21514
21911
|
return first.value.value.items.map((item) => {
|
|
21515
|
-
if (!
|
|
21912
|
+
if (!isRecord9(item) || typeof item.workspaceId !== "string" || typeof item.path !== "string" || typeof item.title !== "string") {
|
|
21516
21913
|
throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned an invalid Workspace row.");
|
|
21517
21914
|
}
|
|
21518
21915
|
return { workspaceId: item.workspaceId, path: item.path, title: item.title };
|
|
@@ -21578,7 +21975,7 @@ async function probeRemoteHostFeatures(client, clientVersion) {
|
|
|
21578
21975
|
if (error instanceof Error && "code" in error && error.code === "METHOD_NOT_FOUND") return fallback;
|
|
21579
21976
|
throw error;
|
|
21580
21977
|
}
|
|
21581
|
-
if (!
|
|
21978
|
+
if (!isRecord9(value) || !Array.isArray(value.capabilities) || value.capabilities.some((capability) => typeof capability !== "string")) {
|
|
21582
21979
|
throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned invalid transport capabilities.");
|
|
21583
21980
|
}
|
|
21584
21981
|
const capabilities = new Set(value.capabilities);
|
|
@@ -21663,6 +22060,67 @@ function iceServersForAttempt(attempt, iceServers) {
|
|
|
21663
22060
|
return attempt === "direct" ? stunOnlyIceServers(iceServers) : iceServers;
|
|
21664
22061
|
}
|
|
21665
22062
|
|
|
22063
|
+
// src/client-target-store.ts
|
|
22064
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22065
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
22066
|
+
import { join as join5 } from "node:path";
|
|
22067
|
+
var FILE_NAME = "client-target.json";
|
|
22068
|
+
var ClientTargetStore = class {
|
|
22069
|
+
constructor(directory) {
|
|
22070
|
+
this.directory = directory;
|
|
22071
|
+
}
|
|
22072
|
+
get file() {
|
|
22073
|
+
return join5(this.directory, FILE_NAME);
|
|
22074
|
+
}
|
|
22075
|
+
/** Read the record. A missing, unreadable or malformed file reads as "no record". */
|
|
22076
|
+
async load() {
|
|
22077
|
+
let raw;
|
|
22078
|
+
try {
|
|
22079
|
+
raw = await readFile3(this.file, "utf8");
|
|
22080
|
+
} catch {
|
|
22081
|
+
return void 0;
|
|
22082
|
+
}
|
|
22083
|
+
try {
|
|
22084
|
+
const value = JSON.parse(raw);
|
|
22085
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
22086
|
+
const record7 = value;
|
|
22087
|
+
if (record7.schemaVersion !== 1) return void 0;
|
|
22088
|
+
if (record7.mode !== "local" && record7.mode !== "remote") return void 0;
|
|
22089
|
+
if (record7.mode === "remote" && (typeof record7.hostDeviceId !== "string" || record7.hostDeviceId.length === 0)) {
|
|
22090
|
+
return void 0;
|
|
22091
|
+
}
|
|
22092
|
+
return {
|
|
22093
|
+
schemaVersion: 1,
|
|
22094
|
+
mode: record7.mode,
|
|
22095
|
+
...typeof record7.serverUrl === "string" && record7.serverUrl.length > 0 ? { serverUrl: record7.serverUrl } : {},
|
|
22096
|
+
...typeof record7.hostDeviceId === "string" && record7.hostDeviceId.length > 0 ? { hostDeviceId: record7.hostDeviceId } : {},
|
|
22097
|
+
...typeof record7.hostName === "string" && record7.hostName.length > 0 ? { hostName: record7.hostName } : {},
|
|
22098
|
+
savedAt: typeof record7.savedAt === "number" ? record7.savedAt : 0
|
|
22099
|
+
};
|
|
22100
|
+
} catch {
|
|
22101
|
+
return void 0;
|
|
22102
|
+
}
|
|
22103
|
+
}
|
|
22104
|
+
/** Record the current target. Failures reach the caller so a boot can log them. */
|
|
22105
|
+
async save(target2) {
|
|
22106
|
+
const value = {
|
|
22107
|
+
schemaVersion: 1,
|
|
22108
|
+
mode: target2.mode,
|
|
22109
|
+
...target2.serverUrl === void 0 ? {} : { serverUrl: target2.serverUrl },
|
|
22110
|
+
...target2.hostDeviceId === void 0 ? {} : { hostDeviceId: target2.hostDeviceId },
|
|
22111
|
+
...target2.hostName === void 0 ? {} : { hostName: target2.hostName },
|
|
22112
|
+
savedAt: Date.now()
|
|
22113
|
+
};
|
|
22114
|
+
await mkdir2(this.directory, { recursive: true, mode: 448 });
|
|
22115
|
+
await sweepStaleTemporaries(this.directory);
|
|
22116
|
+
const temporary = this.file + "." + String(process.pid) + "." + randomUUID2() + ".tmp";
|
|
22117
|
+
await writeFile2(temporary, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
22118
|
+
await chmod2(temporary, 384);
|
|
22119
|
+
await replaceFile(temporary, this.file);
|
|
22120
|
+
await chmod2(this.file, 384);
|
|
22121
|
+
}
|
|
22122
|
+
};
|
|
22123
|
+
|
|
21666
22124
|
// src/control-runtime.ts
|
|
21667
22125
|
import { hostname as hostname2 } from "node:os";
|
|
21668
22126
|
import { existsSync as existsSync3 } from "node:fs";
|
|
@@ -21670,9 +22128,9 @@ import { execFileSync } from "node:child_process";
|
|
|
21670
22128
|
|
|
21671
22129
|
// src/identity-store.ts
|
|
21672
22130
|
import { createHash } from "node:crypto";
|
|
21673
|
-
import { chmod as
|
|
22131
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, rm as rm3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
21674
22132
|
import { homedir } from "node:os";
|
|
21675
|
-
import { dirname as dirname4, join as
|
|
22133
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
21676
22134
|
var identitySchema = external_exports.object({
|
|
21677
22135
|
schemaVersion: external_exports.literal(1),
|
|
21678
22136
|
deviceId: external_exports.string().uuid(),
|
|
@@ -21701,14 +22159,14 @@ var IdentityStore = class {
|
|
|
21701
22159
|
peers = /* @__PURE__ */ new Map();
|
|
21702
22160
|
constructor(options = {}) {
|
|
21703
22161
|
const env = options.env ?? process.env;
|
|
21704
|
-
const dshHome = env.DSH_HOME ||
|
|
21705
|
-
this.directory = options.directory ??
|
|
22162
|
+
const dshHome = env.DSH_HOME || join6(options.homeDirectory ?? homedir(), ".dsh");
|
|
22163
|
+
this.directory = options.directory ?? join6(dshHome, "remote");
|
|
21706
22164
|
}
|
|
21707
22165
|
async loadOrCreate(deviceName) {
|
|
21708
|
-
await
|
|
21709
|
-
await
|
|
21710
|
-
const devicePath =
|
|
21711
|
-
const keyPath =
|
|
22166
|
+
await mkdir3(this.directory, { recursive: true, mode: 448 });
|
|
22167
|
+
await chmod3(this.directory, 448);
|
|
22168
|
+
const devicePath = join6(this.directory, "device.json");
|
|
22169
|
+
const keyPath = join6(this.directory, "device.key");
|
|
21712
22170
|
const [hasDevice, hasKey] = await Promise.all([exists2(devicePath), exists2(keyPath)]);
|
|
21713
22171
|
if (hasDevice !== hasKey) {
|
|
21714
22172
|
throw new IdentityInvalidError("device identity is incomplete; repair it explicitly before reconnecting");
|
|
@@ -21722,8 +22180,8 @@ var IdentityStore = class {
|
|
|
21722
22180
|
}
|
|
21723
22181
|
await assertPrivateMode2(keyPath);
|
|
21724
22182
|
try {
|
|
21725
|
-
let record7 = identitySchema.parse(JSON.parse(await
|
|
21726
|
-
const privateKey = (await
|
|
22183
|
+
let record7 = identitySchema.parse(JSON.parse(await readFile4(devicePath, "utf8")));
|
|
22184
|
+
const privateKey = (await readFile4(keyPath, "utf8")).trim();
|
|
21727
22185
|
const regenerated = generateKeyPair(fromBase64Url2(privateKey));
|
|
21728
22186
|
if (regenerated.publicKey !== record7.publicKey) {
|
|
21729
22187
|
throw new IdentityInvalidError("device public and private keys do not match");
|
|
@@ -21777,11 +22235,11 @@ var IdentityStore = class {
|
|
|
21777
22235
|
return removed;
|
|
21778
22236
|
}
|
|
21779
22237
|
async loadPeers() {
|
|
21780
|
-
const path =
|
|
22238
|
+
const path = join6(this.directory, "trusted-peers.json");
|
|
21781
22239
|
if (!await exists2(path)) {
|
|
21782
22240
|
await atomicJsonWrite(path, { schemaVersion: 1, peers: [] }, 384);
|
|
21783
22241
|
}
|
|
21784
|
-
const parsed = trustedPeersSchema.parse(JSON.parse(await
|
|
22242
|
+
const parsed = trustedPeersSchema.parse(JSON.parse(await readFile4(path, "utf8")));
|
|
21785
22243
|
const peers = /* @__PURE__ */ new Map();
|
|
21786
22244
|
for (const peer of parsed.peers) {
|
|
21787
22245
|
if (peer.fingerprint !== fingerprint(peer.publicKey)) {
|
|
@@ -21793,7 +22251,7 @@ var IdentityStore = class {
|
|
|
21793
22251
|
this.peers = peers;
|
|
21794
22252
|
}
|
|
21795
22253
|
async savePeers() {
|
|
21796
|
-
await atomicJsonWrite(
|
|
22254
|
+
await atomicJsonWrite(join6(this.directory, "trusted-peers.json"), {
|
|
21797
22255
|
schemaVersion: 1,
|
|
21798
22256
|
peers: [...this.peers.values()]
|
|
21799
22257
|
}, 384);
|
|
@@ -21802,7 +22260,7 @@ var IdentityStore = class {
|
|
|
21802
22260
|
function serverStorageDirectory(root, serverUrl, role) {
|
|
21803
22261
|
const origin = new URL(serverUrl).origin;
|
|
21804
22262
|
const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
|
|
21805
|
-
return
|
|
22263
|
+
return join6(root, "servers", scope, role);
|
|
21806
22264
|
}
|
|
21807
22265
|
function fingerprint(publicKey) {
|
|
21808
22266
|
const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
|
|
@@ -21821,13 +22279,13 @@ async function atomicJsonWrite(path, value, mode) {
|
|
|
21821
22279
|
}
|
|
21822
22280
|
async function atomicTextWrite(path, value, mode) {
|
|
21823
22281
|
const directory = dirname4(path);
|
|
21824
|
-
await
|
|
22282
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
21825
22283
|
await sweepStaleTemporaries(directory);
|
|
21826
22284
|
const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
|
|
21827
|
-
await
|
|
21828
|
-
await
|
|
22285
|
+
await writeFile3(temporary, value, { encoding: "utf8", mode, flag: "wx" });
|
|
22286
|
+
await chmod3(temporary, mode);
|
|
21829
22287
|
await replaceFile(temporary, path);
|
|
21830
|
-
await
|
|
22288
|
+
await chmod3(path, mode);
|
|
21831
22289
|
}
|
|
21832
22290
|
async function exists2(path) {
|
|
21833
22291
|
try {
|
|
@@ -21989,11 +22447,11 @@ var ControlStatusStream = class {
|
|
|
21989
22447
|
};
|
|
21990
22448
|
|
|
21991
22449
|
// src/codex/domain.ts
|
|
21992
|
-
import { randomUUID as
|
|
22450
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
21993
22451
|
import { accessSync, constants, existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
21994
22452
|
import { readdir as readdir2, realpath, stat as stat4 } from "node:fs/promises";
|
|
21995
22453
|
import { homedir as homedir2 } from "node:os";
|
|
21996
|
-
import { basename as basename2, isAbsolute as isAbsolute2, join as
|
|
22454
|
+
import { basename as basename2, isAbsolute as isAbsolute2, join as join7, posix, relative, resolve } from "node:path";
|
|
21997
22455
|
|
|
21998
22456
|
// src/codex/app-server.ts
|
|
21999
22457
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -22179,7 +22637,7 @@ var CodexAppServerClient = class {
|
|
|
22179
22637
|
this.handleProcessFailure("CODEX_INVALID_RESPONSE", new Error("Codex App Server emitted invalid JSON."));
|
|
22180
22638
|
return;
|
|
22181
22639
|
}
|
|
22182
|
-
if (!
|
|
22640
|
+
if (!isRecord10(value)) {
|
|
22183
22641
|
this.handleProcessFailure("CODEX_INVALID_RESPONSE", new Error("Codex App Server emitted an invalid message."));
|
|
22184
22642
|
return;
|
|
22185
22643
|
}
|
|
@@ -22224,12 +22682,12 @@ var CodexAppServerClient = class {
|
|
|
22224
22682
|
}
|
|
22225
22683
|
};
|
|
22226
22684
|
function safeUpstreamError(value) {
|
|
22227
|
-
if (!
|
|
22685
|
+
if (!isRecord10(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
|
|
22228
22686
|
const message = value.message.toLowerCase();
|
|
22229
22687
|
if (message.includes("active writer")) return "Codex thread already has an active writer.";
|
|
22230
22688
|
return message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request.";
|
|
22231
22689
|
}
|
|
22232
|
-
function
|
|
22690
|
+
function isRecord10(value) {
|
|
22233
22691
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22234
22692
|
}
|
|
22235
22693
|
|
|
@@ -22489,15 +22947,15 @@ function decodeCanonicalBase64(value) {
|
|
|
22489
22947
|
}
|
|
22490
22948
|
return decoded;
|
|
22491
22949
|
}
|
|
22492
|
-
function
|
|
22950
|
+
function isRecord11(value) {
|
|
22493
22951
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22494
22952
|
}
|
|
22495
22953
|
function safeErrorCode2(error) {
|
|
22496
|
-
if (
|
|
22954
|
+
if (isRecord11(error) && typeof error.code === "string") return error.code;
|
|
22497
22955
|
return "UNKNOWN";
|
|
22498
22956
|
}
|
|
22499
22957
|
function safeMethod(input2) {
|
|
22500
|
-
return
|
|
22958
|
+
return isRecord11(input2) && typeof input2.method === "string" ? input2.method : "invalid";
|
|
22501
22959
|
}
|
|
22502
22960
|
function concatChunks(chunks, totalBytes) {
|
|
22503
22961
|
const output = new Uint8Array(totalBytes);
|
|
@@ -22512,6 +22970,7 @@ function concatChunks(chunks, totalBytes) {
|
|
|
22512
22970
|
// src/codex/domain.ts
|
|
22513
22971
|
var APPROVAL_TTL_MS = 5 * 6e4;
|
|
22514
22972
|
var DEFAULT_RESTART_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
22973
|
+
var CODEX_START_BUDGET_MS = 5e3;
|
|
22515
22974
|
var CODEX_PAGE_LIMIT2 = 100;
|
|
22516
22975
|
var MAX_CODEX_PAGES2 = 32;
|
|
22517
22976
|
var CODEX_HISTORY_PAGE_LIMIT = 25;
|
|
@@ -22544,7 +23003,7 @@ var CodexRemoteDomain = class {
|
|
|
22544
23003
|
if (!this.config.enabled) return;
|
|
22545
23004
|
try {
|
|
22546
23005
|
this.state = "starting";
|
|
22547
|
-
await this.
|
|
23006
|
+
await this.launchWithinBudget();
|
|
22548
23007
|
} catch (error) {
|
|
22549
23008
|
this.available = false;
|
|
22550
23009
|
this.state = "unavailable";
|
|
@@ -22775,6 +23234,32 @@ var CodexRemoteDomain = class {
|
|
|
22775
23234
|
await this.appServer?.close();
|
|
22776
23235
|
this.appServer = void 0;
|
|
22777
23236
|
}
|
|
23237
|
+
/**
|
|
23238
|
+
* Launch the App Server within {@link CODEX_START_BUDGET_MS}.
|
|
23239
|
+
*
|
|
23240
|
+
* The losing side of the race keeps running until the disposal in start()'s catch stops it, so
|
|
23241
|
+
* it needs its own handler: an unhandled rejection here would surface as a process-level error
|
|
23242
|
+
* for what is only an optional feature being unavailable.
|
|
23243
|
+
* @returns nothing once a candidate is ready.
|
|
23244
|
+
*/
|
|
23245
|
+
async launchWithinBudget() {
|
|
23246
|
+
let timer;
|
|
23247
|
+
const attempt = this.launchAppServer();
|
|
23248
|
+
attempt.catch(() => void 0);
|
|
23249
|
+
try {
|
|
23250
|
+
await Promise.race([
|
|
23251
|
+
attempt,
|
|
23252
|
+
new Promise((_resolve, reject) => {
|
|
23253
|
+
timer = setTimeout(() => {
|
|
23254
|
+
reject(new RpcError("CODEX_START_TIMEOUT", `Codex App Server did not start within ${CODEX_START_BUDGET_MS}ms.`));
|
|
23255
|
+
}, CODEX_START_BUDGET_MS);
|
|
23256
|
+
timer.unref?.();
|
|
23257
|
+
})
|
|
23258
|
+
]);
|
|
23259
|
+
} finally {
|
|
23260
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
23261
|
+
}
|
|
23262
|
+
}
|
|
22778
23263
|
async launchAppServer() {
|
|
22779
23264
|
let lastError;
|
|
22780
23265
|
for (const binary of codexBinaryCandidates(this.config.binary)) {
|
|
@@ -22927,14 +23412,14 @@ var CodexRemoteDomain = class {
|
|
|
22927
23412
|
await this.assertResultThreadAllowed(result);
|
|
22928
23413
|
if (codexPermissionPresetFromResponse(result) !== params.permissionPreset) {
|
|
22929
23414
|
await this.callUpstream("thread/settings/update", { threadId, ...settings });
|
|
22930
|
-
result = { ...
|
|
23415
|
+
result = { ...isRecord12(result) ? result : {}, ...settings, sandbox: settings.sandboxPolicy };
|
|
22931
23416
|
}
|
|
22932
23417
|
}
|
|
22933
23418
|
await this.publishPermission(threadId, result);
|
|
22934
23419
|
return result;
|
|
22935
23420
|
}
|
|
22936
23421
|
async publishPermission(threadId, value) {
|
|
22937
|
-
const source =
|
|
23422
|
+
const source = isRecord12(value) ? value : {};
|
|
22938
23423
|
const threadSettings = { approvalPolicy: source.approvalPolicy, sandboxPolicy: source.sandboxPolicy ?? source.sandbox };
|
|
22939
23424
|
await this.handleInbound({ kind: "notification", method: "thread/settings/updated", params: { threadId, threadSettings } });
|
|
22940
23425
|
}
|
|
@@ -22952,7 +23437,7 @@ var CodexRemoteDomain = class {
|
|
|
22952
23437
|
await appServer.respond(message.id, { decision: "decline" });
|
|
22953
23438
|
return;
|
|
22954
23439
|
}
|
|
22955
|
-
const requestHandle =
|
|
23440
|
+
const requestHandle = randomUUID3();
|
|
22956
23441
|
this.approvals.set(requestHandle, {
|
|
22957
23442
|
upstreamId: message.id,
|
|
22958
23443
|
connectionId: owner.connectionId,
|
|
@@ -23030,9 +23515,9 @@ var CodexRemoteDomain = class {
|
|
|
23030
23515
|
itemsView,
|
|
23031
23516
|
...cursor2 === void 0 ? {} : { cursor: cursor2 }
|
|
23032
23517
|
});
|
|
23033
|
-
const pageResult =
|
|
23518
|
+
const pageResult = isRecord12(result) ? result : {};
|
|
23034
23519
|
for (const rawTurn of array2(pageResult.data)) {
|
|
23035
|
-
if (!
|
|
23520
|
+
if (!isRecord12(rawTurn)) continue;
|
|
23036
23521
|
const turnId = typeof rawTurn.id === "string" ? rawTurn.id : void 0;
|
|
23037
23522
|
const items = rawTurn.itemsView === "full" || turnId === void 0 ? array2(rawTurn.items) : await this.readThreadItems(connectionId, threadId, turnId, array2(rawTurn.items));
|
|
23038
23523
|
turns.push({ ...rawTurn, items });
|
|
@@ -23054,9 +23539,9 @@ var CodexRemoteDomain = class {
|
|
|
23054
23539
|
sortDirection: "asc",
|
|
23055
23540
|
...cursor2 === void 0 ? {} : { cursor: cursor2 }
|
|
23056
23541
|
});
|
|
23057
|
-
const pageResult =
|
|
23542
|
+
const pageResult = isRecord12(result) ? result : {};
|
|
23058
23543
|
for (const entry of array2(pageResult.data)) {
|
|
23059
|
-
if (
|
|
23544
|
+
if (isRecord12(entry) && entry.item !== void 0) items.push(entry.item);
|
|
23060
23545
|
}
|
|
23061
23546
|
cursor2 = typeof pageResult.nextCursor === "string" && pageResult.nextCursor.length > 0 ? pageResult.nextCursor : void 0;
|
|
23062
23547
|
if (cursor2 === void 0) break;
|
|
@@ -23120,7 +23605,7 @@ var CodexRemoteDomain = class {
|
|
|
23120
23605
|
return [...this.peers.values()].some((peer) => peer.hasThreadSubscription(threadId));
|
|
23121
23606
|
}
|
|
23122
23607
|
resolveUpstreamApproval(params) {
|
|
23123
|
-
if (!
|
|
23608
|
+
if (!isRecord12(params) || typeof params.requestId !== "string" && typeof params.requestId !== "number") return;
|
|
23124
23609
|
for (const [handle, approval] of this.approvals) {
|
|
23125
23610
|
if (approval.upstreamId === params.requestId) this.approvals.delete(handle);
|
|
23126
23611
|
}
|
|
@@ -23334,15 +23819,15 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
|
|
|
23334
23819
|
if (hostPlatform !== "darwin") return [configured];
|
|
23335
23820
|
const bundledCandidates = [
|
|
23336
23821
|
"/Applications/ChatGPT.app",
|
|
23337
|
-
|
|
23822
|
+
posix.join(userHome, "Applications", "ChatGPT.app")
|
|
23338
23823
|
].flatMap((chatGptApp) => {
|
|
23339
|
-
const codexCli =
|
|
23824
|
+
const codexCli = posix.join(chatGptApp, "Contents", "Resources", "codex-cli");
|
|
23340
23825
|
try {
|
|
23341
|
-
const manifest = JSON.parse(readFileSync(
|
|
23342
|
-
if (!
|
|
23826
|
+
const manifest = JSON.parse(readFileSync(posix.join(codexCli, "codex-package.json"), "utf8"));
|
|
23827
|
+
if (!isRecord12(manifest) || typeof manifest.entrypoint !== "string" || manifest.entrypoint.length === 0) {
|
|
23343
23828
|
return [];
|
|
23344
23829
|
}
|
|
23345
|
-
const candidate =
|
|
23830
|
+
const candidate = posix.join(codexCli, manifest.entrypoint);
|
|
23346
23831
|
if (!existsSync2(candidate)) return [];
|
|
23347
23832
|
accessSync(candidate, constants.X_OK);
|
|
23348
23833
|
return [candidate];
|
|
@@ -23353,14 +23838,14 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
|
|
|
23353
23838
|
return [.../* @__PURE__ */ new Set([
|
|
23354
23839
|
...bundledCandidates,
|
|
23355
23840
|
"/Applications/ChatGPT.app/Contents/Resources/codex",
|
|
23356
|
-
|
|
23841
|
+
posix.join(userHome, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
|
|
23357
23842
|
configured
|
|
23358
23843
|
])];
|
|
23359
23844
|
}
|
|
23360
23845
|
function bundledWindowsCodex(userHome) {
|
|
23361
|
-
const bin =
|
|
23846
|
+
const bin = join7(userHome, "AppData", "Local", "OpenAI", "Codex", "bin");
|
|
23362
23847
|
try {
|
|
23363
|
-
const newest = readdirSync2(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) =>
|
|
23848
|
+
const newest = readdirSync2(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join7(bin, entry.name, "codex.exe")).filter((candidate2) => existsSync2(candidate2)).map((candidate2) => ({ candidate: candidate2, modified: statSync2(candidate2).mtimeMs })).sort((left, right) => right.modified - left.modified);
|
|
23364
23849
|
const candidate = newest[0]?.candidate;
|
|
23365
23850
|
return candidate === void 0 ? [] : [candidate];
|
|
23366
23851
|
} catch {
|
|
@@ -23368,24 +23853,24 @@ function bundledWindowsCodex(userHome) {
|
|
|
23368
23853
|
}
|
|
23369
23854
|
}
|
|
23370
23855
|
function parseCallEnvelope(input2) {
|
|
23371
|
-
if (!
|
|
23856
|
+
if (!isRecord12(input2) || typeof input2.method !== "string" || !("params" in input2) || Object.keys(input2).some((key) => key !== "method" && key !== "params")) {
|
|
23372
23857
|
throw new RpcError("INVALID_MESSAGE", "The Codex call envelope is invalid.");
|
|
23373
23858
|
}
|
|
23374
23859
|
return { method: input2.method, params: input2.params };
|
|
23375
23860
|
}
|
|
23376
23861
|
function parseRespond(input2) {
|
|
23377
|
-
if (!
|
|
23862
|
+
if (!isRecord12(input2) || typeof input2.requestHandle !== "string" || !["accept", "decline", "cancel"].includes(String(input2.decision)) || Object.keys(input2).some((key) => key !== "requestHandle" && key !== "decision")) {
|
|
23378
23863
|
throw new RpcError("INVALID_MESSAGE", "The Codex approval response is invalid.");
|
|
23379
23864
|
}
|
|
23380
23865
|
return input2;
|
|
23381
23866
|
}
|
|
23382
23867
|
function accountCanRun(result) {
|
|
23383
|
-
if (!
|
|
23384
|
-
return result.requiresOpenaiAuth === false ||
|
|
23868
|
+
if (!isRecord12(result) || typeof result.requiresOpenaiAuth !== "boolean") return false;
|
|
23869
|
+
return result.requiresOpenaiAuth === false || isRecord12(result.account);
|
|
23385
23870
|
}
|
|
23386
23871
|
function sanitizeAccount(result) {
|
|
23387
|
-
if (!
|
|
23388
|
-
const account =
|
|
23872
|
+
if (!isRecord12(result)) throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned invalid account state.");
|
|
23873
|
+
const account = isRecord12(result.account) ? result.account : void 0;
|
|
23389
23874
|
return {
|
|
23390
23875
|
authenticated: account !== void 0 || result.requiresOpenaiAuth === false,
|
|
23391
23876
|
requiresOpenaiAuth: result.requiresOpenaiAuth === true,
|
|
@@ -23398,11 +23883,11 @@ function sanitizeAccount(result) {
|
|
|
23398
23883
|
};
|
|
23399
23884
|
}
|
|
23400
23885
|
function sanitizeThreadList(result) {
|
|
23401
|
-
if (!
|
|
23886
|
+
if (!isRecord12(result) || !Array.isArray(result.data)) {
|
|
23402
23887
|
throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread list.");
|
|
23403
23888
|
}
|
|
23404
23889
|
const data2 = result.data.flatMap((value) => {
|
|
23405
|
-
if (!
|
|
23890
|
+
if (!isRecord12(value) || typeof value.id !== "string") return [];
|
|
23406
23891
|
return [{
|
|
23407
23892
|
id: value.id,
|
|
23408
23893
|
...typeof value.sessionId === "string" ? { sessionId: value.sessionId } : {},
|
|
@@ -23414,7 +23899,7 @@ function sanitizeThreadList(result) {
|
|
|
23414
23899
|
...typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {},
|
|
23415
23900
|
...typeof value.archived === "boolean" ? { archived: value.archived } : {},
|
|
23416
23901
|
...typeof value.isPinned === "boolean" ? { isPinned: value.isPinned } : {},
|
|
23417
|
-
...
|
|
23902
|
+
...isRecord12(value.status) ? { status: value.status } : {}
|
|
23418
23903
|
}];
|
|
23419
23904
|
});
|
|
23420
23905
|
return {
|
|
@@ -23424,18 +23909,18 @@ function sanitizeThreadList(result) {
|
|
|
23424
23909
|
};
|
|
23425
23910
|
}
|
|
23426
23911
|
function filterThreadListByWorkspaceAuthority(result, authority) {
|
|
23427
|
-
if (!
|
|
23912
|
+
if (!isRecord12(result) || !Array.isArray(result.data)) {
|
|
23428
23913
|
throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread list.");
|
|
23429
23914
|
}
|
|
23430
23915
|
return {
|
|
23431
23916
|
...result,
|
|
23432
|
-
data: result.data.map((record7) =>
|
|
23917
|
+
data: result.data.map((record7) => isRecord12(record7) ? record7 : void 0).filter((thread) => thread !== void 0 && isThreadAllowedByWorkspaceAuthority(thread, authority))
|
|
23433
23918
|
};
|
|
23434
23919
|
}
|
|
23435
23920
|
function sanitizeProject(value) {
|
|
23436
|
-
if (!
|
|
23921
|
+
if (!isRecord12(value) || typeof value.id !== "string" || typeof value.name !== "string") return void 0;
|
|
23437
23922
|
const roots = Array.isArray(value.roots) ? value.roots.flatMap((root) => {
|
|
23438
|
-
const path =
|
|
23923
|
+
const path = isRecord12(root) && typeof root.path === "string" && root.path.length > 0 ? root.path : void 0;
|
|
23439
23924
|
return path === void 0 || !isAbsolute2(path) ? [] : [{ path }];
|
|
23440
23925
|
}) : [];
|
|
23441
23926
|
if (roots.length === 0) return void 0;
|
|
@@ -23449,7 +23934,7 @@ function sanitizeProject(value) {
|
|
|
23449
23934
|
};
|
|
23450
23935
|
}
|
|
23451
23936
|
function sanitizeProjectList(result) {
|
|
23452
|
-
if (!
|
|
23937
|
+
if (!isRecord12(result) || !Array.isArray(result.data)) {
|
|
23453
23938
|
throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid project list.");
|
|
23454
23939
|
}
|
|
23455
23940
|
const data2 = result.data.flatMap((value) => sanitizeProject(value) ?? []);
|
|
@@ -23459,7 +23944,7 @@ function sanitizeProjectList(result) {
|
|
|
23459
23944
|
};
|
|
23460
23945
|
}
|
|
23461
23946
|
function sanitizeProjectCreate(result) {
|
|
23462
|
-
const project =
|
|
23947
|
+
const project = isRecord12(result) ? sanitizeProject(result.project) : void 0;
|
|
23463
23948
|
if (project === void 0) {
|
|
23464
23949
|
throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid created project.");
|
|
23465
23950
|
}
|
|
@@ -23492,19 +23977,19 @@ function codexDirectoryCrumbs(root, path) {
|
|
|
23492
23977
|
return crumbs2;
|
|
23493
23978
|
}
|
|
23494
23979
|
function extractThread(result) {
|
|
23495
|
-
return
|
|
23980
|
+
return isRecord12(result) && isRecord12(result.thread) ? result.thread : void 0;
|
|
23496
23981
|
}
|
|
23497
23982
|
function extractTurnId(result) {
|
|
23498
|
-
if (!
|
|
23983
|
+
if (!isRecord12(result)) return void 0;
|
|
23499
23984
|
if (typeof result.turnId === "string" && result.turnId.length > 0) return result.turnId;
|
|
23500
|
-
if (
|
|
23985
|
+
if (isRecord12(result.turn) && typeof result.turn.id === "string" && result.turn.id.length > 0) return result.turn.id;
|
|
23501
23986
|
return void 0;
|
|
23502
23987
|
}
|
|
23503
23988
|
function extractThreadId(params) {
|
|
23504
|
-
if (!
|
|
23989
|
+
if (!isRecord12(params)) return void 0;
|
|
23505
23990
|
if (typeof params.threadId === "string") return params.threadId;
|
|
23506
|
-
if (
|
|
23507
|
-
if (
|
|
23991
|
+
if (isRecord12(params.thread) && typeof params.thread.id === "string") return params.thread.id;
|
|
23992
|
+
if (isRecord12(params.turn) && typeof params.turn.threadId === "string") return params.turn.threadId;
|
|
23508
23993
|
return void 0;
|
|
23509
23994
|
}
|
|
23510
23995
|
function optionalInteger2(value) {
|
|
@@ -23546,13 +24031,13 @@ function mapCodexImageInputs(params) {
|
|
|
23546
24031
|
return {
|
|
23547
24032
|
...params,
|
|
23548
24033
|
input: params.input.map((value) => {
|
|
23549
|
-
if (!
|
|
24034
|
+
if (!isRecord12(value) || value.type !== "image" || typeof value.mediaType !== "string" || typeof value.data !== "string") return value;
|
|
23550
24035
|
return { type: "image", url: `data:${value.mediaType};base64,${value.data}` };
|
|
23551
24036
|
})
|
|
23552
24037
|
};
|
|
23553
24038
|
}
|
|
23554
24039
|
function sanitizeApprovalParams(params, requestHandle) {
|
|
23555
|
-
if (!
|
|
24040
|
+
if (!isRecord12(params)) return { requestHandle };
|
|
23556
24041
|
const safe = { ...params };
|
|
23557
24042
|
delete safe.proposedExecpolicyAmendment;
|
|
23558
24043
|
delete safe.additionalPermissions;
|
|
@@ -23583,7 +24068,7 @@ function isProjectListFallbackError(error) {
|
|
|
23583
24068
|
function canTryNextBinary(error) {
|
|
23584
24069
|
return !(error instanceof RpcError) || !["CODEX_AUTH_REQUIRED", "CODEX_CLOSED"].includes(error.code);
|
|
23585
24070
|
}
|
|
23586
|
-
function
|
|
24071
|
+
function isRecord12(value) {
|
|
23587
24072
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23588
24073
|
}
|
|
23589
24074
|
function array2(value) {
|
|
@@ -24019,11 +24504,11 @@ function editableConfig(config) {
|
|
|
24019
24504
|
...config.acp === void 0 ? {} : { acp: { enabled: config.acp.enabled, backends: config.acp.backends } }
|
|
24020
24505
|
};
|
|
24021
24506
|
}
|
|
24022
|
-
function
|
|
24507
|
+
function isRecord13(value) {
|
|
24023
24508
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24024
24509
|
}
|
|
24025
24510
|
function record5(value) {
|
|
24026
|
-
if (!
|
|
24511
|
+
if (!isRecord13(value)) throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
|
|
24027
24512
|
return value;
|
|
24028
24513
|
}
|
|
24029
24514
|
function ok3(value) {
|
|
@@ -24148,7 +24633,7 @@ var TerminalPolicy = class {
|
|
|
24148
24633
|
};
|
|
24149
24634
|
|
|
24150
24635
|
// src/service.ts
|
|
24151
|
-
import { randomUUID as
|
|
24636
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
24152
24637
|
|
|
24153
24638
|
// src/connection-controller.ts
|
|
24154
24639
|
var ConnectionController = class {
|
|
@@ -26824,11 +27309,11 @@ var HarnessRemoteBridge = class {
|
|
|
26824
27309
|
};
|
|
26825
27310
|
function normalizeWorkspaceChangesPayload(endpoint, payload, harnessVersion) {
|
|
26826
27311
|
if (endpoint !== "workspaceFiles/changes" || !requiresWorkspaceChangePath(harnessVersion)) return payload;
|
|
26827
|
-
if (!
|
|
27312
|
+
if (!isRecord14(payload) || !isRecord14(payload.args) || Object.hasOwn(payload.args, "path")) return payload;
|
|
26828
27313
|
return { ...payload, args: { ...payload.args, path: "." } };
|
|
26829
27314
|
}
|
|
26830
27315
|
function normalizeWorkspaceRequestPayload(endpoint, payload, harnessVersion) {
|
|
26831
|
-
if (!
|
|
27316
|
+
if (!isRecord14(payload) || !isRecord14(payload.args)) return payload;
|
|
26832
27317
|
const args = payload.args;
|
|
26833
27318
|
if (endpoint === "workspaceFiles/readBytes" && requiresWorkspaceChangePath(harnessVersion)) {
|
|
26834
27319
|
if (!Object.hasOwn(args, "range") || Object.hasOwn(args, "options")) return payload;
|
|
@@ -26896,13 +27381,13 @@ function needsDirectoryFallback(result) {
|
|
|
26896
27381
|
}
|
|
26897
27382
|
function requestArgs(payload) {
|
|
26898
27383
|
const root = record6(payload);
|
|
26899
|
-
const args =
|
|
27384
|
+
const args = isRecord14(root.args) ? root.args : root;
|
|
26900
27385
|
return record6(args.request ?? args._request ?? args);
|
|
26901
27386
|
}
|
|
26902
27387
|
function record6(value) {
|
|
26903
|
-
return
|
|
27388
|
+
return isRecord14(value) ? value : {};
|
|
26904
27389
|
}
|
|
26905
|
-
function
|
|
27390
|
+
function isRecord14(value) {
|
|
26906
27391
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26907
27392
|
}
|
|
26908
27393
|
|
|
@@ -27022,8 +27507,8 @@ import { execFileSync as execFileSync2 } from "node:child_process";
|
|
|
27022
27507
|
|
|
27023
27508
|
// src/codex-workspace-bridge.ts
|
|
27024
27509
|
import { spawn as spawn4 } from "node:child_process";
|
|
27025
|
-
import { realpath as realpath2, lstat, readdir as readdir4, readFile as
|
|
27026
|
-
import { isAbsolute as isAbsolute4, join as
|
|
27510
|
+
import { realpath as realpath2, lstat, readdir as readdir4, readFile as readFile5, stat as stat6, watch } from "node:fs/promises";
|
|
27511
|
+
import { isAbsolute as isAbsolute4, join as join8, relative as relative2, resolve as resolve3 } from "node:path";
|
|
27027
27512
|
var MAX_READ_BYTES = 4 * 1024 * 1024;
|
|
27028
27513
|
var MAX_INPUT_BYTES = 64 * 1024;
|
|
27029
27514
|
var MAX_COLS = 240;
|
|
@@ -27113,7 +27598,7 @@ var CodexWorkspaceBridge = class {
|
|
|
27113
27598
|
const entries = await readdir4(target2, { withFileTypes: true });
|
|
27114
27599
|
const result = [];
|
|
27115
27600
|
for (const entry of entries.slice(0, 500)) {
|
|
27116
|
-
const item =
|
|
27601
|
+
const item = join8(target2, entry.name);
|
|
27117
27602
|
const info2 = await lstat(item);
|
|
27118
27603
|
if (info2.isSymbolicLink()) continue;
|
|
27119
27604
|
result.push({ name: entry.name, type: info2.isDirectory() ? "directory" : info2.isFile() ? "file" : "other", ...info2.isFile() ? { size: info2.size } : {} });
|
|
@@ -27129,7 +27614,7 @@ var CodexWorkspaceBridge = class {
|
|
|
27129
27614
|
if (!info.isFile()) throw new RpcError("CODEX_WORKSPACE_INVALID_PATH", "The requested workspace path is not a file.");
|
|
27130
27615
|
const offset = readOffset(args);
|
|
27131
27616
|
const limit = readLimit(args);
|
|
27132
|
-
const bytes = await
|
|
27617
|
+
const bytes = await readFile5(target2);
|
|
27133
27618
|
if (bytes.byteLength > MAX_READ_BYTES) throw new RpcError("CODEX_WORKSPACE_TOO_LARGE", "The requested workspace file is too large.");
|
|
27134
27619
|
if (endpoint === "workspaceFiles/readBytes") {
|
|
27135
27620
|
const slice2 = bytes.subarray(offset, Math.min(offset + limit, bytes.length));
|
|
@@ -27253,7 +27738,7 @@ var CodexWorkspaceBridge = class {
|
|
|
27253
27738
|
throw new RpcError("METHOD_NOT_FOUND", "The requested terminal method does not exist.");
|
|
27254
27739
|
}
|
|
27255
27740
|
async createTerminal(sessionId, cwd, args) {
|
|
27256
|
-
const request =
|
|
27741
|
+
const request = isRecord15(args.request) ? args.request : {};
|
|
27257
27742
|
const id4 = stringId(request.id);
|
|
27258
27743
|
if ([...this.terminals.values()].some((item) => item.sessionId === sessionId && item.id === id4)) throw new RpcError("REQUEST_CONFLICT", "The terminal id is already active.");
|
|
27259
27744
|
if (this.terminals.size >= MAX_TERMINALS) throw new RpcError("RATE_LIMITED", "Too many remote terminals are active.", void 0, true);
|
|
@@ -27454,10 +27939,10 @@ function screenOf(terminal) {
|
|
|
27454
27939
|
return (terminal.truncated ? "\x1Bc" : "") + terminal.screen.join("");
|
|
27455
27940
|
}
|
|
27456
27941
|
function argsOf(payload) {
|
|
27457
|
-
const value =
|
|
27458
|
-
return
|
|
27942
|
+
const value = isRecord15(payload) ? payload : {};
|
|
27943
|
+
return isRecord15(value.args) ? value.args : value;
|
|
27459
27944
|
}
|
|
27460
|
-
function
|
|
27945
|
+
function isRecord15(value) {
|
|
27461
27946
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27462
27947
|
}
|
|
27463
27948
|
function stringId(value) {
|
|
@@ -27468,9 +27953,9 @@ function bounded(value, fallback, max) {
|
|
|
27468
27953
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? Math.min(value, max) : fallback;
|
|
27469
27954
|
}
|
|
27470
27955
|
function byteOrLineRange(args) {
|
|
27471
|
-
const options =
|
|
27472
|
-
if (options !== void 0 &&
|
|
27473
|
-
return
|
|
27956
|
+
const options = isRecord15(args.options) ? args.options : void 0;
|
|
27957
|
+
if (options !== void 0 && isRecord15(options.range)) return options.range;
|
|
27958
|
+
return isRecord15(args.range) ? args.range : args;
|
|
27474
27959
|
}
|
|
27475
27960
|
function readOffset(args) {
|
|
27476
27961
|
const range = byteOrLineRange(args);
|
|
@@ -27561,6 +28046,14 @@ var HostPluginRuntime = class {
|
|
|
27561
28046
|
* device identity, unlike clearing the authorization.
|
|
27562
28047
|
*/
|
|
27563
28048
|
paused;
|
|
28049
|
+
/**
|
|
28050
|
+
* Whether start() has finished wiring the Server connection.
|
|
28051
|
+
*
|
|
28052
|
+
* The Codex domain is optional business that waits on an external binary, so it runs in the
|
|
28053
|
+
* background; without this flag a Host that is merely still starting would report itself as
|
|
28054
|
+
* offline and look broken to the user and to other clients.
|
|
28055
|
+
*/
|
|
28056
|
+
starting = true;
|
|
27564
28057
|
harnessVersion;
|
|
27565
28058
|
closed = false;
|
|
27566
28059
|
codex;
|
|
@@ -27588,7 +28081,6 @@ var HostPluginRuntime = class {
|
|
|
27588
28081
|
fingerprint: this.identity.fingerprint,
|
|
27589
28082
|
server: this.config.serverUrl ?? "not configured"
|
|
27590
28083
|
});
|
|
27591
|
-
await this.codex.start();
|
|
27592
28084
|
if (this.serverApi !== void 0) {
|
|
27593
28085
|
this.harnessVersion = await this.readHarnessVersion();
|
|
27594
28086
|
this.serverApi.setHarnessVersion(this.harnessVersion);
|
|
@@ -27596,6 +28088,9 @@ var HostPluginRuntime = class {
|
|
|
27596
28088
|
this.serverConnection = this.createServerConnection(this.identity);
|
|
27597
28089
|
if (!this.paused) this.serverConnection.start();
|
|
27598
28090
|
}
|
|
28091
|
+
this.starting = false;
|
|
28092
|
+
void this.codex.start().catch(() => {
|
|
28093
|
+
});
|
|
27599
28094
|
}
|
|
27600
28095
|
currentIdentity() {
|
|
27601
28096
|
if (this.identity === void 0) throw new Error("remote runtime has not started");
|
|
@@ -27613,6 +28108,7 @@ var HostPluginRuntime = class {
|
|
|
27613
28108
|
configured: this.serverApi !== void 0,
|
|
27614
28109
|
online: this.serverConnection?.isOnline() ?? false,
|
|
27615
28110
|
reconnecting: this.serverConnection?.isReconnecting() ?? false,
|
|
28111
|
+
starting: this.starting,
|
|
27616
28112
|
...this.serverConnection?.lastActivity() === void 0 ? {} : { lastActiveAt: this.serverConnection.lastActivity() },
|
|
27617
28113
|
...error === void 0 ? {} : { error },
|
|
27618
28114
|
...authorization?.account === void 0 ? {} : { account: authorization.account },
|
|
@@ -27818,7 +28314,7 @@ var HostPluginRuntime = class {
|
|
|
27818
28314
|
let reportedVersion;
|
|
27819
28315
|
let errorCode6;
|
|
27820
28316
|
try {
|
|
27821
|
-
const response = await this.apiProxy?.host.describe({ rpcId:
|
|
28317
|
+
const response = await this.apiProxy?.host.describe({ rpcId: randomUUID4(), payload: {} });
|
|
27822
28318
|
if (response === void 0) throw new Error("ApiProxy is unavailable");
|
|
27823
28319
|
if (!response.result.ok) {
|
|
27824
28320
|
errorCode6 = response.result.error.code;
|
|
@@ -27888,7 +28384,7 @@ function isPlainRecord(value) {
|
|
|
27888
28384
|
// src/cli.ts
|
|
27889
28385
|
import { stat as stat7 } from "node:fs/promises";
|
|
27890
28386
|
import { hostname as hostname3 } from "node:os";
|
|
27891
|
-
import { join as
|
|
28387
|
+
import { join as join9 } from "node:path";
|
|
27892
28388
|
var QR_POLL_INTERVAL_MS = 2e3;
|
|
27893
28389
|
var TERMINAL_QR_MARGIN = 4;
|
|
27894
28390
|
async function runCli(args = process.argv.slice(2), dependencies = {}) {
|
|
@@ -27976,7 +28472,7 @@ async function status(args, runtime) {
|
|
|
27976
28472
|
`Server: ${serverUrl}`,
|
|
27977
28473
|
"Host control: enabled (dsh-TUI default)"
|
|
27978
28474
|
];
|
|
27979
|
-
if (!await exists3(
|
|
28475
|
+
if (!await exists3(join9(directory, "device.json"))) {
|
|
27980
28476
|
lines.push("Device: not initialized", "Authorization: logged out", "Credential: unavailable");
|
|
27981
28477
|
write(runtime.stdout, `${lines.join("\n")}
|
|
27982
28478
|
|
|
@@ -28020,7 +28516,7 @@ async function logout(args, runtime) {
|
|
|
28020
28516
|
const serverUrl = selectedServer();
|
|
28021
28517
|
const root = new IdentityStore({ env: runtime.env }).directory;
|
|
28022
28518
|
const directory = serverStorageDirectory(root, serverUrl, "host");
|
|
28023
|
-
if (!await exists3(
|
|
28519
|
+
if (!await exists3(join9(directory, "device.json"))) {
|
|
28024
28520
|
await new ServerCredentialStore(directory).clear();
|
|
28025
28521
|
write(runtime.stdout, "This Host is already logged out.\n");
|
|
28026
28522
|
return 0;
|
|
@@ -28306,7 +28802,7 @@ function remoteStatusLines(target2) {
|
|
|
28306
28802
|
const diagnostics = runtime.diagnostics();
|
|
28307
28803
|
const codex = runtime.codexStatus();
|
|
28308
28804
|
const capabilities = new Set(diagnostics.capabilities);
|
|
28309
|
-
const connection = status2.online ? "online" : status2.reconnecting ? "reconnecting" : status2.accountRequired ? "authorization required" : "offline";
|
|
28805
|
+
const connection = status2.online ? "online" : status2.starting ? "starting" : status2.reconnecting ? "reconnecting" : status2.accountRequired ? "authorization required" : "offline";
|
|
28310
28806
|
return [
|
|
28311
28807
|
`Server: ${config.serverUrl ?? "not configured"}`,
|
|
28312
28808
|
"Host control: enabled",
|
|
@@ -28537,7 +29033,7 @@ function createRemoteFileContentProvider(call, options = {}) {
|
|
|
28537
29033
|
const offset = request.offset + received;
|
|
28538
29034
|
const range = await call("fileviewer.readRange", { path: locator, offset, length }, request.signal);
|
|
28539
29035
|
if (range.offset !== offset) throw new Error("The Remote Host returned a mismatched file range.");
|
|
28540
|
-
const bytes =
|
|
29036
|
+
const bytes = decodeBase642(range.data);
|
|
28541
29037
|
if (bytes.byteLength > length) throw new Error("The Remote Host returned more file bytes than requested.");
|
|
28542
29038
|
chunks.push(bytes);
|
|
28543
29039
|
received += bytes.byteLength;
|
|
@@ -28569,7 +29065,7 @@ function currentSaveAsAllowed(value) {
|
|
|
28569
29065
|
function currentSaveAsMaxBytes(value) {
|
|
28570
29066
|
return typeof value === "function" ? value() : value ?? REMOTE_FILE_SAVE_AS_MAX_BYTES;
|
|
28571
29067
|
}
|
|
28572
|
-
function
|
|
29068
|
+
function decodeBase642(value) {
|
|
28573
29069
|
const binary = atob(value);
|
|
28574
29070
|
const bytes = new Uint8Array(binary.length);
|
|
28575
29071
|
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
@@ -28722,7 +29218,10 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
|
|
|
28722
29218
|
apiProxy,
|
|
28723
29219
|
nativeTypertGateway,
|
|
28724
29220
|
logger,
|
|
28725
|
-
hostControl
|
|
29221
|
+
hostControl,
|
|
29222
|
+
// The RTC factory keeps its default; the target store lives beside the plugin state.
|
|
29223
|
+
void 0,
|
|
29224
|
+
new ClientTargetStore(defaultIdentityDirectory)
|
|
28726
29225
|
);
|
|
28727
29226
|
}
|
|
28728
29227
|
const controlRuntime = connection === void 0 ? void 0 : new PluginControlRuntime(config, defaultIdentityDirectory, settingsBinding, clientRuntime, hostControl, deepseekSession);
|
|
@@ -28737,6 +29236,7 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
|
|
|
28737
29236
|
await runtime.start();
|
|
28738
29237
|
if (clientRuntime !== void 0) {
|
|
28739
29238
|
await clientRuntime.start();
|
|
29239
|
+
await clientRuntime.restoreLastTarget();
|
|
28740
29240
|
} else {
|
|
28741
29241
|
logger.warn("client remote mode is unavailable", {
|
|
28742
29242
|
serverConfigured: config.serverUrl !== void 0,
|