@cjhyy/code-shell-server 0.9.32 → 0.9.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hub/run-replay.d.ts +29 -1
- package/dist/hub/run-replay.js +90 -1
- package/dist/hub/session-management.js +2 -0
- package/dist/index.panels.d.ts +2 -0
- package/dist/index.panels.js +2 -0
- package/dist/panels/automations.d.ts +2 -0
- package/dist/panels/bridge-contract.d.ts +1 -1
- package/dist/panels/bridge-contract.js +1 -0
- package/dist/panels/http.d.ts +2 -0
- package/dist/panels/project-scope.d.ts +24 -0
- package/dist/panels/project-scope.js +101 -0
- package/dist/panels/runtime-services.d.ts +2 -0
- package/dist/panels/runtime-services.js +2 -2
- package/dist/panels/runtime.d.ts +3 -0
- package/dist/panels/runtime.js +34 -1
- package/dist/panels/storage-store.d.ts +1 -1
- package/dist/panels/storage-store.js +8 -3
- package/dist/serve/headless-server.js +63 -10
- package/package.json +5 -5
package/dist/hub/run-replay.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionOutputCoverageStore } from "@cjhyy/code-shell-core/internal";
|
|
1
2
|
export interface HubStreamCursor {
|
|
2
3
|
epoch: string;
|
|
3
4
|
sequence: number;
|
|
@@ -10,20 +11,36 @@ export interface HubStreamCursor {
|
|
|
10
11
|
*/
|
|
11
12
|
export declare class HubRunReplay {
|
|
12
13
|
private readonly limits;
|
|
14
|
+
private readonly persistence?;
|
|
13
15
|
private readonly epoch;
|
|
14
16
|
private sequence;
|
|
15
17
|
private totalBytes;
|
|
16
18
|
private readonly sessions;
|
|
19
|
+
private readonly coverage;
|
|
20
|
+
private coverageBytes;
|
|
17
21
|
constructor(limits?: {
|
|
18
22
|
maxBytes: number;
|
|
19
23
|
maxEvents: number;
|
|
20
24
|
maxTotalBytes?: number;
|
|
21
|
-
});
|
|
25
|
+
}, persistence?: SessionOutputCoverageStore | undefined);
|
|
22
26
|
begin(sessionId: string, requestId: string, baselineEvents: number): void;
|
|
23
27
|
/** Only a new, still-tracked conversation can safely replay before disk publication. */
|
|
24
28
|
hasEmptyBaseline(sessionId: string): boolean;
|
|
25
29
|
append(sessionId: string, event: unknown): HubStreamCursor;
|
|
26
30
|
finish(sessionId: string, requestId: string): void;
|
|
31
|
+
private retireCoverage;
|
|
32
|
+
outputCoverage(sessionId: string): {
|
|
33
|
+
streamCursor: {
|
|
34
|
+
epoch: `${string}-${string}-${string}-${string}-${string}`;
|
|
35
|
+
sequence: number;
|
|
36
|
+
};
|
|
37
|
+
outputCursor?: string | undefined;
|
|
38
|
+
journalIdentity?: string | undefined;
|
|
39
|
+
incarnation?: string | undefined;
|
|
40
|
+
unpaired: boolean;
|
|
41
|
+
inputIds: string[];
|
|
42
|
+
};
|
|
43
|
+
forget(sessionId: string): void;
|
|
27
44
|
snapshot<T>(sessionId: string, durableEvents: readonly T[]): {
|
|
28
45
|
liveStream?: {
|
|
29
46
|
events: {
|
|
@@ -37,6 +54,17 @@ export declare class HubRunReplay {
|
|
|
37
54
|
epoch: `${string}-${string}-${string}-${string}-${string}`;
|
|
38
55
|
sequence: number;
|
|
39
56
|
};
|
|
57
|
+
outputCoverage: {
|
|
58
|
+
streamCursor: {
|
|
59
|
+
epoch: `${string}-${string}-${string}-${string}-${string}`;
|
|
60
|
+
sequence: number;
|
|
61
|
+
};
|
|
62
|
+
outputCursor?: string | undefined;
|
|
63
|
+
journalIdentity?: string | undefined;
|
|
64
|
+
incarnation?: string | undefined;
|
|
65
|
+
unpaired: boolean;
|
|
66
|
+
inputIds: string[];
|
|
67
|
+
};
|
|
40
68
|
};
|
|
41
69
|
clear(): void;
|
|
42
70
|
}
|
package/dist/hub/run-replay.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OutputCoverage } from "@cjhyy/code-shell-web";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
/**
|
|
3
4
|
* A refresh can happen before the worker persists its current assistant text.
|
|
@@ -7,16 +8,20 @@ import { randomUUID } from "node:crypto";
|
|
|
7
8
|
*/
|
|
8
9
|
export class HubRunReplay {
|
|
9
10
|
limits;
|
|
11
|
+
persistence;
|
|
10
12
|
epoch = randomUUID();
|
|
11
13
|
sequence = 0;
|
|
12
14
|
totalBytes = 0;
|
|
13
15
|
sessions = new Map();
|
|
16
|
+
coverage = new Map();
|
|
17
|
+
coverageBytes = 0;
|
|
14
18
|
constructor(limits = {
|
|
15
19
|
maxBytes: 8 * 1024 * 1024,
|
|
16
20
|
maxEvents: 16_000,
|
|
17
21
|
maxTotalBytes: 64 * 1024 * 1024,
|
|
18
|
-
}) {
|
|
22
|
+
}, persistence) {
|
|
19
23
|
this.limits = limits;
|
|
24
|
+
this.persistence = persistence;
|
|
20
25
|
}
|
|
21
26
|
begin(sessionId, requestId, baselineEvents) {
|
|
22
27
|
let log = this.sessions.get(sessionId);
|
|
@@ -37,6 +42,48 @@ export class HubRunReplay {
|
|
|
37
42
|
return this.sessions.get(sessionId)?.baselineEvents === 0;
|
|
38
43
|
}
|
|
39
44
|
append(sessionId, event) {
|
|
45
|
+
let coverage = this.coverage.get(sessionId);
|
|
46
|
+
if (!coverage) {
|
|
47
|
+
coverage = { proof: new OutputCoverage() };
|
|
48
|
+
this.coverage.set(sessionId, coverage);
|
|
49
|
+
}
|
|
50
|
+
this.coverageBytes -= coverage.proof.memoryBytes;
|
|
51
|
+
const previousProof = coverage.proof;
|
|
52
|
+
coverage.proof = previousProof.copy();
|
|
53
|
+
coverage.proof.observe(event);
|
|
54
|
+
if (this.coverageBytes + coverage.proof.memoryBytes > 4 * 1024 * 1024)
|
|
55
|
+
coverage.proof.discardProof();
|
|
56
|
+
let withheldBeforeSession = false;
|
|
57
|
+
try {
|
|
58
|
+
if (this.persistence &&
|
|
59
|
+
((coverage.proof.incomplete && !coverage.persistedBlocked) ||
|
|
60
|
+
coverage.proof.inputIds.length)) {
|
|
61
|
+
const saved = this.persistence.record(sessionId, {
|
|
62
|
+
unpaired: coverage.proof.incomplete,
|
|
63
|
+
inputIds: coverage.proof.inputIds,
|
|
64
|
+
});
|
|
65
|
+
if (!saved.incarnation) {
|
|
66
|
+
withheldBeforeSession = saved.status === "unavailable";
|
|
67
|
+
throw new Error("Session output coverage is unavailable before publication");
|
|
68
|
+
}
|
|
69
|
+
if (saved.incarnation) {
|
|
70
|
+
coverage.proof.acknowledgeInputs(coverage.proof.inputIds);
|
|
71
|
+
coverage.persistedBlocked = saved.unpaired;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
coverage.proof = previousProof;
|
|
77
|
+
if (!withheldBeforeSession)
|
|
78
|
+
coverage.publicationFailed = true;
|
|
79
|
+
throw cause;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
this.coverageBytes += coverage.proof.memoryBytes;
|
|
83
|
+
}
|
|
84
|
+
const outputCursor = event?.outputCursor;
|
|
85
|
+
if (typeof outputCursor === "string")
|
|
86
|
+
coverage.outputCursor = outputCursor;
|
|
40
87
|
const sequence = ++this.sequence;
|
|
41
88
|
const log = this.sessions.get(sessionId);
|
|
42
89
|
if (log && !log.truncated) {
|
|
@@ -70,13 +117,53 @@ export class HubRunReplay {
|
|
|
70
117
|
if (!log.requests.size) {
|
|
71
118
|
this.totalBytes -= log.bytes;
|
|
72
119
|
this.sessions.delete(sessionId);
|
|
120
|
+
this.retireCoverage(sessionId);
|
|
73
121
|
}
|
|
74
122
|
}
|
|
123
|
+
retireCoverage(sessionId) {
|
|
124
|
+
const live = this.coverage.get(sessionId);
|
|
125
|
+
if (!live || !this.persistence || live.publicationFailed)
|
|
126
|
+
return;
|
|
127
|
+
const saved = this.persistence.read(sessionId);
|
|
128
|
+
// Only disk-owned proof can outlive RAM. In-memory-only embedders retain
|
|
129
|
+
// their existing gap barrier, and an unknown publication failure stays live.
|
|
130
|
+
if (saved.status === "incomplete" ||
|
|
131
|
+
!saved.incarnation ||
|
|
132
|
+
live.proof.inputIds.length ||
|
|
133
|
+
(live.proof.incomplete && !saved.unpaired))
|
|
134
|
+
return;
|
|
135
|
+
this.coverageBytes -= live.proof.memoryBytes;
|
|
136
|
+
this.coverage.delete(sessionId);
|
|
137
|
+
}
|
|
138
|
+
outputCoverage(sessionId) {
|
|
139
|
+
const saved = this.persistence?.read(sessionId);
|
|
140
|
+
const live = this.coverage.get(sessionId);
|
|
141
|
+
return {
|
|
142
|
+
unpaired: !!saved?.unpaired || !!live?.proof.incomplete || !!live?.publicationFailed,
|
|
143
|
+
inputIds: [...new Set([...(saved?.inputIds ?? []), ...(live?.proof.inputIds ?? [])])],
|
|
144
|
+
...(saved?.incarnation ? { incarnation: saved.incarnation } : {}),
|
|
145
|
+
...(saved?.journalIdentity ? { journalIdentity: saved.journalIdentity } : {}),
|
|
146
|
+
...(live?.outputCursor ? { outputCursor: live.outputCursor } : {}),
|
|
147
|
+
streamCursor: { epoch: this.epoch, sequence: this.sequence },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
forget(sessionId) {
|
|
151
|
+
const log = this.sessions.get(sessionId);
|
|
152
|
+
if (log)
|
|
153
|
+
this.totalBytes -= log.bytes;
|
|
154
|
+
this.sessions.delete(sessionId);
|
|
155
|
+
const coverage = this.coverage.get(sessionId);
|
|
156
|
+
if (coverage)
|
|
157
|
+
this.coverageBytes -= coverage.proof.memoryBytes;
|
|
158
|
+
this.coverage.delete(sessionId);
|
|
159
|
+
this.persistence?.forget(sessionId);
|
|
160
|
+
}
|
|
75
161
|
snapshot(sessionId, durableEvents) {
|
|
76
162
|
const log = this.sessions.get(sessionId);
|
|
77
163
|
return {
|
|
78
164
|
transcript: log && !log.truncated ? durableEvents.slice(0, log.baselineEvents) : durableEvents.slice(),
|
|
79
165
|
streamCursor: { epoch: this.epoch, sequence: this.sequence },
|
|
166
|
+
outputCoverage: this.outputCoverage(sessionId),
|
|
80
167
|
...(log
|
|
81
168
|
? { liveStream: { events: structuredClone(log.events), truncated: log.truncated } }
|
|
82
169
|
: {}),
|
|
@@ -85,5 +172,7 @@ export class HubRunReplay {
|
|
|
85
172
|
clear() {
|
|
86
173
|
this.sessions.clear();
|
|
87
174
|
this.totalBytes = 0;
|
|
175
|
+
for (const sessionId of this.coverage.keys())
|
|
176
|
+
this.retireCoverage(sessionId);
|
|
88
177
|
}
|
|
89
178
|
}
|
|
@@ -369,6 +369,8 @@ export function transcriptMarkdown(title, transcript) {
|
|
|
369
369
|
}
|
|
370
370
|
else if (item.kind === "system_error")
|
|
371
371
|
sections.push(`### 错误\n\n${item.text}`);
|
|
372
|
+
else if (item.kind === "system")
|
|
373
|
+
sections.push(`### 通知\n\n${item.text}`);
|
|
372
374
|
else
|
|
373
375
|
sections.push(`### 子代理:${item.label}\n\n${item.status}`);
|
|
374
376
|
}
|
package/dist/index.panels.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from "./panels/automations.js";
|
|
|
3
3
|
export * from "./panels/hub-automations.js";
|
|
4
4
|
export { createPanelManagement, PanelManagementError } from "./panels/management.js";
|
|
5
5
|
export { createPanelRuntime, panelWebCompatibility } from "./panels/runtime.js";
|
|
6
|
+
export { createHubPanelBinding } from "./panels/hub-binding.js";
|
|
7
|
+
export * from "./panels/project-scope.js";
|
|
6
8
|
export { PanelRuntimeServices } from "./panels/runtime-services.js";
|
|
7
9
|
export * from "./panels/process-service.js";
|
|
8
10
|
export * from "./panels/agent-task-service.js";
|
package/dist/index.panels.js
CHANGED
|
@@ -3,6 +3,8 @@ export * from "./panels/automations.js";
|
|
|
3
3
|
export * from "./panels/hub-automations.js";
|
|
4
4
|
export { createPanelManagement, PanelManagementError } from "./panels/management.js";
|
|
5
5
|
export { createPanelRuntime, panelWebCompatibility } from "./panels/runtime.js";
|
|
6
|
+
export { createHubPanelBinding } from "./panels/hub-binding.js";
|
|
7
|
+
export * from "./panels/project-scope.js";
|
|
6
8
|
export { PanelRuntimeServices } from "./panels/runtime-services.js";
|
|
7
9
|
export * from "./panels/process-service.js";
|
|
8
10
|
export * from "./panels/agent-task-service.js";
|
|
@@ -7,6 +7,8 @@ export interface PanelAutomationScope {
|
|
|
7
7
|
sessionId: string;
|
|
8
8
|
/** Host-selected installed package revision, never a field from Panel call JSON. */
|
|
9
9
|
revision?: string;
|
|
10
|
+
/** Optional synchronous transport fence immediately before a Host mutation. */
|
|
11
|
+
assertAuthorized?(): void;
|
|
10
12
|
isAuthorized(): Promise<boolean>;
|
|
11
13
|
}
|
|
12
14
|
/** Borrowed Host service. A page/transport closing never stops the scheduler. */
|
|
@@ -11,7 +11,7 @@ export declare const panelBridgeLimits: Readonly<{
|
|
|
11
11
|
callTimeoutMs: 15000;
|
|
12
12
|
consentTimeoutMs: number;
|
|
13
13
|
}>;
|
|
14
|
-
export type PanelBridgeErrorCode = "PERMISSION_DENIED" | "REVOKED" | "NOT_SUPPORTED" | "RATE_LIMITED" | "PARAMS_TOO_LARGE" | "RESULT_TOO_LARGE" | "TIMEOUT" | "INVALID_ARGUMENT" | "OPERATION_FAILED";
|
|
14
|
+
export type PanelBridgeErrorCode = "PERMISSION_DENIED" | "REVOKED" | "SESSION_CHANGED" | "NOT_SUPPORTED" | "RATE_LIMITED" | "PARAMS_TOO_LARGE" | "RESULT_TOO_LARGE" | "TIMEOUT" | "INVALID_ARGUMENT" | "OPERATION_FAILED";
|
|
15
15
|
export declare class PanelBridgeError extends Error {
|
|
16
16
|
readonly code: PanelBridgeErrorCode;
|
|
17
17
|
readonly retryAfterMs?: number | undefined;
|
package/dist/panels/http.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ import type { PanelDirectoryAuthorizer } from "./directory-bookmarks.js";
|
|
|
4
4
|
import type { SharedPanelToolHost } from "./shared-tool-jobs.js";
|
|
5
5
|
import type { PanelAutomationHost } from "./automations.js";
|
|
6
6
|
import { type PanelAgentTaskHostOptions } from "./agent-task-host.js";
|
|
7
|
+
import type { PanelProjectScopeBinding } from "./project-scope.js";
|
|
7
8
|
export declare function createPanelHttp(options: PanelManagementHttpOptions & {
|
|
8
9
|
dataDir: string;
|
|
9
10
|
host: "hub" | "desktop";
|
|
11
|
+
projectScope?: () => Promise<PanelProjectScopeBinding | undefined>;
|
|
10
12
|
publicPathPrefix?: string;
|
|
11
13
|
agentTaskOptions?: PanelAgentTaskHostOptions;
|
|
12
14
|
sharedToolJobs?: SharedPanelToolHost;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Context identity only. This value grants no Host permissions. */
|
|
2
|
+
export interface PanelProjectScope {
|
|
3
|
+
version: 1;
|
|
4
|
+
environmentId: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
rootId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PanelProjectScopeBinding {
|
|
9
|
+
readonly scope: Readonly<PanelProjectScope>;
|
|
10
|
+
/** Revalidates the frozen authority; a failed binding can never be revived. */
|
|
11
|
+
assertCurrent(): void;
|
|
12
|
+
}
|
|
13
|
+
/** Only Host composition calls this factory, after resolving its selected roots. */
|
|
14
|
+
export declare function capturePanelProjectScope(options: {
|
|
15
|
+
environmentDir: string;
|
|
16
|
+
/** Hub supplies its already advertised descriptor identity. */
|
|
17
|
+
environmentId?: string;
|
|
18
|
+
projectPath: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
/** Desktop supplies opaque IDs resolved from its authoritative project registry. */
|
|
21
|
+
projectId?: string;
|
|
22
|
+
rootId?: string;
|
|
23
|
+
assertAuthority(): void;
|
|
24
|
+
}): Promise<PanelProjectScopeBinding>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { environmentIdentity } from "../environment-identity.js";
|
|
5
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
6
|
+
function directory(path) {
|
|
7
|
+
const selected = resolve(path);
|
|
8
|
+
const canonical = realpathSync(selected);
|
|
9
|
+
const info = lstatSync(canonical, { bigint: true });
|
|
10
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
11
|
+
throw new Error("Invalid Panel project root");
|
|
12
|
+
const identity = [canonical, String(info.dev), String(info.ino), String(info.birthtimeNs)];
|
|
13
|
+
return {
|
|
14
|
+
identity,
|
|
15
|
+
assertCurrent() {
|
|
16
|
+
if (realpathSync(selected) !== canonical)
|
|
17
|
+
throw new Error("Panel project root changed");
|
|
18
|
+
const current = lstatSync(canonical, { bigint: true });
|
|
19
|
+
if (!current.isDirectory() ||
|
|
20
|
+
current.isSymbolicLink() ||
|
|
21
|
+
current.dev !== info.dev ||
|
|
22
|
+
current.ino !== info.ino ||
|
|
23
|
+
current.birthtimeNs !== info.birthtimeNs)
|
|
24
|
+
throw new Error("Panel project root changed");
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function readEnvironment(path, expectedId) {
|
|
29
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
30
|
+
try {
|
|
31
|
+
const before = fstatSync(fd, { bigint: true });
|
|
32
|
+
if (!before.isFile() || before.size <= 0n || before.size > 256n)
|
|
33
|
+
throw new Error("Invalid Panel environment identity");
|
|
34
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
35
|
+
let offset = 0;
|
|
36
|
+
while (offset < bytes.length) {
|
|
37
|
+
const count = readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
38
|
+
if (!count)
|
|
39
|
+
throw new Error("Panel environment identity changed");
|
|
40
|
+
offset += count;
|
|
41
|
+
}
|
|
42
|
+
const after = fstatSync(fd, { bigint: true });
|
|
43
|
+
const named = lstatSync(path, { bigint: true });
|
|
44
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
45
|
+
if (after.dev !== before.dev ||
|
|
46
|
+
after.ino !== before.ino ||
|
|
47
|
+
after.size !== before.size ||
|
|
48
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
49
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
50
|
+
named.isSymbolicLink() ||
|
|
51
|
+
named.dev !== before.dev ||
|
|
52
|
+
named.ino !== before.ino ||
|
|
53
|
+
value?.version !== 1 ||
|
|
54
|
+
value.id !== expectedId)
|
|
55
|
+
throw new Error("Panel environment identity changed");
|
|
56
|
+
return [String(before.dev), String(before.ino), String(before.birthtimeNs)];
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Only Host composition calls this factory, after resolving its selected roots. */
|
|
63
|
+
export async function capturePanelProjectScope(options) {
|
|
64
|
+
options.assertAuthority();
|
|
65
|
+
const environmentId = options.environmentId ?? (await environmentIdentity(options.environmentDir));
|
|
66
|
+
options.assertAuthority();
|
|
67
|
+
const environmentRoot = directory(options.environmentDir);
|
|
68
|
+
const environmentFile = join(options.environmentDir, "environment.json");
|
|
69
|
+
const environmentPin = readEnvironment(environmentFile, environmentId);
|
|
70
|
+
const project = directory(options.projectPath);
|
|
71
|
+
const root = directory(options.cwd);
|
|
72
|
+
const scope = Object.freeze({
|
|
73
|
+
version: 1,
|
|
74
|
+
environmentId,
|
|
75
|
+
projectId: options.projectId ?? digest(["project", project.identity]),
|
|
76
|
+
// The registered mount and the execution root are different for worktrees.
|
|
77
|
+
rootId: digest(["root", options.rootId ?? null, root.identity]),
|
|
78
|
+
});
|
|
79
|
+
let revoked = false;
|
|
80
|
+
const binding = {
|
|
81
|
+
scope,
|
|
82
|
+
assertCurrent() {
|
|
83
|
+
if (revoked)
|
|
84
|
+
throw new Error("Panel project scope was revoked; reopen the panel");
|
|
85
|
+
try {
|
|
86
|
+
options.assertAuthority();
|
|
87
|
+
environmentRoot.assertCurrent();
|
|
88
|
+
if (digest(readEnvironment(environmentFile, environmentId)) !== digest(environmentPin))
|
|
89
|
+
throw new Error("Panel environment identity changed");
|
|
90
|
+
project.assertCurrent();
|
|
91
|
+
root.assertCurrent();
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
revoked = true;
|
|
95
|
+
throw new Error("Panel project scope changed; reopen the panel", { cause });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
binding.assertCurrent();
|
|
100
|
+
return binding;
|
|
101
|
+
}
|
|
@@ -5,6 +5,8 @@ export interface PanelRuntimeScope {
|
|
|
5
5
|
permissions: readonly string[];
|
|
6
6
|
/** Checks the live owner, its project binding, and the installed revision. */
|
|
7
7
|
isAuthorized: () => Promise<boolean>;
|
|
8
|
+
/** Optional synchronous transport check at the storage publication boundary. */
|
|
9
|
+
assertAuthorized?: () => void;
|
|
8
10
|
}
|
|
9
11
|
/** Node-only implementation of the existing Panel SDK's bounded disk operations. */
|
|
10
12
|
export declare class PanelRuntimeServices {
|
|
@@ -157,7 +157,7 @@ export class PanelRuntimeServices {
|
|
|
157
157
|
if (change) {
|
|
158
158
|
const result = applyPanelAppStorageChange(state, change);
|
|
159
159
|
if (result.updated)
|
|
160
|
-
await writePanelAppStorage(file, state, quota, () => authorized(scope));
|
|
160
|
+
await writePanelAppStorage(file, state, quota, () => authorized(scope), scope.assertAuthorized);
|
|
161
161
|
return result;
|
|
162
162
|
}
|
|
163
163
|
const existed = Object.hasOwn(state, key);
|
|
@@ -165,7 +165,7 @@ export class PanelRuntimeServices {
|
|
|
165
165
|
state[key] = JSON.parse(encoded);
|
|
166
166
|
else
|
|
167
167
|
delete state[key];
|
|
168
|
-
await writePanelAppStorage(file, state, quota, () => authorized(scope));
|
|
168
|
+
await writePanelAppStorage(file, state, quota, () => authorized(scope), scope.assertAuthorized);
|
|
169
169
|
return method === "storage.set" ? true : existed;
|
|
170
170
|
}
|
|
171
171
|
finally {
|
package/dist/panels/runtime.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { SharedPanelToolHost } from "./shared-tool-jobs.js";
|
|
|
4
4
|
import { type PanelDirectoryAuthorizer } from "./directory-bookmarks.js";
|
|
5
5
|
import type { PanelSnapshot } from "./types.js";
|
|
6
6
|
import { type PanelAutomationHost } from "./automations.js";
|
|
7
|
+
import type { PanelProjectScopeBinding } from "./project-scope.js";
|
|
7
8
|
export declare const panelWebCompatibility: (app: Pick<InstalledPanelApp, "permissions">, options?: {
|
|
8
9
|
automations?: boolean;
|
|
9
10
|
}) => {
|
|
@@ -34,6 +35,8 @@ export interface PanelRuntimeOptions {
|
|
|
34
35
|
projectPackages?: boolean;
|
|
35
36
|
dataDir: string;
|
|
36
37
|
host: "hub" | "desktop";
|
|
38
|
+
/** Host authority only, never a field accepted from Panel HTTP requests. */
|
|
39
|
+
projectScope?: () => Promise<PanelProjectScopeBinding | undefined>;
|
|
37
40
|
publicPathPrefix?: string;
|
|
38
41
|
snapshot: (appId?: string) => Promise<PanelSnapshot>;
|
|
39
42
|
ownerId: (request: IncomingMessage) => Promise<string | undefined>;
|
package/dist/panels/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { panelVideoLimits, panelVideoOptions } from "./browser-capture.js";
|
|
2
2
|
import { panelExecutionGate, PanelExecutionBusyError } from "./execution-gate.js";
|
|
3
3
|
import { randomBytes, createHash } from "node:crypto";
|
|
4
|
-
import { constants } from "node:fs";
|
|
4
|
+
import { constants, lstatSync } from "node:fs";
|
|
5
5
|
import { lstat, mkdir, open, opendir, realpath } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
@@ -1001,12 +1001,14 @@ export function createPanelRuntime(options) {
|
|
|
1001
1001
|
return false;
|
|
1002
1002
|
}
|
|
1003
1003
|
try {
|
|
1004
|
+
grant.projectScope?.assertCurrent();
|
|
1004
1005
|
const panel = (await snapshot(grant.app.id)).panels.find((candidate) => candidate.id === grant.app.id);
|
|
1005
1006
|
const info = await lstat(grant.root);
|
|
1006
1007
|
const ownerStillAuthorized = await options.isAuthorized(grant.request);
|
|
1007
1008
|
const currentOwner = await options.ownerId(grant.request);
|
|
1008
1009
|
if (grant.app.permissions.includes("process"))
|
|
1009
1010
|
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1011
|
+
grant.projectScope?.assertCurrent();
|
|
1010
1012
|
const valid = !closed &&
|
|
1011
1013
|
grants.get(grant.id) === grant &&
|
|
1012
1014
|
grant.expiresAt > now() &&
|
|
@@ -1199,7 +1201,17 @@ export function createPanelRuntime(options) {
|
|
|
1199
1201
|
context.sessionId = input.sessionId;
|
|
1200
1202
|
context.busy = false;
|
|
1201
1203
|
}
|
|
1204
|
+
const projectScope = app.permissions.includes("context.workspace")
|
|
1205
|
+
? await options.projectScope?.()
|
|
1206
|
+
: undefined;
|
|
1207
|
+
if (projectScope) {
|
|
1208
|
+
projectScope.assertCurrent();
|
|
1209
|
+
context.projectScope = { ...projectScope.scope };
|
|
1210
|
+
}
|
|
1211
|
+
if (closed || generation !== startedGeneration || preparation.cancelled)
|
|
1212
|
+
error(410, "面板授权已失效,请重新打开。");
|
|
1202
1213
|
const grant = {
|
|
1214
|
+
projectScope,
|
|
1203
1215
|
sharedTools,
|
|
1204
1216
|
guestId: ++nextGuest,
|
|
1205
1217
|
id,
|
|
@@ -1600,12 +1612,33 @@ export function createPanelRuntime(options) {
|
|
|
1600
1612
|
error(400, "面板通知内容无效。");
|
|
1601
1613
|
return { effect: method, title: value.title ?? grant.app.title.default, body: value.body };
|
|
1602
1614
|
}
|
|
1615
|
+
const expectedRevision = grant.revision;
|
|
1603
1616
|
return services.call({
|
|
1604
1617
|
appId: grant.app.id,
|
|
1605
1618
|
cwd: options.cwd,
|
|
1606
1619
|
projectPath: options.bindingCwd ?? options.cwd,
|
|
1607
1620
|
permissions: grant.app.permissions,
|
|
1608
1621
|
isAuthorized: () => authorized(grant),
|
|
1622
|
+
assertAuthorized: () => {
|
|
1623
|
+
try {
|
|
1624
|
+
grant.projectScope?.assertCurrent();
|
|
1625
|
+
const root = lstatSync(grant.root);
|
|
1626
|
+
if (closed ||
|
|
1627
|
+
grants.get(grant.id) !== grant ||
|
|
1628
|
+
byGuest.get(grant.guestId) !== grant ||
|
|
1629
|
+
grant.revision !== expectedRevision ||
|
|
1630
|
+
grant.expiresAt <= now() ||
|
|
1631
|
+
!grant.app.permissions.includes("storage") ||
|
|
1632
|
+
root.isSymbolicLink() ||
|
|
1633
|
+
!root.isDirectory() ||
|
|
1634
|
+
root.dev + ":" + root.ino !== grant.rootIdentity)
|
|
1635
|
+
throw new PanelBridgeError("REVOKED", "Panel App storage scope changed");
|
|
1636
|
+
}
|
|
1637
|
+
catch (cause) {
|
|
1638
|
+
remove(grant);
|
|
1639
|
+
throw cause;
|
|
1640
|
+
}
|
|
1641
|
+
},
|
|
1609
1642
|
}, method, params);
|
|
1610
1643
|
}
|
|
1611
1644
|
return {
|
|
@@ -27,4 +27,4 @@ export declare function applyPanelAppStorageChange(storage: PanelAppStorage, cha
|
|
|
27
27
|
};
|
|
28
28
|
export declare function preparePanelAppStorage(file: string): Promise<void>;
|
|
29
29
|
export declare function readPanelAppStorage(file: string, quotaBytes: number): Promise<PanelAppStorage>;
|
|
30
|
-
export declare function writePanelAppStorage(file: string, storage: PanelAppStorage, quotaBytes: number, beforeCommit?: () => Promise<void
|
|
30
|
+
export declare function writePanelAppStorage(file: string, storage: PanelAppStorage, quotaBytes: number, beforeCommit?: () => Promise<void>, finalCommitFence?: () => void): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { constants } from "node:fs";
|
|
2
|
+
import { constants, renameSync } from "node:fs";
|
|
3
3
|
import { chmod, lstat, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
/** Matches the existing Desktop namespace, so its remote Web shares saved panel state. */
|
|
@@ -161,7 +161,7 @@ export async function readPanelAppStorage(file, quotaBytes) {
|
|
|
161
161
|
await handle?.close().catch(() => undefined);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
export async function writePanelAppStorage(file, storage, quotaBytes, beforeCommit) {
|
|
164
|
+
export async function writePanelAppStorage(file, storage, quotaBytes, beforeCommit, finalCommitFence) {
|
|
165
165
|
const quota = panelAppStorageQuotaBytes(quotaBytes);
|
|
166
166
|
const entries = Object.entries(storage);
|
|
167
167
|
if (entries.length > MAX_PANEL_APP_STORAGE_KEYS) {
|
|
@@ -187,7 +187,12 @@ export async function writePanelAppStorage(file, storage, quotaBytes, beforeComm
|
|
|
187
187
|
// Recheck both authorization and the target after staging, before publishing.
|
|
188
188
|
await beforeCommit?.();
|
|
189
189
|
await preparePanelAppStorage(file);
|
|
190
|
-
|
|
190
|
+
if (finalCommitFence) {
|
|
191
|
+
finalCommitFence();
|
|
192
|
+
renameSync(temporary, file);
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
await rename(temporary, file);
|
|
191
196
|
if (process.platform !== "win32")
|
|
192
197
|
await chmod(file, 0o600);
|
|
193
198
|
}
|
|
@@ -22,7 +22,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
|
|
|
22
22
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
23
23
|
import { WebSocketServer } from "ws";
|
|
24
24
|
import { SessionManager, SettingsManager } from "@cjhyy/code-shell-core";
|
|
25
|
-
import { readOutputJournal, readOutputJournalLegacyBase } from "@cjhyy/code-shell-core/internal";
|
|
25
|
+
import { SessionOutputCoverageStore, readOutputJournal, readOutputJournalLegacyBase, } from "@cjhyy/code-shell-core/internal";
|
|
26
26
|
import { AccessPasscode } from "../mobile-remote/access-passcode.js";
|
|
27
27
|
import { resolveSafe } from "../mobile-remote/mobile-static.js";
|
|
28
28
|
import { contentTypeFor } from "../static-files.js";
|
|
@@ -37,6 +37,7 @@ import { createHubMcpConfiguration } from "../hub/mcp-configuration.js";
|
|
|
37
37
|
import { createLinkHttp } from "../links/http.js";
|
|
38
38
|
import { createPanelHttp } from "../panels/http.js";
|
|
39
39
|
import { createHubPanelBinding } from "../panels/hub-binding.js";
|
|
40
|
+
import { capturePanelProjectScope } from "../panels/project-scope.js";
|
|
40
41
|
import { createHubSessions, hubSessionPreview, readHubSessionState, readHubTranscript, } from "../hub/session-management.js";
|
|
41
42
|
import { createHubFiles } from "../hub/files.js";
|
|
42
43
|
import { createSessionHttpResponses } from "../session-http-responses.js";
|
|
@@ -123,7 +124,7 @@ export async function startHeadlessServer(opts) {
|
|
|
123
124
|
// Independent of response routing: leaving a tab does not stop its worker run.
|
|
124
125
|
const runningSessions = new Map();
|
|
125
126
|
const runOwners = new Map();
|
|
126
|
-
const runReplay = new HubRunReplay();
|
|
127
|
+
const runReplay = new HubRunReplay(undefined, new SessionOutputCoverageStore(() => sessionRootDir));
|
|
127
128
|
const runInputs = new Map();
|
|
128
129
|
let preparingRuns = 0;
|
|
129
130
|
let configurationChanging = false;
|
|
@@ -146,13 +147,21 @@ export async function startHeadlessServer(opts) {
|
|
|
146
147
|
broadcast(JSON.stringify({ jsonrpc: "2.0", method, params }));
|
|
147
148
|
};
|
|
148
149
|
const publishStream = (sessionId, event) => {
|
|
149
|
-
|
|
150
|
+
let cursor;
|
|
151
|
+
try {
|
|
152
|
+
cursor = runReplay.append(sessionId, event);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
log("stream.coverage_unavailable", { sessionId });
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
150
158
|
notify("agent/streamEvent", {
|
|
151
159
|
sessionId,
|
|
152
160
|
event,
|
|
153
161
|
hubSequence: cursor.sequence,
|
|
154
162
|
hubEpoch: cursor.epoch,
|
|
155
163
|
});
|
|
164
|
+
return true;
|
|
156
165
|
};
|
|
157
166
|
const leases = new ApprovalLeases((lease) => notify("serve/approvalLease", lease));
|
|
158
167
|
const authReaper = hubAuth
|
|
@@ -235,6 +244,7 @@ export async function startHeadlessServer(opts) {
|
|
|
235
244
|
const params = message.params;
|
|
236
245
|
if (typeof params?.sessionId === "string" && params.event !== undefined) {
|
|
237
246
|
let event = params.event;
|
|
247
|
+
let deliveredInput;
|
|
238
248
|
const value = event;
|
|
239
249
|
if (value?.type === "session_user_message" && typeof value.clientMessageId === "string") {
|
|
240
250
|
const input = [...runInputs.values()].find((candidate) => candidate.sessionId === params.sessionId &&
|
|
@@ -242,11 +252,17 @@ export async function startHeadlessServer(opts) {
|
|
|
242
252
|
if (input) {
|
|
243
253
|
if (input.echoed)
|
|
244
254
|
return;
|
|
245
|
-
|
|
246
|
-
event =
|
|
255
|
+
deliveredInput = input;
|
|
256
|
+
event = {
|
|
257
|
+
...input.event,
|
|
258
|
+
...(typeof value.outputCursor === "string"
|
|
259
|
+
? { outputCursor: value.outputCursor }
|
|
260
|
+
: {}),
|
|
261
|
+
};
|
|
247
262
|
}
|
|
248
263
|
}
|
|
249
|
-
publishStream(params.sessionId, event)
|
|
264
|
+
if (publishStream(params.sessionId, event) && deliveredInput)
|
|
265
|
+
deliveredInput.echoed = true;
|
|
250
266
|
return;
|
|
251
267
|
}
|
|
252
268
|
}
|
|
@@ -255,8 +271,7 @@ export async function startHeadlessServer(opts) {
|
|
|
255
271
|
const requestId = String(params?.requestId);
|
|
256
272
|
const input = runInputs.get(requestId);
|
|
257
273
|
if (input && !input.echoed) {
|
|
258
|
-
input.echoed =
|
|
259
|
-
publishStream(input.sessionId, input.event);
|
|
274
|
+
input.echoed = publishStream(input.sessionId, input.event);
|
|
260
275
|
}
|
|
261
276
|
const route = pendingWorkerResponses.get(requestId);
|
|
262
277
|
if (route)
|
|
@@ -462,6 +477,17 @@ export async function startHeadlessServer(opts) {
|
|
|
462
477
|
...panelBinding,
|
|
463
478
|
dataDir: opts.dataDir,
|
|
464
479
|
host: "hub",
|
|
480
|
+
projectScope: () => {
|
|
481
|
+
if (!environment || !panelBinding)
|
|
482
|
+
throw new Error("Hub project identity is unavailable");
|
|
483
|
+
return capturePanelProjectScope({
|
|
484
|
+
environmentDir: opts.dataDir,
|
|
485
|
+
environmentId: environment.id,
|
|
486
|
+
projectPath: panelBinding.bindingCwd,
|
|
487
|
+
cwd: workspaceCwd,
|
|
488
|
+
assertAuthority: panelBinding.assertBinding,
|
|
489
|
+
});
|
|
490
|
+
},
|
|
465
491
|
automations: {
|
|
466
492
|
conditionalMutations: true,
|
|
467
493
|
call: (scope, method, params) => {
|
|
@@ -506,7 +532,11 @@ export async function startHeadlessServer(opts) {
|
|
|
506
532
|
dataDir: opts.dataDir,
|
|
507
533
|
isAuthorized: configurationOptions.isAuthorized,
|
|
508
534
|
isRunning,
|
|
509
|
-
onChanged: (sessionId) =>
|
|
535
|
+
onChanged: (sessionId) => {
|
|
536
|
+
if (!existsSync(join(sessionRootDir, sessionId, "state.json")))
|
|
537
|
+
runReplay.forget(sessionId);
|
|
538
|
+
notify("serve/sessionsChanged", { sessionId });
|
|
539
|
+
},
|
|
510
540
|
})
|
|
511
541
|
: undefined;
|
|
512
542
|
const files = hubAuth
|
|
@@ -1135,7 +1165,9 @@ function replyToHostSessionQuery(message, sessionManager, workspaceCwd, isRunnin
|
|
|
1135
1165
|
result: { type: "sessions", data: sessions },
|
|
1136
1166
|
});
|
|
1137
1167
|
}
|
|
1138
|
-
if (queryType !== "session_detail" &&
|
|
1168
|
+
if (queryType !== "session_detail" &&
|
|
1169
|
+
queryType !== "output_journal" &&
|
|
1170
|
+
queryType !== "output_coverage")
|
|
1139
1171
|
return undefined;
|
|
1140
1172
|
const sessionId = message.params?.sessionId;
|
|
1141
1173
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
@@ -1150,6 +1182,27 @@ function replyToHostSessionQuery(message, sessionManager, workspaceCwd, isRunnin
|
|
|
1150
1182
|
if (resolve(state.cwd) !== workspaceCwd) {
|
|
1151
1183
|
return hostQueryError(message.id, -32001, "Session not found in this workspace");
|
|
1152
1184
|
}
|
|
1185
|
+
if (queryType === "output_coverage") {
|
|
1186
|
+
// Completed RAM entries can be retired once their proof belongs to disk.
|
|
1187
|
+
// Freeze the real current journal head, so a lost live notification or
|
|
1188
|
+
// run finish during pagination cannot hide a later durable prefix.
|
|
1189
|
+
const head = readOutputJournal(sessionManager.getStorageDir(), sessionId, { maxFrames: 1 });
|
|
1190
|
+
const coverage = runReplay?.outputCoverage(sessionId);
|
|
1191
|
+
return JSON.stringify({
|
|
1192
|
+
jsonrpc: "2.0",
|
|
1193
|
+
id: message.id,
|
|
1194
|
+
result: {
|
|
1195
|
+
type: "output_coverage",
|
|
1196
|
+
data: {
|
|
1197
|
+
...coverage,
|
|
1198
|
+
unpaired: !!coverage?.unpaired ||
|
|
1199
|
+
(head.status !== "ok" &&
|
|
1200
|
+
(head.status !== "unavailable" || !!coverage?.journalIdentity)),
|
|
1201
|
+
...(head.through ? { outputCursor: head.through } : {}),
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1153
1206
|
if (queryType === "output_journal") {
|
|
1154
1207
|
const data = readOutputJournal(sessionManager.getStorageDir(), sessionId, {
|
|
1155
1208
|
after: message.params?.after,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.33",
|
|
4
4
|
"description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
64
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
65
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
63
|
+
"@cjhyy/code-shell-capability-coding": "0.9.33",
|
|
64
|
+
"@cjhyy/code-shell-core": "0.9.33",
|
|
65
|
+
"@cjhyy/code-shell-web": "0.9.33",
|
|
66
66
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
67
67
|
"@types/ws": "^8.18.1",
|
|
68
68
|
"ws": "^8.21.0",
|
|
69
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
69
|
+
"@cjhyy/code-shell-link": "0.9.33"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=20.10"
|