@cjhyy/code-shell-server 0.9.8 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -23
- package/dist/bin/code-shell-project-runtime.d.ts +2 -0
- package/dist/bin/code-shell-project-runtime.js +8 -0
- package/dist/desktop-web/http-api.d.ts +43 -0
- package/dist/desktop-web/http-api.js +376 -0
- package/dist/hub/approval-lease.d.ts +30 -0
- package/dist/hub/approval-lease.js +89 -0
- package/dist/hub/auth-http.d.ts +23 -0
- package/dist/hub/auth-http.js +259 -0
- package/dist/hub/auth-store.d.ts +52 -0
- package/dist/hub/auth-store.js +316 -0
- package/dist/hub/configuration.d.ts +64 -0
- package/dist/hub/configuration.js +555 -0
- package/dist/hub/files.d.ts +9 -0
- package/dist/hub/files.js +278 -0
- package/dist/hub/mcp-configuration.d.ts +51 -0
- package/dist/hub/mcp-configuration.js +558 -0
- package/dist/hub/mcp-probe.d.ts +24 -0
- package/dist/hub/mcp-probe.js +392 -0
- package/dist/hub/model-probe.d.ts +20 -0
- package/dist/hub/model-probe.js +193 -0
- package/dist/hub/outbound-transport.d.ts +36 -0
- package/dist/hub/outbound-transport.js +122 -0
- package/dist/hub/run-replay.d.ts +42 -0
- package/dist/hub/run-replay.js +89 -0
- package/dist/hub/session-management.d.ts +36 -0
- package/dist/hub/session-management.js +405 -0
- package/dist/hub/skills-management.d.ts +43 -0
- package/dist/hub/skills-management.js +487 -0
- package/dist/hub/uploads.d.ts +22 -0
- package/dist/hub/uploads.js +241 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.desktop-web.d.ts +2 -0
- package/dist/index.desktop-web.js +2 -0
- package/dist/index.js +3 -0
- package/dist/index.links.d.ts +6 -0
- package/dist/index.links.js +6 -0
- package/dist/index.panels.d.ts +9 -0
- package/dist/index.panels.js +8 -0
- package/dist/index.serve.d.ts +1 -0
- package/dist/index.serve.js +1 -0
- package/dist/index.storage.d.ts +2 -0
- package/dist/index.storage.js +2 -0
- package/dist/links/catalog.d.ts +10 -0
- package/dist/links/catalog.js +41 -0
- package/dist/links/device-oauth.d.ts +52 -0
- package/dist/links/device-oauth.js +255 -0
- package/dist/links/http.d.ts +34 -0
- package/dist/links/http.js +144 -0
- package/dist/links/service.d.ts +54 -0
- package/dist/links/service.js +578 -0
- package/dist/links/types.d.ts +6 -0
- package/dist/links/types.js +1 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
- package/dist/mobile-remote/pending-approvals.js +2 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
- package/dist/mobile-remote/remote-host-manager.js +90 -15
- package/dist/mobile-remote/room-manager.js +17 -5
- package/dist/panels/agent-task-host.d.ts +32 -0
- package/dist/panels/agent-task-host.js +457 -0
- package/dist/panels/agent-task-models.d.ts +27 -0
- package/dist/panels/agent-task-models.js +51 -0
- package/dist/panels/agent-task-service.d.ts +105 -0
- package/dist/panels/agent-task-service.js +283 -0
- package/dist/panels/http.d.ts +34 -0
- package/dist/panels/http.js +37 -0
- package/dist/panels/hub-binding.d.ts +12 -0
- package/dist/panels/hub-binding.js +174 -0
- package/dist/panels/management-http.d.ts +29 -0
- package/dist/panels/management-http.js +119 -0
- package/dist/panels/management.d.ts +39 -0
- package/dist/panels/management.js +494 -0
- package/dist/panels/process-files.d.ts +14 -0
- package/dist/panels/process-files.js +299 -0
- package/dist/panels/process-service.d.ts +96 -0
- package/dist/panels/process-service.js +501 -0
- package/dist/panels/runtime-services.d.ts +24 -0
- package/dist/panels/runtime-services.js +462 -0
- package/dist/panels/runtime.d.ts +51 -0
- package/dist/panels/runtime.js +1063 -0
- package/dist/panels/storage-store.d.ts +9 -0
- package/dist/panels/storage-store.js +154 -0
- package/dist/panels/types.d.ts +52 -0
- package/dist/panels/types.js +1 -0
- package/dist/project-runtime/control-server.d.ts +19 -0
- package/dist/project-runtime/control-server.js +300 -0
- package/dist/project-runtime/docker-provider.d.ts +33 -0
- package/dist/project-runtime/docker-provider.js +487 -0
- package/dist/project-runtime/lock.d.ts +6 -0
- package/dist/project-runtime/lock.js +43 -0
- package/dist/project-runtime/managed-entry.d.ts +10 -0
- package/dist/project-runtime/managed-entry.js +113 -0
- package/dist/project-runtime/manager.d.ts +40 -0
- package/dist/project-runtime/manager.js +249 -0
- package/dist/project-runtime/proxy.d.ts +22 -0
- package/dist/project-runtime/proxy.js +706 -0
- package/dist/project-runtime/registry.d.ts +54 -0
- package/dist/project-runtime/registry.js +286 -0
- package/dist/project-runtime/types.d.ts +43 -0
- package/dist/project-runtime/types.js +1 -0
- package/dist/serve/cli.d.ts +5 -0
- package/dist/serve/cli.js +82 -23
- package/dist/serve/headless-server.d.ts +11 -0
- package/dist/serve/headless-server.js +737 -83
- package/dist/session-titles-store.d.ts +17 -0
- package/dist/session-titles-store.js +174 -0
- package/dist/static-files.js +1 -0
- package/dist/worker-bridge-core.js +17 -6
- package/package.json +19 -5
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { renameSync } from "node:fs";
|
|
2
|
+
import type { ProjectRuntimeRecord } from "./types.js";
|
|
3
|
+
export type ProjectStatus = "stopped" | "starting" | "running" | "stopping" | "error";
|
|
4
|
+
export interface StoredProject extends ProjectRuntimeRecord {
|
|
5
|
+
name: string;
|
|
6
|
+
status: ProjectStatus;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
updatedAt: number;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ProjectView {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
status: ProjectStatus;
|
|
15
|
+
generation: number;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class ProjectRegistryError extends Error {
|
|
21
|
+
readonly status: number;
|
|
22
|
+
constructor(status: number, message: string);
|
|
23
|
+
}
|
|
24
|
+
/** The control plane owns identifiers and credentials; callers receive explicit public views. */
|
|
25
|
+
export declare class ProjectRegistry {
|
|
26
|
+
private readonly now;
|
|
27
|
+
private readonly io;
|
|
28
|
+
private readonly directory;
|
|
29
|
+
private readonly file;
|
|
30
|
+
private readonly marker;
|
|
31
|
+
private data;
|
|
32
|
+
private readonly controllerLock;
|
|
33
|
+
private diskDigest;
|
|
34
|
+
private markerEstablished;
|
|
35
|
+
constructor(dataDir: string, now?: () => number, io?: {
|
|
36
|
+
rename?: typeof renameSync;
|
|
37
|
+
});
|
|
38
|
+
/** Release the single-controller lease on shutdown or startup failure. */
|
|
39
|
+
close(): void;
|
|
40
|
+
get installationId(): string;
|
|
41
|
+
private validate;
|
|
42
|
+
private assertStorage;
|
|
43
|
+
private write;
|
|
44
|
+
all(): StoredProject[];
|
|
45
|
+
list(username: string): ProjectView[];
|
|
46
|
+
get(username: string, id: string): StoredProject;
|
|
47
|
+
create(username: string, name: unknown): StoredProject;
|
|
48
|
+
update(id: string, patch: {
|
|
49
|
+
status: ProjectStatus;
|
|
50
|
+
generation?: number;
|
|
51
|
+
error?: string;
|
|
52
|
+
}): StoredProject;
|
|
53
|
+
}
|
|
54
|
+
export declare function projectView(project: StoredProject): ProjectView;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants, chmodSync, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { acquireProjectControllerLock } from "./lock.js";
|
|
5
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
6
|
+
const TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
|
7
|
+
const STATUSES = new Set(["stopped", "starting", "running", "stopping", "error"]);
|
|
8
|
+
const MAX_STORE_BYTES = 512 * 1024;
|
|
9
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
10
|
+
function privateFileBytes(file, maximum = MAX_STORE_BYTES) {
|
|
11
|
+
const entry = lstatSync(file);
|
|
12
|
+
if (!entry.isFile() ||
|
|
13
|
+
entry.isSymbolicLink() ||
|
|
14
|
+
entry.nlink !== 1 ||
|
|
15
|
+
entry.size > maximum ||
|
|
16
|
+
entry.mode & 0o077)
|
|
17
|
+
throw new Error("Project registry file must be a bounded private regular file.");
|
|
18
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
19
|
+
try {
|
|
20
|
+
const opened = fstatSync(fd);
|
|
21
|
+
if (!opened.isFile() ||
|
|
22
|
+
opened.ino !== entry.ino ||
|
|
23
|
+
opened.dev !== entry.dev ||
|
|
24
|
+
opened.nlink !== 1 ||
|
|
25
|
+
opened.size > maximum)
|
|
26
|
+
throw new Error("Project registry file changed while opening it.");
|
|
27
|
+
const bytes = Buffer.alloc(maximum + 1);
|
|
28
|
+
let size = 0;
|
|
29
|
+
while (size < bytes.length) {
|
|
30
|
+
const count = readSync(fd, bytes, size, bytes.length - size, size);
|
|
31
|
+
if (!count)
|
|
32
|
+
break;
|
|
33
|
+
size += count;
|
|
34
|
+
}
|
|
35
|
+
if (size > maximum)
|
|
36
|
+
throw new Error("Project registry file is too large.");
|
|
37
|
+
return bytes.subarray(0, size);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class ProjectRegistryError extends Error {
|
|
44
|
+
status;
|
|
45
|
+
constructor(status, message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.status = status;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The control plane owns identifiers and credentials; callers receive explicit public views. */
|
|
51
|
+
export class ProjectRegistry {
|
|
52
|
+
now;
|
|
53
|
+
io;
|
|
54
|
+
directory;
|
|
55
|
+
file;
|
|
56
|
+
marker;
|
|
57
|
+
data;
|
|
58
|
+
controllerLock;
|
|
59
|
+
diskDigest;
|
|
60
|
+
markerEstablished = false;
|
|
61
|
+
constructor(dataDir, now = Date.now, io = {}) {
|
|
62
|
+
this.now = now;
|
|
63
|
+
this.io = io;
|
|
64
|
+
this.directory = resolve(dataDir, "project-control");
|
|
65
|
+
const existed = existsSync(this.directory);
|
|
66
|
+
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
67
|
+
const directory = lstatSync(this.directory);
|
|
68
|
+
if (!directory.isDirectory() || directory.isSymbolicLink())
|
|
69
|
+
throw new Error("Project registry must be a private directory");
|
|
70
|
+
chmodSync(this.directory, 0o700);
|
|
71
|
+
this.file = join(this.directory, "registry.json");
|
|
72
|
+
this.marker = join(this.directory, "initialized");
|
|
73
|
+
this.controllerLock = acquireProjectControllerLock(this.directory);
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(this.marker)) {
|
|
76
|
+
if (privateFileBytes(this.marker, 16).toString() !== "1\n")
|
|
77
|
+
throw new Error("Project initialization marker is corrupt.");
|
|
78
|
+
this.markerEstablished = true;
|
|
79
|
+
}
|
|
80
|
+
if (existsSync(this.file)) {
|
|
81
|
+
const bytes = privateFileBytes(this.file);
|
|
82
|
+
this.data = JSON.parse(bytes.toString("utf8"));
|
|
83
|
+
this.validate();
|
|
84
|
+
this.diskDigest = digest(bytes);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (existsSync(this.marker) || existed)
|
|
88
|
+
throw new Error("Project registry is missing; restore the existing registry before starting");
|
|
89
|
+
this.data = { version: 1, installationId: randomUUID(), owners: [], projects: [] };
|
|
90
|
+
this.write();
|
|
91
|
+
}
|
|
92
|
+
if (!this.markerEstablished) {
|
|
93
|
+
writeFileSync(this.marker, "1\n", { mode: 0o600, flag: "wx" });
|
|
94
|
+
this.markerEstablished = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
this.controllerLock.release();
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Release the single-controller lease on shutdown or startup failure. */
|
|
103
|
+
close() {
|
|
104
|
+
this.controllerLock.release();
|
|
105
|
+
}
|
|
106
|
+
get installationId() {
|
|
107
|
+
this.assertStorage();
|
|
108
|
+
return this.data.installationId;
|
|
109
|
+
}
|
|
110
|
+
validate(data = this.data) {
|
|
111
|
+
if (!data ||
|
|
112
|
+
data.version !== 1 ||
|
|
113
|
+
!UUID.test(data.installationId) ||
|
|
114
|
+
!Array.isArray(data.owners) ||
|
|
115
|
+
!Array.isArray(data.projects) ||
|
|
116
|
+
data.owners.length > 128 ||
|
|
117
|
+
data.projects.length > 128)
|
|
118
|
+
throw new Error("Project registry is corrupt; restore its backup");
|
|
119
|
+
const owners = new Set();
|
|
120
|
+
const names = new Set();
|
|
121
|
+
for (const owner of data.owners) {
|
|
122
|
+
if (!owner ||
|
|
123
|
+
!UUID.test(owner.id) ||
|
|
124
|
+
typeof owner.username !== "string" ||
|
|
125
|
+
!owner.username.trim() ||
|
|
126
|
+
owner.username.length > 64 ||
|
|
127
|
+
owner.username.trim() !== owner.username ||
|
|
128
|
+
/[\x00-\x1f\x7f]/.test(owner.username) ||
|
|
129
|
+
owners.has(owner.id) ||
|
|
130
|
+
names.has(owner.username))
|
|
131
|
+
throw new Error("Project owner registry is corrupt");
|
|
132
|
+
owners.add(owner.id);
|
|
133
|
+
names.add(owner.username);
|
|
134
|
+
}
|
|
135
|
+
const ids = new Set();
|
|
136
|
+
for (const project of data.projects) {
|
|
137
|
+
if (!project ||
|
|
138
|
+
!UUID.test(project.id) ||
|
|
139
|
+
ids.has(project.id) ||
|
|
140
|
+
!owners.has(project.ownerId) ||
|
|
141
|
+
typeof project.name !== "string" ||
|
|
142
|
+
!project.name.trim() ||
|
|
143
|
+
project.name.length > 80 ||
|
|
144
|
+
project.name.trim() !== project.name ||
|
|
145
|
+
/[\x00-\x1f\x7f]/.test(project.name) ||
|
|
146
|
+
!STATUSES.has(project.status) ||
|
|
147
|
+
!Number.isSafeInteger(project.generation) ||
|
|
148
|
+
project.generation < 0 ||
|
|
149
|
+
project.runtimeUsername !== "project-runtime" ||
|
|
150
|
+
!TOKEN.test(project.runtimePassword) ||
|
|
151
|
+
!Number.isSafeInteger(project.createdAt) ||
|
|
152
|
+
project.createdAt < 0 ||
|
|
153
|
+
!Number.isSafeInteger(project.updatedAt) ||
|
|
154
|
+
project.updatedAt < project.createdAt ||
|
|
155
|
+
(project.error !== undefined &&
|
|
156
|
+
(typeof project.error !== "string" || project.error.length > 1000)))
|
|
157
|
+
throw new Error("Project registry entry is corrupt");
|
|
158
|
+
ids.add(project.id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
assertStorage() {
|
|
162
|
+
this.controllerLock.assertHeld();
|
|
163
|
+
if (this.markerEstablished && privateFileBytes(this.marker, 16).toString() !== "1\n")
|
|
164
|
+
throw new Error("Project initialization marker changed.");
|
|
165
|
+
if (this.diskDigest !== undefined) {
|
|
166
|
+
if (digest(privateFileBytes(this.file)) !== this.diskDigest)
|
|
167
|
+
throw new Error("Project registry changed while the controller was running.");
|
|
168
|
+
}
|
|
169
|
+
else if (existsSync(this.file))
|
|
170
|
+
throw new Error("Unexpected existing project registry.");
|
|
171
|
+
}
|
|
172
|
+
write(next = this.data) {
|
|
173
|
+
this.validate(next);
|
|
174
|
+
this.assertStorage();
|
|
175
|
+
const bytes = Buffer.from(JSON.stringify(next, null, 2) + "\n");
|
|
176
|
+
if (bytes.length > MAX_STORE_BYTES)
|
|
177
|
+
throw new Error("Project registry is too large.");
|
|
178
|
+
const file = join(this.directory, `.registry-${randomUUID()}.tmp`);
|
|
179
|
+
let fd;
|
|
180
|
+
try {
|
|
181
|
+
fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
182
|
+
writeFileSync(fd, bytes);
|
|
183
|
+
fsyncSync(fd);
|
|
184
|
+
closeSync(fd);
|
|
185
|
+
fd = undefined;
|
|
186
|
+
this.assertStorage();
|
|
187
|
+
(this.io.rename ?? renameSync)(file, this.file);
|
|
188
|
+
this.diskDigest = digest(bytes);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (fd !== undefined)
|
|
192
|
+
closeSync(fd);
|
|
193
|
+
rmSync(file, { force: true });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
all() {
|
|
197
|
+
this.assertStorage();
|
|
198
|
+
return this.data.projects.map((item) => ({ ...item }));
|
|
199
|
+
}
|
|
200
|
+
list(username) {
|
|
201
|
+
this.assertStorage();
|
|
202
|
+
const owner = this.data.owners.find((item) => item.username === username);
|
|
203
|
+
return this.data.projects.filter((item) => item.ownerId === owner?.id).map(projectView);
|
|
204
|
+
}
|
|
205
|
+
get(username, id) {
|
|
206
|
+
this.assertStorage();
|
|
207
|
+
const owner = this.data.owners.find((item) => item.username === username);
|
|
208
|
+
const project = this.data.projects.find((item) => item.id === id && item.ownerId === owner?.id);
|
|
209
|
+
if (!project)
|
|
210
|
+
throw new ProjectRegistryError(404, "找不到这个项目。");
|
|
211
|
+
return { ...project };
|
|
212
|
+
}
|
|
213
|
+
create(username, name) {
|
|
214
|
+
this.assertStorage();
|
|
215
|
+
if (typeof name !== "string" ||
|
|
216
|
+
!name.trim() ||
|
|
217
|
+
name.trim().length > 80 ||
|
|
218
|
+
/[\x00-\x1f\x7f]/.test(name))
|
|
219
|
+
throw new ProjectRegistryError(400, "请输入 1 至 80 个字符的项目名称。");
|
|
220
|
+
if (typeof username !== "string" ||
|
|
221
|
+
!username ||
|
|
222
|
+
username.trim() !== username ||
|
|
223
|
+
username.length > 64 ||
|
|
224
|
+
/[\x00-\x1f\x7f]/.test(username))
|
|
225
|
+
throw new ProjectRegistryError(401, "请先登录。");
|
|
226
|
+
if (this.data.projects.length >= 32)
|
|
227
|
+
throw new ProjectRegistryError(429, "项目数量已达到上限(32 个)。");
|
|
228
|
+
const next = structuredClone(this.data);
|
|
229
|
+
let owner = next.owners.find((item) => item.username === username);
|
|
230
|
+
if (!owner) {
|
|
231
|
+
owner = { id: randomUUID(), username };
|
|
232
|
+
next.owners.push(owner);
|
|
233
|
+
}
|
|
234
|
+
const timestamp = this.now();
|
|
235
|
+
const project = {
|
|
236
|
+
id: randomUUID(),
|
|
237
|
+
ownerId: owner.id,
|
|
238
|
+
name: name.trim(),
|
|
239
|
+
status: "stopped",
|
|
240
|
+
generation: 0,
|
|
241
|
+
runtimeUsername: "project-runtime",
|
|
242
|
+
runtimePassword: randomBytes(32).toString("base64url"),
|
|
243
|
+
createdAt: timestamp,
|
|
244
|
+
updatedAt: timestamp,
|
|
245
|
+
};
|
|
246
|
+
next.projects.push(project);
|
|
247
|
+
this.write(next);
|
|
248
|
+
this.data = next;
|
|
249
|
+
return { ...project };
|
|
250
|
+
}
|
|
251
|
+
update(id, patch) {
|
|
252
|
+
this.assertStorage();
|
|
253
|
+
const index = this.data.projects.findIndex((item) => item.id === id);
|
|
254
|
+
if (index === -1)
|
|
255
|
+
throw new ProjectRegistryError(404, "找不到这个项目。");
|
|
256
|
+
const previous = this.data.projects[index];
|
|
257
|
+
const next = {
|
|
258
|
+
...previous,
|
|
259
|
+
status: patch.status,
|
|
260
|
+
updatedAt: Math.max(previous.updatedAt, this.now()),
|
|
261
|
+
...(patch.generation !== undefined ? { generation: patch.generation } : {}),
|
|
262
|
+
...(patch.error !== undefined ? { error: patch.error } : {}),
|
|
263
|
+
};
|
|
264
|
+
if (patch.error === undefined)
|
|
265
|
+
delete next.error;
|
|
266
|
+
if (next.generation < previous.generation || !Number.isSafeInteger(next.generation))
|
|
267
|
+
throw new Error("Project generation cannot move backwards");
|
|
268
|
+
const candidate = structuredClone(this.data);
|
|
269
|
+
candidate.projects[index] = next;
|
|
270
|
+
this.write(candidate);
|
|
271
|
+
this.data = candidate;
|
|
272
|
+
return { ...next };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export function projectView(project) {
|
|
276
|
+
const error = project.error?.split(project.runtimePassword).join("[redacted]");
|
|
277
|
+
return {
|
|
278
|
+
id: project.id,
|
|
279
|
+
name: project.name,
|
|
280
|
+
status: project.status,
|
|
281
|
+
generation: project.generation,
|
|
282
|
+
createdAt: project.createdAt,
|
|
283
|
+
updatedAt: project.updatedAt,
|
|
284
|
+
...(error ? { error } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Stored only by the trusted control plane; credentials must never reach browser DTOs. */
|
|
2
|
+
export interface ProjectRuntimeRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
ownerId: string;
|
|
5
|
+
generation: number;
|
|
6
|
+
runtimeUsername: string;
|
|
7
|
+
runtimePassword: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectRuntimeConnection {
|
|
10
|
+
/** Always an HTTP URL bound to the Docker host's loopback interface. */
|
|
11
|
+
url: string;
|
|
12
|
+
username: string;
|
|
13
|
+
password: string;
|
|
14
|
+
generation: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ProjectRuntimeAvailability {
|
|
17
|
+
available: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProjectRuntimeStatus {
|
|
21
|
+
state: "running" | "stopped" | "missing";
|
|
22
|
+
url?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ProjectRuntimeProvider {
|
|
25
|
+
availability(): Promise<ProjectRuntimeAvailability>;
|
|
26
|
+
ensure(project: ProjectRuntimeRecord, options: {
|
|
27
|
+
publicOrigin: string;
|
|
28
|
+
}): Promise<ProjectRuntimeConnection>;
|
|
29
|
+
stop(project: ProjectRuntimeRecord): Promise<void>;
|
|
30
|
+
status(project: ProjectRuntimeRecord): Promise<ProjectRuntimeStatus>;
|
|
31
|
+
/** Cancels control commands; never removes containers or persistent project volumes. */
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface ManagedProjectSecret {
|
|
35
|
+
version: 1;
|
|
36
|
+
projectId: string;
|
|
37
|
+
ownerId: string;
|
|
38
|
+
generation: number;
|
|
39
|
+
username: string;
|
|
40
|
+
password: string;
|
|
41
|
+
publicOrigin: string;
|
|
42
|
+
publicPathPrefix: string;
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/serve/cli.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ interface CliArgs {
|
|
|
3
3
|
host: string;
|
|
4
4
|
port: number;
|
|
5
5
|
passcode?: string;
|
|
6
|
+
authMode: "hub" | "passcode";
|
|
7
|
+
runtime: "local" | "docker";
|
|
8
|
+
runtimeImage?: string;
|
|
9
|
+
debugLogs?: boolean;
|
|
10
|
+
publicOrigin?: string;
|
|
6
11
|
dataDir: string;
|
|
7
12
|
staticRootDir?: string;
|
|
8
13
|
}
|
package/dist/serve/cli.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
// packages/server/src/serve/cli.ts
|
|
2
2
|
//
|
|
3
|
-
// `code-shell-serve`
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// [--passcode <code>] [--data-dir <dir>]
|
|
7
|
-
//
|
|
8
|
-
// Access control is passcode + remember-cookie only (决策见 TODO 约束边界
|
|
9
|
-
// 「服务端部署不做账号体系」). Default bind is loopback; binding 0.0.0.0 is a
|
|
10
|
-
// deliberate operator choice.
|
|
3
|
+
// `code-shell-serve` boots a single-workspace Hub under Node.js.
|
|
4
|
+
// New deployments use one administrator and revocable device sessions.
|
|
5
|
+
// --auth passcode preserves the original passcode-only host.
|
|
11
6
|
import { createRequire } from "node:module";
|
|
12
7
|
import { homedir } from "node:os";
|
|
13
8
|
import { join, resolve } from "node:path";
|
|
14
9
|
import { existsSync } from "node:fs";
|
|
15
10
|
import { startHeadlessServer } from "./headless-server.js";
|
|
11
|
+
import { startProjectControlServer } from "../project-runtime/control-server.js";
|
|
16
12
|
const SERVE_HELP = `Usage: code-shell-serve [options]
|
|
17
13
|
|
|
18
14
|
Run the CodeShell headless Web host for a workspace.
|
|
@@ -21,15 +17,24 @@ Options:
|
|
|
21
17
|
--cwd <path> Workspace root (default: current directory)
|
|
22
18
|
--host <host> Bind host (default: 127.0.0.1)
|
|
23
19
|
--port <port> Bind port, 0 selects a free port (default: 8790)
|
|
24
|
-
--
|
|
25
|
-
--
|
|
20
|
+
--auth <mode> hub (default) or legacy passcode
|
|
21
|
+
--runtime <mode> local (default) or Docker project sandboxes
|
|
22
|
+
--runtime-image <tag> Prebuilt project image (default: codeshell-project-runtime:local)
|
|
23
|
+
--public-url <url> External HTTPS origin for reverse-proxy deployments
|
|
24
|
+
--passcode <code> Set or rotate the legacy passcode (selects passcode mode)
|
|
25
|
+
--data-dir <path> Persistent server data (auth, uploads and worker sessions)
|
|
26
26
|
--static-root <path> Override the built Web app directory
|
|
27
|
+
--debug-logs Include raw worker diagnostics (may contain task content)
|
|
27
28
|
-h, --help Show this help and exit`;
|
|
28
29
|
const VALUE_FLAGS = new Set([
|
|
29
30
|
"--cwd",
|
|
30
31
|
"--host",
|
|
31
32
|
"--port",
|
|
32
33
|
"--passcode",
|
|
34
|
+
"--auth",
|
|
35
|
+
"--runtime",
|
|
36
|
+
"--runtime-image",
|
|
37
|
+
"--public-url",
|
|
33
38
|
"--data-dir",
|
|
34
39
|
"--static-root",
|
|
35
40
|
]);
|
|
@@ -37,6 +42,10 @@ export function parseServeArgs(argv, env = process.env) {
|
|
|
37
42
|
const args = {};
|
|
38
43
|
for (let i = 0; i < argv.length; i++) {
|
|
39
44
|
const key = argv[i];
|
|
45
|
+
if (key === "--debug-logs") {
|
|
46
|
+
args["debug-logs"] = "true";
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
40
49
|
if (!key || !VALUE_FLAGS.has(key)) {
|
|
41
50
|
throw new Error(`unknown argument: ${key ?? ""}\n\n${SERVE_HELP}`);
|
|
42
51
|
}
|
|
@@ -52,7 +61,39 @@ export function parseServeArgs(argv, env = process.env) {
|
|
|
52
61
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
53
62
|
throw new Error(`invalid --port: ${args.port}`);
|
|
54
63
|
}
|
|
64
|
+
const authMode = args.auth ?? (args.passcode ? "passcode" : "hub");
|
|
65
|
+
if (authMode !== "hub" && authMode !== "passcode")
|
|
66
|
+
throw new Error("--auth must be hub or passcode");
|
|
67
|
+
if (authMode === "hub" && args.passcode)
|
|
68
|
+
throw new Error("--passcode cannot be combined with --auth hub");
|
|
69
|
+
const runtime = args.runtime ?? "local";
|
|
70
|
+
if (runtime !== "local" && runtime !== "docker")
|
|
71
|
+
throw new Error("--runtime must be local or docker");
|
|
72
|
+
if (runtime === "docker" && authMode !== "hub")
|
|
73
|
+
throw new Error("Docker projects require --auth hub");
|
|
74
|
+
if (args["runtime-image"] && runtime !== "docker")
|
|
75
|
+
throw new Error("--runtime-image requires --runtime docker");
|
|
76
|
+
let publicOrigin;
|
|
77
|
+
const publicUrl = args["public-url"] ?? env.CODE_SHELL_SERVE_PUBLIC_URL;
|
|
78
|
+
if (publicUrl) {
|
|
79
|
+
const url = new URL(publicUrl);
|
|
80
|
+
if (url.username ||
|
|
81
|
+
url.password ||
|
|
82
|
+
url.search ||
|
|
83
|
+
url.hash ||
|
|
84
|
+
url.pathname !== "/" ||
|
|
85
|
+
(url.protocol !== "https:" &&
|
|
86
|
+
!(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))) {
|
|
87
|
+
throw new Error("--public-url must be an HTTPS origin (HTTP allowed only on loopback)");
|
|
88
|
+
}
|
|
89
|
+
publicOrigin = url.origin;
|
|
90
|
+
}
|
|
55
91
|
return {
|
|
92
|
+
authMode,
|
|
93
|
+
runtime,
|
|
94
|
+
...(args["runtime-image"] ? { runtimeImage: args["runtime-image"] } : {}),
|
|
95
|
+
...(args["debug-logs"] ? { debugLogs: true } : {}),
|
|
96
|
+
...(publicOrigin ? { publicOrigin } : {}),
|
|
56
97
|
cwd: resolve(args.cwd ?? process.cwd()),
|
|
57
98
|
host: args.host ?? "127.0.0.1",
|
|
58
99
|
port,
|
|
@@ -96,22 +137,40 @@ export async function runServeCli(argv = process.argv.slice(2)) {
|
|
|
96
137
|
}
|
|
97
138
|
const parsed = parseServeArgs(argv);
|
|
98
139
|
const staticRootDir = parsed.staticRootDir ?? resolveWebAppRoot();
|
|
99
|
-
const server =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
const server = parsed.runtime === "docker"
|
|
141
|
+
? await startProjectControlServer({
|
|
142
|
+
host: parsed.host,
|
|
143
|
+
port: parsed.port,
|
|
144
|
+
dataDir: parsed.dataDir,
|
|
145
|
+
publicOrigin: parsed.publicOrigin,
|
|
146
|
+
staticRootDir,
|
|
147
|
+
runtimeImage: parsed.runtimeImage,
|
|
148
|
+
})
|
|
149
|
+
: await startHeadlessServer({
|
|
150
|
+
host: parsed.host,
|
|
151
|
+
port: parsed.port,
|
|
152
|
+
cwd: parsed.cwd,
|
|
153
|
+
dataDir: parsed.dataDir,
|
|
154
|
+
authMode: parsed.authMode,
|
|
155
|
+
debugLogs: parsed.debugLogs,
|
|
156
|
+
...(parsed.publicOrigin ? { publicOrigin: parsed.publicOrigin } : {}),
|
|
157
|
+
workerEntryPath: resolveWorkerEntry(),
|
|
158
|
+
workerCapabilityModules: resolveWorkerCapabilityModules(),
|
|
159
|
+
...(staticRootDir ? { staticRootDir } : {}),
|
|
160
|
+
...(parsed.passcode ? { passcode: parsed.passcode } : {}),
|
|
161
|
+
log: (event, data) => console.error(`[serve] ${event}${data ? ` ${JSON.stringify(data)}` : ""}`),
|
|
162
|
+
});
|
|
163
|
+
console.log(`CodeShell ${parsed.authMode === "hub" ? "Hub" : "web host"} listening at ${parsed.publicOrigin ?? server.url}`);
|
|
164
|
+
console.log(parsed.runtime === "docker"
|
|
165
|
+
? "Project sandboxes: Docker (project files persist in separate volumes)"
|
|
166
|
+
: `Workspace: ${parsed.cwd}`);
|
|
112
167
|
if (!staticRootDir) {
|
|
113
168
|
console.log("No web app build found — WS endpoint only (/ws). Build packages/web first.");
|
|
114
169
|
}
|
|
170
|
+
if (server.bootstrapToken) {
|
|
171
|
+
const origin = parsed.publicOrigin ?? server.url.replace(/\/\/(0\.0\.0\.0|\[?::\]?)(?=:)/, "//127.0.0.1");
|
|
172
|
+
console.log(`Administrator setup (one-time link, save it now): ${origin}/#setup=${server.bootstrapToken}`);
|
|
173
|
+
}
|
|
115
174
|
if (server.generatedPasscode) {
|
|
116
175
|
console.log(`Access passcode (generated, save it now): ${server.generatedPasscode}`);
|
|
117
176
|
}
|
|
@@ -30,6 +30,16 @@ export interface HeadlessServeOptions {
|
|
|
30
30
|
* `generatedPasscode` — the CLI prints it once.
|
|
31
31
|
*/
|
|
32
32
|
passcode?: string;
|
|
33
|
+
/** CLI defaults to hub; omitted by existing API hosts keeps passcode compatibility. */
|
|
34
|
+
authMode?: "hub" | "passcode";
|
|
35
|
+
/** Explicit diagnostics opt-in; may include task content and worker stderr. */
|
|
36
|
+
debugLogs?: boolean;
|
|
37
|
+
/** Exact external origin; required for HTTPS reverse proxies. Never trust forwarded headers. */
|
|
38
|
+
publicOrigin?: string;
|
|
39
|
+
/** Trusted project gateway prefix; generated by the controller, never a request parameter. */
|
|
40
|
+
publicPathPrefix?: string;
|
|
41
|
+
/** Session revocation/expiry sweep interval; defaults to 5 seconds. */
|
|
42
|
+
authRecheckMs?: number;
|
|
33
43
|
log?: WorkerBridgeLog;
|
|
34
44
|
/** Test/host tuning; production defaults to 60 seconds. */
|
|
35
45
|
pendingWorkerResponseTtlMs?: number;
|
|
@@ -42,6 +52,7 @@ export interface HeadlessServer {
|
|
|
42
52
|
port: number;
|
|
43
53
|
/** Present only when this boot had to generate a fresh passcode. */
|
|
44
54
|
generatedPasscode?: string;
|
|
55
|
+
bootstrapToken?: string;
|
|
45
56
|
passcode: AccessPasscode;
|
|
46
57
|
bridge: WorkerBridgeCore;
|
|
47
58
|
/** Live authenticated tab count (for tests/status). */
|