@jabberwocky238/dsh-ssh 0.0.0-stage → 0.2.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.
@@ -0,0 +1,276 @@
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 };
@@ -0,0 +1,2 @@
1
+ import { a as SshRpcPeer, i as SSH_PROTOCOL_VERSION, n as SSH_MAX_PROCESS_HANDLES, r as SSH_MAX_TEXT_STREAMS, t as RemoteOperationError } from "./protocol-CDhW_IHU.js";
2
+ export { RemoteOperationError, SSH_MAX_PROCESS_HANDLES, SSH_MAX_TEXT_STREAMS, SSH_PROTOCOL_VERSION, SshRpcPeer };
@@ -0,0 +1,189 @@
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 };
package/lib/schemas.js ADDED
@@ -0,0 +1,2 @@
1
+ import { C as writeResultSchema, S as textStreamIdSchema, _ as remotePath, a as environmentSchema, b as targetSchema, c as infoSchema, d as outputSnapshotFrameLimit, f as outputSnapshotSchema, g as processIdSchema, h as preparedSchema, i as entriesSchema, l as intentSchema, m as policySchema, n as editResultSchema, o as foregroundSchema, p as pathInfoSchema, r as editSchema, s as helloSchema, t as doneSchema, u as outcomeSchema, v as spawnSchema, x as terminalActivitySchema, y as streamEndpointSchema } from "./schemas-BD3YbDxe.js";
2
+ export { doneSchema, editResultSchema, editSchema, entriesSchema, environmentSchema, foregroundSchema, helloSchema, infoSchema, intentSchema, outcomeSchema, outputSnapshotFrameLimit, outputSnapshotSchema, pathInfoSchema, policySchema, preparedSchema, processIdSchema, remotePath, spawnSchema, streamEndpointSchema, targetSchema, terminalActivitySchema, textStreamIdSchema, writeResultSchema };
@@ -0,0 +1,75 @@
1
+ import { connect } from "node:tls";
2
+ //#region lib/types/stream-security.js
3
+ /** TLS-PSK authenticates forwarded streams even when a remote pathname is replaced. */
4
+ /** Certificate-free PSK authentication and AEAD records; no unauthenticated cipher fallback. */
5
+ const SSH_STREAM_TLS_OPTIONS = {
6
+ ciphers: "PSK-AES256-GCM-SHA384",
7
+ minVersion: "TLSv1.2",
8
+ maxVersion: "TLSv1.2"
9
+ };
10
+ /**
11
+ * Authenticate a forwarded socket with its private administrative-channel key.
12
+ * @param socket - the connected OpenSSH forwarding socket.
13
+ * @param capability - the per-stream 256-bit key encoded as hexadecimal.
14
+ * @param timeoutMs - deadline for completing TLS authentication.
15
+ * @param signal - cancellation of authentication and the resulting TLS stream.
16
+ * @returns an authenticated paused stream; the key is never transmitted as data.
17
+ */
18
+ async function authenticateStream(socket, capability, timeoutMs, signal) {
19
+ if (signal?.aborted) {
20
+ socket.destroy();
21
+ signal.throwIfAborted();
22
+ }
23
+ const stream = connect({
24
+ ...SSH_STREAM_TLS_OPTIONS,
25
+ socket,
26
+ rejectUnauthorized: true,
27
+ pskCallback: () => ({
28
+ psk: Buffer.from(capability, "hex"),
29
+ identity: "dsh-stream"
30
+ }),
31
+ checkServerIdentity: () => void 0
32
+ });
33
+ const abort = () => {
34
+ stream.destroy(signal?.reason instanceof Error ? signal.reason : new Error(String(signal?.reason)));
35
+ };
36
+ signal?.addEventListener("abort", abort, { once: true });
37
+ stream.once("close", () => {
38
+ signal?.removeEventListener("abort", abort);
39
+ });
40
+ try {
41
+ await new Promise((resolve, reject) => {
42
+ const cleanup = () => {
43
+ stream.off("secureConnect", secured);
44
+ stream.off("error", failed);
45
+ stream.off("close", closed);
46
+ clearTimeout(timer);
47
+ };
48
+ const secured = () => {
49
+ cleanup();
50
+ stream.disableRenegotiation();
51
+ stream.pause();
52
+ resolve();
53
+ };
54
+ const failed = (error) => {
55
+ cleanup();
56
+ reject(error);
57
+ };
58
+ const closed = () => {
59
+ failed(/* @__PURE__ */ new Error("SSH stream closed during authentication"));
60
+ };
61
+ const timer = setTimeout(() => {
62
+ failed(/* @__PURE__ */ new Error("SSH stream authentication timed out"));
63
+ }, timeoutMs);
64
+ stream.once("secureConnect", secured);
65
+ stream.once("error", failed);
66
+ stream.once("close", closed);
67
+ });
68
+ return stream;
69
+ } catch (error) {
70
+ stream.destroy();
71
+ throw error;
72
+ }
73
+ }
74
+ //#endregion
75
+ export { authenticateStream as n, SSH_STREAM_TLS_OPTIONS as t };
@@ -0,0 +1,121 @@
1
+ /** OpenSSH connection owner for version-matched POSIX helpers, one per login, and their independent forwarded streams. */
2
+ import { type Socket } from 'node:net';
3
+ import { Context, Service } from '@deepseek-ai/cordis';
4
+ import schema from '@deepseek-ai/schemastery';
5
+ import { z } from 'zod';
6
+ import { helloSchema, type SshStreamEndpoint } from './schemas.ts';
7
+ type Hello = z.infer<typeof helloSchema>;
8
+ /** Installed helper invocation; script deployments may also install a PTC bootstrap. */
9
+ export type HelperLaunch = {
10
+ /** Run the installed script with a separately installed Node executable. */
11
+ kind: 'node-script';
12
+ /** Absolute remote Node executable. */
13
+ node: string;
14
+ /** Absolute remote PTC bootstrap; requires bootstrapHash. */
15
+ bootstrapPath?: string;
16
+ /** Lowercase SHA-256 of bootstrapPath; requires that path. */
17
+ bootstrapHash?: string;
18
+ } | {
19
+ /** Run the helper executable with its embedded Node and PTC worker. */
20
+ kind: 'executable';
21
+ };
22
+ /** Deployment-owned SSH identity and installed helper; no model argument selects these values. */
23
+ export interface Config {
24
+ /** OpenSSH host alias, including its existing user, key and known-host configuration. */
25
+ host: string;
26
+ /** Explicit script or self-contained executable invocation. */
27
+ launch: HelperLaunch;
28
+ /** Absolute path to the installed, bundled helper entry. */
29
+ helper: string;
30
+ /** SHA-256 of that bundled helper; mismatches refuse the connection. */
31
+ helperHash: string;
32
+ /** Absolute remote default workspace. */
33
+ workspace: string;
34
+ /** Connection and administrative-request deadline, at most 2,147,483,647 milliseconds. */
35
+ requestTimeoutMs?: number;
36
+ /** Maximum JSON payload bytes per helper request or response. */
37
+ maxFrameBytes?: number;
38
+ /** Maximum ordinary requests; heartbeat and bounded resource cleanup have reserved capacity. */
39
+ maxPending?: number;
40
+ /** Remote helper lease; loss of heartbeats starts remote managed cleanup. */
41
+ leaseMs?: number;
42
+ /** Idle time after which a connection opened for a bound login closes; it reopens on the next request. */
43
+ loginIdleMs?: number;
44
+ }
45
+ declare module '@deepseek-ai/cordis' {
46
+ interface Context {
47
+ ssh: SshConnection;
48
+ }
49
+ }
50
+ /**
51
+ * The deployment's SSH service. Every operation runs on the alias's own login
52
+ * unless the Agent driving it was bound to another login with
53
+ * {@link SshConnection.bindLogin}; that Agent's operations then run on a
54
+ * connection authenticated as the bound login, opened on first use and closed
55
+ * after `loginIdleMs` without work. A process, terminal or text stream stays on
56
+ * the connection that created it, whoever later observes or cancels it.
57
+ */
58
+ export declare class SshConnection extends Service {
59
+ static Config: schema<Config>;
60
+ /** Verified remote helper coordinates of the alias's own login; callers must await this before launch. */
61
+ readonly ready: Promise<Hello>;
62
+ private readonly config;
63
+ private readonly primary;
64
+ /** Login each bound Agent runs as. */
65
+ private readonly bindings;
66
+ private readonly logins;
67
+ private readonly idle;
68
+ /** Helper-issued handle ids and the channel whose helper issued them. */
69
+ private readonly handles;
70
+ private closed;
71
+ private disposal;
72
+ constructor(ctx: Context, config: Config);
73
+ /** Hold plugin readiness until the remote identity and helper digest are verified. */
74
+ [Service.init](): Promise<void>;
75
+ /** Verified remote PTC launch configuration; script deployments require a verified bootstrap. */
76
+ get ptcLaunch(): {
77
+ kind: 'embedded';
78
+ executable: string;
79
+ } | {
80
+ kind: 'node-script';
81
+ executable: string;
82
+ bootstrapPath: string;
83
+ };
84
+ /**
85
+ * Run every operation an Agent drives as `login` instead of the alias's own
86
+ * login. The remote host must accept the alias's key for that login. An
87
+ * Agent's operations are those made while it is the current initiator
88
+ * (`ctx.agents.currentInitiator()`); operations outside any initiator, or
89
+ * inside `ctx.agents.withoutInitiator()`, keep the alias's own login.
90
+ * @param agent - the Agent to bind; a child Agent it creates is not bound.
91
+ * @param login - remote login name.
92
+ * @returns a disposer that removes the binding; register it as an effect of the Agent's context.
93
+ * @throws when `login` is not a valid login name or the Agent is already bound.
94
+ */
95
+ bindLogin(agent: object, login: string): () => void;
96
+ /**
97
+ * Send a helper operation; cancellation never replays an ambiguous mutation.
98
+ * @param method - the private helper operation.
99
+ * @param params - JSON request fields validated by the helper.
100
+ * @param result - response validation before returning provider-visible data.
101
+ * @param signal - cancellation, which does not undo completed remote effects.
102
+ * @param wait - allow a process observation to outlast the administrative deadline.
103
+ * @returns the validated remote result.
104
+ */
105
+ request<T>(method: string, params: unknown, result: z.ZodType<T>, signal?: AbortSignal, wait?: boolean): Promise<T>;
106
+ /**
107
+ * Forward one authenticated stream through an independent SSH channel.
108
+ * @param endpoint - private coordinates issued by one of this service's helpers.
109
+ * @param signal - cancellation of allocation and the resulting socket.
110
+ * @returns a paused socket; attach a consumer before resuming it.
111
+ */
112
+ connectStream(endpoint: SshStreamEndpoint, signal?: AbortSignal): Promise<Socket>;
113
+ /** Tear down every helper's remote managed ranges before releasing their SSH masters when reachable. */
114
+ dispose(): Promise<void>;
115
+ /** The channel for the Agent driving the current operation. */
116
+ private channelForInitiator;
117
+ /** Close a login channel once it holds no work and no handles for `loginIdleMs`. */
118
+ private scheduleIdle;
119
+ }
120
+ export default SshConnection;
121
+ //# sourceMappingURL=index.d.ts.map