@cjhyy/code-shell-server 0.9.51 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account/cli.d.ts +23 -0
- package/dist/account/cli.js +249 -0
- package/dist/account/cloud-contract.d.ts +39 -0
- package/dist/account/cloud-contract.js +134 -0
- package/dist/account/cloud-types.d.ts +47 -0
- package/dist/account/cloud-types.js +1 -0
- package/dist/account/feature-service.d.ts +47 -0
- package/dist/account/feature-service.js +184 -0
- package/dist/account/http.d.ts +17 -0
- package/dist/account/http.js +158 -0
- package/dist/account/links.d.ts +13 -0
- package/dist/account/links.js +137 -0
- package/dist/account/manager.d.ts +96 -0
- package/dist/account/manager.js +976 -0
- package/dist/account/native-lock.d.ts +6 -0
- package/dist/account/native-lock.js +26 -0
- package/dist/account/native-store.d.ts +32 -0
- package/dist/account/native-store.js +162 -0
- package/dist/account/profile.d.ts +4 -0
- package/dist/account/profile.js +46 -0
- package/dist/account/store.d.ts +34 -0
- package/dist/account/store.js +118 -0
- package/dist/account/types.d.ts +138 -0
- package/dist/account/types.js +2 -0
- package/dist/desktop-web/http-api.d.ts +4 -0
- package/dist/desktop-web/http-api.js +12 -4
- package/dist/environment-identity.d.ts +1 -1
- package/dist/environment-identity.js +2 -1
- package/dist/headless-worker-env.d.ts +2 -0
- package/dist/headless-worker-env.js +15 -0
- package/dist/hub/approval-lease.js +6 -1
- package/dist/hub/configuration.d.ts +11 -0
- package/dist/hub/configuration.js +34 -1
- package/dist/hub/model-parameter-metadata.d.ts +26 -0
- package/dist/hub/model-parameter-metadata.js +92 -0
- package/dist/hub/run-replay.js +1 -1
- package/dist/hub/session-management.js +1 -1
- package/dist/index.account.d.ts +12 -0
- package/dist/index.account.js +12 -0
- package/dist/index.mobile-remote.d.ts +2 -0
- package/dist/index.mobile-remote.js +1 -0
- package/dist/index.worker.d.ts +1 -0
- package/dist/index.worker.js +1 -0
- package/dist/mobile-remote/account-relay-access.d.ts +59 -0
- package/dist/mobile-remote/account-relay-access.js +174 -0
- package/dist/mobile-remote/approval-clock.d.ts +3 -0
- package/dist/mobile-remote/approval-clock.js +15 -0
- package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
- package/dist/mobile-remote/codex-room-agent.js +17 -1
- package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
- package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
- package/dist/mobile-remote/mobile-upload-service.js +25 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
- package/dist/mobile-remote/remote-host-manager.js +142 -25
- package/dist/mobile-remote/resident-agent.d.ts +14 -0
- package/dist/mobile-remote/resident-agent.js +38 -11
- package/dist/mobile-remote/room-history.d.ts +7 -0
- package/dist/mobile-remote/room-history.js +56 -1
- package/dist/mobile-remote/room-manager.d.ts +216 -9
- package/dist/mobile-remote/room-manager.js +700 -65
- package/dist/mobile-remote/room-process-stop.d.ts +15 -0
- package/dist/mobile-remote/room-process-stop.js +41 -0
- package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
- package/dist/mobile-remote/trusted-device-store.js +54 -1
- package/dist/mobile-remote/turn-queue.d.ts +183 -0
- package/dist/mobile-remote/turn-queue.js +422 -0
- package/dist/mobile-remote/types.d.ts +1 -1
- package/dist/panels/agent-task-host.js +6 -12
- package/dist/panels/agent-task-service.d.ts +2 -0
- package/dist/panels/agent-task-service.js +7 -0
- package/dist/panels/browser-capture.d.ts +1 -15
- package/dist/panels/browser-capture.js +1 -35
- package/dist/panels/directory-picker.d.ts +48 -0
- package/dist/panels/directory-picker.js +247 -0
- package/dist/panels/http.d.ts +15 -15
- package/dist/panels/management-http.d.ts +9 -9
- package/dist/panels/process-service.d.ts +5 -4
- package/dist/panels/process-service.js +54 -16
- package/dist/panels/runtime.js +148 -30
- package/dist/panels/tool-jobs-storage.d.ts +4 -0
- package/dist/panels/tool-jobs-storage.js +111 -34
- package/dist/panels/types.d.ts +1 -97
- package/dist/remote-relay/connector.d.ts +8 -0
- package/dist/remote-relay/connector.js +29 -2
- package/dist/remote-relay/protocol.d.ts +21 -0
- package/dist/remote-relay/protocol.js +31 -0
- package/dist/serve/cli.js +6 -0
- package/dist/serve/headless-server.js +24 -19
- package/dist/sessions-service.d.ts +4 -0
- package/dist/sessions-service.js +14 -1
- package/package.json +10 -6
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Desktop host clients that have no IPC peer in a headless worker. */
|
|
2
|
+
const HEADLESS_WORKER_HOST_ENV_KEYS = [
|
|
3
|
+
"CODE_SHELL_REMOTE_LINK_CLIENT_SECRET",
|
|
4
|
+
"CODE_SHELL_OPERATION_BATCH_ACCESS",
|
|
5
|
+
"CODE_SHELL_MODEL_REQUEST_SIGNING",
|
|
6
|
+
"CODE_SHELL_WORKSPACE_ACCESS",
|
|
7
|
+
"CODE_SHELL_HOST_MCP_SERVERS",
|
|
8
|
+
];
|
|
9
|
+
/** Preserve the caller's environment while disabling Desktop-only host access. */
|
|
10
|
+
export function headlessWorkerEnv(base, extra = {}) {
|
|
11
|
+
const env = { ...base };
|
|
12
|
+
for (const key of HEADLESS_WORKER_HOST_ENV_KEYS)
|
|
13
|
+
env[key] = undefined;
|
|
14
|
+
return { ...env, CODE_SHELL_CREDENTIAL_ACCESS: "local", ...extra };
|
|
15
|
+
}
|
|
@@ -9,8 +9,13 @@ export class ApprovalLeases {
|
|
|
9
9
|
this.now = now;
|
|
10
10
|
}
|
|
11
11
|
add(approval) {
|
|
12
|
-
|
|
12
|
+
const existing = this.entries.get(approval.requestId);
|
|
13
|
+
if (existing) {
|
|
14
|
+
// A republished request (the user kept a question open) replaces its
|
|
15
|
+
// payload for later snapshots; the holder lease is unchanged.
|
|
16
|
+
existing.approval = approval;
|
|
13
17
|
return;
|
|
18
|
+
}
|
|
14
19
|
if (this.entries.size >= 256)
|
|
15
20
|
throw new Error("too many pending approvals");
|
|
16
21
|
this.entries.set(approval.requestId, {
|
|
@@ -31,6 +31,10 @@ export declare function createHubConfiguration(options: HubConfigurationOptions)
|
|
|
31
31
|
text?: string | undefined;
|
|
32
32
|
};
|
|
33
33
|
connections: {
|
|
34
|
+
paramValues?: {
|
|
35
|
+
[k: string]: string | number | boolean;
|
|
36
|
+
} | undefined;
|
|
37
|
+
unavailableParameters?: string[] | undefined;
|
|
34
38
|
id: string;
|
|
35
39
|
revision: string;
|
|
36
40
|
catalogId: string;
|
|
@@ -46,6 +50,13 @@ export declare function createHubConfiguration(options: HubConfigurationOptions)
|
|
|
46
50
|
adapterKind: string;
|
|
47
51
|
defaultBaseUrl: string;
|
|
48
52
|
needsKey: boolean;
|
|
53
|
+
modelPresets: {
|
|
54
|
+
parameters: import("./model-parameter-metadata.js").PublicModelParameter[];
|
|
55
|
+
supportsVision?: boolean | undefined;
|
|
56
|
+
maxContextTokens?: number | undefined;
|
|
57
|
+
value: string;
|
|
58
|
+
label: string;
|
|
59
|
+
}[];
|
|
49
60
|
}[];
|
|
50
61
|
skills: {
|
|
51
62
|
disabledReason?: string | undefined;
|
|
@@ -4,6 +4,7 @@ import { SettingsManager, invalidateSkillCache, mergePluginMcpServers, scanSkill
|
|
|
4
4
|
import { computeEffectiveDisabledLists, getMergedCatalog } from "@cjhyy/code-shell-core/internal";
|
|
5
5
|
import { withModelTraceContext } from "@cjhyy/code-shell-core/internal";
|
|
6
6
|
import { probeConfiguredModel } from "@cjhyy/code-shell-host";
|
|
7
|
+
import { publicModelPresets, publicParameterValues, validateParameterPatch, unavailableParameters, } from "./model-parameter-metadata.js";
|
|
7
8
|
const ROOT = "/api/v1/configuration";
|
|
8
9
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
9
10
|
const MAX_SKILL_BYTES = 2 * 1024 * 1024;
|
|
@@ -69,6 +70,7 @@ export function createHubConfiguration(options) {
|
|
|
69
70
|
protocol: template?.protocol,
|
|
70
71
|
adapterKind: template?.adapterKind,
|
|
71
72
|
defaultBaseUrl: template?.defaultBaseUrl,
|
|
73
|
+
modelPresets: template?.modelPresets,
|
|
72
74
|
},
|
|
73
75
|
}))
|
|
74
76
|
.digest("hex");
|
|
@@ -131,6 +133,12 @@ export function createHubConfiguration(options) {
|
|
|
131
133
|
baseUrl: displayUrl(connection.baseUrl ?? credential?.baseUrl ?? template?.defaultBaseUrl),
|
|
132
134
|
hasApiKey: Boolean(credential?.apiKey),
|
|
133
135
|
needsKey: template?.needsKey !== false,
|
|
136
|
+
...(connection.tag === "text"
|
|
137
|
+
? {
|
|
138
|
+
paramValues: publicParameterValues(template?.modelPresets?.find((preset) => preset.value === connection.model), connection.paramValues),
|
|
139
|
+
unavailableParameters: unavailableParameters(template?.modelPresets?.find((preset) => preset.value === connection.model), connection.paramValues),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
134
142
|
};
|
|
135
143
|
}),
|
|
136
144
|
catalog: catalog
|
|
@@ -141,6 +149,7 @@ export function createHubConfiguration(options) {
|
|
|
141
149
|
adapterKind: entry.adapterKind,
|
|
142
150
|
defaultBaseUrl: displayUrl(entry.defaultBaseUrl),
|
|
143
151
|
needsKey: entry.needsKey !== false,
|
|
152
|
+
modelPresets: publicModelPresets(entry),
|
|
144
153
|
})),
|
|
145
154
|
skills: allSkills.map((skill) => ({
|
|
146
155
|
name: skill.name,
|
|
@@ -200,7 +209,15 @@ export function createHubConfiguration(options) {
|
|
|
200
209
|
});
|
|
201
210
|
}
|
|
202
211
|
function updateConnection(input) {
|
|
203
|
-
knownFields(input, [
|
|
212
|
+
knownFields(input, [
|
|
213
|
+
"id",
|
|
214
|
+
"catalogId",
|
|
215
|
+
"model",
|
|
216
|
+
"baseUrl",
|
|
217
|
+
"apiKey",
|
|
218
|
+
"expectedRevision",
|
|
219
|
+
"paramValues",
|
|
220
|
+
]);
|
|
204
221
|
const id = boundedString(input.id, "id", 128);
|
|
205
222
|
const catalogId = boundedString(input.catalogId, "catalogId", 128);
|
|
206
223
|
const model = boundedString(input.model, "model", 256);
|
|
@@ -213,6 +230,21 @@ export function createHubConfiguration(options) {
|
|
|
213
230
|
const effective = settingsManager().get();
|
|
214
231
|
checkConnectionRevision(input, id, effective);
|
|
215
232
|
const existing = effective.modelConnections.find((entry) => entry.id === id);
|
|
233
|
+
if (input.paramValues !== undefined &&
|
|
234
|
+
(existing ? typeof input.expectedRevision !== "string" : input.expectedRevision !== null))
|
|
235
|
+
throw new HubConfigurationError(409, "请载入最新连接后再保存模型参数。");
|
|
236
|
+
const parameters = input.paramValues === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: validateParameterPatch(template.modelPresets?.find((preset) => preset.value === model), input.paramValues);
|
|
239
|
+
if (input.paramValues !== undefined && parameters === undefined)
|
|
240
|
+
throw new HubConfigurationError(400, "模型参数不受支持或超出允许范围,请刷新后检查。");
|
|
241
|
+
const mergedParameters = { ...existing?.paramValues };
|
|
242
|
+
for (const [key, value] of Object.entries(parameters ?? {})) {
|
|
243
|
+
if (value === null)
|
|
244
|
+
delete mergedParameters[key];
|
|
245
|
+
else
|
|
246
|
+
mergedParameters[key] = value;
|
|
247
|
+
}
|
|
216
248
|
if (existing && (existing.tag !== "text" || existing.catalogId !== catalogId)) {
|
|
217
249
|
throw new HubConfigurationError(400, "existing connection provider cannot be changed");
|
|
218
250
|
}
|
|
@@ -246,6 +278,7 @@ export function createHubConfiguration(options) {
|
|
|
246
278
|
model,
|
|
247
279
|
...(credentialId ? { credentialId } : {}),
|
|
248
280
|
...(baseUrl !== undefined ? { baseUrl: baseUrl || template.defaultBaseUrl } : {}),
|
|
281
|
+
...(parameters !== undefined ? { paramValues: mergedParameters } : {}),
|
|
249
282
|
};
|
|
250
283
|
current.modelConnections = [
|
|
251
284
|
...effective.modelConnections.filter((entry) => entry.id !== id),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { getMergedCatalog } from "@cjhyy/code-shell-core/internal";
|
|
2
|
+
type Catalog = ReturnType<typeof getMergedCatalog>[number];
|
|
3
|
+
type Preset = NonNullable<Catalog["modelPresets"]>[number];
|
|
4
|
+
export interface PublicModelParameter {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
control: "enum" | "number" | "toggle";
|
|
8
|
+
options?: string[];
|
|
9
|
+
min?: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
default?: string | number | boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function modelParameters(preset?: Preset): PublicModelParameter[];
|
|
14
|
+
export declare function publicParameterValues(preset: Preset | undefined, values: unknown): {
|
|
15
|
+
[k: string]: string | number | boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function unavailableParameters(preset: Preset | undefined, values: unknown): string[];
|
|
18
|
+
export declare function validateParameterPatch(preset: Preset | undefined, input: unknown): Record<string, string | number | boolean | null> | undefined;
|
|
19
|
+
export declare function publicModelPresets(entry: Catalog): {
|
|
20
|
+
parameters: PublicModelParameter[];
|
|
21
|
+
supportsVision?: boolean | undefined;
|
|
22
|
+
maxContextTokens?: number | undefined;
|
|
23
|
+
value: string;
|
|
24
|
+
label: string;
|
|
25
|
+
}[];
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Deliberately limited to text-generation controls; credential, endpoint,
|
|
2
|
+
* header, arbitrary text and wire-path declarations never reach the browser. */
|
|
3
|
+
const NAMES = new Set([
|
|
4
|
+
"reasoning",
|
|
5
|
+
"reasoning_effort",
|
|
6
|
+
"thinking_type",
|
|
7
|
+
"temperature",
|
|
8
|
+
"top_p",
|
|
9
|
+
"max_tokens",
|
|
10
|
+
]);
|
|
11
|
+
const FIELDS = {
|
|
12
|
+
reasoning: [
|
|
13
|
+
"reasoning",
|
|
14
|
+
"reasoning_effort",
|
|
15
|
+
"reasoning.effort",
|
|
16
|
+
"thinking.budget_tokens",
|
|
17
|
+
"thinking.type",
|
|
18
|
+
],
|
|
19
|
+
reasoning_effort: ["reasoning_effort"],
|
|
20
|
+
thinking_type: ["thinking_type", "thinking.type"],
|
|
21
|
+
temperature: ["temperature"],
|
|
22
|
+
top_p: ["top_p"],
|
|
23
|
+
max_tokens: ["max_tokens"],
|
|
24
|
+
};
|
|
25
|
+
export function modelParameters(preset) {
|
|
26
|
+
return (preset?.params ?? []).flatMap((spec) => {
|
|
27
|
+
if (!NAMES.has(spec.name) ||
|
|
28
|
+
preset.params.filter((candidate) => candidate.name === spec.name).length !== 1 ||
|
|
29
|
+
(spec.control !== "enum" && spec.control !== "number" && spec.control !== "toggle") ||
|
|
30
|
+
!FIELDS[spec.name]?.includes(spec.wire?.field ?? spec.name))
|
|
31
|
+
return [];
|
|
32
|
+
const result = {
|
|
33
|
+
name: spec.name,
|
|
34
|
+
label: spec.label ?? spec.name,
|
|
35
|
+
control: spec.control,
|
|
36
|
+
...(spec.control === "enum" ? { options: [...(spec.options ?? [])] } : {}),
|
|
37
|
+
...(spec.control === "number" ? { min: spec.min ?? 0, max: spec.max ?? 1_000_000_000 } : {}),
|
|
38
|
+
};
|
|
39
|
+
if (validParameter(result, spec.default))
|
|
40
|
+
result.default = spec.default;
|
|
41
|
+
return [result];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function validParameter(spec, value) {
|
|
45
|
+
if (spec.control === "enum")
|
|
46
|
+
return typeof value === "string" && value.length <= 256 && !!spec.options?.includes(value);
|
|
47
|
+
if (spec.control === "toggle")
|
|
48
|
+
return typeof value === "boolean";
|
|
49
|
+
if (spec.control !== "number")
|
|
50
|
+
return false;
|
|
51
|
+
return (typeof value === "number" &&
|
|
52
|
+
Number.isFinite(value) &&
|
|
53
|
+
value >= (spec.min ?? 0) &&
|
|
54
|
+
value <= (spec.max ?? 1_000_000_000));
|
|
55
|
+
}
|
|
56
|
+
export function publicParameterValues(preset, values) {
|
|
57
|
+
const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
|
58
|
+
return Object.fromEntries(modelParameters(preset).flatMap((spec) => {
|
|
59
|
+
const value = source[spec.name];
|
|
60
|
+
return validParameter(spec, value) ? [[spec.name, value]] : [];
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
export function unavailableParameters(preset, values) {
|
|
64
|
+
const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
|
65
|
+
return modelParameters(preset)
|
|
66
|
+
.filter((spec) => Object.hasOwn(source, spec.name) &&
|
|
67
|
+
!validParameter(spec, source[spec.name]))
|
|
68
|
+
.map((spec) => spec.name);
|
|
69
|
+
}
|
|
70
|
+
export function validateParameterPatch(preset, input) {
|
|
71
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
72
|
+
return undefined;
|
|
73
|
+
const specs = modelParameters(preset);
|
|
74
|
+
const patch = Object.entries(input);
|
|
75
|
+
if (patch.length > NAMES.size)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (patch.some(([key, value]) => {
|
|
78
|
+
const spec = specs.find((candidate) => candidate.name === key);
|
|
79
|
+
return !spec || (value !== null && !validParameter(spec, value));
|
|
80
|
+
}))
|
|
81
|
+
return undefined;
|
|
82
|
+
return input;
|
|
83
|
+
}
|
|
84
|
+
export function publicModelPresets(entry) {
|
|
85
|
+
return (entry.modelPresets ?? []).map((preset) => ({
|
|
86
|
+
value: preset.value,
|
|
87
|
+
label: preset.label ?? preset.value,
|
|
88
|
+
...(preset.maxContextTokens !== undefined ? { maxContextTokens: preset.maxContextTokens } : {}),
|
|
89
|
+
...(preset.supportsVision !== undefined ? { supportsVision: preset.supportsVision } : {}),
|
|
90
|
+
parameters: modelParameters(preset),
|
|
91
|
+
}));
|
|
92
|
+
}
|
package/dist/hub/run-replay.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SessionFileError, readSessionFileSync, sessionListStorage, } from "@cjh
|
|
|
2
2
|
import { listSessionEntries } from "@cjhyy/code-shell-core/internal";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { SessionManager } from "@cjhyy/code-shell-core";
|
|
5
|
-
import { replayTranscript, transcriptUserDisplay } from "@cjhyy/code-shell-web";
|
|
5
|
+
import { replayTranscript, transcriptUserDisplay } from "@cjhyy/code-shell-web/transcript-replay";
|
|
6
6
|
import { createSessionTitlesStore, SessionTitleConflictError } from "../session-titles-store.js";
|
|
7
7
|
import { hubJson } from "./uploads.js";
|
|
8
8
|
const ROOT = "/api/v1/sessions";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Optional native account authority. No Electron or renderer dependencies. */
|
|
2
|
+
export * from "./account/types.js";
|
|
3
|
+
export * from "./account/manager.js";
|
|
4
|
+
export * from "./account/http.js";
|
|
5
|
+
export * from "./account/store.js";
|
|
6
|
+
export * from "./account/links.js";
|
|
7
|
+
export * from "./account/profile.js";
|
|
8
|
+
export * from "./account/cloud-contract.js";
|
|
9
|
+
export * from "./account/cloud-types.js";
|
|
10
|
+
export * from "./account/feature-service.js";
|
|
11
|
+
export * from "./account/native-store.js";
|
|
12
|
+
export * from "./account/cli.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Optional native account authority. No Electron or renderer dependencies. */
|
|
2
|
+
export * from "./account/types.js";
|
|
3
|
+
export * from "./account/manager.js";
|
|
4
|
+
export * from "./account/http.js";
|
|
5
|
+
export * from "./account/store.js";
|
|
6
|
+
export * from "./account/links.js";
|
|
7
|
+
export * from "./account/profile.js";
|
|
8
|
+
export * from "./account/cloud-contract.js";
|
|
9
|
+
export * from "./account/cloud-types.js";
|
|
10
|
+
export * from "./account/feature-service.js";
|
|
11
|
+
export * from "./account/native-store.js";
|
|
12
|
+
export * from "./account/cli.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Mobile HTTP/WebSocket transport, rooms, uploads, pairing, and tunnels. */
|
|
2
2
|
export * from "./mobile-remote/access-passcode.js";
|
|
3
|
+
export type { AccountRelayAccessProvider, AccountRelayBinding, AccountRelayProject, } from "./mobile-remote/account-relay-access.js";
|
|
3
4
|
export * from "./mobile-remote/cloudflared-binary.js";
|
|
4
5
|
export * from "./mobile-remote/codex-parse.js";
|
|
5
6
|
export * from "./mobile-remote/codex-room-agent.js";
|
|
@@ -17,5 +18,6 @@ export * from "./mobile-remote/resident-agent.js";
|
|
|
17
18
|
export * from "./mobile-remote/room-manager.js";
|
|
18
19
|
export * from "./mobile-remote/trusted-device-store.js";
|
|
19
20
|
export * from "./mobile-remote/tunnel-manager.js";
|
|
21
|
+
export * from "./mobile-remote/turn-queue.js";
|
|
20
22
|
export * from "./mobile-remote/types.js";
|
|
21
23
|
export * from "./mobile-remote/viewer-identity.js";
|
|
@@ -17,5 +17,6 @@ export * from "./mobile-remote/resident-agent.js";
|
|
|
17
17
|
export * from "./mobile-remote/room-manager.js";
|
|
18
18
|
export * from "./mobile-remote/trusted-device-store.js";
|
|
19
19
|
export * from "./mobile-remote/tunnel-manager.js";
|
|
20
|
+
export * from "./mobile-remote/turn-queue.js";
|
|
20
21
|
export * from "./mobile-remote/types.js";
|
|
21
22
|
export * from "./mobile-remote/viewer-identity.js";
|
package/dist/index.worker.d.ts
CHANGED
package/dist/index.worker.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { TrustedDeviceStore } from "./trusted-device-store.js";
|
|
3
|
+
export interface AccountRelayBinding {
|
|
4
|
+
authorizationId: string;
|
|
5
|
+
accountId: string;
|
|
6
|
+
rootSessionId: string;
|
|
7
|
+
hostId: string;
|
|
8
|
+
environmentId: string;
|
|
9
|
+
leaseId: string;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface AccountRelayProject {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
/** Main owns device credentials and redeems only against its configured directory. */
|
|
18
|
+
export interface AccountRelayAccessProvider {
|
|
19
|
+
hostId: string;
|
|
20
|
+
environmentId: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
redeem: (handoff: string) => Promise<AccountRelayBinding>;
|
|
23
|
+
listProjects: () => Promise<AccountRelayProject[]>;
|
|
24
|
+
}
|
|
25
|
+
export interface CapturedAccountRelayAccess {
|
|
26
|
+
deviceId: string;
|
|
27
|
+
isAuthorized: () => boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Independent entrance gate; an account credential is never a Host credential. */
|
|
30
|
+
export declare class AccountRelayAccessGate {
|
|
31
|
+
private readonly devices;
|
|
32
|
+
private readonly onRevoke;
|
|
33
|
+
private provider?;
|
|
34
|
+
private readonly grants;
|
|
35
|
+
private readonly consumed;
|
|
36
|
+
private readonly pending;
|
|
37
|
+
private generation;
|
|
38
|
+
constructor(devices: TrustedDeviceStore, onRevoke: (deviceId: string) => void);
|
|
39
|
+
setProvider(provider?: AccountRelayAccessProvider): void;
|
|
40
|
+
get available(): boolean;
|
|
41
|
+
clear(): void;
|
|
42
|
+
revokeDevice(deviceId: string): boolean;
|
|
43
|
+
private retire;
|
|
44
|
+
exchange(handoff: string): Promise<{
|
|
45
|
+
hostAccessToken: string;
|
|
46
|
+
deviceId: string;
|
|
47
|
+
secretHash: string;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
hostId: string;
|
|
50
|
+
environmentId: string;
|
|
51
|
+
}>;
|
|
52
|
+
capture(request: IncomingMessage): CapturedAccountRelayAccess | undefined;
|
|
53
|
+
projects(access: CapturedAccountRelayAccess): Promise<{
|
|
54
|
+
hostId: string;
|
|
55
|
+
environmentId: string;
|
|
56
|
+
projects: AccountRelayProject[];
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
export declare function readRelayHandoff(request: IncomingMessage): Promise<string>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
/** Independent entrance gate; an account credential is never a Host credential. */
|
|
3
|
+
export class AccountRelayAccessGate {
|
|
4
|
+
devices;
|
|
5
|
+
onRevoke;
|
|
6
|
+
provider;
|
|
7
|
+
grants = new Map();
|
|
8
|
+
consumed = new Map();
|
|
9
|
+
pending = new Set();
|
|
10
|
+
generation = 0;
|
|
11
|
+
constructor(devices, onRevoke) {
|
|
12
|
+
this.devices = devices;
|
|
13
|
+
this.onRevoke = onRevoke;
|
|
14
|
+
}
|
|
15
|
+
setProvider(provider) {
|
|
16
|
+
this.clear();
|
|
17
|
+
this.provider = provider;
|
|
18
|
+
}
|
|
19
|
+
get available() {
|
|
20
|
+
return !!this.provider;
|
|
21
|
+
}
|
|
22
|
+
clear() {
|
|
23
|
+
this.generation++;
|
|
24
|
+
for (const token of this.grants.keys())
|
|
25
|
+
this.retire(token);
|
|
26
|
+
this.consumed.clear();
|
|
27
|
+
this.pending.clear();
|
|
28
|
+
}
|
|
29
|
+
revokeDevice(deviceId) {
|
|
30
|
+
let revoked = false;
|
|
31
|
+
for (const [token, grant] of this.grants)
|
|
32
|
+
if (grant.deviceId === deviceId) {
|
|
33
|
+
this.retire(token);
|
|
34
|
+
revoked = true;
|
|
35
|
+
}
|
|
36
|
+
return revoked;
|
|
37
|
+
}
|
|
38
|
+
retire(token) {
|
|
39
|
+
const grant = this.grants.get(token);
|
|
40
|
+
if (!grant)
|
|
41
|
+
return;
|
|
42
|
+
this.grants.delete(token);
|
|
43
|
+
clearTimeout(grant.timer);
|
|
44
|
+
this.devices.removeEphemeralDevice(grant.deviceId);
|
|
45
|
+
this.onRevoke(grant.deviceId);
|
|
46
|
+
}
|
|
47
|
+
async exchange(handoff) {
|
|
48
|
+
const provider = this.provider;
|
|
49
|
+
const generation = this.generation;
|
|
50
|
+
const digest = createHash("sha256").update(handoff).digest("hex");
|
|
51
|
+
for (const [key, expiresAt] of this.consumed)
|
|
52
|
+
if (expiresAt <= Date.now())
|
|
53
|
+
this.consumed.delete(key);
|
|
54
|
+
if (!provider ||
|
|
55
|
+
this.grants.size + this.pending.size >= 128 ||
|
|
56
|
+
this.pending.has(digest) ||
|
|
57
|
+
this.consumed.has(digest))
|
|
58
|
+
throw new Error("Account relay handoff is unavailable");
|
|
59
|
+
this.pending.add(digest);
|
|
60
|
+
try {
|
|
61
|
+
const binding = await provider.redeem(handoff);
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (this.provider !== provider ||
|
|
64
|
+
this.generation !== generation ||
|
|
65
|
+
!binding ||
|
|
66
|
+
typeof binding !== "object" ||
|
|
67
|
+
![
|
|
68
|
+
binding.authorizationId,
|
|
69
|
+
binding.accountId,
|
|
70
|
+
binding.rootSessionId,
|
|
71
|
+
binding.hostId,
|
|
72
|
+
binding.environmentId,
|
|
73
|
+
binding.leaseId,
|
|
74
|
+
].every((value) => typeof value === "string" &&
|
|
75
|
+
value.length > 0 &&
|
|
76
|
+
value.length <= 512 &&
|
|
77
|
+
!value.includes("\0")) ||
|
|
78
|
+
binding.hostId !== provider.hostId ||
|
|
79
|
+
binding.environmentId !== provider.environmentId ||
|
|
80
|
+
(provider.accountId !== undefined && binding.accountId !== provider.accountId) ||
|
|
81
|
+
!Number.isSafeInteger(binding.expiresAt) ||
|
|
82
|
+
binding.expiresAt <= now ||
|
|
83
|
+
binding.expiresAt > now + 10 * 60_000)
|
|
84
|
+
throw new Error("Account relay handoff target is invalid");
|
|
85
|
+
this.consumed.set(digest, binding.expiresAt);
|
|
86
|
+
const hostAccessToken = randomBytes(32).toString("base64url");
|
|
87
|
+
const secretHash = randomBytes(32).toString("base64url");
|
|
88
|
+
const device = this.devices.addEphemeralDevice({
|
|
89
|
+
name: "Account device session",
|
|
90
|
+
secretHash,
|
|
91
|
+
expiresAt: binding.expiresAt,
|
|
92
|
+
});
|
|
93
|
+
const timer = setTimeout(() => this.retire(hostAccessToken), binding.expiresAt - now);
|
|
94
|
+
timer.unref();
|
|
95
|
+
this.grants.set(hostAccessToken, {
|
|
96
|
+
provider,
|
|
97
|
+
deviceId: device.id,
|
|
98
|
+
expiresAt: binding.expiresAt,
|
|
99
|
+
timer,
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
hostAccessToken,
|
|
103
|
+
deviceId: device.id,
|
|
104
|
+
secretHash,
|
|
105
|
+
expiresAt: binding.expiresAt,
|
|
106
|
+
hostId: binding.hostId,
|
|
107
|
+
environmentId: binding.environmentId,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
// A retired generation cannot release a newer redemption of the same handoff.
|
|
112
|
+
if (this.generation === generation)
|
|
113
|
+
this.pending.delete(digest);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
capture(request) {
|
|
117
|
+
const token = request.headers["x-codeshell-host-access"];
|
|
118
|
+
if (typeof token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(token))
|
|
119
|
+
return;
|
|
120
|
+
const grant = this.grants.get(token);
|
|
121
|
+
if (!grant)
|
|
122
|
+
return;
|
|
123
|
+
const isAuthorized = () => this.provider === grant.provider &&
|
|
124
|
+
this.grants.get(token) === grant &&
|
|
125
|
+
this.devices.isEphemeralDevice(grant.deviceId) &&
|
|
126
|
+
grant.expiresAt > Date.now();
|
|
127
|
+
if (!isAuthorized()) {
|
|
128
|
+
this.retire(token);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
return { deviceId: grant.deviceId, isAuthorized };
|
|
132
|
+
}
|
|
133
|
+
async projects(access) {
|
|
134
|
+
const provider = this.provider;
|
|
135
|
+
if (!provider || !access.isAuthorized())
|
|
136
|
+
throw new Error("Account relay access expired");
|
|
137
|
+
const projects = await provider.listProjects();
|
|
138
|
+
if (this.provider !== provider || !access.isAuthorized())
|
|
139
|
+
throw new Error("Account relay access expired");
|
|
140
|
+
return { hostId: provider.hostId, environmentId: provider.environmentId, projects };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export async function readRelayHandoff(request) {
|
|
144
|
+
if (request.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json")
|
|
145
|
+
throw new Error("Invalid relay handoff");
|
|
146
|
+
const declared = Number(request.headers["content-length"] ?? 0);
|
|
147
|
+
if (!Number.isFinite(declared) || declared > 8192)
|
|
148
|
+
throw new Error("Invalid relay handoff");
|
|
149
|
+
const chunks = [];
|
|
150
|
+
let bytes = 0;
|
|
151
|
+
const timeout = setTimeout(() => request.destroy(), 10_000);
|
|
152
|
+
timeout.unref();
|
|
153
|
+
try {
|
|
154
|
+
for await (const chunk of request) {
|
|
155
|
+
bytes += Buffer.byteLength(chunk);
|
|
156
|
+
if (bytes > 8192)
|
|
157
|
+
throw new Error("Invalid relay handoff");
|
|
158
|
+
chunks.push(Buffer.from(chunk));
|
|
159
|
+
}
|
|
160
|
+
const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
161
|
+
if (!input ||
|
|
162
|
+
typeof input !== "object" ||
|
|
163
|
+
Array.isArray(input) ||
|
|
164
|
+
Object.keys(input).length !== 1 ||
|
|
165
|
+
!("handoff" in input) ||
|
|
166
|
+
typeof input.handoff !== "string" ||
|
|
167
|
+
!/^[A-Za-z0-9_-]{32,4096}$/.test(input.handoff))
|
|
168
|
+
throw new Error("Invalid relay handoff");
|
|
169
|
+
return input.handoff;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Timestamp at delivery, including reconnect replay, so a phone never compares
|
|
2
|
+
* a worker deadline directly with its own potentially skewed wall clock. */
|
|
3
|
+
export function stampApprovalClock(line, now = Date.now()) {
|
|
4
|
+
if (!line.includes("agent/approvalRequest"))
|
|
5
|
+
return line;
|
|
6
|
+
try {
|
|
7
|
+
const message = JSON.parse(line);
|
|
8
|
+
if (message?.method !== "agent/approvalRequest" || !Number.isFinite(message.params?.expiresAt))
|
|
9
|
+
return line;
|
|
10
|
+
return JSON.stringify({ ...message, serverTime: now });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return line;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -74,6 +74,8 @@ export interface CodexRoomAgentOptions {
|
|
|
74
74
|
export declare class CodexRoomAgent implements RoomAgent {
|
|
75
75
|
private readonly opts;
|
|
76
76
|
private child?;
|
|
77
|
+
/** Last turn process, kept so stopAndWait() can prove it exited. */
|
|
78
|
+
private lastChild?;
|
|
77
79
|
private threadId?;
|
|
78
80
|
private started;
|
|
79
81
|
constructor(opts: CodexRoomAgentOptions);
|
|
@@ -84,5 +86,9 @@ export declare class CodexRoomAgent implements RoomAgent {
|
|
|
84
86
|
* process is currently alive (turns are ephemeral). */
|
|
85
87
|
isRunning(): boolean;
|
|
86
88
|
stop(): void;
|
|
89
|
+
/** A turn's `codex exec` child may still be alive after turn_end was parsed. */
|
|
90
|
+
isBusy(): boolean;
|
|
91
|
+
/** Stop, then resolve true only once the last turn process has exited. */
|
|
92
|
+
stopAndWait(timeoutMs: number): Promise<boolean>;
|
|
87
93
|
}
|
|
88
94
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { releaseStdioAfterExit, stopRoomProcess } from "./room-process-stop.js";
|
|
2
|
+
import { releaseStdioAfterExit, stopRoomProcess, stopRoomProcessAndWait, } from "./room-process-stop.js";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { pathWithCommonBins } from "./path-bins.js";
|
|
5
5
|
import { parseCodexJsonLine, extractThreadId } from "./codex-parse.js";
|
|
@@ -85,6 +85,8 @@ export function sealEventOnExit(opts) {
|
|
|
85
85
|
export class CodexRoomAgent {
|
|
86
86
|
opts;
|
|
87
87
|
child;
|
|
88
|
+
/** Last turn process, kept so stopAndWait() can prove it exited. */
|
|
89
|
+
lastChild;
|
|
88
90
|
threadId;
|
|
89
91
|
started = false;
|
|
90
92
|
constructor(opts) {
|
|
@@ -109,6 +111,7 @@ export class CodexRoomAgent {
|
|
|
109
111
|
stdio: ["pipe", "pipe", "pipe"],
|
|
110
112
|
});
|
|
111
113
|
this.child = child;
|
|
114
|
+
this.lastChild = child;
|
|
112
115
|
// Settle on `close` so trailing output is read, without waiting on grandchildren.
|
|
113
116
|
releaseStdioAfterExit(child);
|
|
114
117
|
if (child.stdout) {
|
|
@@ -210,4 +213,17 @@ export class CodexRoomAgent {
|
|
|
210
213
|
if (child)
|
|
211
214
|
stopRoomProcess(child);
|
|
212
215
|
}
|
|
216
|
+
/** A turn's `codex exec` child may still be alive after turn_end was parsed. */
|
|
217
|
+
isBusy() {
|
|
218
|
+
return Boolean(this.child);
|
|
219
|
+
}
|
|
220
|
+
/** Stop, then resolve true only once the last turn process has exited. */
|
|
221
|
+
stopAndWait(timeoutMs) {
|
|
222
|
+
this.started = false;
|
|
223
|
+
const child = this.child ?? this.lastChild;
|
|
224
|
+
this.child = undefined;
|
|
225
|
+
if (!child)
|
|
226
|
+
return Promise.resolve(true);
|
|
227
|
+
return stopRoomProcessAndWait(child, timeoutMs);
|
|
228
|
+
}
|
|
213
229
|
}
|
|
@@ -14,6 +14,10 @@ export interface DispatchMobileChatTurnInput {
|
|
|
14
14
|
attachments?: MobileImageAttachment[];
|
|
15
15
|
clientMessageId?: string;
|
|
16
16
|
modelTraceSource?: string;
|
|
17
|
+
/** Host-resolved per-run model pool key; never a global model setting. */
|
|
18
|
+
model?: string;
|
|
19
|
+
configuredModel?: import("@cjhyy/code-shell-core/internal").ConfiguredTextModelIntent;
|
|
20
|
+
workspaceProfile?: string;
|
|
17
21
|
permissionMode?: PermissionMode;
|
|
18
22
|
runId: string;
|
|
19
23
|
bridge: MobileRunBridge;
|