@cjhyy/code-shell-server 0.9.44 → 0.9.47
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/attachment-service.d.ts +2 -0
- package/dist/attachment-service.js +40 -27
- package/dist/bin/code-shell-link-cleanup.d.ts +2 -0
- package/dist/bin/code-shell-link-cleanup.js +3 -0
- package/dist/desktop-web/http-api.d.ts +3 -0
- package/dist/desktop-web/http-api.js +16 -1
- package/dist/hub/configuration.d.ts +6 -3
- package/dist/hub/configuration.js +19 -6
- package/dist/hub/mcp-probe.d.ts +4 -24
- package/dist/hub/mcp-probe.js +18 -389
- package/dist/hub/model-probe.d.ts +2 -20
- package/dist/hub/model-probe.js +2 -193
- package/dist/hub/run-replay.d.ts +7 -1
- package/dist/hub/run-replay.js +10 -2
- package/dist/hub/session-management.js +18 -53
- package/dist/hub/skills-management.js +6 -8
- package/dist/hub/uploads.d.ts +10 -0
- package/dist/hub/uploads.js +107 -15
- package/dist/index.desktop-web.d.ts +1 -0
- package/dist/index.desktop-web.js +1 -0
- package/dist/index.links.d.ts +3 -0
- package/dist/index.links.js +2 -0
- package/dist/index.panels.d.ts +1 -0
- package/dist/index.panels.js +1 -0
- package/dist/index.serve.d.ts +1 -0
- package/dist/index.serve.js +1 -0
- package/dist/index.storage.d.ts +1 -0
- package/dist/index.storage.js +1 -0
- package/dist/links/catalog.js +2 -0
- package/dist/links/diagnostics.d.ts +60 -0
- package/dist/links/diagnostics.js +184 -0
- package/dist/links/http.d.ts +10 -8
- package/dist/links/http.js +144 -125
- package/dist/links/legacy-cleanup-cli.d.ts +3 -0
- package/dist/links/legacy-cleanup-cli.js +64 -0
- package/dist/links/providers/defaults.d.ts +5 -0
- package/dist/links/providers/defaults.js +782 -0
- package/dist/links/providers/index.d.ts +7 -0
- package/dist/links/providers/index.js +22 -0
- package/dist/links/remote-catalog.d.ts +10 -3
- package/dist/links/remote-catalog.js +76 -29
- package/dist/links/runtime/operation.d.ts +9 -0
- package/dist/links/runtime/operation.js +16 -0
- package/dist/links/service.d.ts +18 -2
- package/dist/links/service.js +593 -62
- package/dist/links/types.d.ts +21 -1
- package/dist/mobile-remote/codex-parse.d.ts +3 -1
- package/dist/mobile-remote/codex-parse.js +45 -11
- package/dist/mobile-remote/codex-room-agent.d.ts +0 -6
- package/dist/mobile-remote/codex-room-agent.js +30 -32
- package/dist/mobile-remote/mobile-chat-turn.d.ts +1 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +21 -0
- package/dist/mobile-remote/mobile-history.d.ts +10 -1
- package/dist/mobile-remote/mobile-history.js +24 -16
- package/dist/mobile-remote/remote-host-manager.d.ts +10 -0
- package/dist/mobile-remote/remote-host-manager.js +50 -13
- package/dist/mobile-remote/resident-agent.js +38 -22
- package/dist/mobile-remote/room-manager.d.ts +23 -13
- package/dist/mobile-remote/room-manager.js +106 -22
- package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
- package/dist/mobile-remote/room-model-tracing.js +196 -0
- package/dist/mobile-remote/room-process-stop.d.ts +3 -0
- package/dist/mobile-remote/room-process-stop.js +28 -0
- package/dist/model-trace-history.d.ts +29 -0
- package/dist/model-trace-history.js +198 -0
- package/dist/model-trace-http.d.ts +14 -0
- package/dist/model-trace-http.js +105 -0
- package/dist/panels/agent-task-host.js +1 -0
- package/dist/panels/agent-task-service.d.ts +2 -1
- package/dist/panels/agent-task-service.js +10 -3
- package/dist/panels/directory-bookmarks.js +62 -29
- package/dist/panels/execution-gate.d.ts +19 -1
- package/dist/panels/execution-gate.js +33 -5
- package/dist/panels/http.d.ts +1 -0
- package/dist/panels/hub-automations.js +1 -0
- package/dist/panels/hub-binding.js +5 -5
- package/dist/panels/model-trace-channel.d.ts +21 -0
- package/dist/panels/model-trace-channel.js +110 -0
- package/dist/panels/model-traces.d.ts +39 -0
- package/dist/panels/model-traces.js +222 -0
- package/dist/panels/process-service.d.ts +12 -2
- package/dist/panels/process-service.js +49 -6
- package/dist/panels/resources/uploads.js +15 -6
- package/dist/panels/runtime.d.ts +1 -0
- package/dist/panels/runtime.js +66 -8
- package/dist/panels/task-cookies.js +8 -16
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +28 -3
- package/dist/serve/cli.js +8 -1
- package/dist/serve/headless-server.js +68 -14
- package/dist/sessions-service.js +17 -27
- package/dist/worker-bridge-core.js +11 -0
- package/package.json +8 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SessionFileError, readSessionFileSync, sessionListStorage, } from "@cjhyy/code-shell-host/session-files";
|
|
2
|
+
import { listSessionEntries } from "@cjhyy/code-shell-core/internal";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { SessionManager } from "@cjhyy/code-shell-core";
|
|
4
5
|
import { replayTranscript, transcriptUserDisplay } from "@cjhyy/code-shell-web";
|
|
@@ -21,62 +22,25 @@ function safeSessionId(sessionId) {
|
|
|
21
22
|
}
|
|
22
23
|
function boundedSessionFile(rootDir, sessionId, filename, limit) {
|
|
23
24
|
safeSessionId(sessionId);
|
|
24
|
-
let directory;
|
|
25
|
-
let descriptor;
|
|
26
25
|
try {
|
|
27
|
-
|
|
28
|
-
const path = join(root, sessionId);
|
|
29
|
-
const info = lstatSync(path);
|
|
30
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
31
|
-
throw new SessionRequestError(403, "会话存储位置无效。");
|
|
32
|
-
directory = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_DIRECTORY ?? 0));
|
|
33
|
-
const openedDirectory = fstatSync(directory);
|
|
34
|
-
if (openedDirectory.dev !== info.dev || openedDirectory.ino !== info.ino)
|
|
35
|
-
throw new SessionRequestError(409, "会话存储正在变化,请重试。");
|
|
36
|
-
descriptor = openSync(process.platform === "linux"
|
|
37
|
-
? `/proc/self/fd/${directory}/${filename}`
|
|
38
|
-
: join(path, filename), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
39
|
-
const file = fstatSync(descriptor);
|
|
40
|
-
if (!file.isFile())
|
|
41
|
-
throw new SessionRequestError(403, "会话记录不是普通文件。");
|
|
42
|
-
if (file.size > limit)
|
|
43
|
-
throw new SessionRequestError(413, filename === "transcript.jsonl"
|
|
44
|
-
? "对话记录超过浏览器可读取的 32 MB 上限,请从服务器会话备份中获取完整记录。"
|
|
45
|
-
: "会话状态记录过大,无法在浏览器读取。");
|
|
46
|
-
// Snapshot exactly the checked prefix. An append during a running turn can
|
|
47
|
-
// be picked up by the live overlay without growing this allocation.
|
|
48
|
-
const buffer = Buffer.alloc(file.size);
|
|
49
|
-
let bytes = 0;
|
|
50
|
-
while (bytes < buffer.length) {
|
|
51
|
-
const count = readSync(descriptor, buffer, bytes, buffer.length - bytes, bytes);
|
|
52
|
-
if (!count)
|
|
53
|
-
break;
|
|
54
|
-
bytes += count;
|
|
55
|
-
}
|
|
56
|
-
const currentDirectory = lstatSync(path);
|
|
57
|
-
const currentFile = lstatSync(join(path, filename));
|
|
58
|
-
if (currentDirectory.isSymbolicLink() ||
|
|
59
|
-
currentFile.isSymbolicLink() ||
|
|
60
|
-
currentDirectory.dev !== info.dev ||
|
|
61
|
-
currentDirectory.ino !== info.ino ||
|
|
62
|
-
currentFile.dev !== file.dev ||
|
|
63
|
-
currentFile.ino !== file.ino)
|
|
64
|
-
throw new SessionRequestError(409, "会话存储正在变化,请重试。");
|
|
65
|
-
return buffer.subarray(0, bytes).toString("utf8");
|
|
26
|
+
return readSessionFileSync(resolve(rootDir), sessionId, filename, limit);
|
|
66
27
|
}
|
|
67
28
|
catch (error) {
|
|
29
|
+
if (error instanceof SessionFileError) {
|
|
30
|
+
if (error.code === "too_large")
|
|
31
|
+
throw new SessionRequestError(413, filename === "transcript.jsonl"
|
|
32
|
+
? "对话记录超过浏览器可读取的 32 MB 上限,请从服务器会话备份中获取完整记录。"
|
|
33
|
+
: "会话状态记录过大,无法在浏览器读取。");
|
|
34
|
+
if (error.code === "changed")
|
|
35
|
+
throw new SessionRequestError(409, "会话存储正在变化,请重试。");
|
|
36
|
+
throw new SessionRequestError(403, "会话存储位置无效。");
|
|
37
|
+
}
|
|
68
38
|
if (error.code === "ENOENT")
|
|
69
39
|
throw new SessionRequestError(404, "会话记录不存在或已移动。");
|
|
70
40
|
if (["ELOOP", "ENOTDIR"].includes(error.code ?? ""))
|
|
71
41
|
throw new SessionRequestError(403, "不能读取符号链接中的会话记录。");
|
|
72
42
|
throw error;
|
|
73
43
|
}
|
|
74
|
-
finally {
|
|
75
|
-
if (descriptor !== undefined)
|
|
76
|
-
closeSync(descriptor);
|
|
77
|
-
if (directory !== undefined)
|
|
78
|
-
closeSync(directory);
|
|
79
|
-
}
|
|
80
44
|
}
|
|
81
45
|
/** Synchronous, side-effect-free transcript snapshot used by HTTP exports and WS replay. */
|
|
82
46
|
export function readHubTranscript(sessionRootDir, sessionId) {
|
|
@@ -120,7 +84,8 @@ export function readHubSessionState(sessionRootDir, sessionId) {
|
|
|
120
84
|
}
|
|
121
85
|
}
|
|
122
86
|
export function createHubSessions(options) {
|
|
123
|
-
const
|
|
87
|
+
const sessionRootDir = resolve(options.sessionRootDir);
|
|
88
|
+
const manager = new SessionManager(sessionRootDir);
|
|
124
89
|
const titles = createSessionTitlesStore(join(options.dataDir, "session-titles.json"), {
|
|
125
90
|
strictMutations: true,
|
|
126
91
|
});
|
|
@@ -130,7 +95,7 @@ export function createHubSessions(options) {
|
|
|
130
95
|
safeSessionId(sessionId);
|
|
131
96
|
try {
|
|
132
97
|
// Reading state alone avoids loading a large transcript for a rename.
|
|
133
|
-
const state = readHubSessionState(
|
|
98
|
+
const state = readHubSessionState(sessionRootDir, sessionId);
|
|
134
99
|
if (!state ||
|
|
135
100
|
state.sessionId !== sessionId ||
|
|
136
101
|
typeof state.cwd !== "string" ||
|
|
@@ -190,7 +155,7 @@ export function createHubSessions(options) {
|
|
|
190
155
|
}
|
|
191
156
|
const customTitles = await titles.listTitles();
|
|
192
157
|
const search = query.toLocaleLowerCase();
|
|
193
|
-
const entries =
|
|
158
|
+
const entries = listSessionEntries(sessionRootDir, limit + 1, {
|
|
194
159
|
cwd,
|
|
195
160
|
rootsOnly: true,
|
|
196
161
|
...(archived !== "all" ? { archived: archived === "true" } : {}),
|
|
@@ -200,7 +165,7 @@ export function createHubSessions(options) {
|
|
|
200
165
|
.toLocaleLowerCase()
|
|
201
166
|
.includes(search)
|
|
202
167
|
: undefined,
|
|
203
|
-
});
|
|
168
|
+
}, sessionListStorage(sessionRootDir));
|
|
204
169
|
const page = entries.slice(0, limit);
|
|
205
170
|
const last = page.at(-1);
|
|
206
171
|
return {
|
|
@@ -249,7 +214,7 @@ export function createHubSessions(options) {
|
|
|
249
214
|
throw new SessionRequestError(429, "正在导出的对话过多,请稍后重试。");
|
|
250
215
|
activeExports++;
|
|
251
216
|
exporting = true;
|
|
252
|
-
const transcript = readHubTranscript(
|
|
217
|
+
const transcript = readHubTranscript(sessionRootDir, sessionId);
|
|
253
218
|
const format = url.searchParams.get("format") ?? "markdown";
|
|
254
219
|
if (format !== "json" && format !== "markdown")
|
|
255
220
|
throw new SessionRequestError(400, "不支持这个导出格式。");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomBytes, createHash } from "node:crypto";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { scanSkills, invalidateSkillCache, userHome } from "@cjhyy/code-shell-core";
|
|
5
|
+
import { readSkillInventory } from "@cjhyy/code-shell-host";
|
|
6
6
|
import { assertSafeSkillName, assertOwnedSkillDirectory, readSkillBundle, readBoundedSkillFile, validateSkillMarkdown, skillRoot, stageSkillDirectory, commitSkillDirectory, editSkillMarkdown, removeOwnedSkill, SkillConflictError, inspectRepo, getRefCommit, downloadSkillTree, parseSkillSourceMeta, SKILL_META_FILE, } from "@cjhyy/code-shell-core/internal/skills";
|
|
7
7
|
import { HubConfigurationError } from "./configuration.js";
|
|
8
8
|
const ROOT = "/api/v1/skills";
|
|
@@ -59,11 +59,9 @@ export function createHubSkills(options) {
|
|
|
59
59
|
return skill;
|
|
60
60
|
}
|
|
61
61
|
function snapshot() {
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const enabled = new Set(scanSkills(options.cwd, disabled).map((entry) => entry.name));
|
|
66
|
-
const skills = scanSkills(options.cwd).map((skill) => {
|
|
62
|
+
const inventory = readSkillInventory(options.cwd, { refresh: true });
|
|
63
|
+
const disabled = inventory;
|
|
64
|
+
const skills = inventory.skills.map(({ definition: skill, enabled }) => {
|
|
67
65
|
let editable = false;
|
|
68
66
|
let revision = createHash("sha256").update(skill.content).digest("hex");
|
|
69
67
|
let readOnlyReason;
|
|
@@ -89,7 +87,7 @@ export function createHubSkills(options) {
|
|
|
89
87
|
name: skill.name,
|
|
90
88
|
description: skill.description,
|
|
91
89
|
source: skill.source,
|
|
92
|
-
enabled
|
|
90
|
+
enabled,
|
|
93
91
|
editable,
|
|
94
92
|
removable: editable,
|
|
95
93
|
revision,
|
package/dist/hub/uploads.d.ts
CHANGED
|
@@ -13,10 +13,20 @@ export declare class HubUploads {
|
|
|
13
13
|
private readonly uploads;
|
|
14
14
|
private readonly active;
|
|
15
15
|
private closed;
|
|
16
|
+
/** This Host's own spool directory, held by a renewed lease while it runs. */
|
|
17
|
+
private readonly spool;
|
|
18
|
+
private releaseLease?;
|
|
16
19
|
constructor(root: string, cwd: string);
|
|
17
20
|
ready(): Promise<void>;
|
|
21
|
+
private reclaimAbandoned;
|
|
18
22
|
accept(id: string, owner: string, req: IncomingMessage, res: ServerResponse, stillAuthorized: () => Promise<boolean>): Promise<void>;
|
|
19
23
|
prepare(ids: unknown, owner: string, sessionId: unknown): Promise<PreparedHubUploads>;
|
|
24
|
+
/**
|
|
25
|
+
* Drop an owner's unsent upload, for example when it is removed from the
|
|
26
|
+
* draft. An upload being claimed by a send stays put. Returns whether the
|
|
27
|
+
* upload was discarded.
|
|
28
|
+
*/
|
|
29
|
+
discard(id: string, owner: string): Promise<boolean>;
|
|
20
30
|
sweep(): Promise<void>;
|
|
21
31
|
close(): Promise<void>;
|
|
22
32
|
}
|
package/dist/hub/uploads.js
CHANGED
|
@@ -2,9 +2,18 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { chmod, lstat, mkdir, open, readdir, rm } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { lockSync } from "@cjhyy/code-shell-core/internal";
|
|
6
|
+
import { isStageableImageMime, markAttachmentsSent, MAX_STAGED_IMAGE_BYTES, stageFileBytes, stageImageBytes, } from "../attachment-service.js";
|
|
6
7
|
const MAX_BYTES = 20 * 1024 * 1024;
|
|
7
8
|
const TTL_MS = 15 * 60 * 1000;
|
|
9
|
+
/** Instance leases are renewed every 10s; a lease idle for 30s belongs to a dead Host. */
|
|
10
|
+
const LEASE_STALE_MS = 30_000;
|
|
11
|
+
const LEASE_UPDATE_MS = 10_000;
|
|
12
|
+
const UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
|
|
13
|
+
const LEGACY_SPOOL = new RegExp(`^${UUID}\\.upload$`);
|
|
14
|
+
const INSTANCE_ENTRY = new RegExp(`^(instance-${UUID})(?:\\.lock)?$`);
|
|
15
|
+
/** Image MIME types go through image staging; everything else is staged as a file. */
|
|
16
|
+
const routesToImageStaging = (mimeType) => mimeType.startsWith("image/") && mimeType !== "image/svg+xml";
|
|
8
17
|
const SAFE_ID = /^[a-zA-Z0-9_-]{8,80}$/;
|
|
9
18
|
// Match the session/history boundary so existing dotted session IDs can attach files.
|
|
10
19
|
const SAFE_SESSION_ID = /^(?!.*\.\.)[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$/;
|
|
@@ -30,9 +39,13 @@ export class HubUploads {
|
|
|
30
39
|
uploads = new Map();
|
|
31
40
|
active = new Set();
|
|
32
41
|
closed = false;
|
|
42
|
+
/** This Host's own spool directory, held by a renewed lease while it runs. */
|
|
43
|
+
spool;
|
|
44
|
+
releaseLease;
|
|
33
45
|
constructor(root, cwd) {
|
|
34
46
|
this.root = root;
|
|
35
47
|
this.cwd = cwd;
|
|
48
|
+
this.spool = join(root, `instance-${randomUUID()}`);
|
|
36
49
|
}
|
|
37
50
|
async ready() {
|
|
38
51
|
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
|
@@ -40,10 +53,55 @@ export class HubUploads {
|
|
|
40
53
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
41
54
|
throw new Error("unsafe upload directory");
|
|
42
55
|
await chmod(this.root, 0o700);
|
|
43
|
-
// A pending draft cannot survive a restart; staged attachments live in the
|
|
56
|
+
// A pending draft cannot survive a restart; staged attachments live in the
|
|
57
|
+
// workspace. Several Hosts (a duplicate launch that has not bound its port
|
|
58
|
+
// yet, or another port) can share this root, so reclaim only spools that
|
|
59
|
+
// are provably unused instead of everything present.
|
|
60
|
+
await this.reclaimAbandoned();
|
|
61
|
+
await mkdir(this.spool, { mode: 0o700 });
|
|
62
|
+
this.releaseLease = lockSync(this.spool, {
|
|
63
|
+
stale: LEASE_STALE_MS,
|
|
64
|
+
update: LEASE_UPDATE_MS,
|
|
65
|
+
realpath: false,
|
|
66
|
+
retries: 0,
|
|
67
|
+
// A lost lease only risks another Host reclaiming these drafts; sends then
|
|
68
|
+
// fail with "upload not found", which the client already handles.
|
|
69
|
+
onCompromised: () => { },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async reclaimAbandoned() {
|
|
73
|
+
const instances = new Set();
|
|
44
74
|
for (const name of await readdir(this.root)) {
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
const instance = INSTANCE_ENTRY.exec(name)?.[1];
|
|
76
|
+
if (instance) {
|
|
77
|
+
instances.add(instance);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Hosts before instance leases spooled into the root. An older live Host
|
|
81
|
+
// rejects anything past the TTL, so only expired files are safe to remove.
|
|
82
|
+
if (!LEGACY_SPOOL.test(name))
|
|
83
|
+
continue;
|
|
84
|
+
const path = join(this.root, name);
|
|
85
|
+
const entry = await lstat(path).catch(() => undefined);
|
|
86
|
+
if (entry?.isFile() && Date.now() - entry.mtimeMs >= TTL_MS)
|
|
87
|
+
await rm(path, { force: true });
|
|
88
|
+
}
|
|
89
|
+
for (const instance of instances) {
|
|
90
|
+
const path = join(this.root, instance);
|
|
91
|
+
let release;
|
|
92
|
+
try {
|
|
93
|
+
// Succeeds only when no live Host renews this instance's lease.
|
|
94
|
+
release = lockSync(path, { stale: LEASE_STALE_MS, realpath: false, retries: 0 });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await rm(path, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
release();
|
|
104
|
+
}
|
|
47
105
|
}
|
|
48
106
|
}
|
|
49
107
|
async accept(id, owner, req, res, stillAuthorized) {
|
|
@@ -70,6 +128,21 @@ export class HubUploads {
|
|
|
70
128
|
return reject(400, "empty or invalid upload");
|
|
71
129
|
if (size > MAX_BYTES)
|
|
72
130
|
return reject(413, "attachment exceeds 20 MiB");
|
|
131
|
+
const mimeType = String(req.headers["content-type"] ?? "application/octet-stream")
|
|
132
|
+
.split(";", 1)[0]
|
|
133
|
+
.trim()
|
|
134
|
+
.toLowerCase();
|
|
135
|
+
if (mimeType.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mimeType)) {
|
|
136
|
+
return reject(400, "invalid content type");
|
|
137
|
+
}
|
|
138
|
+
// Admit only what the send path can stage, so a draft never looks
|
|
139
|
+
// uploaded and then fails on every Send.
|
|
140
|
+
if (routesToImageStaging(mimeType)) {
|
|
141
|
+
if (!isStageableImageMime(mimeType))
|
|
142
|
+
return reject(415, "unsupported image type");
|
|
143
|
+
if (size > MAX_STAGED_IMAGE_BYTES)
|
|
144
|
+
return reject(413, "image exceeds 10 MiB");
|
|
145
|
+
}
|
|
73
146
|
if ([...this.uploads.values()].reduce((sum, entry) => sum + entry.size, size) >
|
|
74
147
|
128 * 1024 * 1024) {
|
|
75
148
|
return reject(429, "upload storage quota exceeded");
|
|
@@ -84,20 +157,13 @@ export class HubUploads {
|
|
|
84
157
|
if (!name.trim() || name.length > 255 || /[\x00-\x1f/\\]/.test(name)) {
|
|
85
158
|
return reject(400, "invalid file name");
|
|
86
159
|
}
|
|
87
|
-
const mimeType = String(req.headers["content-type"] ?? "application/octet-stream")
|
|
88
|
-
.split(";", 1)[0]
|
|
89
|
-
.trim()
|
|
90
|
-
.toLowerCase();
|
|
91
|
-
if (mimeType.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mimeType)) {
|
|
92
|
-
return reject(400, "invalid content type");
|
|
93
|
-
}
|
|
94
160
|
const upload = {
|
|
95
161
|
id,
|
|
96
162
|
owner,
|
|
97
163
|
name,
|
|
98
164
|
mimeType,
|
|
99
165
|
size,
|
|
100
|
-
path: join(this.
|
|
166
|
+
path: join(this.spool, `${randomUUID()}.upload`),
|
|
101
167
|
createdAt: Date.now(),
|
|
102
168
|
status: "receiving",
|
|
103
169
|
};
|
|
@@ -108,8 +174,11 @@ export class HubUploads {
|
|
|
108
174
|
deadline.unref?.();
|
|
109
175
|
try {
|
|
110
176
|
// Recheck the managed root: a workspace tool must not redirect the upload writer via symlink.
|
|
111
|
-
|
|
112
|
-
|
|
177
|
+
for (const directory of [this.root, this.spool]) {
|
|
178
|
+
const current = await lstat(directory);
|
|
179
|
+
if (!current.isDirectory() || current.isSymbolicLink()) {
|
|
180
|
+
throw new Error("unsafe upload directory");
|
|
181
|
+
}
|
|
113
182
|
}
|
|
114
183
|
handle = await open(upload.path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
115
184
|
let received = 0;
|
|
@@ -188,7 +257,7 @@ export class HubUploads {
|
|
|
188
257
|
bytes,
|
|
189
258
|
origin: "mobile",
|
|
190
259
|
};
|
|
191
|
-
result.push(record.mimeType
|
|
260
|
+
result.push(routesToImageStaging(record.mimeType)
|
|
192
261
|
? await stageImageBytes(input)
|
|
193
262
|
: await stageFileBytes(input));
|
|
194
263
|
}
|
|
@@ -224,6 +293,19 @@ export class HubUploads {
|
|
|
224
293
|
throw error;
|
|
225
294
|
}
|
|
226
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Drop an owner's unsent upload, for example when it is removed from the
|
|
298
|
+
* draft. An upload being claimed by a send stays put. Returns whether the
|
|
299
|
+
* upload was discarded.
|
|
300
|
+
*/
|
|
301
|
+
async discard(id, owner) {
|
|
302
|
+
const record = this.uploads.get(id);
|
|
303
|
+
if (!record || record.owner !== owner || record.status !== "ready")
|
|
304
|
+
return false;
|
|
305
|
+
this.uploads.delete(id);
|
|
306
|
+
await rm(record.path, { force: true });
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
227
309
|
async sweep() {
|
|
228
310
|
for (const [id, record] of this.uploads) {
|
|
229
311
|
if (record.status === "ready" && Date.now() - record.createdAt >= TTL_MS) {
|
|
@@ -239,5 +321,15 @@ export class HubUploads {
|
|
|
239
321
|
for (const record of this.uploads.values())
|
|
240
322
|
await rm(record.path, { force: true });
|
|
241
323
|
this.uploads.clear();
|
|
324
|
+
if (this.releaseLease) {
|
|
325
|
+
await rm(this.spool, { recursive: true, force: true });
|
|
326
|
+
try {
|
|
327
|
+
this.releaseLease();
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
/* A compromised lease has nothing left to release. */
|
|
331
|
+
}
|
|
332
|
+
this.releaseLease = undefined;
|
|
333
|
+
}
|
|
242
334
|
}
|
|
243
335
|
}
|
package/dist/index.links.d.ts
CHANGED
|
@@ -4,4 +4,7 @@ export * from "./links/device-oauth.js";
|
|
|
4
4
|
export * from "./links/service.js";
|
|
5
5
|
export * from "./links/types.js";
|
|
6
6
|
export * from "./links/http.js";
|
|
7
|
+
export type { LinkAuthorizationDiagnostic, LinkAuthorizationDiagnosticStage, LinkAuthorizationDiagnosticCode, LinkAuthorizationDiagnosticSink, } from "./links/diagnostics.js";
|
|
7
8
|
export * from "./links/remote-configuration.js";
|
|
9
|
+
export { readRemoteProviderCatalog, readRemoteProviderCapabilities, RemoteLinkCatalogError, type RemoteLinkCatalogFailureReason, type RemoteProviderCapabilities, } from "./links/remote-catalog.js";
|
|
10
|
+
export { createDefaultLinkProviders, installDefaultLinkProviders, createLinkProviderModule, } from "./links/providers/index.js";
|
package/dist/index.links.js
CHANGED
|
@@ -5,3 +5,5 @@ export * from "./links/service.js";
|
|
|
5
5
|
export * from "./links/types.js";
|
|
6
6
|
export * from "./links/http.js";
|
|
7
7
|
export * from "./links/remote-configuration.js";
|
|
8
|
+
export { readRemoteProviderCatalog, readRemoteProviderCapabilities, RemoteLinkCatalogError, } from "./links/remote-catalog.js";
|
|
9
|
+
export { createDefaultLinkProviders, installDefaultLinkProviders, createLinkProviderModule, } from "./links/providers/index.js";
|
package/dist/index.panels.d.ts
CHANGED
package/dist/index.panels.js
CHANGED
package/dist/index.serve.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Ready-made headless HTTP/WebSocket host and CLI composition helpers. */
|
|
2
2
|
export * from "./serve/headless-server.js";
|
|
3
|
+
export * from "./model-trace-http.js";
|
|
3
4
|
export { startProjectControlServer, type ProjectControlServerOptions, } from "./project-runtime/control-server.js";
|
|
4
5
|
export { CLOUD_MANAGEMENT_AUTHORITY_VERSION } from "./project-runtime/cloud-account-auth.js";
|
|
5
6
|
export { CLOUD_BROWSER_AUTHORITY_VERSION } from "./project-runtime/cloud-browser-auth.js";
|
package/dist/index.serve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Ready-made headless HTTP/WebSocket host and CLI composition helpers. */
|
|
2
2
|
export * from "./serve/headless-server.js";
|
|
3
|
+
export * from "./model-trace-http.js";
|
|
3
4
|
export { startProjectControlServer, } from "./project-runtime/control-server.js";
|
|
4
5
|
export { CLOUD_MANAGEMENT_AUTHORITY_VERSION } from "./project-runtime/cloud-account-auth.js";
|
|
5
6
|
export { CLOUD_BROWSER_AUTHORITY_VERSION } from "./project-runtime/cloud-browser-auth.js";
|
package/dist/index.storage.d.ts
CHANGED
package/dist/index.storage.js
CHANGED
package/dist/links/catalog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installDefaultLinkProviders } from "./providers/index.js";
|
|
1
2
|
import { LINK_PROVIDER_MANIFESTS } from "@cjhyy/code-shell-link";
|
|
2
3
|
import { listLocalLinkProviders } from "@cjhyy/code-shell-core";
|
|
3
4
|
function unique(values) {
|
|
@@ -37,5 +38,6 @@ export function composeLinkProviderCatalog(manifests, executors) {
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
export function listDesktopLinkProviders() {
|
|
41
|
+
installDefaultLinkProviders();
|
|
40
42
|
return composeLinkProviderCatalog(LINK_PROVIDER_MANIFESTS, listLocalLinkProviders());
|
|
41
43
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LinkAuthorization, LinkErrorCode } from "./types.js";
|
|
2
|
+
export type LinkAuthorizationDiagnosticStage = "authorization" | "body_validation" | "input_validation" | "host_store_read" | "remote_configuration" | "remote_catalog" | "mode_selection" | "connection_review" | "job_creation" | "final_authorization";
|
|
3
|
+
/** Fixed internal reasons, independent of exception text and provider responses. */
|
|
4
|
+
export type LinkAuthorizationDiagnosticCode = "authorization_check_failed" | "request_body_invalid" | "input_validation_failed" | "host_store_unreadable" | "host_store_read_failed" | "remote_configuration_missing" | "remote_configuration_invalid" | "catalog_network" | "catalog_timeout" | "catalog_http_status" | "catalog_invalid_json" | "catalog_invalid_schema" | "catalog_unreviewed_capability" | "catalog_unsupported_auth" | "catalog_response_limit" | "catalog_unexpected_error" | "catalog_unavailable" | "provider_capability_missing" | "auth_mode_unavailable" | "connection_review_failed" | "job_capacity_exceeded" | "remote_authorization_invalid" | "authorization_job_failed" | "final_authorization_failed";
|
|
5
|
+
/** Deliberately excludes all URLs, request data, account IDs, secrets, and exception text. */
|
|
6
|
+
export interface LinkAuthorizationDiagnostic {
|
|
7
|
+
event: "link_authorization";
|
|
8
|
+
transport: "http" | "host";
|
|
9
|
+
requestId: string;
|
|
10
|
+
outcome: "stage" | "success" | "failure";
|
|
11
|
+
stage: LinkAuthorizationDiagnosticStage;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
providerId?: string;
|
|
14
|
+
methodId?: string;
|
|
15
|
+
authModeId?: string;
|
|
16
|
+
internalCode?: LinkAuthorizationDiagnosticCode;
|
|
17
|
+
publicCode?: LinkErrorCode;
|
|
18
|
+
httpStatus?: number;
|
|
19
|
+
upstreamStatus?: number;
|
|
20
|
+
/** API success can return an authorization job that has already failed. */
|
|
21
|
+
authorizationState?: LinkAuthorization["state"];
|
|
22
|
+
}
|
|
23
|
+
export type LinkAuthorizationDiagnosticSink = (diagnostic: Readonly<LinkAuthorizationDiagnostic>) => void | Promise<void>;
|
|
24
|
+
interface Failure {
|
|
25
|
+
stage: LinkAuthorizationDiagnosticStage;
|
|
26
|
+
internalCode: LinkAuthorizationDiagnosticCode;
|
|
27
|
+
upstreamStatus?: number;
|
|
28
|
+
}
|
|
29
|
+
declare class AuthorizationTrace {
|
|
30
|
+
private readonly sink?;
|
|
31
|
+
private readonly transport;
|
|
32
|
+
readonly requestId: `${string}-${string}-${string}-${string}-${string}`;
|
|
33
|
+
private readonly startedAt;
|
|
34
|
+
private active;
|
|
35
|
+
private stage;
|
|
36
|
+
private failure?;
|
|
37
|
+
private identity;
|
|
38
|
+
constructor(sink?: LinkAuthorizationDiagnosticSink | undefined, transport?: "http" | "host");
|
|
39
|
+
run<T>(operation: () => Promise<T>): Promise<T>;
|
|
40
|
+
isActive(): boolean;
|
|
41
|
+
setStage(stage: LinkAuthorizationDiagnosticStage): void;
|
|
42
|
+
setIdentity(identity: typeof this.identity): void;
|
|
43
|
+
setFailure(failure: Failure): void;
|
|
44
|
+
complete(httpStatus: number, publicCode?: LinkErrorCode, authorizationState?: LinkAuthorization["state"]): void;
|
|
45
|
+
private httpStatus;
|
|
46
|
+
/** Detached authorization jobs inherit ALS; they must lose authority to edit this trace. */
|
|
47
|
+
deactivate(): void;
|
|
48
|
+
private emit;
|
|
49
|
+
}
|
|
50
|
+
export declare function createLinkAuthorizationTrace(sink?: LinkAuthorizationDiagnosticSink): AuthorizationTrace;
|
|
51
|
+
/** Native Host callers share the same diagnostics; HTTP already owns its longer request lifetime. */
|
|
52
|
+
export declare function withLinkAuthorizationTrace(sink: LinkAuthorizationDiagnosticSink | undefined, operation: () => Promise<LinkAuthorization>, failure: (error: unknown) => {
|
|
53
|
+
status: number;
|
|
54
|
+
code: LinkErrorCode;
|
|
55
|
+
}): Promise<LinkAuthorization>;
|
|
56
|
+
export declare function authorizationDiagnosticStage(stage: LinkAuthorizationDiagnosticStage): void;
|
|
57
|
+
/** Callers must select identity values from the compiled provider/method/mode catalog. */
|
|
58
|
+
export declare function authorizationDiagnosticIdentity(identity: Pick<LinkAuthorizationDiagnostic, "providerId" | "methodId" | "authModeId">): void;
|
|
59
|
+
export declare function authorizationDiagnosticFailure(stage: LinkAuthorizationDiagnosticStage, internalCode: LinkAuthorizationDiagnosticCode, upstreamStatus?: number): void;
|
|
60
|
+
export {};
|