Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `getPlatformSupport()` now reports `uiCapabilities` on Windows when the
native probe can determine which UI restrictions the host can enforce.

## [0.7.0]

### ⚠️ Breaking changes

- **`usePty` now defaults to `false`.** `spawnSandboxFromConfig` and
`spawnSandboxAsync` spawn via `child_process` (pipe mode) unless called with
`usePty: true`, so the default path no longer requires the optional `node-pty`
peer dependency. `spawnSandboxFromConfig` therefore returns a `ChildProcess` by
default (and `IPty` only when `usePty: true`); `spawnSandboxAsync` now returns
real, separated `stdout`/`stderr` on the default path. `spawnSandbox` and
`execInSandbox` are unchanged β€” they always use PTY and require `node-pty`.
- **`node-pty` moved from `dependencies` to an optional `peerDependency`.**
Pipe-only consumers no longer pull it in transitively; consumers that use PTY
mode must install `node-pty` themselves. `loadPty()` surfaces an actionable
error when PTY mode is requested but the peer dependency is missing.

### Changed

- `node-pty` is loaded lazily, only when a PTY is actually spawned. Importing the
SDK and spawning in pipe mode never evaluates `node-pty` or loads its native
addon.
- The SDK's public PTY types (`IPty`, `IPtyForkOptions`, etc.) are now vendored
and exported from the package itself. Consumers no longer need `node-pty`
installed to type-check against the SDK (previously this failed with
`TS2307: Cannot find module 'node-pty'`).

## [0.3.0]

### ⚠️ Breaking changes
Expand Down
12 changes: 12 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
npm install @microsoft/mxc-sdk
```

`node-pty` is an **optional peer dependency**, used only for PTY mode. PTY mode is
opt-in: `spawnSandbox` (and `execInSandbox`) always use it, and
`spawnSandboxFromConfig` / `spawnSandboxAsync` use it when called with
`usePty: true`. The default path (`usePty` unset/`false`) spawns via
`child_process` and needs no native addon. The SDK's public types are
self-contained, so pipe-only consumers can install and type-check the SDK without
`node-pty`. If you use PTY mode, install it alongside the SDK:

```bash
npm install node-pty
```

```typescript
import {
spawnSandboxFromConfig, createConfigFromPolicy,
Expand Down
16 changes: 13 additions & 3 deletions sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microsoft/mxc-sdk",
"version": "0.6.1",
"version": "0.7.0",
"description": "TypeScript SDK for MXC (Microsoft eXecution Containers)",
"type": "module",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -41,13 +41,21 @@
"@types/node": "^20.10.0",
"@types/semver": "^7.7.1",
"node-gyp": "^12.2.0",
"node-pty": "^1.2.0-beta.12",
Comment thread
caarlos0 marked this conversation as resolved.
"rimraf": "^6.1.3",
"typescript": "^5.3.3"
},
"dependencies": {
"node-pty": "^1.2.0-beta.12",
"semver": "^7.7.4"
},
"peerDependencies": {
"node-pty": "^1.2.0-beta.12"
},
"peerDependenciesMeta": {
"node-pty": {
"optional": true
}
},
Comment thread
caarlos0 marked this conversation as resolved.
"engines": {
"node": ">=18.0.0"
}
Expand Down
15 changes: 13 additions & 2 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
*
* @example
* ```typescript
* import { spawnSandbox, spawnSandboxWithPty, SandboxPolicy, getPlatformSupport } from '@microsoft/mxc-sdk';
* import { spawnSandbox, SandboxPolicy, getPlatformSupport } from '@microsoft/mxc-sdk';
*
* if (getPlatformSupport().isSupported) {
* const policy: SandboxPolicy = {
* version: '0.4.0-alpha',
* network: { allowOutbound: true },
* };
*
* const ptyProcess = spawnSandboxWithPty('python -c "print(\'Hello from sandbox\')"', policy);
* const ptyProcess = spawnSandbox('python -c "print(\'Hello from sandbox\')"', policy);
* ptyProcess.onData((data) => console.log(data));
* ptyProcess.onExit((event) => console.log('Exit code:', event.exitCode));
* }
Expand Down Expand Up @@ -54,6 +54,17 @@ export {
SandboxSpawnOptions,
} from './sandbox.js';

// Export vendored node-pty type surface (so PTY-mode consumers can name these
// types without depending on the optional `node-pty` peer dependency).
export type {
IPty,
IPtyForkOptions,
IWindowsPtyForkOptions,
IBasePtyForkOptions,
IDisposable,
IEvent,
} from './pty-types.js';

// Export policy discovery functions
export {
getAvailableToolsPolicy,
Expand Down
43 changes: 43 additions & 0 deletions sdk/src/lazyPty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { createRequire } from 'node:module';
import type { NodePty } from './pty-types.js';

const require = createRequire(import.meta.url);

let cached: NodePty | undefined;

/**
* Lazily loads the `node-pty` native addon.
*
* `node-pty` loads its native binary during module evaluation, so a top-level
* `import` would force every consumer of the SDK to ship and load the addon β€”
* even those that only ever spawn in pipe mode (`usePty: false`). Deferring the
* require keeps the `usePty: false` path from ever touching `node-pty`.
*
* The return type is the vendored {@link NodePty} shape rather than node-pty's
* own module type so the generated `.d.ts` stays self-contained and consumers
* without the optional peer dependency can still type-check.
*
* Uses `createRequire` because `node-pty` is CommonJS and the call sites are
* synchronous.
*/
export function loadPty(): NodePty {
if (cached) {
return cached;
}
try {
cached = require('node-pty') as NodePty;
} catch (err) {
const e = err as NodeJS.ErrnoException;
if (e?.code === 'MODULE_NOT_FOUND' && /'node-pty'/.test(e.message)) {
throw new Error(
"PTY mode requires the optional peer dependency 'node-pty', which is not " +
'installed. Install it (e.g. `npm install node-pty`) or spawn with `usePty: false`.',
);
}
throw err;
}
return cached;
}
109 changes: 109 additions & 0 deletions sdk/src/pty-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Minimal, vendored subset of the `node-pty` public type surface.
*
* `node-pty` is an *optional* peer dependency (see {@link ./lazyPty}). If the
* SDK referenced `node-pty`'s own types in its public API, every consumer would
* need `node-pty` installed just to type-check β€” even pipe-only consumers that
* never spawn a PTY (they would otherwise hit `TS2307: Cannot find module
* 'node-pty'`). Re-declaring the slice we expose keeps the generated `.d.ts`
* self-contained.
*
* These declarations are structurally compatible with the real `node-pty`
* types, so values produced by the actual module satisfy them at runtime.
*/

/** An object that can be disposed via a dispose function. */
export interface IDisposable {
dispose(): void;
}

/**
* An event that can be listened to.
* @returns an `IDisposable` to stop listening.
*/
export interface IEvent<T> {
(listener: (e: T) => unknown): IDisposable;
}

/** Options shared by all platforms when forking a pseudoterminal. */
export interface IBasePtyForkOptions {
/** Name of the terminal to be set in environment ($TERM variable). */
name?: string;
/** Number of initial cols of the pty. */
cols?: number;
/** Number of initial rows of the pty. */
rows?: number;
/** Working directory to be set for the child program. */
cwd?: string;
/** Environment to be set for the child program. */
env?: { [key: string]: string | undefined };
/** String encoding of the underlying pty. */
encoding?: string | null;
/** (EXPERIMENTAL) Whether to enable flow control handling. */
handleFlowControl?: boolean;
/** (EXPERIMENTAL) String that pauses the pty when `handleFlowControl` is true. */
flowControlPause?: string;
/** (EXPERIMENTAL) String that resumes the pty when `handleFlowControl` is true. */
flowControlResume?: string;
}

/** POSIX-specific fork options. */
export interface IPtyForkOptions extends IBasePtyForkOptions {
uid?: number;
gid?: number;
}

/** Windows-specific fork options. */
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
/** @deprecated Ignored by node-pty; retained for compatibility. */
useConpty?: boolean;
/** (EXPERIMENTAL) Use the conpty.dll shipped with node-pty. */
useConptyDll?: boolean;
/** Whether to use PSEUDOCONSOLE_INHERIT_CURSOR in conpty. */
conptyInheritCursor?: boolean;
}

/** An interface representing a pseudoterminal. */
export interface IPty {
/** The process ID of the outer process. */
readonly pid: number;
/** The column size in characters. */
readonly cols: number;
/** The row size in characters. */
readonly rows: number;
/** The title of the active process. */
readonly process: string;
/** (EXPERIMENTAL) Whether to handle flow control at runtime. */
handleFlowControl: boolean;
/** Fires when data is returned from the pty. */
readonly onData: IEvent<string>;
/** Fires when the pty exits. */
readonly onExit: IEvent<{ exitCode: number; signal?: number }>;
/** Resizes the dimensions of the pty. */
resize(columns: number, rows: number, pixelSize?: { width: number; height: number }): void;
/** Clears the pty's internal representation of its buffer (ConPTY only). */
clear(): void;
/** Writes data to the pty. */
write(data: string | Buffer): void;
/** Kills the pty. */
kill(signal?: string): void;
/** Pauses the pty for customizable flow control. */
pause(): void;
/** Resumes the pty for customizable flow control. */
resume(): void;
}

/**
* The slice of the `node-pty` module shape that the SDK calls into. Returned by
* {@link ./lazyPty.loadPty}.
*/
export interface NodePty {
spawn(
file: string,
args: string[] | string,
options: IPtyForkOptions | IWindowsPtyForkOptions,
): IPty;
}
Loading
Loading