@tickernelz/paperclip-pro-plugin-daytona 2026.925.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/duplex-command-stream.d.ts +97 -0
- package/dist/duplex-command-stream.d.ts.map +1 -0
- package/dist/duplex-command-stream.js +205 -0
- package/dist/duplex-command-stream.js.map +1 -0
- package/dist/duplex-command-stream.live.test.d.ts +2 -0
- package/dist/duplex-command-stream.live.test.d.ts.map +1 -0
- package/dist/duplex-command-stream.live.test.js +324 -0
- package/dist/duplex-command-stream.live.test.js.map +1 -0
- package/dist/duplex-command-stream.test.d.ts +2 -0
- package/dist/duplex-command-stream.test.d.ts.map +1 -0
- package/dist/duplex-command-stream.test.js +519 -0
- package/dist/duplex-command-stream.test.js.map +1 -0
- package/dist/file-sync.d.ts +77 -0
- package/dist/file-sync.d.ts.map +1 -0
- package/dist/file-sync.js +1055 -0
- package/dist/file-sync.js.map +1 -0
- package/dist/file-sync.test.d.ts +2 -0
- package/dist/file-sync.test.d.ts.map +1 -0
- package/dist/file-sync.test.js +974 -0
- package/dist/file-sync.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/login-pty.d.ts +162 -0
- package/dist/login-pty.d.ts.map +1 -0
- package/dist/login-pty.js +258 -0
- package/dist/login-pty.js.map +1 -0
- package/dist/login-pty.test.d.ts +2 -0
- package/dist/login-pty.test.d.ts.map +1 -0
- package/dist/login-pty.test.js +319 -0
- package/dist/login-pty.test.js.map +1 -0
- package/dist/manifest.d.ts +4 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +179 -0
- package/dist/manifest.js.map +1 -0
- package/dist/plugin.d.ts +49 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2563 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.test.d.ts +2 -0
- package/dist/plugin.test.d.ts.map +1 -0
- package/dist/plugin.test.js +4701 -0
- package/dist/plugin.test.js.map +1 -0
- package/dist/pty-chunked-input.d.ts +48 -0
- package/dist/pty-chunked-input.d.ts.map +1 -0
- package/dist/pty-chunked-input.js +74 -0
- package/dist/pty-chunked-input.js.map +1 -0
- package/dist/pty-chunked-input.test.d.ts +2 -0
- package/dist/pty-chunked-input.test.d.ts.map +1 -0
- package/dist/pty-chunked-input.test.js +115 -0
- package/dist/pty-chunked-input.test.js.map +1 -0
- package/dist/worker.d.ts +3 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +5 -0
- package/dist/worker.js.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# `@tickernelz/paperclip-pro-plugin-daytona`
|
|
2
|
+
|
|
3
|
+
Published Daytona sandbox provider plugin for Paperclip.
|
|
4
|
+
|
|
5
|
+
This package lives in the Paperclip monorepo, but it is intentionally excluded from the root `pnpm` workspace and shaped to publish and install like a standalone npm package. That lets operators install it from the Plugins page by package name without introducing root lockfile churn for Daytona's SDK dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
From a Paperclip instance, install:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
@tickernelz/paperclip-pro-plugin-daytona
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The host plugin installer runs `npm install` into the managed plugin directory, so transitive dependencies such as `@daytonaio/sdk` are pulled in during installation.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Configure Daytona from `Instance Settings -> Environments`, not from the plugin's plugin page.
|
|
20
|
+
|
|
21
|
+
- Put the Daytona API key on the sandbox environment itself.
|
|
22
|
+
- When you save an environment, Paperclip stores pasted API keys as company secrets.
|
|
23
|
+
- `DAYTONA_API_KEY` remains an optional host-level fallback when an environment omits the key.
|
|
24
|
+
- Optional `apiUrl` and `target` settings map directly to the Daytona SDK/client configuration. If `apiUrl` is omitted, the Daytona SDK uses its default endpoint.
|
|
25
|
+
|
|
26
|
+
Notes:
|
|
27
|
+
|
|
28
|
+
- The current published Daytona SDK package is `@daytonaio/sdk`.
|
|
29
|
+
- The driver supports both `snapshot`-based and `image`-based sandbox creation. If both are set, validation rejects the config as ambiguous.
|
|
30
|
+
- Reusable leases map to Daytona stop/start semantics. Non-reusable leases are deleted on release. A provider-resolved `target` does not change the identity of an existing sandbox. Release closes the same scoped lease that a later sentinel-verified resume reopens.
|
|
31
|
+
- A sandbox record can survive the loss of its underlying container. Resume treats it as expired only when a fresh provider read confirms the exact missing-container error for that sandbox and marks it unrecoverable. Unknown errors and failed confirmation reads preserve the lease. The host still requires a verified native-runner backup before replacement.
|
|
32
|
+
|
|
33
|
+
## Local development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd packages/plugins/sandbox-providers/daytona
|
|
37
|
+
pnpm install --ignore-workspace --no-lockfile
|
|
38
|
+
pnpm build
|
|
39
|
+
pnpm test
|
|
40
|
+
pnpm typecheck
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
These commands assume the repo root has already been installed once so the local `@tickernelz/paperclip-pro-plugin-sdk` workspace package is available to the compiler during development.
|
|
44
|
+
|
|
45
|
+
## Package layout
|
|
46
|
+
|
|
47
|
+
- `src/manifest.ts` declares the sandbox-provider driver metadata
|
|
48
|
+
- `src/plugin.ts` implements the environment lifecycle hooks
|
|
49
|
+
- `paperclipPlugin.manifest` and `paperclipPlugin.worker` point the host at the built plugin entrypoints in `dist/`
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { DaytonaPtyProcess } from "./login-pty.js";
|
|
2
|
+
export type { DaytonaPtyCreateOptions, DaytonaPtyHandle, DaytonaPtyProcess, } from "./login-pty.js";
|
|
3
|
+
/**
|
|
4
|
+
* A live duplex channel session for one command. The session allocates a real
|
|
5
|
+
* pseudo-terminal in raw mode, streams the raw output, accepts host input, and
|
|
6
|
+
* stops the child. The shape matches the worker duplex channel session, so the
|
|
7
|
+
* worker forwards the data and the exit with no adapter.
|
|
8
|
+
*/
|
|
9
|
+
export interface DuplexChannelSession {
|
|
10
|
+
/** Registers the one data listener. The session streams each raw byte chunk in order. */
|
|
11
|
+
onData(listener: (chunk: Uint8Array) => void): void;
|
|
12
|
+
/** Writes raw input bytes to the pseudo-terminal. */
|
|
13
|
+
write(data: Uint8Array): void;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves when the command ends or the transport closes. A numeric `exitCode`
|
|
16
|
+
* is a real process exit. `transportClosed` is true when the pseudo-terminal
|
|
17
|
+
* socket closed with no exit data, so the caller can tell a real process exit
|
|
18
|
+
* from a reason-less transport close.
|
|
19
|
+
*/
|
|
20
|
+
wait(): Promise<{
|
|
21
|
+
exitCode: number | null;
|
|
22
|
+
transportClosed: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/** Stops the child process. Safe to call more than one time. */
|
|
25
|
+
kill(): void;
|
|
26
|
+
/** Releases the session resources. Safe to call more than one time. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Opens a {@link DuplexChannelSession} for `command`. The transport calls it one
|
|
31
|
+
* time. `command` is an argument vector: element 0 is the program and the rest
|
|
32
|
+
* are its arguments. The session quotes each element, so a shell metacharacter in
|
|
33
|
+
* an element cannot inject a shell command.
|
|
34
|
+
*/
|
|
35
|
+
export type DuplexChannelSessionOpener = (command: readonly string[]) => Promise<DuplexChannelSession>;
|
|
36
|
+
/**
|
|
37
|
+
* The typed, closed reason for a duplex channel write failure. The seam reports
|
|
38
|
+
* only this constant, never the raw provider error text. It maps to the host
|
|
39
|
+
* telemetry `write_error` loss reason.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DAYTONA_DUPLEX_WRITE_ERROR_REASON: "write_error";
|
|
42
|
+
/** The typed reason a rejected host-to-sandbox write reports. */
|
|
43
|
+
export type DaytonaDuplexWriteErrorReason = typeof DAYTONA_DUPLEX_WRITE_ERROR_REASON;
|
|
44
|
+
/** The options for the Daytona duplex channel session. */
|
|
45
|
+
export interface DaytonaDuplexChannelOptions {
|
|
46
|
+
/** The working directory for the duplex channel PTY. Defaults to the sandbox default. */
|
|
47
|
+
cwd?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The absolute sandbox path for the gateway diagnostics. The wrapper redirects
|
|
50
|
+
* the terminal's stderr to this path, so a diagnostic line never reaches the
|
|
51
|
+
* stdout frame stream. Defaults to a per-channel path under `/tmp`.
|
|
52
|
+
*/
|
|
53
|
+
diagnosticsPath?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The write-error seam. The session calls it one time when a host-to-sandbox
|
|
56
|
+
* `sendInput` rejects. The session then ends the channel at once. The seam
|
|
57
|
+
* carries only the typed {@link DaytonaDuplexWriteErrorReason}; the raw provider
|
|
58
|
+
* error never reaches it.
|
|
59
|
+
*/
|
|
60
|
+
onWriteError?: (reason: DaytonaDuplexWriteErrorReason) => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds the launch wrapper input line for the duplex channel. `command` is an
|
|
64
|
+
* argument vector: element 0 is the program and the rest are its arguments.
|
|
65
|
+
*
|
|
66
|
+
* The wrapper does three steps in order:
|
|
67
|
+
* 1. `exec 2>'<diagnosticsPath>'` redirects the shell's stderr to the file. The
|
|
68
|
+
* later `exec` inherits it, so the gateway diagnostics land in the file and
|
|
69
|
+
* never on the stdout frame stream.
|
|
70
|
+
* 2. `stty raw -echo` sets the terminal to raw mode with echo off, so the
|
|
71
|
+
* terminal neither echoes host input as data nor translates newlines.
|
|
72
|
+
* 3. `exec '<program>' '<arg>'...` replaces the shell with the gateway, so the
|
|
73
|
+
* PTY runs the gateway directly and the gateway exit code becomes the PTY
|
|
74
|
+
* exit code.
|
|
75
|
+
*
|
|
76
|
+
* The wrapper quotes the diagnostics path and every command argument as a
|
|
77
|
+
* single-quoted shell word. So a shell metacharacter in a path or an argument
|
|
78
|
+
* stays literal text and cannot inject a shell command.
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildDuplexChannelLaunchWrapper(command: readonly string[], diagnosticsPath: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Opens a Daytona duplex channel PTY session for `command` and returns it as a
|
|
83
|
+
* {@link DuplexChannelSession}. The session allocates a real pseudo-terminal in
|
|
84
|
+
* raw mode, streams the raw output, accepts host input, and stops the child.
|
|
85
|
+
*
|
|
86
|
+
* The function forwards the terminal bytes unchanged. It buffers the output
|
|
87
|
+
* until the transport registers the listener, so no early chunk is lost.
|
|
88
|
+
*/
|
|
89
|
+
export declare function openDaytonaDuplexChannelSession(process: DaytonaPtyProcess, command: readonly string[], options?: DaytonaDuplexChannelOptions): Promise<DuplexChannelSession>;
|
|
90
|
+
/**
|
|
91
|
+
* Creates a {@link DuplexChannelSessionOpener} bound to a Daytona `process`. Pass
|
|
92
|
+
* `sandbox.process` from the Daytona SDK. The opener runs the gateway command on a
|
|
93
|
+
* raw pseudo-terminal, streams the output, accepts host input, and stops the child
|
|
94
|
+
* for a terminal state.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createDaytonaDuplexChannelSessionOpener(process: DaytonaPtyProcess, options?: DaytonaDuplexChannelOptions): DuplexChannelSessionOpener;
|
|
97
|
+
//# sourceMappingURL=duplex-command-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplex-command-stream.d.ts","sourceRoot":"","sources":["../src/duplex-command-stream.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAIxB;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,yFAAyF;IACzF,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACpD,qDAAqD;IACrD,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B;;;;;OAKG;IACH,IAAI,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACvE,gEAAgE;IAChE,IAAI,IAAI,IAAI,CAAC;IACb,uEAAuE;IACvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG,CACvC,OAAO,EAAE,SAAS,MAAM,EAAE,KACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEnC;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,EAAG,aAAsB,CAAC;AAExE,iEAAiE;AACjE,MAAM,MAAM,6BAA6B,GAAG,OAAO,iCAAiC,CAAC;AAErF,0DAA0D;AAC1D,MAAM,WAAW,2BAA2B;IAC1C,yFAAyF;IACzF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,6BAA6B,KAAK,IAAI,CAAC;CAChE;AAwBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,eAAe,EAAE,MAAM,GACtB,MAAM,CASR;AAED;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,oBAAoB,CAAC,CA2G/B;AAED;;;;;GAKG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE,2BAA2B,GACpC,0BAA0B,CAG5B"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// The Daytona duplex command stream for the sandbox callback bridge. The bridge
|
|
2
|
+
// needs one live bidirectional byte stream: the host writes request frames to the
|
|
3
|
+
// process, and the process writes response frames back, on one connection while
|
|
4
|
+
// the command runs. This module binds a Daytona pseudo-terminal (PTY) to a small
|
|
5
|
+
// session that the duplex transport consumes, so the gateway runs on a real
|
|
6
|
+
// terminal, streams its output, and receives host input.
|
|
7
|
+
//
|
|
8
|
+
// Primitive choice (the first acceptance criterion of the phase): the Daytona SDK
|
|
9
|
+
// proves two primitives. The ordinary session primitive (`createSession` plus
|
|
10
|
+
// `executeSessionCommand` plus `getSessionCommandLogs`) dispatches a command and
|
|
11
|
+
// tails its output, split into separate stdout and stderr streams. It exposes no
|
|
12
|
+
// call that writes bytes to a running command's stdin. The PTY primitive
|
|
13
|
+
// (`createPty`) exposes `sendInput` for host-to-process bytes and one `onData`
|
|
14
|
+
// callback for process-to-host bytes, on one live socket. Only the PTY carries a
|
|
15
|
+
// bidirectional stream on one connection, so this module uses the PTY. The
|
|
16
|
+
// characterization test in `duplex-command-stream.test.ts` records the evidence.
|
|
17
|
+
//
|
|
18
|
+
// Clean stream: a PTY echoes its input and translates newlines by default
|
|
19
|
+
// (`ICRNL` on input, `ONLCR` on output). The frames are newline-delimited JSON, so
|
|
20
|
+
// echo and newline translation corrupt the stream. The launch wrapper runs
|
|
21
|
+
// `stty raw -echo` before it starts the gateway, so the terminal becomes an 8-bit
|
|
22
|
+
// transparent path with no echo and no newline translation. The wrapper also
|
|
23
|
+
// redirects the gateway diagnostics to a file, so a diagnostic line never reaches
|
|
24
|
+
// the stdout frame stream. The wrapper starts the gateway with `exec`, so the PTY
|
|
25
|
+
// runs the gateway directly and the gateway exit code becomes the PTY exit code.
|
|
26
|
+
//
|
|
27
|
+
// Dependency boundary: this provider plugin ships standalone (the workspace
|
|
28
|
+
// excludes `packages/plugins/sandbox-providers/**`). So the module imports no
|
|
29
|
+
// workspace package. It reuses the narrow Daytona PTY surface that
|
|
30
|
+
// `login-pty.ts` declares (`DaytonaPtyProcess`, `DaytonaPtyHandle`,
|
|
31
|
+
// `DaytonaPtyCreateOptions`), so a caller passes `sandbox.process` with no
|
|
32
|
+
// adapter. The narrow surface keeps the module unit-testable with a fake PTY.
|
|
33
|
+
import { randomUUID } from "node:crypto";
|
|
34
|
+
import { sendPtyInputInChunks } from "./pty-chunked-input.js";
|
|
35
|
+
/**
|
|
36
|
+
* The typed, closed reason for a duplex channel write failure. The seam reports
|
|
37
|
+
* only this constant, never the raw provider error text. It maps to the host
|
|
38
|
+
* telemetry `write_error` loss reason.
|
|
39
|
+
*/
|
|
40
|
+
export const DAYTONA_DUPLEX_WRITE_ERROR_REASON = "write_error";
|
|
41
|
+
// The terminal size for the duplex channel PTY. The channel carries bytes, not a
|
|
42
|
+
// visible UI, so a fixed standard size is enough. A fixed size keeps the launch
|
|
43
|
+
// deterministic.
|
|
44
|
+
const DUPLEX_CHANNEL_PTY_COLS = 120;
|
|
45
|
+
const DUPLEX_CHANNEL_PTY_ROWS = 30;
|
|
46
|
+
/**
|
|
47
|
+
* The input terminator that submits the launch wrapper line. The terminal reads
|
|
48
|
+
* the carriage return as the Enter key, so the shell runs the wrapper line.
|
|
49
|
+
*/
|
|
50
|
+
const PTY_COMMAND_TERMINATOR = "\r";
|
|
51
|
+
/**
|
|
52
|
+
* Quotes one value for a POSIX shell. The result is one single-quoted word, so
|
|
53
|
+
* the shell reads it as literal text and never expands or splits it. The function
|
|
54
|
+
* closes the quote, adds an escaped single quote, and reopens the quote for each
|
|
55
|
+
* single quote in the value.
|
|
56
|
+
*/
|
|
57
|
+
function shellQuote(value) {
|
|
58
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Builds the launch wrapper input line for the duplex channel. `command` is an
|
|
62
|
+
* argument vector: element 0 is the program and the rest are its arguments.
|
|
63
|
+
*
|
|
64
|
+
* The wrapper does three steps in order:
|
|
65
|
+
* 1. `exec 2>'<diagnosticsPath>'` redirects the shell's stderr to the file. The
|
|
66
|
+
* later `exec` inherits it, so the gateway diagnostics land in the file and
|
|
67
|
+
* never on the stdout frame stream.
|
|
68
|
+
* 2. `stty raw -echo` sets the terminal to raw mode with echo off, so the
|
|
69
|
+
* terminal neither echoes host input as data nor translates newlines.
|
|
70
|
+
* 3. `exec '<program>' '<arg>'...` replaces the shell with the gateway, so the
|
|
71
|
+
* PTY runs the gateway directly and the gateway exit code becomes the PTY
|
|
72
|
+
* exit code.
|
|
73
|
+
*
|
|
74
|
+
* The wrapper quotes the diagnostics path and every command argument as a
|
|
75
|
+
* single-quoted shell word. So a shell metacharacter in a path or an argument
|
|
76
|
+
* stays literal text and cannot inject a shell command.
|
|
77
|
+
*/
|
|
78
|
+
export function buildDuplexChannelLaunchWrapper(command, diagnosticsPath) {
|
|
79
|
+
if (command.length === 0) {
|
|
80
|
+
throw new Error("The duplex channel launch command needs at least one argument.");
|
|
81
|
+
}
|
|
82
|
+
const quotedCommand = command.map(shellQuote).join(" ");
|
|
83
|
+
return (`exec 2>${shellQuote(diagnosticsPath)}; stty raw -echo; ` +
|
|
84
|
+
`exec ${quotedCommand}${PTY_COMMAND_TERMINATOR}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Opens a Daytona duplex channel PTY session for `command` and returns it as a
|
|
88
|
+
* {@link DuplexChannelSession}. The session allocates a real pseudo-terminal in
|
|
89
|
+
* raw mode, streams the raw output, accepts host input, and stops the child.
|
|
90
|
+
*
|
|
91
|
+
* The function forwards the terminal bytes unchanged. It buffers the output
|
|
92
|
+
* until the transport registers the listener, so no early chunk is lost.
|
|
93
|
+
*/
|
|
94
|
+
export async function openDaytonaDuplexChannelSession(process, command, options) {
|
|
95
|
+
let listener = null;
|
|
96
|
+
let buffered = Buffer.alloc(0);
|
|
97
|
+
const diagnosticsPath = options?.diagnosticsPath ?? `/tmp/paperclip-duplex-${randomUUID()}.log`;
|
|
98
|
+
const handle = await process.createPty({
|
|
99
|
+
id: `paperclip-duplex-${randomUUID()}`,
|
|
100
|
+
...(options?.cwd ? { cwd: options.cwd } : {}),
|
|
101
|
+
cols: DUPLEX_CHANNEL_PTY_COLS,
|
|
102
|
+
rows: DUPLEX_CHANNEL_PTY_ROWS,
|
|
103
|
+
onData: (data) => {
|
|
104
|
+
// Forward the raw bytes unchanged; the frame codec owns the
|
|
105
|
+
// newline-delimited JSON parsing and any multi-byte character
|
|
106
|
+
// reassembly on the read side.
|
|
107
|
+
if (data.byteLength === 0)
|
|
108
|
+
return;
|
|
109
|
+
if (listener)
|
|
110
|
+
listener(data);
|
|
111
|
+
else
|
|
112
|
+
buffered = buffered.length === 0 ? Buffer.from(data) : Buffer.concat([buffered, data]);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
await handle.waitForConnection();
|
|
116
|
+
// Start the gateway through the raw-mode wrapper. Raw mode with echo off stops
|
|
117
|
+
// the terminal from echoing host input back as data and from translating the
|
|
118
|
+
// frame newlines. The diagnostics redirect keeps the stdout frame stream clean.
|
|
119
|
+
await handle.sendInput(buildDuplexChannelLaunchWrapper(command, diagnosticsPath));
|
|
120
|
+
// Report a host-to-sandbox write failure one time and end the channel at once.
|
|
121
|
+
// The seam carries only the typed reason; the raw provider error never leaves
|
|
122
|
+
// this scope. The channel end propagates the loss up through the exit.
|
|
123
|
+
//
|
|
124
|
+
// `channelTerminated` turns true on the first rejected chunk, before the early
|
|
125
|
+
// return, so a later queued write reads it and sends no chunk. Without the flag
|
|
126
|
+
// a queued write runs after the terminal kill and calls `sendInput` on the
|
|
127
|
+
// closed transport.
|
|
128
|
+
let channelTerminated = false;
|
|
129
|
+
let writeErrorReported = false;
|
|
130
|
+
const endOnWriteError = () => {
|
|
131
|
+
channelTerminated = true;
|
|
132
|
+
if (writeErrorReported)
|
|
133
|
+
return;
|
|
134
|
+
writeErrorReported = true;
|
|
135
|
+
options?.onWriteError?.(DAYTONA_DUPLEX_WRITE_ERROR_REASON);
|
|
136
|
+
void handle.kill().catch(() => undefined);
|
|
137
|
+
};
|
|
138
|
+
// The tail of the write chain. The chunker awaits each chunk, so one write can
|
|
139
|
+
// suspend between its chunks. The chain runs each write after the previous
|
|
140
|
+
// write ends, so the chunks of two writes never interleave on the wire. The
|
|
141
|
+
// chain never rejects, because the chunker maps a rejected send to
|
|
142
|
+
// `endOnWriteError` and returns.
|
|
143
|
+
let writeChain = Promise.resolve();
|
|
144
|
+
return {
|
|
145
|
+
onData(next) {
|
|
146
|
+
listener = next;
|
|
147
|
+
if (buffered.length > 0) {
|
|
148
|
+
const pending = buffered;
|
|
149
|
+
buffered = Buffer.alloc(0);
|
|
150
|
+
next(pending);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
write(data) {
|
|
154
|
+
// Send the input as byte-bounded chunks under the provider message cap. A
|
|
155
|
+
// whole payload in one message can cross the cap and take the channel down,
|
|
156
|
+
// so the chunker slices the payload and sends each chunk in order. The chain
|
|
157
|
+
// runs this write after the previous write ends, so two writes keep their
|
|
158
|
+
// order and never interleave their chunks. A write error must not throw into
|
|
159
|
+
// the transport, so the transport's stream stays the single result path. On
|
|
160
|
+
// a rejected chunk, the chunker ends the channel one time through
|
|
161
|
+
// `endOnWriteError` and sends no later chunk. The raw provider error never
|
|
162
|
+
// reaches a sink.
|
|
163
|
+
//
|
|
164
|
+
// A rejected chunk terminalizes the channel and kills the transport. So this
|
|
165
|
+
// write skips the send when `channelTerminated` is true, and a queued write
|
|
166
|
+
// never calls `sendInput` on the closed transport.
|
|
167
|
+
writeChain = writeChain.then(() => {
|
|
168
|
+
if (channelTerminated)
|
|
169
|
+
return;
|
|
170
|
+
return sendPtyInputInChunks((chunk) => handle.sendInput(chunk), data, endOnWriteError);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
async wait() {
|
|
174
|
+
const result = await handle.wait();
|
|
175
|
+
if (typeof result.exitCode === "number") {
|
|
176
|
+
// A numeric exit code is a real process exit. The SDK parses it from the
|
|
177
|
+
// pseudo-terminal WebSocket close reason (for example `{"exitCode":0}`).
|
|
178
|
+
return { exitCode: result.exitCode, transportClosed: false };
|
|
179
|
+
}
|
|
180
|
+
// A non-numeric exit code marks a reason-less transport close: the socket
|
|
181
|
+
// closed with no exit data. It is a transport close, not a process exit.
|
|
182
|
+
return { exitCode: null, transportClosed: true };
|
|
183
|
+
},
|
|
184
|
+
kill() {
|
|
185
|
+
void handle.kill().catch(() => undefined);
|
|
186
|
+
},
|
|
187
|
+
async close() {
|
|
188
|
+
// Stop the child and release the socket. `kill` ends the gateway and `wait`
|
|
189
|
+
// resolves with the exit; `disconnect` releases the PTY socket. Both are
|
|
190
|
+
// safe to call more than one time, so close stays idempotent.
|
|
191
|
+
await handle.kill().catch(() => undefined);
|
|
192
|
+
await handle.disconnect().catch(() => undefined);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Creates a {@link DuplexChannelSessionOpener} bound to a Daytona `process`. Pass
|
|
198
|
+
* `sandbox.process` from the Daytona SDK. The opener runs the gateway command on a
|
|
199
|
+
* raw pseudo-terminal, streams the output, accepts host input, and stops the child
|
|
200
|
+
* for a terminal state.
|
|
201
|
+
*/
|
|
202
|
+
export function createDaytonaDuplexChannelSessionOpener(process, options) {
|
|
203
|
+
return (command) => openDaytonaDuplexChannelSession(process, command, options);
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=duplex-command-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplex-command-stream.js","sourceRoot":"","sources":["../src/duplex-command-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplex-command-stream.live.test.d.ts","sourceRoot":"","sources":["../src/duplex-command-stream.live.test.ts"],"names":[],"mappings":""}
|