@tickernelz/wardmote-plugin-daytona 0.0.0-stage → 2026.1108.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.
Files changed (66) hide show
  1. package/README.md +52 -2
  2. package/dist/duplex-command-stream.d.ts +97 -0
  3. package/dist/duplex-command-stream.d.ts.map +1 -0
  4. package/dist/duplex-command-stream.js +205 -0
  5. package/dist/duplex-command-stream.js.map +1 -0
  6. package/dist/duplex-command-stream.live.test.d.ts +2 -0
  7. package/dist/duplex-command-stream.live.test.d.ts.map +1 -0
  8. package/dist/duplex-command-stream.live.test.js +324 -0
  9. package/dist/duplex-command-stream.live.test.js.map +1 -0
  10. package/dist/duplex-command-stream.test.d.ts +2 -0
  11. package/dist/duplex-command-stream.test.d.ts.map +1 -0
  12. package/dist/duplex-command-stream.test.js +519 -0
  13. package/dist/duplex-command-stream.test.js.map +1 -0
  14. package/dist/file-sync-diagnostics.test.d.ts +2 -0
  15. package/dist/file-sync-diagnostics.test.d.ts.map +1 -0
  16. package/dist/file-sync-diagnostics.test.js +117 -0
  17. package/dist/file-sync-diagnostics.test.js.map +1 -0
  18. package/dist/file-sync.d.ts +79 -0
  19. package/dist/file-sync.d.ts.map +1 -0
  20. package/dist/file-sync.js +1197 -0
  21. package/dist/file-sync.js.map +1 -0
  22. package/dist/file-sync.test.d.ts +2 -0
  23. package/dist/file-sync.test.d.ts.map +1 -0
  24. package/dist/file-sync.test.js +1047 -0
  25. package/dist/file-sync.test.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/login-pty.d.ts +162 -0
  31. package/dist/login-pty.d.ts.map +1 -0
  32. package/dist/login-pty.js +258 -0
  33. package/dist/login-pty.js.map +1 -0
  34. package/dist/login-pty.test.d.ts +2 -0
  35. package/dist/login-pty.test.d.ts.map +1 -0
  36. package/dist/login-pty.test.js +319 -0
  37. package/dist/login-pty.test.js.map +1 -0
  38. package/dist/manifest.d.ts +5 -0
  39. package/dist/manifest.d.ts.map +1 -0
  40. package/dist/manifest.js +182 -0
  41. package/dist/manifest.js.map +1 -0
  42. package/dist/plugin.d.ts +49 -0
  43. package/dist/plugin.d.ts.map +1 -0
  44. package/dist/plugin.js +2930 -0
  45. package/dist/plugin.js.map +1 -0
  46. package/dist/plugin.test.d.ts +2 -0
  47. package/dist/plugin.test.d.ts.map +1 -0
  48. package/dist/plugin.test.js +5627 -0
  49. package/dist/plugin.test.js.map +1 -0
  50. package/dist/pty-chunked-input.d.ts +48 -0
  51. package/dist/pty-chunked-input.d.ts.map +1 -0
  52. package/dist/pty-chunked-input.js +74 -0
  53. package/dist/pty-chunked-input.js.map +1 -0
  54. package/dist/pty-chunked-input.test.d.ts +2 -0
  55. package/dist/pty-chunked-input.test.d.ts.map +1 -0
  56. package/dist/pty-chunked-input.test.js +115 -0
  57. package/dist/pty-chunked-input.test.js.map +1 -0
  58. package/dist/tar-listing.test.d.ts +2 -0
  59. package/dist/tar-listing.test.d.ts.map +1 -0
  60. package/dist/tar-listing.test.js +220 -0
  61. package/dist/tar-listing.test.js.map +1 -0
  62. package/dist/worker.d.ts +3 -0
  63. package/dist/worker.d.ts.map +1 -0
  64. package/dist/worker.js +5 -0
  65. package/dist/worker.js.map +1 -0
  66. package/package.json +41 -3
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # `@tickernelz/wardmote-plugin-daytona`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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/wardmote-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
+ - Each cold create uses a unique provider name and ownership labels. If creation fails after Daytona has allocated a sandbox, the driver looks up that exact name, checks every ownership label, and waits for deletion. Failed, missing, or timed-out lookups and failed deletions report unconfirmed cleanup with the provider name; they never return a usable lease. A missing name lookup after an uncertain create is not proof that a delayed provider request cannot create a resource.
31
+ - For plugin-backed sandbox lease acquisition, unresolved creation cleanup crosses the worker RPC as a validated ownership envelope. The host records `pending_cleanup` before retrying deletion. If only a creation name is known, the provider first returns an ownership-verified sandbox ID without deleting it. The host saves that observation before authorizing deletion. Its existing cleanup sweep and durable spool preserve retries across controller restarts and environment deletion. A missing observed ID confirms cleanup after a lost deletion reply or database update; a name that has never been observed remains unresolved. Provider exceptions and resolved credentials are excluded from that envelope. This requires the matching host and plugin SDK update; probe and custom-image interactive-setup calls still report unconfirmed immediate cleanup without this lease-recovery path.
32
+ - 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.
33
+ - A session log socket can close while its command still runs. The driver requires a recorded command exit before returning a completed execution. It reconnects the log stream once, then polls status and log snapshots at most once per second. Replayed output is removed by byte offset, new output continues to reach the host, and the command is never dispatched again. A socket that delivers no output for 15 seconds switches directly to polling, even if the socket stays open or its initial connection never completes. Late callbacks from that socket are ignored. This recovers pending permission requests and cancellation results from saved output without replaying the command. Healthy initial and reconnected streams retain their existing lifetime under the caller’s RPC/run guard. After a clean close or an idle socket, each status or snapshot read gets a fresh operation timeout; successful recovery polling does not impose a new command lifetime. When both stream attempts fail without a clean close, fallback retains its original timeout budget starting after the stream attempts.
34
+ - The SDK returns full log snapshots, so fallback bandwidth grows with retained output; it has no paged snapshot or stream cancellation API. A recovery-read timeout keeps partial output and explicitly reports whether command exit remains unconfirmed; it does not prove the remote command stopped. Provider/session teardown remains responsible for closing an outstanding socket. A final snapshot reconciles bytes missed by the stream before returning a recorded exit.
35
+ - 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.
36
+
37
+ ## Local development
38
+
39
+ ```bash
40
+ cd packages/plugins/sandbox-providers/daytona
41
+ pnpm install --ignore-workspace --no-lockfile
42
+ pnpm build
43
+ pnpm test
44
+ pnpm typecheck
45
+ ```
46
+
47
+ These commands assume the repo root has already been installed once so the local `@tickernelz/wardmote-plugin-sdk` workspace package is available to the compiler during development.
48
+
49
+ ## Package layout
50
+
51
+ - `src/manifest.ts` declares the sandbox-provider driver metadata
52
+ - `src/plugin.ts` implements the environment lifecycle hooks
53
+ - `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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=duplex-command-stream.live.test.d.ts.map
@@ -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":""}