Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/Build.MacOS.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:

# mxc-sdk includes the Seatbelt executor characterization tests.
# mxc-exec-mac (built above via -p mxc_darwin) is what they drive; the
# tests skip via has_platform_exec() if it is missing. sandbox-exec needs
# tests skip via has_platform_exec() if it is missing. sandbox_init needs
# no elevation, so they run in this standard macOS job.
- name: Test
run: |
Expand Down
3 changes: 1 addition & 2 deletions docs/api-reference/dotnet/v1/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ Creation takes `ContainerRequest` and operation options. Existing-container
execution takes the `ContainerId` returned by provision, `ExecutionRequest`,
and operation options. Create-and-run PTY support covers IsolationSession, Bubblewrap,
LXC, and Seatbelt direct execution. Existing-container PTY support remains
IsolationSession-only. Seatbelt PTY rejects `guiAccess` and legacy
`launchMethod: "open"`. Terminal handles
IsolationSession-only. Seatbelt PTY rejects `guiAccess`. Terminal handles
give the caller explicit input, output, resize, and process ownership; there
is no separate public attached-console launch API or API accepting ["MXC request JSON"](../../../schema.md#mxc-request-json).

Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/node/v1/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ execution takes the `ContainerId` returned by provision, `ExecutionRequest`,
and operation options. PTY operations are asynchronous. Create-and-run PTY support
covers IsolationSession, Bubblewrap, LXC, and Seatbelt direct execution.
Existing-container PTY support remains IsolationSession-only. Seatbelt PTY
rejects `guiAccess` and legacy `launchMethod: "open"`.
rejects `guiAccess`.
Terminal handles give the caller explicit input, output, resize, and process
ownership; there is no separate attached-console launch API or API accepting ["MXC request JSON"](../../../schema.md#mxc-request-json).

Expand Down
3 changes: 1 addition & 2 deletions docs/api-reference/rust/v1/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ Creation takes `ContainerRequest` and operation options. Existing-container
execution takes the `ContainerId` returned by provision, `ExecutionRequest`,
and operation options. Create-and-run PTY support covers IsolationSession, Bubblewrap,
LXC, and Seatbelt direct execution. Existing-container PTY support remains
IsolationSession-only. Seatbelt PTY rejects `guiAccess` and legacy
`launchMethod: "open"`. A PTY gives the
IsolationSession-only. Seatbelt PTY rejects `guiAccess`. A PTY gives the
caller explicit input, output, resize, and process ownership instead of
attaching the workload to the host process's global console streams.

Expand Down
15 changes: 2 additions & 13 deletions docs/backends/seatbelt/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ remains deny rather than inheriting ingress allow.
| Firewall / packet-filter enforcement mode | ❌ | Rejected β€” no packet-filter layer |
| Proxy peer identity pinning | ❌ | Rejected β€” not supported |
| Named containers, attach, lifecycle | ❌ | Not applicable β€” process-scoped |
| Caller-controlled PTY | βœ… | Direct `exec` only; incompatible with `guiAccess` and legacy `launchMethod: "open"` |
| Caller-controlled PTY | βœ… | Incompatible with `guiAccess` |

The short version: **Seatbelt gives you an on/off switch for outbound network
plus a loopback exception. It has no concept of "this host but not that one."**
Expand Down Expand Up @@ -382,14 +382,6 @@ in `global-name`). Read access to `/private/var/db/mds` and
baseline `/Library` and `/System` allows.
</details>


> **`launchMethod` is retired with the pre-0.9 contracts.** A
> `0.9.0-alpha` config that sets it is rejected; drop the field and the process
> is launched with `exec`, like every other backend. On the older schemas,
> `"exec"` (the default) applies `sandbox_init()` then execs directly, while
> `"open"` runs the command as the first shell of a Terminal.app instance β€” and
> sandboxes that shell. Terminal itself runs unsandboxed.

## Process environment

**The host environment is never inherited.** The child always starts from a
Expand Down Expand Up @@ -456,10 +448,7 @@ adds to or overrides that baseline. `inheritDefaultEnv` is rejected.
`readonlyPaths[0]` β†’ `/`. A `~` default is tilde-expanded the same way policy
paths are. `PWD` is exported to the resolved directory.

Both launch methods apply it: `exec` sets it on the child process, while `open`
performs the `cd` and the `PWD` export inside the generated helper script,
since Terminal would otherwise start the workload in its own directory. A
relative `cwd` is resolved against the MXC process's directory on both paths.
A relative `cwd` is resolved against the MXC process's directory.

**Note:** `getcwd()` only succeeds when the directory *itself* is readable under the profile. An
out-of-policy `cwd` makes callers that resolve relative paths (`git`, Python's
Expand Down
8 changes: 5 additions & 3 deletions docs/development/plans/backend-support-probe-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ pub enum BackendCapability {

/// Probe the host and return only the backends it can currently run.
/// An empty `Vec` means "no backend this API can currently affirm on this host"
/// (e.g. an unsupported platform, Linux with neither `bwrap` nor `lxc`, or
/// macOS without `sandbox-exec`) β€” it is a normal result, not an error.
/// (e.g. an unsupported platform, or Linux with neither `bwrap` nor `lxc`) β€” it is a normal result, not an error.
pub fn available_backends() -> Vec<AvailableBackend>;
```

Expand Down Expand Up @@ -145,7 +144,7 @@ See Β§7.9 for why the canonical probe stays in Rust rather than moving into the
- On Windows the result contains `processcontainer` with a `tier` of one of the three known strings;
`appcontainer-dacl` is the floor when nothing higher is reachable.
- On non-Windows, `processcontainer` never appears.
- On macOS, `seatbelt` appears with `tier: None` when `/usr/bin/sandbox-exec` exists.
- On macOS, `seatbelt` appears with `tier: None` when `sandbox_init` resolves.
- A serde snapshot pins the camelCase JSON shape
(`{"backend":"…","tier":"…"}`) and verifies that `tier` is **omitted** when `None`
(`#[serde(skip_serializing_if = "Option::is_none")]`), never serialized as `null`.
Expand Down Expand Up @@ -195,6 +194,9 @@ contract of this API.
Still open: it is not yet the transport for the rest of `getPlatformSupport()` on Linux,
so the drift risk in step 2 stands, and Windows is untouched.

**Landed (macOS).** `getPlatformSupport()` reads Seatbelt availability from
`getAvailableBackends()` (`mxc_ffi`) and fails closed if that call throws.

---

## 7. Appendix - Decisions & Notes
Expand Down
2 changes: 1 addition & 1 deletion sdk/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Console.WriteLine($"exit={result.ExitCode}");
resizing support. Initial dimensions default to 24 rows by 80 columns.
PTY stderr is merged into `Output`. Closing `Input` requests terminal EOF in
canonical mode; raw-mode applications must use their own completion protocol.
Seatbelt rejects PTY mode with `guiAccess` or legacy `launchMethod: "open"`.
Seatbelt rejects PTY mode with `guiAccess`.
Unsupported combinations are rejected before sandbox creation.

## Lifecycle API
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ try {
and resizing support. Initial dimensions default to 24 rows by 80 columns.
Terminal stderr is merged into `output`. Closing `input` requests terminal EOF
when supported; raw-mode applications must use their own completion protocol.
Seatbelt rejects PTY mode with `guiAccess` or legacy `launchMethod: "open"`.
Seatbelt rejects PTY mode with `guiAccess`.
Unsupported combinations are rejected before sandbox creation.

## Lifecycle API
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/src/v1/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function buildLinuxProcessConfig(
/**
* Builds the macOS process container (seatbelt) portion of a ContainerConfig.
*
* The seatbelt backend's `sandbox-exec` reads a TinyScheme profile
* The seatbelt backend applies a TinyScheme profile
* generated server-side by `seatbelt_common::profile_builder`, so the SDK
* only needs to set the containment type and ensure the top-level `seatbelt`
* config block exists β€” the policy fields on `ContainerConfig` (filesystem /
Expand Down
40 changes: 20 additions & 20 deletions sdk/node/src/v1/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,17 +419,23 @@ function computeSupport(): PlatformSupport {
const platform = os.platform();
const support: PlatformSupport = { isSupported: false, reason: '', availableMethods: [] };

// Non-Windows platforms do not currently have native probes, so fields that
// depend on probe data (including uiCapabilities) stay omitted.
// Tier and UI fields come only from the Windows probe, so they stay omitted
// elsewhere.
if (platform === 'darwin') {
// seatbelt is the only containment backend on macOS.
// /usr/bin/sandbox-exec ships with every release of macOS so the check
// is effectively just confirming we're on a supported OS.
if (isSeatbeltAvailable()) {
// Project the native probe rather than re-detecting Seatbelt here.
let backends: AvailableBackend[];
try {
backends = darwinBackendsReader();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
support.reason = `Seatbelt availability could not be read from the native probe: ${message}`;
return support;
}
if (backends.some((entry) => entry.backend === 'seatbelt')) {
support.isSupported = true;
support.availableMethods = ['seatbelt'];
} else {
support.reason = '/usr/bin/sandbox-exec not found; macOS install is incomplete';
support.reason = 'Seatbelt could not be found on this installation of macOS. Your macOS install might be incomplete. (sandbox_init not found)';
}
Comment on lines +438 to 439

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: we'll have to see how we can get this error message from the ffi directly so we don't have to add it in all the sdks. Do note though that I don't see an update to the .NET sdk for this. Was it already there?

return support;
}
Expand Down Expand Up @@ -503,6 +509,13 @@ export function _setLxcAvailabilityProbe(fn: (() => boolean) | null): void {
lxcAvailabilityProbe = fn ?? defaultLxcAvailabilityProbe;
}

let darwinBackendsReader: () => AvailableBackend[] = getAvailableBackends;

/** @internal Test-only: override the native backend reader used on macOS. */
export function _setDarwinBackendsReader(fn: (() => AvailableBackend[]) | null): void {
darwinBackendsReader = fn ?? getAvailableBackends;
}

let platformDiagnosticLogger: (message: string) => void = diagLog;

/** @internal Test-only: override platform-support diagnostic logging. */
Expand Down Expand Up @@ -900,19 +913,6 @@ export function _probeBubblewrap(): BubblewrapProbe {
return { available: true };
}

/**
* Check if the macOS sandbox is available. `/usr/bin/sandbox-exec` is part
* of the macOS base install and present on every shipping version of macOS,
* so this is effectively a sanity check for a corrupted install.
*/
function isSeatbeltAvailable(): boolean {
try {
return fs.existsSync('/usr/bin/sandbox-exec');
} catch {
return false;
}
}

/**
* Get the simplified architecture name used for SDK bin directory layout.
* @returns 'arm64' or 'x64'
Expand Down
22 changes: 21 additions & 1 deletion sdk/node/tests/integration/macos-seatbelt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert';
import { execSync } from 'child_process';
import { execFileSync, execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
Expand Down Expand Up @@ -50,6 +50,26 @@ describe('macOS Seatbelt Container', {
}
});

// A fresh process makes koffi's local-visibility load the first to map
// mxc_ffi, so the native sandbox_init lookup runs unmocked and uncached.
it('reports seatbelt from native discovery in a fresh process', () => {
const script =
"const { getPlatformSupport, getAvailableBackends } = await import('@microsoft/mxc-sdk/v1');" +
'console.log(JSON.stringify({ support: getPlatformSupport(), backends: getAvailableBackends() }));';
const output = execFileSync(process.execPath, ['--input-type=module', '-e', script], {
cwd: new URL('.', import.meta.url),
encoding: 'utf-8',
timeout: 30000,
});
const { support, backends } = JSON.parse(output) as {
support: { isSupported: boolean; availableMethods: string[]; reason?: string };
backends: { backend: string }[];
};
assert.ok(support.isSupported, `seatbelt must be supported: ${output}`);
assert.deepStrictEqual(support.availableMethods, ['seatbelt']);
assert.ok(backends.some((entry) => entry.backend === 'seatbelt'), output);
});

it('should execute hello world in seatbelt sandbox', async () => {
const result = await sdk.runRequestForTest(
"echo 'Hello from seatbelt'",
Expand Down
33 changes: 33 additions & 0 deletions sdk/node/tests/unit/platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
_runLinuxProbe,
_linuxProbeTimeouts,
_setLxcAvailabilityProbe,
_setDarwinBackendsReader,
_setPlatformDiagnosticLogger,
findWxcExecutable,
_resetWxcExecutableCache,
Expand Down Expand Up @@ -226,6 +227,38 @@ const allUiCapabilities = {
canBlockDisplaySettingsChanges: true,
};

describe('getPlatformSupport on macOS', { skip: os.platform() !== 'darwin' }, () => {
beforeEach(() => _resetPlatformSupportCache());
afterEach(() => {
_setDarwinBackendsReader(null);
_resetPlatformSupportCache();
});

it('reports seatbelt when the native probe does', () => {
_setDarwinBackendsReader(() => [{ backend: 'seatbelt', capabilities: [], warnings: [] }]);
const support = getPlatformSupport();
assert.strictEqual(support.isSupported, true);
assert.deepStrictEqual(support.availableMethods, ['seatbelt']);
});

it('is unsupported when the native probe omits seatbelt', () => {
_setDarwinBackendsReader(() => []);
const support = getPlatformSupport();
assert.strictEqual(support.isSupported, false);
assert.deepStrictEqual(support.availableMethods, []);
assert.match(support.reason ?? '', /sandbox_init/);
});

it('fails closed when the native probe throws', () => {
_setDarwinBackendsReader(() => {
throw new Error('mxc_ffi not found');
});
const support = getPlatformSupport();
assert.strictEqual(support.isSupported, false);
assert.match(support.reason ?? '', /mxc_ffi not found/);
});
});

describe('getPlatformSupport probe integration', () => {
beforeEach(() => {
_resetPlatformSupportCache();
Expand Down
2 changes: 1 addition & 1 deletion src/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ operation options and return no execution output.
`v1::spawn_with_pty` creates a container with a caller-controlled terminal.
Create-and-run PTY support is available for IsolationSession on Windows, Bubblewrap
and LXC on Linux, and Seatbelt direct execution on macOS. LXC requires root.
Seatbelt rejects PTY mode with `guiAccess` or legacy `launchMethod: "open"`.
Seatbelt rejects PTY mode with `guiAccess`.
`wait()` requests canonical-mode terminal EOF for untaken input; raw-mode
applications must use their own completion protocol. Untaken merged output is
drained and discarded without waiting indefinitely for descendants that keep
Expand Down
4 changes: 2 additions & 2 deletions src/mxc-sdk/src/backends/seatbelt/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
//! everywhere even though only macOS can execute them.
//! - [`default_env`] resolves the schema-0.9 default `process.env` block.
//! Platform-agnostic for the same reason.
//! - [`seatbelt_runner`] is gated to `target_os = "macos"` since it spawns
//! `/usr/bin/sandbox-exec`.
//! - [`seatbelt_runner`] is gated to `target_os = "macos"` since it calls
//! `sandbox_init()`.

pub mod default_env;
pub mod profile_builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

//! Pure builder that converts an [`ExecutionRequest`] into a TinyScheme sandbox
//! profile string suitable for `/usr/bin/sandbox-exec -p <profile>`.
//! profile string suitable for `sandbox_init()` or `sandbox-exec -p <profile>`.
//!
//! This module is platform-agnostic β€” it is just string generation β€” so it
//! is unit-tested on every host (Windows / Linux / macOS) in CI.
Expand Down
Loading
Loading