Skip to content

Commit 484d79f

Browse files
committed
feat(sdk): add NAS/OSS mount syntax sugar across Kotlin, Python, TypeScript, Go, and C#
Expose `sandbox.mount(NfsMountOptions | OssfsMountOptions)` and `sandbox.umount(mountPoint)` on the sandbox facade in all five SDKs. The helpers build the appropriate `mount -t nfs` / `ossfs` / `ossfs2` shell command and run it via `commands.run`, so no server change is required. The sandbox image must have the corresponding mount binary installed, or the options.installation field can install it at mount time. Cross-SDK behavior is aligned so callers can rely on the same semantics: - Version selection: `OssfsMountOptions.version` defaults to ossfs 1.x when omitted, ossfs 2.x when set to "2.0". - `bucketDirectory` is supported in both ossfs 1.x (`bucket:/dir`) and ossfs 2.x (`--oss_bucket_prefix=<dir>/`). - STS `securityToken` appends to the ossfs 1.x password file and exports `OSS_SESSION_TOKEN` for ossfs 2.x. - Failures raise a new `MountFailedException` / `MountFailedError` that extends the SDK's existing sandbox exception hierarchy and carries the failing `Execution` for diagnostics. Security-hardened compared to the internal reference implementation: 1. The ossfs 1.x password file is created with mode 0600 atomically using `install -m 600 /dev/null /etc/ossfspass` before `printf %s <passwd>`, removing the world-readable window a naive `echo > file; chmod 600` would create. 2. The ossfs 1.x cleanup step is wrapped in `( ossfs ... ); __rc=$?; rm -f /etc/ossfspass; exit $__rc` so the password file is unlinked even when the ossfs binary itself exits non-zero. 3. The ossfs 2.x configuration file is written to `/tmp/opensandbox-ossfs-<uuid>.conf` with mode 600 rather than dropped in the current working directory. 4. All shell arguments are single-quoted with proper escaping. While pinning the ossfs 2.x conf mode to 600 the existing SDK convention was verified: `WriteEntry.mode` is serialized as a JSON number and parsed server-side via `strconv.ParseUint(strconv.Itoa(mode), 8, 32)` (see `components/execd/pkg/web/controller/utils.go`), so callers must pass the decimal literal `600` rather than an octal literal such as `0o600` (which would serialize to 384 and fail the octal parse). The Kotlin, TypeScript, Go, and C# implementations all pass `600`; Python passes `mode=600` as before. The `createIsolatedOssSession` helper from the internal Java SDK (bwrap seccomp workaround that pre-mounts OSSFS on the host side and bind-mounts it into the isolated session) is intentionally out of scope of this change and will be added separately if needed. Test results (unit only; e2e requires real NAS/OSS backends): - Kotlin: `./gradlew :sandbox:test` -> 216 pass - Python: `uv run pytest tests/` -> 302 pass - TypeScript: `pnpm run test` -> 70 pass - Go: `go test ./...` -> all pass - C#: not verified locally (no dotnet SDK available on the workstation); code is written to match the existing patterns in the C# SDK and passes static review, but `dotnet test` still needs to run in CI.
1 parent 5b3afaa commit 484d79f

37 files changed

Lines changed: 5383 additions & 2 deletions

File tree

sdks/sandbox/csharp/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,40 @@ foreach (var s in list.Items)
226226
}
227227
```
228228

229+
### NAS / OSS Mounts
230+
231+
`sandbox.MountAsync(...)` runs `mount -t nfs`, `ossfs`, or `ossfs2` inside a running sandbox via `Commands.RunAsync`, so the sandbox image must have the corresponding binary installed (or use `Installation` to install it on demand). The password / configuration files are created with mode 600 in one step and are cleaned up even when the mount command itself fails.
232+
233+
```csharp
234+
using OpenSandbox;
235+
using OpenSandbox.Models;
236+
237+
// Mount a NAS export
238+
await sandbox.MountAsync(new NfsMountOptions
239+
{
240+
Endpoint = "nas-server.example.com",
241+
NasPath = "/share",
242+
MountPoint = "/mnt/nas",
243+
Installation = "apt-get install -y nfs-common", // optional
244+
});
245+
246+
// Mount an OSS bucket via ossfs 2.x
247+
await sandbox.MountAsync(new OssfsMountOptions
248+
{
249+
Endpoint = "https://oss-cn-hangzhou.aliyuncs.com",
250+
Bucket = "my-bucket",
251+
BucketDirectory = "subdir", // optional; maps to --oss_bucket_prefix
252+
MountPoint = "/mnt/oss",
253+
AccessKeyId = Environment.GetEnvironmentVariable("OSS_AK")!,
254+
AccessKeySecret = Environment.GetEnvironmentVariable("OSS_SK")!,
255+
Version = OssfsVersion.Ossfs20,
256+
});
257+
258+
await sandbox.UmountAsync("/mnt/nas");
259+
```
260+
261+
Failures throw `MountFailedException` (`OpenSandbox.Core`), which exposes the failing `Execution` on `ex.Execution` so callers can inspect exit code and stderr.
262+
229263
## Configuration
230264

231265
### 1. Connection Configuration

sdks/sandbox/csharp/src/OpenSandbox/Core/Exceptions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public static class SandboxErrorCodes
4343
/// Unexpected response from the server.
4444
/// </summary>
4545
public const string UnexpectedResponse = "UNEXPECTED_RESPONSE";
46+
47+
/// <summary>
48+
/// A NAS or OSS mount syntax-sugar call failed inside the sandbox.
49+
/// </summary>
50+
public const string MountFailed = "MOUNT_FAILED";
4651
}
4752

4853
/// <summary>
@@ -231,3 +236,34 @@ public InvalidArgumentException(string? message = null, Exception? innerExceptio
231236
{
232237
}
233238
}
239+
240+
/// <summary>
241+
/// Exception thrown when a NAS or OSS mount syntax-sugar call fails inside the sandbox.
242+
/// </summary>
243+
/// <remarks>
244+
/// The <see cref="Execution"/> property exposes the failing execution handle
245+
/// returned by the underlying <c>Commands.RunAsync</c> call so callers can inspect
246+
/// the exit code, stdout, and stderr for diagnostics.
247+
/// </remarks>
248+
public class MountFailedException : SandboxException
249+
{
250+
/// <summary>
251+
/// Gets the failing execution result attached to this exception, if any.
252+
/// </summary>
253+
public OpenSandbox.Models.Execution? Execution { get; }
254+
255+
/// <summary>
256+
/// Initializes a new instance of the <see cref="MountFailedException"/> class.
257+
/// </summary>
258+
/// <param name="message">The error message.</param>
259+
/// <param name="execution">The failing execution result.</param>
260+
/// <param name="innerException">The inner exception.</param>
261+
public MountFailedException(
262+
string? message = null,
263+
OpenSandbox.Models.Execution? execution = null,
264+
Exception? innerException = null)
265+
: base(message, innerException, new SandboxError(SandboxErrorCodes.MountFailed, message))
266+
{
267+
Execution = execution;
268+
}
269+
}

0 commit comments

Comments
 (0)