@cjhyy/code-shell-server 0.9.22 → 0.9.23
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 +45 -1
- package/dist/bin/code-shell-settings-recovery.d.ts +2 -0
- package/dist/bin/code-shell-settings-recovery.js +4 -0
- package/dist/desktop-web/http-api.js +14 -0
- package/dist/environment-identity.d.ts +4 -0
- package/dist/environment-identity.js +67 -0
- package/dist/hub/auth-store.d.ts +2 -0
- package/dist/hub/auth-store.js +7 -0
- package/dist/index.auth.d.ts +3 -0
- package/dist/index.auth.js +2 -0
- package/dist/index.links.d.ts +1 -0
- package/dist/index.links.js +1 -0
- package/dist/index.panels.d.ts +7 -0
- package/dist/index.panels.js +7 -0
- package/dist/index.remote-relay.d.ts +5 -0
- package/dist/index.remote-relay.js +6 -0
- package/dist/index.serve.d.ts +2 -0
- package/dist/index.serve.js +2 -0
- package/dist/links/http.d.ts +3 -0
- package/dist/links/http.js +11 -2
- package/dist/links/remote-configuration.d.ts +4 -0
- package/dist/links/remote-configuration.js +32 -0
- package/dist/links/service.d.ts +6 -1
- package/dist/links/service.js +241 -19
- package/dist/mobile-remote/mobile-upload-service.d.ts +4 -0
- package/dist/mobile-remote/mobile-upload-service.js +69 -11
- package/dist/mobile-remote/remote-host-manager.d.ts +10 -5
- package/dist/mobile-remote/remote-host-manager.js +53 -5
- package/dist/panels/agent-task-service.js +9 -2
- package/dist/panels/automations.d.ts +49 -0
- package/dist/panels/automations.js +134 -0
- package/dist/panels/bridge-contract.d.ts +5 -3
- package/dist/panels/bridge-contract.js +11 -1
- package/dist/panels/directory-bookmarks.d.ts +9 -1
- package/dist/panels/directory-bookmarks.js +64 -5
- package/dist/panels/execution-gate.d.ts +26 -0
- package/dist/panels/execution-gate.js +70 -0
- package/dist/panels/http.d.ts +26 -2
- package/dist/panels/http.js +7 -1
- package/dist/panels/hub-automations.d.ts +31 -0
- package/dist/panels/hub-automations.js +352 -0
- package/dist/panels/management-http.d.ts +20 -2
- package/dist/panels/management-http.js +15 -2
- package/dist/panels/management.d.ts +26 -4
- package/dist/panels/management.js +382 -39
- package/dist/panels/package-mutation.d.ts +8 -0
- package/dist/panels/package-mutation.js +23 -0
- package/dist/panels/process-service.d.ts +3 -0
- package/dist/panels/process-service.js +77 -4
- package/dist/panels/resources/http.d.ts +11 -0
- package/dist/panels/resources/http.js +74 -0
- package/dist/panels/resources/references.d.ts +5 -0
- package/dist/panels/resources/references.js +26 -0
- package/dist/panels/runtime-services.js +16 -3
- package/dist/panels/runtime.d.ts +16 -4
- package/dist/panels/runtime.js +522 -64
- package/dist/panels/shared-tool-jobs.d.ts +40 -0
- package/dist/panels/shared-tool-jobs.js +37 -0
- package/dist/panels/storage-store.d.ts +21 -0
- package/dist/panels/storage-store.js +43 -0
- package/dist/panels/task-cookie-host.d.ts +26 -0
- package/dist/panels/task-cookie-host.js +145 -0
- package/dist/panels/task-cookies.d.ts +51 -0
- package/dist/panels/task-cookies.js +183 -0
- package/dist/panels/tool-executor.d.ts +21 -5
- package/dist/panels/tool-executor.js +171 -30
- package/dist/panels/tool-jobs-storage.d.ts +2 -0
- package/dist/panels/tool-jobs-storage.js +34 -0
- package/dist/panels/tool-jobs.d.ts +49 -3
- package/dist/panels/tool-jobs.js +270 -15
- package/dist/panels/types.d.ts +43 -0
- package/dist/project-runtime/backup.d.ts +26 -0
- package/dist/project-runtime/backup.js +500 -0
- package/dist/project-runtime/control-server.d.ts +4 -0
- package/dist/project-runtime/control-server.js +19 -1
- package/dist/project-runtime/docker-provider.d.ts +4 -0
- package/dist/project-runtime/docker-provider.js +16 -1
- package/dist/project-runtime/managed-entry.js +4 -0
- package/dist/project-runtime/registry.d.ts +2 -0
- package/dist/project-runtime/registry.js +15 -0
- package/dist/project-runtime/seccomp.d.ts +8 -0
- package/dist/project-runtime/seccomp.js +72 -0
- package/dist/project-runtime/types.d.ts +2 -0
- package/dist/remote-relay/byte-stream.d.ts +9 -0
- package/dist/remote-relay/byte-stream.js +71 -0
- package/dist/remote-relay/connector.d.ts +27 -0
- package/dist/remote-relay/connector.js +197 -0
- package/dist/remote-relay/local-stream.d.ts +10 -0
- package/dist/remote-relay/local-stream.js +120 -0
- package/dist/remote-relay/protocol.d.ts +39 -0
- package/dist/remote-relay/protocol.js +64 -0
- package/dist/serve/automation-worker.d.ts +16 -0
- package/dist/serve/automation-worker.js +126 -0
- package/dist/serve/cli.d.ts +3 -0
- package/dist/serve/cli.js +17 -0
- package/dist/serve/headless-server.d.ts +3 -0
- package/dist/serve/headless-server.js +140 -10
- package/dist/serve/settings-recovery-cli.d.ts +29 -0
- package/dist/serve/settings-recovery-cli.js +128 -0
- package/dist/session-http-responses.d.ts +7 -0
- package/dist/session-http-responses.js +40 -0
- package/dist/worker-bridge-core.d.ts +5 -0
- package/dist/worker-bridge-core.js +23 -0
- package/package.json +15 -6
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { HubAutomationUncertainError, HubAutomationCancelledError, } from "../panels/hub-automations.js";
|
|
3
|
+
/** Borrows the project's live Worker so persisted Session state has one writer. */
|
|
4
|
+
export function createHubAutomationWorker(options) {
|
|
5
|
+
const active = new Map();
|
|
6
|
+
const rpc = (method, params, id = `automation-control-${randomUUID()}`) => options.bridge.request(method, params, {
|
|
7
|
+
id,
|
|
8
|
+
consume: true,
|
|
9
|
+
settleOnExit: true,
|
|
10
|
+
failFast: true,
|
|
11
|
+
timeoutMs: 5000,
|
|
12
|
+
meta: { origin: "host", producer: "hub-automation" },
|
|
13
|
+
});
|
|
14
|
+
/** Called before the interactive approval lease router. */
|
|
15
|
+
function handleApproval(params) {
|
|
16
|
+
const entry = active.get(params.sessionId);
|
|
17
|
+
if (!entry)
|
|
18
|
+
return false;
|
|
19
|
+
if (typeof params.requestId !== "string" ||
|
|
20
|
+
!params.request ||
|
|
21
|
+
typeof params.request.toolName !== "string")
|
|
22
|
+
return true;
|
|
23
|
+
if (entry.approvals.has(params.requestId))
|
|
24
|
+
return true;
|
|
25
|
+
entry.approvals.add(params.requestId);
|
|
26
|
+
void (async () => {
|
|
27
|
+
const request = params.request;
|
|
28
|
+
const decision = request.toolName.startsWith("__")
|
|
29
|
+
? {
|
|
30
|
+
approved: false,
|
|
31
|
+
failure: "unavailable",
|
|
32
|
+
reason: "Unattended runs cannot use a page-owned capability",
|
|
33
|
+
}
|
|
34
|
+
: await entry.request.approvalBackend.requestApproval(request);
|
|
35
|
+
if (active.get(params.sessionId) !== entry || entry.request.signal?.aborted)
|
|
36
|
+
return;
|
|
37
|
+
const outcome = await rpc("agent/approve", {
|
|
38
|
+
sessionId: params.sessionId,
|
|
39
|
+
requestId: params.requestId,
|
|
40
|
+
connectionId: params.connectionId,
|
|
41
|
+
generation: params.generation,
|
|
42
|
+
decision,
|
|
43
|
+
});
|
|
44
|
+
if (outcome.status !== "result" && active.get(params.sessionId) === entry) {
|
|
45
|
+
await rpc("agent/cancel", { sessionId: params.sessionId });
|
|
46
|
+
}
|
|
47
|
+
})().catch(async () => {
|
|
48
|
+
if (active.get(params.sessionId) === entry)
|
|
49
|
+
await rpc("agent/cancel", { sessionId: params.sessionId });
|
|
50
|
+
});
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async function execute(request) {
|
|
54
|
+
request.signal?.throwIfAborted();
|
|
55
|
+
const sessionId = request.job.resumeSessionId;
|
|
56
|
+
if (!sessionId || request.job.cwd !== options.cwd || !request.job.lastRunId)
|
|
57
|
+
throw Error("Automation requires a persisted run identity and project Session");
|
|
58
|
+
if (active.has(sessionId))
|
|
59
|
+
throw Error("The Session already has an active automation");
|
|
60
|
+
const requestId = `automation-run-${request.job.lastRunId}`;
|
|
61
|
+
const release = options.reserve(request.job, requestId);
|
|
62
|
+
const entry = { request, requestId, approvals: new Set() };
|
|
63
|
+
active.set(sessionId, entry);
|
|
64
|
+
let cancel;
|
|
65
|
+
try {
|
|
66
|
+
const model = options.model();
|
|
67
|
+
if (!model)
|
|
68
|
+
throw Error("Configure a default text model before running automations");
|
|
69
|
+
request.signal?.throwIfAborted();
|
|
70
|
+
const result = options.bridge.request("agent/run", {
|
|
71
|
+
sessionId,
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
task: request.prompt,
|
|
74
|
+
model,
|
|
75
|
+
requireExisting: true,
|
|
76
|
+
clientMessageId: requestId,
|
|
77
|
+
displayText: request.prompt,
|
|
78
|
+
disableGoal: true,
|
|
79
|
+
permissionMode: request.permissionMode,
|
|
80
|
+
sandboxMode: request.sandboxMode,
|
|
81
|
+
allowBackgroundShells: false,
|
|
82
|
+
}, {
|
|
83
|
+
id: requestId,
|
|
84
|
+
timeoutMs: options.admissionTimeoutMs ?? 60000,
|
|
85
|
+
consume: true,
|
|
86
|
+
settleOnExit: true,
|
|
87
|
+
failFast: true,
|
|
88
|
+
waitForRunCompletion: true,
|
|
89
|
+
ensureWorker: true,
|
|
90
|
+
ensureWorkerCwd: options.cwd,
|
|
91
|
+
meta: { origin: "host", producer: "hub-automation" },
|
|
92
|
+
});
|
|
93
|
+
cancel = () => {
|
|
94
|
+
void rpc("agent/cancel", { sessionId });
|
|
95
|
+
};
|
|
96
|
+
request.signal?.addEventListener("abort", cancel, { once: true });
|
|
97
|
+
if (request.signal?.aborted)
|
|
98
|
+
cancel();
|
|
99
|
+
const outcome = await result;
|
|
100
|
+
if (outcome.status === "timeout" || outcome.status === "sendFailed") {
|
|
101
|
+
// Admission may have reached the child despite a lost ACK/write error.
|
|
102
|
+
// Never release the Session based only on a local transport timeout.
|
|
103
|
+
await options.bridge.stopAndWait();
|
|
104
|
+
throw new HubAutomationUncertainError("Worker admission was not confirmed; inspect results before resuming");
|
|
105
|
+
}
|
|
106
|
+
if (outcome.status === "workerExit")
|
|
107
|
+
throw new HubAutomationUncertainError("Worker exited before recording the automation outcome");
|
|
108
|
+
if (outcome.status === "error")
|
|
109
|
+
throw Error(outcome.error.message ?? "Automation Worker rejected the run");
|
|
110
|
+
const value = outcome.result;
|
|
111
|
+
if (request.signal?.aborted)
|
|
112
|
+
return;
|
|
113
|
+
if (value?.reason === "aborted_streaming" || value?.reason === "aborted_tools")
|
|
114
|
+
throw new HubAutomationCancelledError("Automation was cancelled");
|
|
115
|
+
if (value?.reason !== "completed")
|
|
116
|
+
throw Error(`Automation ended without completion (${value?.reason ?? "unknown"})`);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
if (cancel)
|
|
120
|
+
request.signal?.removeEventListener("abort", cancel);
|
|
121
|
+
active.delete(sessionId);
|
|
122
|
+
release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { execute, handleApproval, ownsSession: (sessionId) => active.has(sessionId) };
|
|
126
|
+
}
|
package/dist/serve/cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RemoteLinkConfiguration } from "@cjhyy/code-shell-core";
|
|
1
2
|
interface CliArgs {
|
|
2
3
|
cwd: string;
|
|
3
4
|
host: string;
|
|
@@ -6,8 +7,10 @@ interface CliArgs {
|
|
|
6
7
|
authMode: "hub" | "passcode";
|
|
7
8
|
runtime: "local" | "docker";
|
|
8
9
|
runtimeImage?: string;
|
|
10
|
+
runtimeSeccompProfile?: string;
|
|
9
11
|
debugLogs?: boolean;
|
|
10
12
|
publicOrigin?: string;
|
|
13
|
+
remoteLink?: RemoteLinkConfiguration;
|
|
11
14
|
dataDir: string;
|
|
12
15
|
staticRootDir?: string;
|
|
13
16
|
}
|
package/dist/serve/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remoteLinkFromEnvironment } from "../links/remote-configuration.js";
|
|
1
2
|
// packages/server/src/serve/cli.ts
|
|
2
3
|
//
|
|
3
4
|
// `code-shell-serve` boots a single-workspace Hub under Node.js.
|
|
@@ -20,6 +21,7 @@ Options:
|
|
|
20
21
|
--auth <mode> hub (default) or legacy passcode
|
|
21
22
|
--runtime <mode> local (default) or Docker project sandboxes
|
|
22
23
|
--runtime-image <tag> Prebuilt project image (default: codeshell-project-runtime:local)
|
|
24
|
+
--runtime-seccomp-profile <path> Administrator-owned Docker seccomp JSON
|
|
23
25
|
--public-url <url> External HTTPS origin for reverse-proxy deployments
|
|
24
26
|
--passcode <code> Set or rotate the legacy passcode (selects passcode mode)
|
|
25
27
|
--data-dir <path> Persistent server data (auth, uploads and worker sessions)
|
|
@@ -34,6 +36,7 @@ const VALUE_FLAGS = new Set([
|
|
|
34
36
|
"--auth",
|
|
35
37
|
"--runtime",
|
|
36
38
|
"--runtime-image",
|
|
39
|
+
"--runtime-seccomp-profile",
|
|
37
40
|
"--public-url",
|
|
38
41
|
"--data-dir",
|
|
39
42
|
"--static-root",
|
|
@@ -73,6 +76,10 @@ export function parseServeArgs(argv, env = process.env) {
|
|
|
73
76
|
throw new Error("Docker projects require --auth hub");
|
|
74
77
|
if (args["runtime-image"] && runtime !== "docker")
|
|
75
78
|
throw new Error("--runtime-image requires --runtime docker");
|
|
79
|
+
if (args["runtime-seccomp-profile"] !== undefined && runtime !== "docker")
|
|
80
|
+
throw new Error("--runtime-seccomp-profile requires --runtime docker");
|
|
81
|
+
if (args["runtime-seccomp-profile"] === "")
|
|
82
|
+
throw new Error("Empty runtime seccomp profile");
|
|
76
83
|
let publicOrigin;
|
|
77
84
|
const publicUrl = args["public-url"] ?? env.CODE_SHELL_SERVE_PUBLIC_URL;
|
|
78
85
|
if (publicUrl) {
|
|
@@ -88,10 +95,17 @@ export function parseServeArgs(argv, env = process.env) {
|
|
|
88
95
|
}
|
|
89
96
|
publicOrigin = url.origin;
|
|
90
97
|
}
|
|
98
|
+
const remoteLink = remoteLinkFromEnvironment(env, publicOrigin);
|
|
99
|
+
if (remoteLink && authMode !== "hub")
|
|
100
|
+
throw new Error("Remote Link requires --auth hub");
|
|
91
101
|
return {
|
|
92
102
|
authMode,
|
|
103
|
+
...(remoteLink ? { remoteLink } : {}),
|
|
93
104
|
runtime,
|
|
94
105
|
...(args["runtime-image"] ? { runtimeImage: args["runtime-image"] } : {}),
|
|
106
|
+
...(args["runtime-seccomp-profile"]
|
|
107
|
+
? { runtimeSeccompProfile: resolve(args["runtime-seccomp-profile"]) }
|
|
108
|
+
: {}),
|
|
95
109
|
...(args["debug-logs"] ? { debugLogs: true } : {}),
|
|
96
110
|
...(publicOrigin ? { publicOrigin } : {}),
|
|
97
111
|
cwd: resolve(args.cwd ?? process.cwd()),
|
|
@@ -143,8 +157,10 @@ export async function runServeCli(argv = process.argv.slice(2)) {
|
|
|
143
157
|
port: parsed.port,
|
|
144
158
|
dataDir: parsed.dataDir,
|
|
145
159
|
publicOrigin: parsed.publicOrigin,
|
|
160
|
+
remoteLink: parsed.remoteLink,
|
|
146
161
|
staticRootDir,
|
|
147
162
|
runtimeImage: parsed.runtimeImage,
|
|
163
|
+
runtimeSeccompProfile: parsed.runtimeSeccompProfile,
|
|
148
164
|
})
|
|
149
165
|
: await startHeadlessServer({
|
|
150
166
|
host: parsed.host,
|
|
@@ -152,6 +168,7 @@ export async function runServeCli(argv = process.argv.slice(2)) {
|
|
|
152
168
|
cwd: parsed.cwd,
|
|
153
169
|
dataDir: parsed.dataDir,
|
|
154
170
|
authMode: parsed.authMode,
|
|
171
|
+
remoteLink: parsed.remoteLink,
|
|
155
172
|
debugLogs: parsed.debugLogs,
|
|
156
173
|
...(parsed.publicOrigin ? { publicOrigin: parsed.publicOrigin } : {}),
|
|
157
174
|
workerEntryPath: resolveWorkerEntry(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RemoteLinkConfiguration } from "@cjhyy/code-shell-core";
|
|
1
2
|
import { AccessPasscode } from "../mobile-remote/access-passcode.js";
|
|
2
3
|
import { WorkerBridgeCore, type WorkerBridgeLog } from "../worker-bridge-core.js";
|
|
3
4
|
export interface HeadlessServeOptions {
|
|
@@ -36,6 +37,8 @@ export interface HeadlessServeOptions {
|
|
|
36
37
|
debugLogs?: boolean;
|
|
37
38
|
/** Exact external origin; required for HTTPS reverse proxies. Never trust forwarded headers. */
|
|
38
39
|
publicOrigin?: string;
|
|
40
|
+
/** Trusted deployment configuration; not a browser-supplied provider URL. */
|
|
41
|
+
remoteLink?: RemoteLinkConfiguration;
|
|
39
42
|
/** Trusted project gateway prefix; generated by the controller, never a request parameter. */
|
|
40
43
|
publicPathPrefix?: string;
|
|
41
44
|
/** Session revocation/expiry sweep interval; defaults to 5 seconds. */
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createHubPanelAutomationHost } from "../panels/hub-automations.js";
|
|
2
|
+
import { createHubAutomationWorker } from "./automation-worker.js";
|
|
3
|
+
import { remoteLinkHostConfiguration } from "../links/remote-configuration.js";
|
|
4
|
+
import { describeEnvironment, environmentIdentity } from "../environment-identity.js";
|
|
1
5
|
// packages/server/src/serve/headless-server.ts
|
|
2
6
|
//
|
|
3
7
|
// Single-workspace Node host. The CLI uses Hub administrator sessions; the
|
|
@@ -34,8 +38,12 @@ import { createPanelHttp } from "../panels/http.js";
|
|
|
34
38
|
import { createHubPanelBinding } from "../panels/hub-binding.js";
|
|
35
39
|
import { createHubSessions, hubSessionPreview, readHubSessionState, readHubTranscript, } from "../hub/session-management.js";
|
|
36
40
|
import { createHubFiles } from "../hub/files.js";
|
|
41
|
+
import { createSessionHttpResponses } from "../session-http-responses.js";
|
|
37
42
|
import { WorkerBridgeCore, previewLine } from "../worker-bridge-core.js";
|
|
38
43
|
export async function startHeadlessServer(opts) {
|
|
44
|
+
const remoteLink = remoteLinkHostConfiguration(opts.remoteLink, opts.publicOrigin);
|
|
45
|
+
if (remoteLink && opts.authMode !== "hub")
|
|
46
|
+
throw new Error("Remote Link requires Hub authentication.");
|
|
39
47
|
const host = opts.host ?? "127.0.0.1";
|
|
40
48
|
const log = (event, data) => {
|
|
41
49
|
if (!opts.log)
|
|
@@ -45,6 +53,9 @@ export async function startHeadlessServer(opts) {
|
|
|
45
53
|
const { raw: _raw, text: _text, ...metadata } = data ?? {};
|
|
46
54
|
opts.log(event, opts.debugLogs ? data : metadata);
|
|
47
55
|
};
|
|
56
|
+
const environment = opts.authMode === "hub"
|
|
57
|
+
? describeEnvironment(await environmentIdentity(opts.dataDir), "hub")
|
|
58
|
+
: undefined;
|
|
48
59
|
const workspaceCwd = resolve(opts.cwd);
|
|
49
60
|
const panelBinding = opts.authMode === "hub" ? createHubPanelBinding(workspaceCwd) : undefined;
|
|
50
61
|
const workerDataRoot = resolve(opts.workerDataRoot ??
|
|
@@ -72,6 +83,10 @@ export async function startHeadlessServer(opts) {
|
|
|
72
83
|
generatedPasscode = randomBytes(6).toString("base64url");
|
|
73
84
|
passcode.set(generatedPasscode);
|
|
74
85
|
}
|
|
86
|
+
let closing = false;
|
|
87
|
+
const responses = createSessionHttpResponses();
|
|
88
|
+
let panelAutomations;
|
|
89
|
+
let automationWorker;
|
|
75
90
|
const tabs = new Set();
|
|
76
91
|
// Auth revocation callbacks are registered before configuration modules are ready.
|
|
77
92
|
// eslint-disable-next-line prefer-const
|
|
@@ -88,6 +103,7 @@ export async function startHeadlessServer(opts) {
|
|
|
88
103
|
dataDir: opts.dataDir,
|
|
89
104
|
publicOrigin: opts.publicOrigin,
|
|
90
105
|
onRevoke: (sessionId) => {
|
|
106
|
+
responses.cancelOwner(sessionId);
|
|
91
107
|
mcp?.cancelOwner(sessionId);
|
|
92
108
|
configuration?.cancelOwner(sessionId);
|
|
93
109
|
links?.cancelOwner(sessionId);
|
|
@@ -112,7 +128,7 @@ export async function startHeadlessServer(opts) {
|
|
|
112
128
|
let configurationChanging = false;
|
|
113
129
|
let configurationReloadFailed = false;
|
|
114
130
|
const runUploads = new Map();
|
|
115
|
-
const isRunning = (sessionId) => [...runningSessions.values()].includes(sessionId);
|
|
131
|
+
const isRunning = (sessionId) => [...runningSessions.values()].includes(sessionId) || !!automationWorker?.ownsSession(sessionId);
|
|
116
132
|
let nextTabId = 1;
|
|
117
133
|
let nextWorkerRequestId = 1;
|
|
118
134
|
const outbound = new HubOutboundTransport({
|
|
@@ -254,23 +270,35 @@ export async function startHeadlessServer(opts) {
|
|
|
254
270
|
if (approval &&
|
|
255
271
|
typeof approval.requestId === "string" &&
|
|
256
272
|
typeof approval.sessionId === "string") {
|
|
273
|
+
if (automationWorker?.handleApproval(approval))
|
|
274
|
+
return;
|
|
257
275
|
const request = approval.request;
|
|
258
276
|
if (request?.toolName === "__panel_action__") {
|
|
259
277
|
const ownerId = runOwners.get(approval.sessionId);
|
|
260
278
|
const result = ownerId && panels
|
|
261
279
|
? panels.panelAction(ownerId, approval.sessionId, request.args ?? {})
|
|
262
280
|
: Promise.resolve({ ok: false, detail: "当前对话没有可用的面板连接。" });
|
|
263
|
-
void result
|
|
281
|
+
void result
|
|
282
|
+
.catch(() => ({ ok: false, detail: "面板操作未完成。" }))
|
|
283
|
+
.then((answer) => {
|
|
264
284
|
if (!isRunning(approval.sessionId) || runOwners.get(approval.sessionId) !== ownerId)
|
|
265
285
|
return;
|
|
266
286
|
return bridge.request("agent/approve", {
|
|
267
|
-
sessionId: approval.sessionId,
|
|
268
|
-
|
|
287
|
+
sessionId: approval.sessionId,
|
|
288
|
+
requestId: approval.requestId,
|
|
289
|
+
connectionId: approval.connectionId,
|
|
290
|
+
generation: approval.generation,
|
|
269
291
|
decision: { approved: true, answer: JSON.stringify(answer) },
|
|
270
|
-
}, {
|
|
292
|
+
}, {
|
|
293
|
+
id: `panel-reply-${randomUUID()}`,
|
|
294
|
+
consume: true,
|
|
295
|
+
settleOnExit: true,
|
|
296
|
+
failFast: true,
|
|
297
|
+
timeoutMs: 5000,
|
|
271
298
|
meta: { origin: "host", producer: "hub-panel-action" },
|
|
272
299
|
});
|
|
273
|
-
})
|
|
300
|
+
})
|
|
301
|
+
.catch(() => { });
|
|
274
302
|
return;
|
|
275
303
|
}
|
|
276
304
|
try {
|
|
@@ -346,6 +374,7 @@ export async function startHeadlessServer(opts) {
|
|
|
346
374
|
fallbackCwd: () => opts.cwd,
|
|
347
375
|
buildEnv: () => ({
|
|
348
376
|
...process.env,
|
|
377
|
+
CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
|
|
349
378
|
CODE_SHELL_DATA_ROOT: workerDataRoot,
|
|
350
379
|
CODE_SHELL_CREDENTIAL_ACCESS: "local",
|
|
351
380
|
...(opts.workerCapabilityModules
|
|
@@ -377,6 +406,7 @@ export async function startHeadlessServer(opts) {
|
|
|
377
406
|
preparingRuns > 0 ||
|
|
378
407
|
runningSessions.size > 0 ||
|
|
379
408
|
(panels?.activeTaskCount() ?? 0) > 0 ||
|
|
409
|
+
(panelAutomations?.activeCount() ?? 0) > 0 ||
|
|
380
410
|
pendingWorkerResponses.size > 0) {
|
|
381
411
|
throw new HubConfigurationError(409, "请等待当前任务完成后再保存配置。");
|
|
382
412
|
}
|
|
@@ -420,6 +450,7 @@ export async function startHeadlessServer(opts) {
|
|
|
420
450
|
links = hubAuth
|
|
421
451
|
? createLinkHttp({
|
|
422
452
|
...configurationOptions,
|
|
453
|
+
remoteLink: () => remoteLink,
|
|
423
454
|
onChanged: () => notify("serve/configurationChanged", {}),
|
|
424
455
|
})
|
|
425
456
|
: undefined;
|
|
@@ -429,14 +460,25 @@ export async function startHeadlessServer(opts) {
|
|
|
429
460
|
...panelBinding,
|
|
430
461
|
dataDir: opts.dataDir,
|
|
431
462
|
host: "hub",
|
|
463
|
+
automations: {
|
|
464
|
+
conditionalMutations: true,
|
|
465
|
+
call: (scope, method, params) => {
|
|
466
|
+
if (!panelAutomations || closing)
|
|
467
|
+
throw Error("Cloud automations are unavailable");
|
|
468
|
+
return panelAutomations.host.call(scope, method, params);
|
|
469
|
+
},
|
|
470
|
+
},
|
|
432
471
|
publicPathPrefix: opts.publicPathPrefix,
|
|
433
472
|
agentTaskOptions: {
|
|
434
473
|
workerEntryPath: opts.workerEntryPath,
|
|
435
474
|
buildEnv: () => ({
|
|
436
475
|
...process.env,
|
|
476
|
+
CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
|
|
437
477
|
CODE_SHELL_DATA_ROOT: workerDataRoot,
|
|
438
478
|
CODE_SHELL_CREDENTIAL_ACCESS: "local",
|
|
439
|
-
...(opts.workerCapabilityModules
|
|
479
|
+
...(opts.workerCapabilityModules
|
|
480
|
+
? { CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules }
|
|
481
|
+
: {}),
|
|
440
482
|
}),
|
|
441
483
|
},
|
|
442
484
|
onChanged: () => notify("serve/configurationChanged", {}),
|
|
@@ -535,6 +577,15 @@ export async function startHeadlessServer(opts) {
|
|
|
535
577
|
hubJson(res, 403, { error: "origin rejected" });
|
|
536
578
|
return;
|
|
537
579
|
}
|
|
580
|
+
if (closing) {
|
|
581
|
+
hubJson(res, 503, { error: "server is closing" });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
responses.track(session.id, res);
|
|
585
|
+
if (pathname === "/api/v1/environment" && req.method === "GET") {
|
|
586
|
+
hubJson(res, 200, environment);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
538
589
|
if (await configuration.handle(req, res))
|
|
539
590
|
return;
|
|
540
591
|
if (await links.handle(req, res))
|
|
@@ -616,6 +667,10 @@ export async function startHeadlessServer(opts) {
|
|
|
616
667
|
ws.close(1009, "payload too large");
|
|
617
668
|
return;
|
|
618
669
|
}
|
|
670
|
+
if (closing) {
|
|
671
|
+
ws.close(1012, "server stopping");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
619
674
|
const line = String(data);
|
|
620
675
|
// Validate framing before touching the worker: a malformed frame from
|
|
621
676
|
// one tab must never kill the shared pipe.
|
|
@@ -798,7 +853,8 @@ export async function startHeadlessServer(opts) {
|
|
|
798
853
|
const runSession = workerMessage.params?.sessionId;
|
|
799
854
|
const ownerId = tabAuth.get(ws)?.sessionId;
|
|
800
855
|
const activeOwner = typeof runSession === "string" ? runOwners.get(runSession) : undefined;
|
|
801
|
-
if (activeOwner && activeOwner !== ownerId)
|
|
856
|
+
if ((activeOwner && activeOwner !== ownerId) ||
|
|
857
|
+
(typeof runSession === "string" && automationWorker?.ownsSession(runSession))) {
|
|
802
858
|
sendToTab(ws, hostQueryError(workerMessage.id, -32009, "此对话正在另一台设备运行,请等待完成后再发送。"), "run-owner");
|
|
803
859
|
return;
|
|
804
860
|
}
|
|
@@ -900,6 +956,69 @@ export async function startHeadlessServer(opts) {
|
|
|
900
956
|
void upgrade(req, socket, head).catch(() => socket.destroy());
|
|
901
957
|
});
|
|
902
958
|
try {
|
|
959
|
+
if (hubAuth) {
|
|
960
|
+
automationWorker = createHubAutomationWorker({
|
|
961
|
+
bridge,
|
|
962
|
+
cwd: workspaceCwd,
|
|
963
|
+
model: () => new SettingsManager(workspaceCwd, "full").get().defaults.text,
|
|
964
|
+
reserve: (job, requestId) => {
|
|
965
|
+
if (closing || configurationChanging || configurationReloadFailed || preparingRuns > 0)
|
|
966
|
+
throw Error("Project configuration or run preparation is busy");
|
|
967
|
+
const sessionId = job.resumeSessionId;
|
|
968
|
+
if (isRunning(sessionId) || runningSessions.size >= 64)
|
|
969
|
+
throw Error("The automation Session is busy; this occurrence was not dispatched");
|
|
970
|
+
if (resolve(readHubSessionState(sessionRootDir, sessionId).cwd) !== workspaceCwd)
|
|
971
|
+
throw Error("Automation Session changed workspace");
|
|
972
|
+
const baseline = readHubTranscript(sessionRootDir, sessionId).length;
|
|
973
|
+
runningSessions.set(requestId, sessionId);
|
|
974
|
+
runOwners.set(sessionId, requestId);
|
|
975
|
+
runReplay.begin(sessionId, requestId, baseline);
|
|
976
|
+
runInputs.set(requestId, {
|
|
977
|
+
sessionId,
|
|
978
|
+
echoed: false,
|
|
979
|
+
event: {
|
|
980
|
+
type: "session_user_message",
|
|
981
|
+
text: job.prompt,
|
|
982
|
+
clientMessageId: requestId,
|
|
983
|
+
attachments: [],
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
notify("serve/sessionStatus", { sessionId, running: true });
|
|
987
|
+
return () => {
|
|
988
|
+
runningSessions.delete(requestId);
|
|
989
|
+
runInputs.delete(requestId);
|
|
990
|
+
if (runOwners.get(sessionId) === requestId)
|
|
991
|
+
runOwners.delete(sessionId);
|
|
992
|
+
runReplay.finish(sessionId, requestId);
|
|
993
|
+
notify("serve/sessionStatus", { sessionId, running: isRunning(sessionId) });
|
|
994
|
+
};
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
panelAutomations = createHubPanelAutomationHost({
|
|
998
|
+
cwd: workspaceCwd,
|
|
999
|
+
bindingCwd: panelBinding?.bindingCwd,
|
|
1000
|
+
dataDir: opts.dataDir,
|
|
1001
|
+
sessionRootDir,
|
|
1002
|
+
assertExecutable: async (job) => {
|
|
1003
|
+
panelBinding?.assertBinding();
|
|
1004
|
+
const panel = (await panels.service.snapshot()).panels.find((app) => app.id === job.panelSource?.appId);
|
|
1005
|
+
panelBinding?.assertBinding();
|
|
1006
|
+
if (!panel?.enabled ||
|
|
1007
|
+
panel.revision !== job.panelSource?.revision ||
|
|
1008
|
+
!["automations.manage", "context.workspace", "context.session"].every((permission) => panel.permissions.includes(permission)))
|
|
1009
|
+
throw Error("The Panel package or automation permission is no longer authorized");
|
|
1010
|
+
},
|
|
1011
|
+
execute: (request) => automationWorker.execute(request),
|
|
1012
|
+
onJobEvent: (event) => notify("serve/automationStatus", {
|
|
1013
|
+
type: event.type,
|
|
1014
|
+
jobId: event.job.id,
|
|
1015
|
+
sessionId: event.job.resumeSessionId,
|
|
1016
|
+
appId: event.job.panelSource?.appId,
|
|
1017
|
+
...("reason" in event ? { reason: event.reason } : {}),
|
|
1018
|
+
...("error" in event ? { error: event.error } : {}),
|
|
1019
|
+
}),
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
903
1022
|
await new Promise((resolve, reject) => {
|
|
904
1023
|
server.once("error", reject);
|
|
905
1024
|
server.listen(opts.port ?? 8790, host, () => {
|
|
@@ -909,6 +1028,9 @@ export async function startHeadlessServer(opts) {
|
|
|
909
1028
|
});
|
|
910
1029
|
}
|
|
911
1030
|
catch (error) {
|
|
1031
|
+
closing = true;
|
|
1032
|
+
responses.clear();
|
|
1033
|
+
await panelAutomations?.close();
|
|
912
1034
|
clearInterval(pendingResponseReaper);
|
|
913
1035
|
if (authReaper)
|
|
914
1036
|
clearInterval(authReaper);
|
|
@@ -919,7 +1041,7 @@ export async function startHeadlessServer(opts) {
|
|
|
919
1041
|
links?.close();
|
|
920
1042
|
await panels?.close();
|
|
921
1043
|
files?.close();
|
|
922
|
-
bridge.
|
|
1044
|
+
await bridge.stopAndWait();
|
|
923
1045
|
wss.close();
|
|
924
1046
|
server.close();
|
|
925
1047
|
throw error;
|
|
@@ -939,6 +1061,9 @@ export async function startHeadlessServer(opts) {
|
|
|
939
1061
|
tabCount: () => tabs.size,
|
|
940
1062
|
pendingResponseCount: () => pendingWorkerResponses.size,
|
|
941
1063
|
close: async () => {
|
|
1064
|
+
closing = true;
|
|
1065
|
+
responses.clear();
|
|
1066
|
+
await panelAutomations?.close();
|
|
942
1067
|
clearInterval(pendingResponseReaper);
|
|
943
1068
|
if (authReaper)
|
|
944
1069
|
clearInterval(authReaper);
|
|
@@ -958,7 +1083,7 @@ export async function startHeadlessServer(opts) {
|
|
|
958
1083
|
}
|
|
959
1084
|
}
|
|
960
1085
|
tabs.clear();
|
|
961
|
-
bridge.
|
|
1086
|
+
await bridge.stopAndWait();
|
|
962
1087
|
await new Promise((resolve) => {
|
|
963
1088
|
wss.close(() => {
|
|
964
1089
|
server.close(() => resolve());
|
|
@@ -1077,6 +1202,11 @@ function authorizeServeRequest(message, sessionManager, workspaceCwd, isRunning
|
|
|
1077
1202
|
if (!message.method || !SERVE_ALLOWED_WORKER_METHODS.has(message.method)) {
|
|
1078
1203
|
return hostQueryError(message.id ?? null, -32601, "Method is not available in Web serve mode");
|
|
1079
1204
|
}
|
|
1205
|
+
if (message.method === "agent/run" &&
|
|
1206
|
+
(Object.hasOwn(message.params ?? {}, "sandboxMode") ||
|
|
1207
|
+
Object.hasOwn(message.params ?? {}, "allowBackgroundShells"))) {
|
|
1208
|
+
return hostQueryError(message.id ?? null, -32602, "Execution policy is selected by the host");
|
|
1209
|
+
}
|
|
1080
1210
|
const rawSessionId = message.params?.sessionId;
|
|
1081
1211
|
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : null;
|
|
1082
1212
|
if (message.method === "agent/run" && sessionId === null)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RecoveryScope } from "@cjhyy/code-shell-core/internal";
|
|
2
|
+
type RecoveryArguments = {
|
|
3
|
+
action: "help";
|
|
4
|
+
} | {
|
|
5
|
+
action: "inspect";
|
|
6
|
+
cwd: string;
|
|
7
|
+
scope: RecoveryScope;
|
|
8
|
+
candidatePath?: string;
|
|
9
|
+
} | {
|
|
10
|
+
action: "repair";
|
|
11
|
+
cwd: string;
|
|
12
|
+
scope: RecoveryScope;
|
|
13
|
+
candidatePath: string;
|
|
14
|
+
expectedRevision: string;
|
|
15
|
+
candidateSha256: string;
|
|
16
|
+
} | {
|
|
17
|
+
action: "restore";
|
|
18
|
+
cwd: string;
|
|
19
|
+
scope: RecoveryScope;
|
|
20
|
+
backupId: string;
|
|
21
|
+
expectedRevision: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function parseSettingsRecoveryArgs(argv: string[], cwd: () => string): RecoveryArguments;
|
|
24
|
+
export declare function runSettingsRecoveryCli(argv?: string[], io?: {
|
|
25
|
+
cwd?: () => string;
|
|
26
|
+
write?: (text: string) => void;
|
|
27
|
+
error?: (text: string) => void;
|
|
28
|
+
}): Promise<number>;
|
|
29
|
+
export {};
|