@deepseek-ai/dsh-ssh 0.1.6-alpha.1

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,177 @@
1
+ import { z } from "zod";
2
+ //#region lib/types/schemas.js
3
+ /** Strict JSON validation for SSH helper requests and remote observations. */
4
+ /** Admit a process identity from the private helper protocol. */
5
+ const processIdSchema = z.uuid().transform((value) => value);
6
+ /** Admit a text iterator identity from the private helper protocol. */
7
+ const textStreamIdSchema = z.uuid().transform((value) => value);
8
+ /** A remote POSIX absolute path; spelling is preserved until remote canonicalization. */
9
+ const remotePath = z.string().min(1).refine((value) => value.startsWith("/") && !value.includes("\0"), "expected an absolute POSIX path");
10
+ /** Filesystem identity returned by the remote filesystem provider. */
11
+ const targetSchema = z.object({
12
+ targetKey: remotePath,
13
+ displayPath: z.string()
14
+ }).strict();
15
+ /** Remote metadata observation. */
16
+ const infoSchema = z.object({
17
+ version: z.string(),
18
+ type: z.enum([
19
+ "file",
20
+ "directory",
21
+ "other"
22
+ ]),
23
+ size: z.number().nonnegative().optional()
24
+ }).strict();
25
+ /** Metadata that preserves a final symlink. */
26
+ const pathInfoSchema = infoSchema.extend({ type: z.enum([
27
+ "file",
28
+ "directory",
29
+ "symlink",
30
+ "other"
31
+ ]) });
32
+ /** A resolved file-effect policy; the remote helper owns path canonicalization. */
33
+ const policySchema = z.object({
34
+ mode: z.enum([
35
+ "read-only",
36
+ "workspace-write",
37
+ "danger-full-access"
38
+ ]),
39
+ workspaceRoot: remotePath,
40
+ sessionId: z.string().optional()
41
+ }).strict();
42
+ /** Complete directory entries. */
43
+ const entriesSchema = z.array(z.object({
44
+ name: z.string(),
45
+ type: z.enum([
46
+ "file",
47
+ "directory",
48
+ "other"
49
+ ]),
50
+ target: targetSchema,
51
+ version: z.string().optional(),
52
+ size: z.number().nonnegative().optional()
53
+ }).strict());
54
+ /** Guarded write intent. */
55
+ const intentSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("createIfAbsent") }).strict(), z.object({
56
+ kind: z.literal("replaceIfVersion"),
57
+ version: z.string()
58
+ }).strict()]);
59
+ /** Literal text edit. */
60
+ const editSchema = z.object({
61
+ oldString: z.string(),
62
+ newString: z.string(),
63
+ replaceAll: z.boolean()
64
+ }).strict();
65
+ /** Atomic write observation. */
66
+ const writeResultSchema = z.object({
67
+ operation: z.enum(["create", "update"]),
68
+ version: z.string(),
69
+ before: z.string().nullable(),
70
+ after: z.string()
71
+ }).strict();
72
+ /** Atomic edit observation. */
73
+ const editResultSchema = z.object({
74
+ version: z.string(),
75
+ before: z.string(),
76
+ after: z.string()
77
+ }).strict();
78
+ /** Explicit child environment; null encodes an environment tombstone. */
79
+ const environmentSchema = z.record(z.string(), z.string().nullable());
80
+ const collection = z.object({
81
+ maxBytes: z.number().int().positive(),
82
+ spill: z.object({ maxBytes: z.number().int().positive() }).strict().optional()
83
+ }).strict();
84
+ /** Ordinary or terminal process requested by a trusted SSH client. */
85
+ const spawnSchema = z.object({
86
+ argv: z.array(z.string().refine((value) => !value.includes("\0"))).min(1),
87
+ cwd: remotePath,
88
+ env: environmentSchema.optional(),
89
+ graceMs: z.number().int().positive().max(2147483647),
90
+ stdio: z.object({
91
+ stdin: z.union([
92
+ z.literal("ignore"),
93
+ z.literal("pipe"),
94
+ z.object({ data: z.string() }).strict()
95
+ ]),
96
+ stdout: z.union([
97
+ z.literal("pipe"),
98
+ z.literal("inherit"),
99
+ collection
100
+ ]),
101
+ stderr: z.union([
102
+ z.literal("pipe"),
103
+ z.literal("inherit"),
104
+ collection
105
+ ]),
106
+ control: z.literal("pipe").optional()
107
+ }).strict().optional(),
108
+ terminal: z.object({
109
+ terminalType: z.string().min(1),
110
+ rows: z.number().int().positive(),
111
+ cols: z.number().int().positive()
112
+ }).strict().optional()
113
+ }).strict().refine((value) => value.stdio === void 0 !== (value.terminal === void 0), "select ordinary or terminal execution");
114
+ /** Connection handshake binds sockets and workspace to one helper process. */
115
+ const helloSchema = z.object({
116
+ protocol: z.literal(1),
117
+ hash: z.string().regex(/^[0-9a-f]{64}$/),
118
+ platform: z.enum(["linux", "darwin"]),
119
+ nodeVersion: z.string(),
120
+ node: remotePath,
121
+ root: remotePath,
122
+ workspace: remotePath,
123
+ bootstrapHash: z.string().regex(/^[0-9a-f]{64}$/).optional()
124
+ }).strict();
125
+ /** A prepared process publishes its sockets before target code may execute. */
126
+ const streamEndpointSchema = z.object({
127
+ path: remotePath,
128
+ capability: z.string().regex(/^[0-9a-f]{64}$/)
129
+ }).strict();
130
+ /** Prepared process and its independently authenticated stream endpoints. */
131
+ const preparedSchema = z.object({
132
+ id: processIdSchema,
133
+ streams: z.partialRecord(z.enum([
134
+ "stdin",
135
+ "stdout",
136
+ "stderr",
137
+ "control",
138
+ "terminal"
139
+ ]), streamEndpointSchema)
140
+ }).strict();
141
+ /** Direct process exit facts. */
142
+ const outcomeSchema = z.object({
143
+ exitCode: z.number().int().nullable(),
144
+ signal: z.string().nullable()
145
+ }).strict();
146
+ /** A bounded raw tail positioned in whole-stream byte coordinates. */
147
+ const outputSnapshotSchema = z.object({
148
+ tail: z.base64(),
149
+ totalBytes: z.number().int().nonnegative()
150
+ }).strict();
151
+ /**
152
+ * Bound one encoded tail and its RPC envelope by the declared collection budget.
153
+ * @param maxBytes - the collector's retained raw-byte limit.
154
+ * @returns the private snapshot frame limit, capped by the helper protocol ceiling.
155
+ */
156
+ function outputSnapshotFrameLimit(maxBytes) {
157
+ return Math.min(64 * 1024 * 1024, maxBytes * 2 + 1024);
158
+ }
159
+ /** Direct exit, retained output snapshots, and optional complete spill locations. */
160
+ const doneSchema = z.object({
161
+ outcome: outcomeSchema,
162
+ spills: z.object({
163
+ stdout: remotePath.optional(),
164
+ stderr: remotePath.optional()
165
+ }).strict(),
166
+ collected: z.object({
167
+ stdout: outputSnapshotSchema.optional(),
168
+ stderr: outputSnapshotSchema.optional()
169
+ }).strict()
170
+ }).strict();
171
+ /** Remote terminal foreground observation. */
172
+ const foregroundSchema = z.object({
173
+ processGroupId: z.number().int().positive(),
174
+ inputWaiting: z.boolean()
175
+ }).strict().nullable();
176
+ //#endregion
177
+ export { writeResultSchema 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, textStreamIdSchema as x, streamEndpointSchema as y };
package/lib/schemas.js ADDED
@@ -0,0 +1,2 @@
1
+ import { S as writeResultSchema, _ 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 textStreamIdSchema, y as streamEndpointSchema } from "./schemas-DBIgsx0q.js";
2
+ export { doneSchema, editResultSchema, editSchema, entriesSchema, environmentSchema, foregroundSchema, helloSchema, infoSchema, intentSchema, outcomeSchema, outputSnapshotFrameLimit, outputSnapshotSchema, pathInfoSchema, policySchema, preparedSchema, processIdSchema, remotePath, spawnSchema, streamEndpointSchema, targetSchema, 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=helper-entry.d.ts.map
@@ -0,0 +1,79 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type SshProcessId, type SshStreamEndpoint } from './schemas.ts';
3
+ type Channel = 'stdin' | 'stdout' | 'stderr' | 'control' | 'terminal';
4
+ /** Owns remote launch reservations through final process-range quiescence. */
5
+ export declare class RemoteProcesses {
6
+ private readonly ctx;
7
+ private readonly root;
8
+ private readonly limit;
9
+ private readonly preparationMs;
10
+ private readonly records;
11
+ private readonly completed;
12
+ private readonly cleanups;
13
+ private closing;
14
+ constructor(ctx: Context, root: string, limit: number, preparationMs: number);
15
+ /**
16
+ * Allocate private stream listeners; no target executes until start().
17
+ * @param raw - untrusted process request received over SSH.
18
+ * @returns the reservation id and authenticated stream coordinates.
19
+ */
20
+ prepare(raw: unknown): Promise<{
21
+ id: SshProcessId;
22
+ streams: Partial<Record<Channel, SshStreamEndpoint>>;
23
+ }>;
24
+ /**
25
+ * Start once all data channels are authenticated; duplicate starts refuse.
26
+ * @param id - the prepared process reservation.
27
+ * @param signal - cancellation of pending process publication.
28
+ * @returns the terminal pid when the request owns a PTY.
29
+ */
30
+ start(id: SshProcessId, signal?: AbortSignal): Promise<{
31
+ pid?: number;
32
+ }>;
33
+ private startOnce;
34
+ /**
35
+ * Await the direct result without claiming all descendants have exited.
36
+ * @param id - the started process reservation.
37
+ * @returns the exit observation and remote spill paths.
38
+ * @throws the original startup or process failure while its completion is retained.
39
+ */
40
+ done(id: SshProcessId): Promise<unknown>;
41
+ /**
42
+ * Observe the native managed range used for termination.
43
+ * @param id - the started process reservation.
44
+ * @param signal - cancellation of this observation, leaving ownership intact.
45
+ * @returns whether the owned process range is empty.
46
+ */
47
+ wait(id: SshProcessId, signal?: AbortSignal): Promise<boolean>;
48
+ /**
49
+ * Terminate and await the managed range independently of output readers.
50
+ * @param id - the process reservation to stop.
51
+ */
52
+ terminate(id: SshProcessId): Promise<void>;
53
+ /**
54
+ * Operate on the terminal owned by the request id.
55
+ * @param id - the terminal reservation.
56
+ * @param operation - terminal input, foreground observation, or signal delivery.
57
+ * @param value - input bytes as text or the signal name.
58
+ * @returns the operation's wire result.
59
+ */
60
+ terminal(id: SshProcessId, operation: 'write' | 'inspect' | 'signal', value?: string): Promise<unknown>;
61
+ /**
62
+ * Resize an allocated terminal without replacing its process.
63
+ * @param id - terminal reservation.
64
+ * @param cols - positive terminal width.
65
+ * @param rows - positive terminal height.
66
+ * @returns after the local provider accepts the dimensions.
67
+ */
68
+ resizeTerminal(id: SshProcessId, cols: number, rows: number): Promise<void>;
69
+ /** Stop every owned process on lease expiry or disconnect. */
70
+ close(): Promise<void>;
71
+ private release;
72
+ private record;
73
+ private finishFailed;
74
+ private rememberCompleted;
75
+ private trackCleanup;
76
+ private endpoint;
77
+ }
78
+ export {};
79
+ //# sourceMappingURL=helper-processes.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ /** The private helper's authenticated transport and process lifetime. */
3
+ export interface HelperTransport {
4
+ /** Authenticated request bytes from OpenSSH. */
5
+ input: Readable;
6
+ /** Responses carried only by OpenSSH exec stdout. */
7
+ output: Writable;
8
+ /** Installed entry whose digest is compared by the client. */
9
+ entryPath: string;
10
+ /** Process termination joins the same cleanup as transport loss. */
11
+ signal: AbortSignal;
12
+ }
13
+ /**
14
+ * Run a helper until its channel closes or its client lease expires.
15
+ * @param transport - private process streams, entry identity, and cancellation.
16
+ */
17
+ export declare function runSshHelper(transport: HelperTransport): Promise<void>;
18
+ //# sourceMappingURL=helper.d.ts.map
@@ -0,0 +1,93 @@
1
+ /** OpenSSH connection owner for one version-matched POSIX helper and its 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
+ /** Deployment-owned SSH identity and installed helper; no model argument selects these values. */
9
+ export interface Config {
10
+ /** OpenSSH host alias, including its existing user, key and known-host configuration. */
11
+ host: string;
12
+ /** Absolute remote Node executable. */
13
+ node: string;
14
+ /** Absolute path to the installed, bundled helper entry. */
15
+ helper: string;
16
+ /** SHA-256 of that bundled helper; mismatches refuse the connection. */
17
+ helperHash: string;
18
+ /** Absolute remote default workspace. */
19
+ workspace: string;
20
+ /** Optional preinstalled built PTC entry, paired with its expected digest. */
21
+ bootstrapPath?: string;
22
+ /** SHA-256 of bootstrapPath; both fields must be supplied together. */
23
+ bootstrapHash?: string;
24
+ /** Connection and administrative-request deadline, at most 2,147,483,647 milliseconds. */
25
+ requestTimeoutMs?: number;
26
+ /** Maximum JSON payload bytes per helper request or response. */
27
+ maxFrameBytes?: number;
28
+ /** Maximum ordinary requests; heartbeat and bounded resource cleanup have reserved capacity. */
29
+ maxPending?: number;
30
+ /** Remote helper lease; loss of heartbeats starts remote managed cleanup. */
31
+ leaseMs?: number;
32
+ }
33
+ declare module '@deepseek-ai/cordis' {
34
+ interface Context {
35
+ ssh: SshConnection;
36
+ }
37
+ }
38
+ /** One non-reconnecting SSH session; loss invalidates all active operations. */
39
+ export declare class SshConnection extends Service {
40
+ static Config: schema<Config>;
41
+ /** Verified remote helper coordinates; callers must await this before launch. */
42
+ readonly ready: Promise<Hello>;
43
+ private rpc;
44
+ private child;
45
+ private childClosed;
46
+ private directory;
47
+ private heartbeat;
48
+ private closed;
49
+ private readonly lifetime;
50
+ private readonly operations;
51
+ private disposal;
52
+ private failure;
53
+ private sockets;
54
+ private nextSocket;
55
+ private readonly config;
56
+ private remote;
57
+ constructor(ctx: Context, config: Config);
58
+ /** Hold plugin readiness until the remote identity and helper digest are verified. */
59
+ [Service.init](): Promise<void>;
60
+ /** Verified remote Node executable for the paired PTC runtime. */
61
+ get nodeExecutable(): string;
62
+ /** Verified preinstalled PTC entry; unconfigured runtimes fail before program execution. */
63
+ get bootstrapPath(): string;
64
+ /**
65
+ * Send a helper operation; cancellation never replays an ambiguous mutation.
66
+ * @param method - the private helper operation.
67
+ * @param params - JSON request fields validated by the helper.
68
+ * @param result - response validation before returning provider-visible data.
69
+ * @param signal - cancellation, which does not undo completed remote effects.
70
+ * @param wait - allow a process observation to outlast the administrative deadline.
71
+ * @returns the validated remote result.
72
+ */
73
+ request<T>(method: string, params: unknown, result: z.ZodType<T>, signal?: AbortSignal, wait?: boolean): Promise<T>;
74
+ /**
75
+ * Forward one authenticated stream through an independent SSH channel.
76
+ * @param endpoint - private coordinates issued by this connection's helper.
77
+ * @param signal - cancellation of allocation and the resulting socket.
78
+ * @returns a paused socket; attach a consumer before resuming it.
79
+ */
80
+ connectStream(endpoint: SshStreamEndpoint, signal?: AbortSignal): Promise<Socket>;
81
+ private establishStream;
82
+ /** Tear down the helper's remote managed ranges before releasing the SSH master when reachable. */
83
+ dispose(): Promise<void>;
84
+ private disposeOnce;
85
+ private controlPath;
86
+ private assertOpen;
87
+ private track;
88
+ private controlCommand;
89
+ private fail;
90
+ private start;
91
+ }
92
+ export default SshConnection;
93
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** Bounded, versioned requests between an SSH client and its private remote helper. */
2
+ import { EventEmitter } from 'node:events';
3
+ import type { Readable, Writable } from 'node:stream';
4
+ import { z } from 'zod';
5
+ /** Wire version shared by the installed helper and client package. */
6
+ export declare const SSH_PROTOCOL_VERSION = 1;
7
+ /** Maximum prepared or running process handles owned by one helper. */
8
+ export declare const SSH_MAX_PROCESS_HANDLES = 128;
9
+ /** Maximum open text iterators owned by one helper. */
10
+ export declare const SSH_MAX_TEXT_STREAMS = 128;
11
+ type RequestHandler = (method: string, params: unknown, signal: AbortSignal) => Promise<unknown>;
12
+ /** A remote error retains its typed filesystem or sandbox code. */
13
+ export declare class RemoteOperationError extends Error {
14
+ readonly code?: string | undefined;
15
+ constructor(message: string, code?: string | undefined);
16
+ }
17
+ /** The peer owns pending calls and rejects ambiguous operations on connection loss; it never replays requests. */
18
+ export declare class SshRpcPeer extends EventEmitter {
19
+ private readonly input;
20
+ private readonly output;
21
+ private readonly maxFrameBytes;
22
+ private readonly maxPending;
23
+ private readonly handler?;
24
+ private readonly pending;
25
+ private readonly active;
26
+ private writeTail;
27
+ private queuedBytes;
28
+ private failure;
29
+ constructor(input: Readable, output: Writable, maxFrameBytes: number, maxPending: number, handler?: RequestHandler | undefined);
30
+ /**
31
+ * Send one request and validate its response before exposing it to the caller.
32
+ * @param method - the private helper operation.
33
+ * @param params - JSON request fields.
34
+ * @param schema - validation for the remote response.
35
+ * @param signal - cancellation without rollback of remote effects.
36
+ * @returns the validated response or a transport/remote-operation rejection.
37
+ */
38
+ request<T>(method: string, params: unknown, schema: z.ZodType<T>, signal?: AbortSignal): Promise<T>;
39
+ /**
40
+ * Fail pending operations and abort remote handlers without claiming rollback.
41
+ * @param error - the transport failure reported to all pending operations.
42
+ */
43
+ close(error?: Error): void;
44
+ private send;
45
+ private readFrames;
46
+ private receive;
47
+ private atCapacity;
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=protocol.d.ts.map