@deepseek-ai/dsh-ssh 0.2.1-alpha.3 → 0.2.1-alpha.4
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/lib/helper.js +30 -26
- package/lib/index.js +2 -2
- package/lib/protocol.js +275 -1
- package/lib/schemas.js +188 -1
- package/package.json +29 -29
- package/lib/protocol-CDhW_IHU.js +0 -276
- package/lib/schemas-BD3YbDxe.js +0 -189
package/lib/helper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { RemoteOperationError, SshRpcPeer } from "./protocol.js";
|
|
2
|
+
import { editSchema, environmentSchema, intentSchema, outputSnapshotFrameLimit, policySchema, processIdSchema, remotePath, spawnSchema, targetSchema, textStreamIdSchema } from "./schemas.js";
|
|
3
3
|
import { t as SSH_STREAM_TLS_OPTIONS } from "./stream-security-DJlQsPG-.js";
|
|
4
4
|
import { chmod, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -813,34 +813,38 @@ async function runSshHelper(transport) {
|
|
|
813
813
|
//#region lib/types/helper-entry.js
|
|
814
814
|
/** Private OpenSSH process entry; the helper module owns request and cleanup behavior. */
|
|
815
815
|
/* v8 ignore file -- launched through plain Node in SSH acceptance; helper behavior is exercised through its explicit streams. */
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
controller
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
"SIGHUP",
|
|
823
|
-
"SIGINT"
|
|
824
|
-
]) process.once(signal, stop);
|
|
825
|
-
try {
|
|
826
|
-
if (process.argv.length !== 2) throw new Error("SSH helper accepts no command arguments");
|
|
827
|
-
const packaged = isSea();
|
|
828
|
-
await runSshHelper({
|
|
829
|
-
input: process.stdin,
|
|
830
|
-
output: process.stdout,
|
|
831
|
-
entryPath: packaged ? process.execPath : fileURLToPath(import.meta.url),
|
|
832
|
-
kind: packaged ? "executable" : "node-script",
|
|
833
|
-
signal: controller.signal
|
|
834
|
-
});
|
|
835
|
-
} catch (error) {
|
|
836
|
-
process.stderr.write(`dsh-ssh-sandbox: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
837
|
-
process.exitCode = 127;
|
|
838
|
-
} finally {
|
|
816
|
+
/** Serve one helper request; every failure is reported on stderr with exit code 127. */
|
|
817
|
+
async function main() {
|
|
818
|
+
const controller = new AbortController();
|
|
819
|
+
const stop = () => {
|
|
820
|
+
controller.abort(/* @__PURE__ */ new Error("SSH helper process terminated"));
|
|
821
|
+
};
|
|
839
822
|
for (const signal of [
|
|
840
823
|
"SIGTERM",
|
|
841
824
|
"SIGHUP",
|
|
842
825
|
"SIGINT"
|
|
843
|
-
]) process.
|
|
826
|
+
]) process.once(signal, stop);
|
|
827
|
+
try {
|
|
828
|
+
if (process.argv.length !== 2) throw new Error("SSH helper accepts no command arguments");
|
|
829
|
+
const packaged = isSea();
|
|
830
|
+
await runSshHelper({
|
|
831
|
+
input: process.stdin,
|
|
832
|
+
output: process.stdout,
|
|
833
|
+
entryPath: packaged ? process.execPath : fileURLToPath(import.meta.url),
|
|
834
|
+
kind: packaged ? "executable" : "node-script",
|
|
835
|
+
signal: controller.signal
|
|
836
|
+
});
|
|
837
|
+
} catch (error) {
|
|
838
|
+
process.stderr.write(`dsh-ssh-sandbox: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
839
|
+
process.exitCode = 127;
|
|
840
|
+
} finally {
|
|
841
|
+
for (const signal of [
|
|
842
|
+
"SIGTERM",
|
|
843
|
+
"SIGHUP",
|
|
844
|
+
"SIGINT"
|
|
845
|
+
]) process.off(signal, stop);
|
|
846
|
+
}
|
|
844
847
|
}
|
|
848
|
+
main();
|
|
845
849
|
//#endregion
|
|
846
850
|
export {};
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { SshRpcPeer } from "./protocol.js";
|
|
2
|
+
import { helloSchema } from "./schemas.js";
|
|
3
3
|
import { n as authenticateStream } from "./stream-security-DJlQsPG-.js";
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
5
|
import { mkdtemp, rm } from "node:fs/promises";
|
package/lib/protocol.js
CHANGED
|
@@ -1,2 +1,276 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
//#region lib/types/protocol.js
|
|
5
|
+
/** Bounded, versioned requests between an SSH client and its private remote helper. */
|
|
6
|
+
/** Wire version shared by the installed helper and client package. */
|
|
7
|
+
const SSH_PROTOCOL_VERSION = 2;
|
|
8
|
+
/** Maximum prepared or running process handles owned by one helper. */
|
|
9
|
+
const SSH_MAX_PROCESS_HANDLES = 128;
|
|
10
|
+
/** Maximum open text iterators owned by one helper. */
|
|
11
|
+
const SSH_MAX_TEXT_STREAMS = 128;
|
|
12
|
+
const managementLimits = {
|
|
13
|
+
heartbeat: 1,
|
|
14
|
+
close: 1,
|
|
15
|
+
"process.terminate": 128,
|
|
16
|
+
"fs.streamClose": 128
|
|
17
|
+
};
|
|
18
|
+
function requestClass(method) {
|
|
19
|
+
switch (method) {
|
|
20
|
+
case "heartbeat":
|
|
21
|
+
case "close":
|
|
22
|
+
case "process.terminate":
|
|
23
|
+
case "fs.streamClose": return method;
|
|
24
|
+
default: return "ordinary";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const errorSchema = z.object({
|
|
28
|
+
name: z.string(),
|
|
29
|
+
message: z.string(),
|
|
30
|
+
code: z.string().optional()
|
|
31
|
+
}).strict();
|
|
32
|
+
const requestIdSchema = z.string().transform((value) => value);
|
|
33
|
+
const frameSchema = z.discriminatedUnion("type", [
|
|
34
|
+
z.object({
|
|
35
|
+
type: z.literal("request"),
|
|
36
|
+
id: requestIdSchema,
|
|
37
|
+
method: z.string(),
|
|
38
|
+
params: z.unknown()
|
|
39
|
+
}).strict(),
|
|
40
|
+
z.object({
|
|
41
|
+
type: z.literal("result"),
|
|
42
|
+
id: requestIdSchema,
|
|
43
|
+
value: z.unknown()
|
|
44
|
+
}).strict(),
|
|
45
|
+
z.object({
|
|
46
|
+
type: z.literal("error"),
|
|
47
|
+
id: requestIdSchema,
|
|
48
|
+
error: errorSchema
|
|
49
|
+
}).strict(),
|
|
50
|
+
z.object({
|
|
51
|
+
type: z.literal("cancel"),
|
|
52
|
+
id: requestIdSchema
|
|
53
|
+
}).strict()
|
|
54
|
+
]);
|
|
55
|
+
function operationError(error) {
|
|
56
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
57
|
+
}
|
|
58
|
+
/** A remote error retains its typed filesystem or sandbox code. */
|
|
59
|
+
var RemoteOperationError = class extends Error {
|
|
60
|
+
code;
|
|
61
|
+
constructor(message, code) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.name = "RemoteOperationError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/** The peer owns pending calls and rejects ambiguous operations on connection loss; it never replays requests. */
|
|
68
|
+
var SshRpcPeer = class extends EventEmitter {
|
|
69
|
+
input;
|
|
70
|
+
output;
|
|
71
|
+
maxFrameBytes;
|
|
72
|
+
maxPending;
|
|
73
|
+
handler;
|
|
74
|
+
pending = /* @__PURE__ */ new Map();
|
|
75
|
+
active = /* @__PURE__ */ new Map();
|
|
76
|
+
writeTail = Promise.resolve();
|
|
77
|
+
queuedBytes = 0;
|
|
78
|
+
failure;
|
|
79
|
+
constructor(input, output, maxFrameBytes, maxPending, handler) {
|
|
80
|
+
super();
|
|
81
|
+
this.input = input;
|
|
82
|
+
this.output = output;
|
|
83
|
+
this.maxFrameBytes = maxFrameBytes;
|
|
84
|
+
this.maxPending = maxPending;
|
|
85
|
+
this.handler = handler;
|
|
86
|
+
input.on("error", (error) => {
|
|
87
|
+
this.close(error);
|
|
88
|
+
});
|
|
89
|
+
output.on("error", (error) => {
|
|
90
|
+
this.close(error);
|
|
91
|
+
});
|
|
92
|
+
output.on("close", () => {
|
|
93
|
+
this.close();
|
|
94
|
+
});
|
|
95
|
+
this.readFrames().catch((error) => {
|
|
96
|
+
this.close(operationError(error));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Send one request and validate its response before exposing it to the caller.
|
|
101
|
+
* @param method - the private helper operation.
|
|
102
|
+
* @param params - JSON request fields.
|
|
103
|
+
* @param schema - validation for the remote response.
|
|
104
|
+
* @param signal - cancellation without rollback of remote effects.
|
|
105
|
+
* @returns the validated response or a transport/remote-operation rejection.
|
|
106
|
+
*/
|
|
107
|
+
async request(method, params, schema, signal) {
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
if (this.failure !== void 0) throw this.failure;
|
|
110
|
+
const kind = requestClass(method);
|
|
111
|
+
if (this.atCapacity(kind, this.pending.values())) throw new Error("SSH helper pending request limit reached");
|
|
112
|
+
const id = randomUUID();
|
|
113
|
+
const result = Promise.withResolvers();
|
|
114
|
+
result.promise.catch(() => {});
|
|
115
|
+
this.pending.set(id, {
|
|
116
|
+
...result,
|
|
117
|
+
requestClass: kind
|
|
118
|
+
});
|
|
119
|
+
const abort = () => {
|
|
120
|
+
result.reject(/* @__PURE__ */ new Error("SSH operation cancelled; a completed remote mutation is not rolled back"));
|
|
121
|
+
this.send({
|
|
122
|
+
type: "cancel",
|
|
123
|
+
id
|
|
124
|
+
}).catch(() => {});
|
|
125
|
+
};
|
|
126
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
127
|
+
try {
|
|
128
|
+
this.send({
|
|
129
|
+
type: "request",
|
|
130
|
+
id,
|
|
131
|
+
method,
|
|
132
|
+
params
|
|
133
|
+
}).catch((error) => {
|
|
134
|
+
this.pending.delete(id);
|
|
135
|
+
result.reject(operationError(error));
|
|
136
|
+
});
|
|
137
|
+
return schema.parse(await result.promise);
|
|
138
|
+
} finally {
|
|
139
|
+
signal?.removeEventListener("abort", abort);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Fail pending operations and abort remote handlers without claiming rollback.
|
|
144
|
+
* @param error - the transport failure reported to all pending operations.
|
|
145
|
+
*/
|
|
146
|
+
close(error = /* @__PURE__ */ new Error("SSH connection lost; remote operation outcome and cleanup are unknown")) {
|
|
147
|
+
if (this.failure !== void 0) return;
|
|
148
|
+
this.failure = error;
|
|
149
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
150
|
+
this.pending.clear();
|
|
151
|
+
for (const { controller } of this.active.values()) controller.abort(error);
|
|
152
|
+
this.active.clear();
|
|
153
|
+
this.input.destroy();
|
|
154
|
+
this.output.destroy();
|
|
155
|
+
this.emit("closed", error);
|
|
156
|
+
}
|
|
157
|
+
async send(frame) {
|
|
158
|
+
if (this.failure !== void 0) return Promise.reject(this.failure);
|
|
159
|
+
const body = Buffer.from(JSON.stringify(frame));
|
|
160
|
+
if (body.length > this.maxFrameBytes || this.queuedBytes + body.length + 4 > this.maxFrameBytes * 2) return Promise.reject(/* @__PURE__ */ new Error("SSH helper frame or write queue limit exceeded"));
|
|
161
|
+
const header = Buffer.alloc(4);
|
|
162
|
+
header.writeUInt32BE(body.length);
|
|
163
|
+
const bytes = Buffer.concat([header, body]);
|
|
164
|
+
this.queuedBytes += bytes.length;
|
|
165
|
+
const write = this.writeTail.then(async () => {
|
|
166
|
+
if (this.failure !== void 0) throw this.failure;
|
|
167
|
+
if (!this.output.write(bytes)) await new Promise((resolve, reject) => {
|
|
168
|
+
const cleanup = () => {
|
|
169
|
+
this.output.off("drain", drained);
|
|
170
|
+
this.off("closed", closed);
|
|
171
|
+
};
|
|
172
|
+
const drained = () => {
|
|
173
|
+
cleanup();
|
|
174
|
+
resolve();
|
|
175
|
+
};
|
|
176
|
+
const closed = (error) => {
|
|
177
|
+
cleanup();
|
|
178
|
+
reject(error);
|
|
179
|
+
};
|
|
180
|
+
this.output.once("drain", drained);
|
|
181
|
+
this.once("closed", closed);
|
|
182
|
+
if (this.failure !== void 0) closed(this.failure);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
this.writeTail = write.catch((error) => {
|
|
186
|
+
this.close(operationError(error));
|
|
187
|
+
});
|
|
188
|
+
return write.finally(() => {
|
|
189
|
+
this.queuedBytes -= bytes.length;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async readFrames() {
|
|
193
|
+
const header = Buffer.alloc(4);
|
|
194
|
+
let headerBytes = 0;
|
|
195
|
+
let payload;
|
|
196
|
+
let payloadBytes = 0;
|
|
197
|
+
for await (const raw of this.input) {
|
|
198
|
+
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
199
|
+
let offset = 0;
|
|
200
|
+
while (offset < chunk.length) {
|
|
201
|
+
if (payload === void 0) {
|
|
202
|
+
const count = Math.min(4 - headerBytes, chunk.length - offset);
|
|
203
|
+
chunk.copy(header, headerBytes, offset, offset + count);
|
|
204
|
+
headerBytes += count;
|
|
205
|
+
offset += count;
|
|
206
|
+
if (headerBytes < 4) continue;
|
|
207
|
+
const size = header.readUInt32BE(0);
|
|
208
|
+
if (size === 0 || size > this.maxFrameBytes) throw new Error("SSH helper sent an invalid frame length");
|
|
209
|
+
payload = Buffer.alloc(size);
|
|
210
|
+
payloadBytes = 0;
|
|
211
|
+
}
|
|
212
|
+
const count = Math.min(payload.length - payloadBytes, chunk.length - offset);
|
|
213
|
+
chunk.copy(payload, payloadBytes, offset, offset + count);
|
|
214
|
+
payloadBytes += count;
|
|
215
|
+
offset += count;
|
|
216
|
+
if (payloadBytes === payload.length) {
|
|
217
|
+
const frame = frameSchema.parse(JSON.parse(payload.toString("utf8")));
|
|
218
|
+
payload = void 0;
|
|
219
|
+
headerBytes = 0;
|
|
220
|
+
this.receive(frame);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error(headerBytes > 0 || payload !== void 0 ? "SSH helper disconnected during a frame; outcome is unknown" : "SSH helper disconnected; outcome is unknown");
|
|
225
|
+
}
|
|
226
|
+
receive(frame) {
|
|
227
|
+
if (frame.type === "result" || frame.type === "error") {
|
|
228
|
+
const pending = this.pending.get(frame.id);
|
|
229
|
+
if (pending === void 0) return;
|
|
230
|
+
this.pending.delete(frame.id);
|
|
231
|
+
if (frame.type === "result") pending.resolve(frame.value);
|
|
232
|
+
else pending.reject(new RemoteOperationError(frame.error.message, frame.error.code));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (frame.type === "cancel") {
|
|
236
|
+
this.active.get(frame.id)?.controller.abort(/* @__PURE__ */ new Error("SSH caller cancelled the operation"));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const kind = requestClass(frame.method);
|
|
240
|
+
if (this.handler === void 0 || this.active.has(frame.id) || this.atCapacity(kind, this.active.values())) throw new Error("SSH helper received an unexpected or excessive request");
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
this.active.set(frame.id, {
|
|
243
|
+
controller,
|
|
244
|
+
requestClass: kind
|
|
245
|
+
});
|
|
246
|
+
this.handler(frame.method, frame.params, controller.signal).then((value) => this.send({
|
|
247
|
+
type: "result",
|
|
248
|
+
id: frame.id,
|
|
249
|
+
value: value ?? null
|
|
250
|
+
}), (error) => {
|
|
251
|
+
const detail = operationError(error);
|
|
252
|
+
const code = "code" in detail && typeof detail.code === "string" ? detail.code : void 0;
|
|
253
|
+
return this.send({
|
|
254
|
+
type: "error",
|
|
255
|
+
id: frame.id,
|
|
256
|
+
error: {
|
|
257
|
+
name: detail.name,
|
|
258
|
+
message: detail.message,
|
|
259
|
+
...code === void 0 ? {} : { code }
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}).catch((error) => {
|
|
263
|
+
this.close(operationError(error));
|
|
264
|
+
}).finally(() => {
|
|
265
|
+
this.active.delete(frame.id);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
atCapacity(kind, requests) {
|
|
269
|
+
const limit = kind === "ordinary" ? this.maxPending : managementLimits[kind];
|
|
270
|
+
let count = 0;
|
|
271
|
+
for (const request of requests) if (request.requestClass === kind) count++;
|
|
272
|
+
return count >= limit;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
2
276
|
export { RemoteOperationError, SSH_MAX_PROCESS_HANDLES, SSH_MAX_TEXT_STREAMS, SSH_PROTOCOL_VERSION, SshRpcPeer };
|
package/lib/schemas.js
CHANGED
|
@@ -1,2 +1,189 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./protocol.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region lib/types/schemas.js
|
|
4
|
+
/** Strict JSON validation for SSH helper requests and remote observations. */
|
|
5
|
+
/** Admit a process identity from the private helper protocol. */
|
|
6
|
+
const processIdSchema = z.uuid().transform((value) => value);
|
|
7
|
+
/** Admit a text iterator identity from the private helper protocol. */
|
|
8
|
+
const textStreamIdSchema = z.uuid().transform((value) => value);
|
|
9
|
+
/** A remote POSIX absolute path; spelling is preserved until remote canonicalization. */
|
|
10
|
+
const remotePath = z.string().min(1).refine((value) => value.startsWith("/") && !value.includes("\0"), "expected an absolute POSIX path");
|
|
11
|
+
/** Filesystem identity returned by the remote filesystem provider. */
|
|
12
|
+
const targetSchema = z.object({
|
|
13
|
+
targetKey: remotePath,
|
|
14
|
+
displayPath: z.string()
|
|
15
|
+
}).strict();
|
|
16
|
+
/** Remote metadata observation. */
|
|
17
|
+
const infoSchema = z.object({
|
|
18
|
+
version: z.string(),
|
|
19
|
+
type: z.enum([
|
|
20
|
+
"file",
|
|
21
|
+
"directory",
|
|
22
|
+
"other"
|
|
23
|
+
]),
|
|
24
|
+
size: z.number().nonnegative().optional()
|
|
25
|
+
}).strict();
|
|
26
|
+
/** Metadata that preserves a final symlink. */
|
|
27
|
+
const pathInfoSchema = infoSchema.extend({ type: z.enum([
|
|
28
|
+
"file",
|
|
29
|
+
"directory",
|
|
30
|
+
"symlink",
|
|
31
|
+
"other"
|
|
32
|
+
]) });
|
|
33
|
+
/** A resolved file-effect policy; the remote helper owns path canonicalization. */
|
|
34
|
+
const policySchema = z.object({
|
|
35
|
+
mode: z.enum([
|
|
36
|
+
"read-only",
|
|
37
|
+
"workspace-write",
|
|
38
|
+
"danger-full-access"
|
|
39
|
+
]),
|
|
40
|
+
workspaceRoot: remotePath,
|
|
41
|
+
sessionId: z.string().optional()
|
|
42
|
+
}).strict();
|
|
43
|
+
/** Complete directory entries. */
|
|
44
|
+
const entriesSchema = z.array(z.object({
|
|
45
|
+
name: z.string(),
|
|
46
|
+
type: z.enum([
|
|
47
|
+
"file",
|
|
48
|
+
"directory",
|
|
49
|
+
"other"
|
|
50
|
+
]),
|
|
51
|
+
target: targetSchema,
|
|
52
|
+
version: z.string().optional(),
|
|
53
|
+
size: z.number().nonnegative().optional()
|
|
54
|
+
}).strict());
|
|
55
|
+
/** Guarded write intent. */
|
|
56
|
+
const intentSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("createIfAbsent") }).strict(), z.object({
|
|
57
|
+
kind: z.literal("replaceIfVersion"),
|
|
58
|
+
version: z.string()
|
|
59
|
+
}).strict()]);
|
|
60
|
+
/** Literal text edit. */
|
|
61
|
+
const editSchema = z.object({
|
|
62
|
+
oldString: z.string(),
|
|
63
|
+
newString: z.string(),
|
|
64
|
+
replaceAll: z.boolean()
|
|
65
|
+
}).strict();
|
|
66
|
+
/** Atomic write observation. */
|
|
67
|
+
const writeResultSchema = z.object({
|
|
68
|
+
operation: z.enum(["create", "update"]),
|
|
69
|
+
version: z.string(),
|
|
70
|
+
before: z.string().nullable(),
|
|
71
|
+
after: z.string()
|
|
72
|
+
}).strict();
|
|
73
|
+
/** Atomic edit observation. */
|
|
74
|
+
const editResultSchema = z.object({
|
|
75
|
+
version: z.string(),
|
|
76
|
+
before: z.string(),
|
|
77
|
+
after: z.string()
|
|
78
|
+
}).strict();
|
|
79
|
+
/** Explicit child environment; null encodes an environment tombstone. */
|
|
80
|
+
const environmentSchema = z.record(z.string(), z.string().nullable());
|
|
81
|
+
const collection = z.object({
|
|
82
|
+
maxBytes: z.number().int().positive(),
|
|
83
|
+
spill: z.object({ maxBytes: z.number().int().positive() }).strict().optional()
|
|
84
|
+
}).strict();
|
|
85
|
+
/** Ordinary or terminal process requested by a trusted SSH client. */
|
|
86
|
+
const spawnSchema = z.object({
|
|
87
|
+
argv: z.array(z.string().refine((value) => !value.includes("\0"))).min(1),
|
|
88
|
+
cwd: remotePath,
|
|
89
|
+
env: environmentSchema.optional(),
|
|
90
|
+
graceMs: z.number().int().positive().max(2147483647),
|
|
91
|
+
stdio: z.object({
|
|
92
|
+
stdin: z.union([
|
|
93
|
+
z.literal("ignore"),
|
|
94
|
+
z.literal("pipe"),
|
|
95
|
+
z.object({ data: z.string() }).strict()
|
|
96
|
+
]),
|
|
97
|
+
stdout: z.union([
|
|
98
|
+
z.literal("pipe"),
|
|
99
|
+
z.literal("inherit"),
|
|
100
|
+
collection
|
|
101
|
+
]),
|
|
102
|
+
stderr: z.union([
|
|
103
|
+
z.literal("pipe"),
|
|
104
|
+
z.literal("inherit"),
|
|
105
|
+
collection
|
|
106
|
+
]),
|
|
107
|
+
control: z.literal("pipe").optional()
|
|
108
|
+
}).strict().optional(),
|
|
109
|
+
terminal: z.object({
|
|
110
|
+
terminalType: z.string().min(1),
|
|
111
|
+
rows: z.number().int().positive(),
|
|
112
|
+
cols: z.number().int().positive(),
|
|
113
|
+
shellActivity: z.boolean().optional()
|
|
114
|
+
}).strict().optional()
|
|
115
|
+
}).strict().refine((value) => value.stdio === void 0 !== (value.terminal === void 0), "select ordinary or terminal execution");
|
|
116
|
+
/** Connection handshake binds sockets and workspace to one helper process. */
|
|
117
|
+
const helloSchema = z.object({
|
|
118
|
+
protocol: z.literal(2),
|
|
119
|
+
hash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
120
|
+
kind: z.enum(["node-script", "executable"]),
|
|
121
|
+
platform: z.enum(["linux", "darwin"]),
|
|
122
|
+
nodeVersion: z.string(),
|
|
123
|
+
executable: remotePath,
|
|
124
|
+
root: remotePath,
|
|
125
|
+
workspace: remotePath,
|
|
126
|
+
bootstrapHash: z.string().regex(/^[0-9a-f]{64}$/).optional()
|
|
127
|
+
}).strict();
|
|
128
|
+
/** A prepared process publishes its sockets before target code may execute. */
|
|
129
|
+
const streamEndpointSchema = z.object({
|
|
130
|
+
path: remotePath,
|
|
131
|
+
capability: z.string().regex(/^[0-9a-f]{64}$/)
|
|
132
|
+
}).strict();
|
|
133
|
+
/** Prepared process and its independently authenticated stream endpoints. */
|
|
134
|
+
const preparedSchema = z.object({
|
|
135
|
+
id: processIdSchema,
|
|
136
|
+
streams: z.partialRecord(z.enum([
|
|
137
|
+
"stdin",
|
|
138
|
+
"stdout",
|
|
139
|
+
"stderr",
|
|
140
|
+
"control",
|
|
141
|
+
"terminal"
|
|
142
|
+
]), streamEndpointSchema)
|
|
143
|
+
}).strict();
|
|
144
|
+
/** Direct process exit facts. */
|
|
145
|
+
const outcomeSchema = z.object({
|
|
146
|
+
exitCode: z.number().int().nullable(),
|
|
147
|
+
signal: z.string().nullable()
|
|
148
|
+
}).strict();
|
|
149
|
+
/** A bounded raw tail positioned in whole-stream byte coordinates. */
|
|
150
|
+
const outputSnapshotSchema = z.object({
|
|
151
|
+
tail: z.base64(),
|
|
152
|
+
totalBytes: z.number().int().nonnegative()
|
|
153
|
+
}).strict();
|
|
154
|
+
/**
|
|
155
|
+
* Bound one encoded tail and its RPC envelope by the declared collection budget.
|
|
156
|
+
* @param maxBytes - the collector's retained raw-byte limit.
|
|
157
|
+
* @returns the private snapshot frame limit, capped by the helper protocol ceiling.
|
|
158
|
+
*/
|
|
159
|
+
function outputSnapshotFrameLimit(maxBytes) {
|
|
160
|
+
return Math.min(64 * 1024 * 1024, maxBytes * 2 + 1024);
|
|
161
|
+
}
|
|
162
|
+
/** Direct exit, retained output snapshots, and optional complete spill locations. */
|
|
163
|
+
const doneSchema = z.object({
|
|
164
|
+
outcome: outcomeSchema,
|
|
165
|
+
spills: z.object({
|
|
166
|
+
stdout: remotePath.optional(),
|
|
167
|
+
stderr: remotePath.optional()
|
|
168
|
+
}).strict(),
|
|
169
|
+
collected: z.object({
|
|
170
|
+
stdout: outputSnapshotSchema.optional(),
|
|
171
|
+
stderr: outputSnapshotSchema.optional()
|
|
172
|
+
}).strict()
|
|
173
|
+
}).strict();
|
|
174
|
+
/** Remote terminal foreground observation. */
|
|
175
|
+
const foregroundSchema = z.object({
|
|
176
|
+
processGroupId: z.number().int().positive(),
|
|
177
|
+
inputWaiting: z.boolean()
|
|
178
|
+
}).strict().nullable();
|
|
179
|
+
/** Conservative shell lifecycle and process observation returned by the execution provider. */
|
|
180
|
+
const terminalActivitySchema = z.object({
|
|
181
|
+
state: z.enum([
|
|
182
|
+
"idle",
|
|
183
|
+
"busy",
|
|
184
|
+
"unknown"
|
|
185
|
+
]),
|
|
186
|
+
revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
|
187
|
+
}).strict();
|
|
188
|
+
//#endregion
|
|
2
189
|
export { doneSchema, editResultSchema, editSchema, entriesSchema, environmentSchema, foregroundSchema, helloSchema, infoSchema, intentSchema, outcomeSchema, outputSnapshotFrameLimit, outputSnapshotSchema, pathInfoSchema, policySchema, preparedSchema, processIdSchema, remotePath, spawnSchema, streamEndpointSchema, targetSchema, terminalActivitySchema, textStreamIdSchema, writeResultSchema };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-ssh",
|
|
3
3
|
"description": "Shared OpenSSH connection and versioned POSIX remote helper",
|
|
4
|
-
"version": "0.2.1-alpha.
|
|
4
|
+
"version": "0.2.1-alpha.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -46,17 +46,17 @@
|
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
49
|
-
"@deepseek-ai/dsh-fs": "0.2.1-alpha.
|
|
50
|
-
"@deepseek-ai/dsh-fs-local": "0.2.1-alpha.
|
|
51
|
-
"@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.
|
|
52
|
-
"@deepseek-ai/dsh-subprocess": "0.2.1-alpha.
|
|
53
|
-
"@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.
|
|
54
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.
|
|
55
|
-
"@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.
|
|
56
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.
|
|
57
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.
|
|
58
|
-
"@deepseek-ai/dsh-brand": "0.2.1-alpha.
|
|
59
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.
|
|
49
|
+
"@deepseek-ai/dsh-fs": "0.2.1-alpha.4",
|
|
50
|
+
"@deepseek-ai/dsh-fs-local": "0.2.1-alpha.4",
|
|
51
|
+
"@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.4",
|
|
52
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-alpha.4",
|
|
53
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.4",
|
|
54
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.4",
|
|
55
|
+
"@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.4",
|
|
56
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.4",
|
|
57
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.4",
|
|
58
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.4",
|
|
59
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.4"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@deepseek-ai/schemastery": "~3.18.5-alpha.2",
|
|
@@ -64,22 +64,22 @@
|
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
67
|
-
"@deepseek-ai/dsh-fs": "0.2.1-alpha.
|
|
68
|
-
"@deepseek-ai/dsh-fs-local": "0.2.1-alpha.
|
|
69
|
-
"@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.
|
|
70
|
-
"@deepseek-ai/dsh-subprocess": "0.2.1-alpha.
|
|
71
|
-
"@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.
|
|
72
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.
|
|
73
|
-
"@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.
|
|
74
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.
|
|
75
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.
|
|
76
|
-
"@deepseek-ai/dsh-ptc-runtime-node": "0.2.1-alpha.
|
|
77
|
-
"@deepseek-ai/dsh-fs-ssh": "0.2.1-alpha.
|
|
78
|
-
"@deepseek-ai/dsh-subprocess-ssh": "0.2.1-alpha.
|
|
79
|
-
"@deepseek-ai/dsh-sandbox-ssh": "0.2.1-alpha.
|
|
80
|
-
"@deepseek-ai/dsh-lsp": "0.2.1-alpha.
|
|
81
|
-
"@deepseek-ai/dsh-lsp-stdio": "0.2.1-alpha.
|
|
82
|
-
"@deepseek-ai/dsh-brand": "0.2.1-alpha.
|
|
83
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.
|
|
67
|
+
"@deepseek-ai/dsh-fs": "0.2.1-alpha.4",
|
|
68
|
+
"@deepseek-ai/dsh-fs-local": "0.2.1-alpha.4",
|
|
69
|
+
"@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.4",
|
|
70
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-alpha.4",
|
|
71
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.4",
|
|
72
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.4",
|
|
73
|
+
"@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.4",
|
|
74
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.4",
|
|
75
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.4",
|
|
76
|
+
"@deepseek-ai/dsh-ptc-runtime-node": "0.2.1-alpha.4",
|
|
77
|
+
"@deepseek-ai/dsh-fs-ssh": "0.2.1-alpha.4",
|
|
78
|
+
"@deepseek-ai/dsh-subprocess-ssh": "0.2.1-alpha.4",
|
|
79
|
+
"@deepseek-ai/dsh-sandbox-ssh": "0.2.1-alpha.4",
|
|
80
|
+
"@deepseek-ai/dsh-lsp": "0.2.1-alpha.4",
|
|
81
|
+
"@deepseek-ai/dsh-lsp-stdio": "0.2.1-alpha.4",
|
|
82
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.4",
|
|
83
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.4"
|
|
84
84
|
}
|
|
85
85
|
}
|
package/lib/protocol-CDhW_IHU.js
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { EventEmitter } from "node:events";
|
|
4
|
-
//#region lib/types/protocol.js
|
|
5
|
-
/** Bounded, versioned requests between an SSH client and its private remote helper. */
|
|
6
|
-
/** Wire version shared by the installed helper and client package. */
|
|
7
|
-
const SSH_PROTOCOL_VERSION = 2;
|
|
8
|
-
/** Maximum prepared or running process handles owned by one helper. */
|
|
9
|
-
const SSH_MAX_PROCESS_HANDLES = 128;
|
|
10
|
-
/** Maximum open text iterators owned by one helper. */
|
|
11
|
-
const SSH_MAX_TEXT_STREAMS = 128;
|
|
12
|
-
const managementLimits = {
|
|
13
|
-
heartbeat: 1,
|
|
14
|
-
close: 1,
|
|
15
|
-
"process.terminate": 128,
|
|
16
|
-
"fs.streamClose": 128
|
|
17
|
-
};
|
|
18
|
-
function requestClass(method) {
|
|
19
|
-
switch (method) {
|
|
20
|
-
case "heartbeat":
|
|
21
|
-
case "close":
|
|
22
|
-
case "process.terminate":
|
|
23
|
-
case "fs.streamClose": return method;
|
|
24
|
-
default: return "ordinary";
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const errorSchema = z.object({
|
|
28
|
-
name: z.string(),
|
|
29
|
-
message: z.string(),
|
|
30
|
-
code: z.string().optional()
|
|
31
|
-
}).strict();
|
|
32
|
-
const requestIdSchema = z.string().transform((value) => value);
|
|
33
|
-
const frameSchema = z.discriminatedUnion("type", [
|
|
34
|
-
z.object({
|
|
35
|
-
type: z.literal("request"),
|
|
36
|
-
id: requestIdSchema,
|
|
37
|
-
method: z.string(),
|
|
38
|
-
params: z.unknown()
|
|
39
|
-
}).strict(),
|
|
40
|
-
z.object({
|
|
41
|
-
type: z.literal("result"),
|
|
42
|
-
id: requestIdSchema,
|
|
43
|
-
value: z.unknown()
|
|
44
|
-
}).strict(),
|
|
45
|
-
z.object({
|
|
46
|
-
type: z.literal("error"),
|
|
47
|
-
id: requestIdSchema,
|
|
48
|
-
error: errorSchema
|
|
49
|
-
}).strict(),
|
|
50
|
-
z.object({
|
|
51
|
-
type: z.literal("cancel"),
|
|
52
|
-
id: requestIdSchema
|
|
53
|
-
}).strict()
|
|
54
|
-
]);
|
|
55
|
-
function operationError(error) {
|
|
56
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
57
|
-
}
|
|
58
|
-
/** A remote error retains its typed filesystem or sandbox code. */
|
|
59
|
-
var RemoteOperationError = class extends Error {
|
|
60
|
-
code;
|
|
61
|
-
constructor(message, code) {
|
|
62
|
-
super(message);
|
|
63
|
-
this.code = code;
|
|
64
|
-
this.name = "RemoteOperationError";
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
/** The peer owns pending calls and rejects ambiguous operations on connection loss; it never replays requests. */
|
|
68
|
-
var SshRpcPeer = class extends EventEmitter {
|
|
69
|
-
input;
|
|
70
|
-
output;
|
|
71
|
-
maxFrameBytes;
|
|
72
|
-
maxPending;
|
|
73
|
-
handler;
|
|
74
|
-
pending = /* @__PURE__ */ new Map();
|
|
75
|
-
active = /* @__PURE__ */ new Map();
|
|
76
|
-
writeTail = Promise.resolve();
|
|
77
|
-
queuedBytes = 0;
|
|
78
|
-
failure;
|
|
79
|
-
constructor(input, output, maxFrameBytes, maxPending, handler) {
|
|
80
|
-
super();
|
|
81
|
-
this.input = input;
|
|
82
|
-
this.output = output;
|
|
83
|
-
this.maxFrameBytes = maxFrameBytes;
|
|
84
|
-
this.maxPending = maxPending;
|
|
85
|
-
this.handler = handler;
|
|
86
|
-
input.on("error", (error) => {
|
|
87
|
-
this.close(error);
|
|
88
|
-
});
|
|
89
|
-
output.on("error", (error) => {
|
|
90
|
-
this.close(error);
|
|
91
|
-
});
|
|
92
|
-
output.on("close", () => {
|
|
93
|
-
this.close();
|
|
94
|
-
});
|
|
95
|
-
this.readFrames().catch((error) => {
|
|
96
|
-
this.close(operationError(error));
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Send one request and validate its response before exposing it to the caller.
|
|
101
|
-
* @param method - the private helper operation.
|
|
102
|
-
* @param params - JSON request fields.
|
|
103
|
-
* @param schema - validation for the remote response.
|
|
104
|
-
* @param signal - cancellation without rollback of remote effects.
|
|
105
|
-
* @returns the validated response or a transport/remote-operation rejection.
|
|
106
|
-
*/
|
|
107
|
-
async request(method, params, schema, signal) {
|
|
108
|
-
signal?.throwIfAborted();
|
|
109
|
-
if (this.failure !== void 0) throw this.failure;
|
|
110
|
-
const kind = requestClass(method);
|
|
111
|
-
if (this.atCapacity(kind, this.pending.values())) throw new Error("SSH helper pending request limit reached");
|
|
112
|
-
const id = randomUUID();
|
|
113
|
-
const result = Promise.withResolvers();
|
|
114
|
-
result.promise.catch(() => {});
|
|
115
|
-
this.pending.set(id, {
|
|
116
|
-
...result,
|
|
117
|
-
requestClass: kind
|
|
118
|
-
});
|
|
119
|
-
const abort = () => {
|
|
120
|
-
result.reject(/* @__PURE__ */ new Error("SSH operation cancelled; a completed remote mutation is not rolled back"));
|
|
121
|
-
this.send({
|
|
122
|
-
type: "cancel",
|
|
123
|
-
id
|
|
124
|
-
}).catch(() => {});
|
|
125
|
-
};
|
|
126
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
127
|
-
try {
|
|
128
|
-
this.send({
|
|
129
|
-
type: "request",
|
|
130
|
-
id,
|
|
131
|
-
method,
|
|
132
|
-
params
|
|
133
|
-
}).catch((error) => {
|
|
134
|
-
this.pending.delete(id);
|
|
135
|
-
result.reject(operationError(error));
|
|
136
|
-
});
|
|
137
|
-
return schema.parse(await result.promise);
|
|
138
|
-
} finally {
|
|
139
|
-
signal?.removeEventListener("abort", abort);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Fail pending operations and abort remote handlers without claiming rollback.
|
|
144
|
-
* @param error - the transport failure reported to all pending operations.
|
|
145
|
-
*/
|
|
146
|
-
close(error = /* @__PURE__ */ new Error("SSH connection lost; remote operation outcome and cleanup are unknown")) {
|
|
147
|
-
if (this.failure !== void 0) return;
|
|
148
|
-
this.failure = error;
|
|
149
|
-
for (const pending of this.pending.values()) pending.reject(error);
|
|
150
|
-
this.pending.clear();
|
|
151
|
-
for (const { controller } of this.active.values()) controller.abort(error);
|
|
152
|
-
this.active.clear();
|
|
153
|
-
this.input.destroy();
|
|
154
|
-
this.output.destroy();
|
|
155
|
-
this.emit("closed", error);
|
|
156
|
-
}
|
|
157
|
-
async send(frame) {
|
|
158
|
-
if (this.failure !== void 0) return Promise.reject(this.failure);
|
|
159
|
-
const body = Buffer.from(JSON.stringify(frame));
|
|
160
|
-
if (body.length > this.maxFrameBytes || this.queuedBytes + body.length + 4 > this.maxFrameBytes * 2) return Promise.reject(/* @__PURE__ */ new Error("SSH helper frame or write queue limit exceeded"));
|
|
161
|
-
const header = Buffer.alloc(4);
|
|
162
|
-
header.writeUInt32BE(body.length);
|
|
163
|
-
const bytes = Buffer.concat([header, body]);
|
|
164
|
-
this.queuedBytes += bytes.length;
|
|
165
|
-
const write = this.writeTail.then(async () => {
|
|
166
|
-
if (this.failure !== void 0) throw this.failure;
|
|
167
|
-
if (!this.output.write(bytes)) await new Promise((resolve, reject) => {
|
|
168
|
-
const cleanup = () => {
|
|
169
|
-
this.output.off("drain", drained);
|
|
170
|
-
this.off("closed", closed);
|
|
171
|
-
};
|
|
172
|
-
const drained = () => {
|
|
173
|
-
cleanup();
|
|
174
|
-
resolve();
|
|
175
|
-
};
|
|
176
|
-
const closed = (error) => {
|
|
177
|
-
cleanup();
|
|
178
|
-
reject(error);
|
|
179
|
-
};
|
|
180
|
-
this.output.once("drain", drained);
|
|
181
|
-
this.once("closed", closed);
|
|
182
|
-
if (this.failure !== void 0) closed(this.failure);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
this.writeTail = write.catch((error) => {
|
|
186
|
-
this.close(operationError(error));
|
|
187
|
-
});
|
|
188
|
-
return write.finally(() => {
|
|
189
|
-
this.queuedBytes -= bytes.length;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
async readFrames() {
|
|
193
|
-
const header = Buffer.alloc(4);
|
|
194
|
-
let headerBytes = 0;
|
|
195
|
-
let payload;
|
|
196
|
-
let payloadBytes = 0;
|
|
197
|
-
for await (const raw of this.input) {
|
|
198
|
-
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
199
|
-
let offset = 0;
|
|
200
|
-
while (offset < chunk.length) {
|
|
201
|
-
if (payload === void 0) {
|
|
202
|
-
const count = Math.min(4 - headerBytes, chunk.length - offset);
|
|
203
|
-
chunk.copy(header, headerBytes, offset, offset + count);
|
|
204
|
-
headerBytes += count;
|
|
205
|
-
offset += count;
|
|
206
|
-
if (headerBytes < 4) continue;
|
|
207
|
-
const size = header.readUInt32BE(0);
|
|
208
|
-
if (size === 0 || size > this.maxFrameBytes) throw new Error("SSH helper sent an invalid frame length");
|
|
209
|
-
payload = Buffer.alloc(size);
|
|
210
|
-
payloadBytes = 0;
|
|
211
|
-
}
|
|
212
|
-
const count = Math.min(payload.length - payloadBytes, chunk.length - offset);
|
|
213
|
-
chunk.copy(payload, payloadBytes, offset, offset + count);
|
|
214
|
-
payloadBytes += count;
|
|
215
|
-
offset += count;
|
|
216
|
-
if (payloadBytes === payload.length) {
|
|
217
|
-
const frame = frameSchema.parse(JSON.parse(payload.toString("utf8")));
|
|
218
|
-
payload = void 0;
|
|
219
|
-
headerBytes = 0;
|
|
220
|
-
this.receive(frame);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
throw new Error(headerBytes > 0 || payload !== void 0 ? "SSH helper disconnected during a frame; outcome is unknown" : "SSH helper disconnected; outcome is unknown");
|
|
225
|
-
}
|
|
226
|
-
receive(frame) {
|
|
227
|
-
if (frame.type === "result" || frame.type === "error") {
|
|
228
|
-
const pending = this.pending.get(frame.id);
|
|
229
|
-
if (pending === void 0) return;
|
|
230
|
-
this.pending.delete(frame.id);
|
|
231
|
-
if (frame.type === "result") pending.resolve(frame.value);
|
|
232
|
-
else pending.reject(new RemoteOperationError(frame.error.message, frame.error.code));
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (frame.type === "cancel") {
|
|
236
|
-
this.active.get(frame.id)?.controller.abort(/* @__PURE__ */ new Error("SSH caller cancelled the operation"));
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const kind = requestClass(frame.method);
|
|
240
|
-
if (this.handler === void 0 || this.active.has(frame.id) || this.atCapacity(kind, this.active.values())) throw new Error("SSH helper received an unexpected or excessive request");
|
|
241
|
-
const controller = new AbortController();
|
|
242
|
-
this.active.set(frame.id, {
|
|
243
|
-
controller,
|
|
244
|
-
requestClass: kind
|
|
245
|
-
});
|
|
246
|
-
this.handler(frame.method, frame.params, controller.signal).then((value) => this.send({
|
|
247
|
-
type: "result",
|
|
248
|
-
id: frame.id,
|
|
249
|
-
value: value ?? null
|
|
250
|
-
}), (error) => {
|
|
251
|
-
const detail = operationError(error);
|
|
252
|
-
const code = "code" in detail && typeof detail.code === "string" ? detail.code : void 0;
|
|
253
|
-
return this.send({
|
|
254
|
-
type: "error",
|
|
255
|
-
id: frame.id,
|
|
256
|
-
error: {
|
|
257
|
-
name: detail.name,
|
|
258
|
-
message: detail.message,
|
|
259
|
-
...code === void 0 ? {} : { code }
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
}).catch((error) => {
|
|
263
|
-
this.close(operationError(error));
|
|
264
|
-
}).finally(() => {
|
|
265
|
-
this.active.delete(frame.id);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
atCapacity(kind, requests) {
|
|
269
|
-
const limit = kind === "ordinary" ? this.maxPending : managementLimits[kind];
|
|
270
|
-
let count = 0;
|
|
271
|
-
for (const request of requests) if (request.requestClass === kind) count++;
|
|
272
|
-
return count >= limit;
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
//#endregion
|
|
276
|
-
export { SshRpcPeer as a, SSH_PROTOCOL_VERSION as i, SSH_MAX_PROCESS_HANDLES as n, SSH_MAX_TEXT_STREAMS as r, RemoteOperationError as t };
|
package/lib/schemas-BD3YbDxe.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import "./protocol-CDhW_IHU.js";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
//#region lib/types/schemas.js
|
|
4
|
-
/** Strict JSON validation for SSH helper requests and remote observations. */
|
|
5
|
-
/** Admit a process identity from the private helper protocol. */
|
|
6
|
-
const processIdSchema = z.uuid().transform((value) => value);
|
|
7
|
-
/** Admit a text iterator identity from the private helper protocol. */
|
|
8
|
-
const textStreamIdSchema = z.uuid().transform((value) => value);
|
|
9
|
-
/** A remote POSIX absolute path; spelling is preserved until remote canonicalization. */
|
|
10
|
-
const remotePath = z.string().min(1).refine((value) => value.startsWith("/") && !value.includes("\0"), "expected an absolute POSIX path");
|
|
11
|
-
/** Filesystem identity returned by the remote filesystem provider. */
|
|
12
|
-
const targetSchema = z.object({
|
|
13
|
-
targetKey: remotePath,
|
|
14
|
-
displayPath: z.string()
|
|
15
|
-
}).strict();
|
|
16
|
-
/** Remote metadata observation. */
|
|
17
|
-
const infoSchema = z.object({
|
|
18
|
-
version: z.string(),
|
|
19
|
-
type: z.enum([
|
|
20
|
-
"file",
|
|
21
|
-
"directory",
|
|
22
|
-
"other"
|
|
23
|
-
]),
|
|
24
|
-
size: z.number().nonnegative().optional()
|
|
25
|
-
}).strict();
|
|
26
|
-
/** Metadata that preserves a final symlink. */
|
|
27
|
-
const pathInfoSchema = infoSchema.extend({ type: z.enum([
|
|
28
|
-
"file",
|
|
29
|
-
"directory",
|
|
30
|
-
"symlink",
|
|
31
|
-
"other"
|
|
32
|
-
]) });
|
|
33
|
-
/** A resolved file-effect policy; the remote helper owns path canonicalization. */
|
|
34
|
-
const policySchema = z.object({
|
|
35
|
-
mode: z.enum([
|
|
36
|
-
"read-only",
|
|
37
|
-
"workspace-write",
|
|
38
|
-
"danger-full-access"
|
|
39
|
-
]),
|
|
40
|
-
workspaceRoot: remotePath,
|
|
41
|
-
sessionId: z.string().optional()
|
|
42
|
-
}).strict();
|
|
43
|
-
/** Complete directory entries. */
|
|
44
|
-
const entriesSchema = z.array(z.object({
|
|
45
|
-
name: z.string(),
|
|
46
|
-
type: z.enum([
|
|
47
|
-
"file",
|
|
48
|
-
"directory",
|
|
49
|
-
"other"
|
|
50
|
-
]),
|
|
51
|
-
target: targetSchema,
|
|
52
|
-
version: z.string().optional(),
|
|
53
|
-
size: z.number().nonnegative().optional()
|
|
54
|
-
}).strict());
|
|
55
|
-
/** Guarded write intent. */
|
|
56
|
-
const intentSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("createIfAbsent") }).strict(), z.object({
|
|
57
|
-
kind: z.literal("replaceIfVersion"),
|
|
58
|
-
version: z.string()
|
|
59
|
-
}).strict()]);
|
|
60
|
-
/** Literal text edit. */
|
|
61
|
-
const editSchema = z.object({
|
|
62
|
-
oldString: z.string(),
|
|
63
|
-
newString: z.string(),
|
|
64
|
-
replaceAll: z.boolean()
|
|
65
|
-
}).strict();
|
|
66
|
-
/** Atomic write observation. */
|
|
67
|
-
const writeResultSchema = z.object({
|
|
68
|
-
operation: z.enum(["create", "update"]),
|
|
69
|
-
version: z.string(),
|
|
70
|
-
before: z.string().nullable(),
|
|
71
|
-
after: z.string()
|
|
72
|
-
}).strict();
|
|
73
|
-
/** Atomic edit observation. */
|
|
74
|
-
const editResultSchema = z.object({
|
|
75
|
-
version: z.string(),
|
|
76
|
-
before: z.string(),
|
|
77
|
-
after: z.string()
|
|
78
|
-
}).strict();
|
|
79
|
-
/** Explicit child environment; null encodes an environment tombstone. */
|
|
80
|
-
const environmentSchema = z.record(z.string(), z.string().nullable());
|
|
81
|
-
const collection = z.object({
|
|
82
|
-
maxBytes: z.number().int().positive(),
|
|
83
|
-
spill: z.object({ maxBytes: z.number().int().positive() }).strict().optional()
|
|
84
|
-
}).strict();
|
|
85
|
-
/** Ordinary or terminal process requested by a trusted SSH client. */
|
|
86
|
-
const spawnSchema = z.object({
|
|
87
|
-
argv: z.array(z.string().refine((value) => !value.includes("\0"))).min(1),
|
|
88
|
-
cwd: remotePath,
|
|
89
|
-
env: environmentSchema.optional(),
|
|
90
|
-
graceMs: z.number().int().positive().max(2147483647),
|
|
91
|
-
stdio: z.object({
|
|
92
|
-
stdin: z.union([
|
|
93
|
-
z.literal("ignore"),
|
|
94
|
-
z.literal("pipe"),
|
|
95
|
-
z.object({ data: z.string() }).strict()
|
|
96
|
-
]),
|
|
97
|
-
stdout: z.union([
|
|
98
|
-
z.literal("pipe"),
|
|
99
|
-
z.literal("inherit"),
|
|
100
|
-
collection
|
|
101
|
-
]),
|
|
102
|
-
stderr: z.union([
|
|
103
|
-
z.literal("pipe"),
|
|
104
|
-
z.literal("inherit"),
|
|
105
|
-
collection
|
|
106
|
-
]),
|
|
107
|
-
control: z.literal("pipe").optional()
|
|
108
|
-
}).strict().optional(),
|
|
109
|
-
terminal: z.object({
|
|
110
|
-
terminalType: z.string().min(1),
|
|
111
|
-
rows: z.number().int().positive(),
|
|
112
|
-
cols: z.number().int().positive(),
|
|
113
|
-
shellActivity: z.boolean().optional()
|
|
114
|
-
}).strict().optional()
|
|
115
|
-
}).strict().refine((value) => value.stdio === void 0 !== (value.terminal === void 0), "select ordinary or terminal execution");
|
|
116
|
-
/** Connection handshake binds sockets and workspace to one helper process. */
|
|
117
|
-
const helloSchema = z.object({
|
|
118
|
-
protocol: z.literal(2),
|
|
119
|
-
hash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
120
|
-
kind: z.enum(["node-script", "executable"]),
|
|
121
|
-
platform: z.enum(["linux", "darwin"]),
|
|
122
|
-
nodeVersion: z.string(),
|
|
123
|
-
executable: remotePath,
|
|
124
|
-
root: remotePath,
|
|
125
|
-
workspace: remotePath,
|
|
126
|
-
bootstrapHash: z.string().regex(/^[0-9a-f]{64}$/).optional()
|
|
127
|
-
}).strict();
|
|
128
|
-
/** A prepared process publishes its sockets before target code may execute. */
|
|
129
|
-
const streamEndpointSchema = z.object({
|
|
130
|
-
path: remotePath,
|
|
131
|
-
capability: z.string().regex(/^[0-9a-f]{64}$/)
|
|
132
|
-
}).strict();
|
|
133
|
-
/** Prepared process and its independently authenticated stream endpoints. */
|
|
134
|
-
const preparedSchema = z.object({
|
|
135
|
-
id: processIdSchema,
|
|
136
|
-
streams: z.partialRecord(z.enum([
|
|
137
|
-
"stdin",
|
|
138
|
-
"stdout",
|
|
139
|
-
"stderr",
|
|
140
|
-
"control",
|
|
141
|
-
"terminal"
|
|
142
|
-
]), streamEndpointSchema)
|
|
143
|
-
}).strict();
|
|
144
|
-
/** Direct process exit facts. */
|
|
145
|
-
const outcomeSchema = z.object({
|
|
146
|
-
exitCode: z.number().int().nullable(),
|
|
147
|
-
signal: z.string().nullable()
|
|
148
|
-
}).strict();
|
|
149
|
-
/** A bounded raw tail positioned in whole-stream byte coordinates. */
|
|
150
|
-
const outputSnapshotSchema = z.object({
|
|
151
|
-
tail: z.base64(),
|
|
152
|
-
totalBytes: z.number().int().nonnegative()
|
|
153
|
-
}).strict();
|
|
154
|
-
/**
|
|
155
|
-
* Bound one encoded tail and its RPC envelope by the declared collection budget.
|
|
156
|
-
* @param maxBytes - the collector's retained raw-byte limit.
|
|
157
|
-
* @returns the private snapshot frame limit, capped by the helper protocol ceiling.
|
|
158
|
-
*/
|
|
159
|
-
function outputSnapshotFrameLimit(maxBytes) {
|
|
160
|
-
return Math.min(64 * 1024 * 1024, maxBytes * 2 + 1024);
|
|
161
|
-
}
|
|
162
|
-
/** Direct exit, retained output snapshots, and optional complete spill locations. */
|
|
163
|
-
const doneSchema = z.object({
|
|
164
|
-
outcome: outcomeSchema,
|
|
165
|
-
spills: z.object({
|
|
166
|
-
stdout: remotePath.optional(),
|
|
167
|
-
stderr: remotePath.optional()
|
|
168
|
-
}).strict(),
|
|
169
|
-
collected: z.object({
|
|
170
|
-
stdout: outputSnapshotSchema.optional(),
|
|
171
|
-
stderr: outputSnapshotSchema.optional()
|
|
172
|
-
}).strict()
|
|
173
|
-
}).strict();
|
|
174
|
-
/** Remote terminal foreground observation. */
|
|
175
|
-
const foregroundSchema = z.object({
|
|
176
|
-
processGroupId: z.number().int().positive(),
|
|
177
|
-
inputWaiting: z.boolean()
|
|
178
|
-
}).strict().nullable();
|
|
179
|
-
/** Conservative shell lifecycle and process observation returned by the execution provider. */
|
|
180
|
-
const terminalActivitySchema = z.object({
|
|
181
|
-
state: z.enum([
|
|
182
|
-
"idle",
|
|
183
|
-
"busy",
|
|
184
|
-
"unknown"
|
|
185
|
-
]),
|
|
186
|
-
revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
|
187
|
-
}).strict();
|
|
188
|
-
//#endregion
|
|
189
|
-
export { writeResultSchema as C, textStreamIdSchema as S, remotePath as _, environmentSchema as a, targetSchema as b, infoSchema as c, outputSnapshotFrameLimit as d, outputSnapshotSchema as f, processIdSchema as g, preparedSchema as h, entriesSchema as i, intentSchema as l, policySchema as m, editResultSchema as n, foregroundSchema as o, pathInfoSchema as p, editSchema as r, helloSchema as s, doneSchema as t, outcomeSchema as u, spawnSchema as v, terminalActivitySchema as x, streamEndpointSchema as y };
|