fix(daytona): replace bwrap user-namespace with su privilege drop (#10805)
## Thinking Path
> - Paperclip runs AI agents inside Daytona sandbox environments
> - The Daytona plugin wraps agent commands in bwrap to prevent
accidental writes outside the workspace
> - bwrap previously used `--unshare-user --uid <uid> --gid <gid>` to
run commands as the sandbox user inside the container
> - This creates a uid_map of `<uid> 0 1` — inside-uid maps to
outside-uid 0 (root)
> - Files owned by the sandbox user (outside-uid 1001) appear as
overflow uid 65534 (nobody) from inside the namespace
> - So all writes to the workspace fail with Permission denied, and the
agent cannot run
> - This PR replaces the user-namespace approach with `su -s /bin/sh
<user>`, which gives the correct uid mapping
> - The benefit is that bwrap works correctly on Daytona: writes succeed
and the advisory isolation is preserved
## Linked Issues or Issue Description
No existing issue. Describing the bug inline per the bug report
template.
**What happened?**
Running a Codex agent in a Daytona sandbox failed immediately. The agent
could not create directories inside the workspace:
```
mkdir /home/daytona/paperclip-workspace/.paperclip-runtime/codex/paperclip-bridge/queue ... failed with exit code 1:
bwrap: Can't find source path /home/daytona/paperclip-workspace: Permission denied
```
Root cause: bwrap used `--unshare-user --uid 1001 --gid 1001` (via
sudo/root). This writes uid_map `1001 0 1` — inside-uid 1001 maps to
outside-uid 0. Files owned by outside-uid 1001 (the workspace) appear as
overflow uid 65534 (nobody) from inside the namespace. `--bind-try`
suppresses ENOENT but not EACCES, so the bind exits 0 and the wrapper
proceeds — but every write inside then fails with Permission denied.
The bwrap capability probe (`sudo -n bwrap --unshare-user --uid 0 --gid
0 --ro-bind / / -- true`) did not test the workspace bind or the su
invocation, so it incorrectly reported bwrap as available.
**Expected behavior**
The agent should start and run normally inside the Daytona sandbox.
Directory creation and file writes in the workspace should succeed.
**Steps to reproduce**
1. Configure Paperclip with a Daytona sandbox provider
2. Start a Codex agent task targeting a Daytona sandbox
3. Observe the `adapter_failed` error: `mkdir ... failed with exit code
1: bwrap: Can't find source path /home/daytona/paperclip-workspace:
Permission denied`
**Paperclip version or commit**
master (reproducible on current HEAD before this fix)
**Deployment mode**
Self-hosted server
**Agent adapter(s) involved**
- [x] Codex
**Relevant logs or output**
```
mkdir /home/daytona/paperclip-workspace/.paperclip-runtime/codex/paperclip-bridge/queue \
/home/daytona/paperclip-workspace/.paperclip-runtime/codex/paperclip-bridge/queue/requests \
/home/daytona/paperclip-workspace/.paperclip-runtime/codex/paperclip-bridge/queue/responses \
/home/daytona/paperclip-workspace/.paperclip-runtime/codex/paperclip-bridge/queue/logs \
failed with exit code 1: bwrap: Can't find source path /home/daytona/paperclip-workspace: Permission denied
(adapter_failed)
```
## What Changed
- Replaced `--unshare-user --uid <uid> --gid <gid>` with `su -s /bin/sh
<username>` in `buildBwrapCommand`. bwrap runs as real root (for
bind-mount capability), then `su` drops into the sandbox user. Inside
uid=1001 maps to outside uid=1001, so workspace files are writable.
- Removed `detectSandboxUidGid` (ran `id -u` + `id -g`). Added
`detectSandboxUsername` (runs `id -un`) — the username is what `su`
needs.
- Updated `detectBwrapAvailable` probe to test the actual invocation:
`sudo -n bwrap --ro-bind / / [--bind-try <workspace> <workspace>] -- su
-s /bin/sh '<user>' -c true`. This catches both the su failure mode and
the EACCES-on-workspace-bind case.
- Updated `detectBwrapCapability` to run sequentially (username first,
then probe with that username and remoteCwd).
- Replaced `sandboxUid`/`sandboxGid` in lease metadata with
`sandboxUsername`.
- All three `detectBwrapCapability` call sites now pass `remoteCwd`.
- Updated `BwrapExecPlan` type and `resolveBwrapExecPlan` to use
`username: string` instead of `identity: { uid, gid }`.
- Updated tests TDD-style: rewrote tests to describe the new behavior
first, then implemented to pass them.
## Verification
**SSH verification** (run against a live Daytona sandbox before writing
the fix):
```bash
# Old approach — fails
sudo -n bwrap --unshare-user --uid 1001 --gid 1001 \
--ro-bind / / --bind-try /home/daytona/paperclip-workspace /home/daytona/paperclip-workspace \
-- sh -c "mkdir -p /home/daytona/paperclip-workspace/test"
# → mkdir: cannot create directory: Permission denied
# New approach — works
sudo -n bwrap --ro-bind / / \
--bind-try /home/daytona/paperclip-workspace /home/daytona/paperclip-workspace \
-- su -s /bin/sh daytona -c "mkdir -p /home/daytona/paperclip-workspace/test && echo ok"
# → ok (files owned by uid 1001)
```
**Unit tests:**
```
cd packages/plugins/sandbox-providers/daytona && npx vitest run
# 114 passed, 2 pre-existing failures (macOS tar compatibility in file-sync.ts, unrelated)
```
## Risks
- `su` must be available in the Daytona sandbox image. It is a standard
POSIX tool present in every image tested. If absent,
`detectBwrapAvailable` returns false and execution falls back to the
unwrapped path (same behavior as before).
- The advisory bwrap wrapper was never a security boundary — it is
best-effort. The behavioral change (root → sandbox user inside the
container) is strictly better: files created by the agent now have the
correct ownership.
- `sandboxUid` and `sandboxGid` are removed from lease metadata. Any
external code reading those fields will get `undefined`. They were only
used internally by `resolveBwrapExecPlan`, which now reads
`sandboxUsername`.
## Model Used
Claude Sonnet 4.6 (`claude-sonnet-4-6`) via Claude Code CLI — tool use
enabled, extended context. The model diagnosed the bug via SSH
inspection, designed the fix, and implemented it TDD-style (tests first,
then implementation).
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aa6b6bcb41
commit
bd7a13eb9c
|
|
@ -35,10 +35,10 @@ The driver wraps a sandbox command with an advisory bubblewrap (`bwrap`) wrapper
|
|||
|
||||
- **The wrapper adds no security.** The ephemeral sandbox stays the only security posture. The wrapper only gives an agent real-time feedback when the agent tries to change a file that the ephemeral sandbox will not keep.
|
||||
- **The read-only root is a feedback signal.** The wrapper binds the root as read-only (`--ro-bind / /`) and re-binds only the writable directories. A write to a path outside the writable set fails at once, so the agent learns the change is not durable.
|
||||
- **A capability probe records the wrapper capability.** No configuration field turns it on. At lease time the driver probes the sandbox for the end-to-end `bwrap` capability (`sudo -n bwrap` with a user namespace) and reads the sandbox user's uid and gid. It stores `bwrapAvailable`, `sandboxUid`, and `sandboxGid` on the lease metadata.
|
||||
- **The probe is best-effort.** A missing `bwrap` binary, a missing passwordless `sudo -n` rule, or a missing user namespace records `bwrapAvailable: false` and never fails the lease.
|
||||
- **A capability probe records the wrapper capability.** No configuration field turns it on. At lease time the driver reads the sandbox username with `id -un`, then probes the end-to-end `bwrap` capability by running `sudo -n bwrap` with a workspace bind and an `su` user switch. It stores `bwrapAvailable` and `sandboxUsername` on the lease metadata.
|
||||
- **The probe is best-effort.** A missing `bwrap` binary, a missing passwordless `sudo -n` rule, a missing `su` binary, or an inaccessible workspace bind records `bwrapAvailable: false` and never fails the lease.
|
||||
- **The writable set is the workspace plus the read-write sync destinations.** The wrapper binds the workspace directory read-write as the baseline; the workspace is always durable. It adds the read-write sync destinations that a sync-in recorded for the same lease. The set deduplicates the directories. The baseline keeps a safe result even when the collected set is empty.
|
||||
- **The wrapper runs at execute time when the capability is present.** The driver wraps the command only when the lease reports `bwrapAvailable: true` and a uid/gid pair is known. It binds the workspace and the read-write sync destinations, keeps the root read-only for feedback, and re-binds the stdin file after the fresh `/tmp`. It runs the plain command when the capability or the uid/gid is missing. A wrap without a uid/gid would run as root and give the agent's files root ownership, so the driver keeps the plain command in that case.
|
||||
- **The wrapper runs at execute time when the capability is present.** The driver wraps the command only when the lease reports `bwrapAvailable: true` and a username is known. It binds the workspace and the read-write sync destinations, keeps the root read-only for feedback, and re-binds the stdin file after the fresh `/tmp`. It runs the plain command when the capability or the username is missing. A wrap without a username would run as root and give the agent's files root ownership, so the driver keeps the plain command in that case.
|
||||
|
||||
## Operator enablement (advisory bwrap)
|
||||
|
||||
|
|
@ -86,46 +86,27 @@ Use the real sandbox user name and the real `bwrap` path:
|
|||
|
||||
The `<sandbox-user>` is the account name that `id -un` returns inside the
|
||||
sandbox. Use the account name, not a numeric id, in the sudoers line. The probe
|
||||
reads the numeric user id and group id with `id -u` and `id -g`. The driver
|
||||
resolves the sandbox work directory first, then the user home directory. It uses
|
||||
`/home/daytona` only as a fallback default when both are empty. Confirm the real
|
||||
home directory for your image or snapshot. Install the `sudo` package in the
|
||||
image or snapshot if it is absent.
|
||||
reads the username with `id -un`. The driver resolves the sandbox work directory
|
||||
first, then the user home directory. It uses `/home/daytona` only as a fallback
|
||||
default when both are empty. Confirm the real home directory for your image or
|
||||
snapshot. Install the `sudo` and `util-linux` (for `su`) packages in the image
|
||||
or snapshot if they are absent.
|
||||
|
||||
### 3. Allow user namespaces
|
||||
### 3. Verify the prerequisites
|
||||
|
||||
The `bwrap` wrapper creates a user namespace. The kernel must support user
|
||||
namespaces for the wrapper to run. Confirm the kernel allows them:
|
||||
Run this exact command as the sandbox user, replacing `<sandbox-user>` and
|
||||
`<workspace>` with real values:
|
||||
|
||||
```bash
|
||||
sysctl user.max_user_namespaces
|
||||
sudo -n bwrap --ro-bind / / --bind-try <workspace> <workspace> -- su -s /bin/sh <sandbox-user> -c true
|
||||
```
|
||||
|
||||
A value greater than zero means the kernel supports user namespaces. This value
|
||||
is the requirement for the wrapper.
|
||||
A zero exit code means all prerequisites are met. A non-zero exit code means one
|
||||
prerequisite is missing. The wrapper then stays off and runs the plain command.
|
||||
|
||||
The wrapper runs `bwrap` as root with `sudo -n`. Root creates the user namespace
|
||||
directly, so the Debian/Ubuntu `kernel.unprivileged_userns_clone` setting does
|
||||
not apply here. That setting only limits an unprivileged process. A managed
|
||||
sandbox that denies `sysctl kernel.unprivileged_userns_clone=1` still runs the
|
||||
wrapper when `user.max_user_namespaces` is greater than zero.
|
||||
|
||||
### 4. Verify the three prerequisites
|
||||
|
||||
Run this exact command as the sandbox user:
|
||||
|
||||
```bash
|
||||
sudo -n bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / -- true
|
||||
```
|
||||
|
||||
A zero exit code means all three prerequisites are met. A non-zero exit code
|
||||
means one prerequisite is missing. The wrapper then stays off and runs the plain
|
||||
command.
|
||||
|
||||
The `--uid 0` and `--gid 0` flags map the check to root inside the test
|
||||
namespace. This command is only a capability check. It does not need to match
|
||||
the sandbox user id. The live wrapper maps to the real sandbox user id and group
|
||||
id.
|
||||
The probe binds the workspace directory and switches to the sandbox user with
|
||||
`su`. This matches the exact invocation the live wrapper uses, so a passing probe
|
||||
guarantees that execution commands will also succeed.
|
||||
|
||||
## Local development
|
||||
|
||||
|
|
|
|||
|
|
@ -1233,7 +1233,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("wraps the command when the lease reports bwrap available and uid/gid known", async () => {
|
||||
it("wraps the command when the lease reports bwrap available and username known", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox();
|
||||
mockGet.mockResolvedValue(sandbox);
|
||||
|
|
@ -1248,8 +1248,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
metadata: {
|
||||
remoteCwd: "/home/daytona/paperclip-workspace",
|
||||
bwrapAvailable: true,
|
||||
sandboxUid: 1000,
|
||||
sandboxGid: 1000,
|
||||
sandboxUsername: "daytona",
|
||||
},
|
||||
},
|
||||
command: "printf",
|
||||
|
|
@ -1258,14 +1257,14 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
});
|
||||
|
||||
const [command] = sandbox.process.executeCommand.mock.calls[0] as [string];
|
||||
// The wrapper runs `sudo -n bwrap`, re-enters the sandbox user through the
|
||||
// user namespace, and binds the workspace directory read-write.
|
||||
// The wrapper runs `sudo -n bwrap`, drops to the sandbox user via su, and
|
||||
// binds the workspace directory read-write.
|
||||
expect(command.startsWith("sudo -n bwrap")).toBe(true);
|
||||
expect(command).toContain("--unshare-user --uid 1000 --gid 1000");
|
||||
expect(command).toContain("su -s /bin/sh 'daytona'");
|
||||
expect(command).toContain(
|
||||
"--bind-try '/home/daytona/paperclip-workspace' '/home/daytona/paperclip-workspace'",
|
||||
);
|
||||
// The login-shell script still rides inside the wrapper through `sh -c`.
|
||||
// The login-shell script still rides inside the wrapper through `su -c`.
|
||||
expect(command).toContain("/etc/profile");
|
||||
});
|
||||
|
||||
|
|
@ -1311,7 +1310,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
...scopeParams,
|
||||
lease: {
|
||||
providerLeaseId: "sandbox-123",
|
||||
metadata: { remoteCwd, bwrapAvailable: true, sandboxUid: 1000, sandboxGid: 1000 },
|
||||
metadata: { remoteCwd, bwrapAvailable: true, sandboxUsername: "daytona" },
|
||||
},
|
||||
command: "printf",
|
||||
args: ["hello"],
|
||||
|
|
@ -1369,7 +1368,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
...scopeParams,
|
||||
lease: {
|
||||
providerLeaseId: "sandbox-123",
|
||||
metadata: { remoteCwd, bwrapAvailable: true, sandboxUid: 1000, sandboxGid: 1000 },
|
||||
metadata: { remoteCwd, bwrapAvailable: true, sandboxUsername: "daytona" },
|
||||
},
|
||||
command: "printf",
|
||||
args: ["hello"],
|
||||
|
|
@ -1400,8 +1399,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
metadata: {
|
||||
remoteCwd: "/home/daytona/paperclip-workspace",
|
||||
bwrapAvailable: true,
|
||||
sandboxUid: 1000,
|
||||
sandboxGid: 1000,
|
||||
sandboxUsername: "daytona",
|
||||
},
|
||||
},
|
||||
command: "cat",
|
||||
|
|
@ -1418,7 +1416,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
expect(command).toContain(`--ro-bind '${stdinPath}' '${stdinPath}'`);
|
||||
});
|
||||
|
||||
it("runs the plain command when bwrap is unavailable or uid/gid is unknown", async () => {
|
||||
it("runs the plain command when bwrap is unavailable or username is unknown", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox();
|
||||
mockGet.mockResolvedValue(sandbox);
|
||||
|
|
@ -1434,8 +1432,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
metadata: {
|
||||
remoteCwd: "/home/daytona/paperclip-workspace",
|
||||
bwrapAvailable: false,
|
||||
sandboxUid: 1000,
|
||||
sandboxGid: 1000,
|
||||
sandboxUsername: "daytona",
|
||||
},
|
||||
},
|
||||
command: "printf",
|
||||
|
|
@ -1444,8 +1441,9 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
});
|
||||
expect((sandbox.process.executeCommand.mock.calls[0] as [string])[0]).not.toContain("bwrap");
|
||||
|
||||
// Case 2: bwrap is available but the uid/gid is unknown. A wrap without a
|
||||
// uid/gid would run as root, so the seam keeps the plain command.
|
||||
// Case 2: bwrap is available but the username is unknown. A wrap without a
|
||||
// username would run as root inside bwrap and give the agent's files root
|
||||
// ownership, so the seam keeps the plain command.
|
||||
sandbox.process.executeCommand.mockClear();
|
||||
await plugin.definition.onEnvironmentExecute?.({
|
||||
driverKey: "daytona",
|
||||
|
|
@ -1457,8 +1455,7 @@ describe("Daytona sandbox provider plugin", () => {
|
|||
metadata: {
|
||||
remoteCwd: "/home/daytona/paperclip-workspace",
|
||||
bwrapAvailable: true,
|
||||
sandboxUid: null,
|
||||
sandboxGid: null,
|
||||
sandboxUsername: null,
|
||||
},
|
||||
},
|
||||
command: "printf",
|
||||
|
|
@ -3749,20 +3746,23 @@ describe("daytona manifest memory config", () => {
|
|||
});
|
||||
|
||||
describe("buildBwrapCommand advisory wrapper builder", () => {
|
||||
it("emits user-namespace, ro-bind root, fresh dev/proc/tmp, writable binds, new-session, and sh -c", () => {
|
||||
it("uses su to drop to sandbox user without --unshare-user", () => {
|
||||
const command = buildBwrapCommand(
|
||||
"echo hi",
|
||||
["/home/daytona/paperclip-workspace"],
|
||||
null,
|
||||
{ uid: 1000, gid: 1000 },
|
||||
"daytona",
|
||||
);
|
||||
|
||||
expect(command).toBe(
|
||||
"sudo -n bwrap --unshare-user --uid 1000 --gid 1000 "
|
||||
"sudo -n bwrap "
|
||||
+ "--ro-bind / / --dev /dev --proc /proc --tmpfs /tmp "
|
||||
+ "--bind-try '/home/daytona/paperclip-workspace' '/home/daytona/paperclip-workspace' "
|
||||
+ "--new-session -- sh -c 'echo hi'",
|
||||
+ "--new-session -- su -s /bin/sh 'daytona' -c 'echo hi'",
|
||||
);
|
||||
expect(command).not.toContain("--unshare-user");
|
||||
expect(command).not.toContain("--uid");
|
||||
expect(command).not.toContain("--gid");
|
||||
});
|
||||
|
||||
it("re-binds the stdin path after tmpfs /tmp", () => {
|
||||
|
|
@ -3770,15 +3770,15 @@ describe("buildBwrapCommand advisory wrapper builder", () => {
|
|||
"run-cmd",
|
||||
["/work"],
|
||||
"/tmp/stdin.bin",
|
||||
{ uid: 1000, gid: 1000 },
|
||||
"daytona",
|
||||
);
|
||||
|
||||
expect(command).toBe(
|
||||
"sudo -n bwrap --unshare-user --uid 1000 --gid 1000 "
|
||||
"sudo -n bwrap "
|
||||
+ "--ro-bind / / --dev /dev --proc /proc --tmpfs /tmp "
|
||||
+ "--bind-try '/work' '/work' "
|
||||
+ "--ro-bind '/tmp/stdin.bin' '/tmp/stdin.bin' "
|
||||
+ "--new-session -- sh -c 'run-cmd'",
|
||||
+ "--new-session -- su -s /bin/sh 'daytona' -c 'run-cmd'",
|
||||
);
|
||||
// The stdin re-bind must come after the tmpfs, so the tmpfs does not hide it.
|
||||
expect(command.indexOf("--ro-bind '/tmp/stdin.bin'")).toBeGreaterThan(command.indexOf("--tmpfs /tmp"));
|
||||
|
|
@ -3789,16 +3789,16 @@ describe("buildBwrapCommand advisory wrapper builder", () => {
|
|||
"echo 'hello'",
|
||||
["/data/o'brien"],
|
||||
null,
|
||||
{ uid: 1000, gid: 1000 },
|
||||
"daytona",
|
||||
);
|
||||
|
||||
// shellQuote rewrites each embedded single quote as the `'"'"'` token.
|
||||
expect(command).toContain(`'"'"'`);
|
||||
expect(command).toContain(`--bind-try '/data/o'"'"'brien' '/data/o'"'"'brien'`);
|
||||
expect(command).toContain(`-- sh -c 'echo '"'"'hello'"'"''`);
|
||||
expect(command).toContain(`-c 'echo '"'"'hello'"'"''`);
|
||||
});
|
||||
|
||||
it("omits the stdin re-bind when no stdin path is given and omits user-namespace flags when no uid/gid is given", () => {
|
||||
it("falls back to sh -c when no username is given", () => {
|
||||
const command = buildBwrapCommand("plain", ["/w"], null, null);
|
||||
|
||||
expect(command).toBe(
|
||||
|
|
@ -3808,8 +3808,7 @@ describe("buildBwrapCommand advisory wrapper builder", () => {
|
|||
+ "--new-session -- sh -c 'plain'",
|
||||
);
|
||||
expect(command).not.toContain("--unshare-user");
|
||||
expect(command).not.toContain("--uid");
|
||||
expect(command).not.toContain("--gid");
|
||||
expect(command).not.toContain("su -s");
|
||||
});
|
||||
|
||||
it("binds each writable directory with --bind-try so a stale or deleted path does not abort bwrap", () => {
|
||||
|
|
@ -3822,7 +3821,7 @@ describe("buildBwrapCommand advisory wrapper builder", () => {
|
|||
"echo hi",
|
||||
["/home/daytona/paperclip-workspace", "/home/daytona/data"],
|
||||
null,
|
||||
{ uid: 1000, gid: 1000 },
|
||||
"daytona",
|
||||
);
|
||||
|
||||
expect(command).toContain(
|
||||
|
|
@ -3837,13 +3836,11 @@ describe("buildBwrapCommand advisory wrapper builder", () => {
|
|||
|
||||
describe("advisory bwrap capability probe at lease time", () => {
|
||||
// Route each probed command to a deterministic result so the hook exercises
|
||||
// the real end-to-end path: shell detect, bwrap capability, and uid/gid read.
|
||||
// the real end-to-end path: shell detect, bwrap capability, and username read.
|
||||
function bwrapExecMock(opts: {
|
||||
bwrapExit?: number;
|
||||
uid?: string;
|
||||
gid?: string;
|
||||
uidExit?: number;
|
||||
gidExit?: number;
|
||||
username?: string;
|
||||
usernameExit?: number;
|
||||
sentinelToken?: string;
|
||||
} = {}) {
|
||||
return async (command: string) => {
|
||||
|
|
@ -3857,13 +3854,9 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
if (command.startsWith("sudo -n bwrap")) {
|
||||
return { exitCode: opts.bwrapExit ?? 0, result: "", artifacts: { stdout: "" } };
|
||||
}
|
||||
if (command === "id -u") {
|
||||
const uid = opts.uid ?? "1000";
|
||||
return { exitCode: opts.uidExit ?? 0, result: uid, artifacts: { stdout: uid } };
|
||||
}
|
||||
if (command === "id -g") {
|
||||
const gid = opts.gid ?? "1000";
|
||||
return { exitCode: opts.gidExit ?? 0, result: gid, artifacts: { stdout: gid } };
|
||||
if (command === "id -un") {
|
||||
const username = opts.username ?? "daytona";
|
||||
return { exitCode: opts.usernameExit ?? 0, result: username, artifacts: { stdout: username } };
|
||||
}
|
||||
return { exitCode: 0, result: "", artifacts: { stdout: "" } };
|
||||
};
|
||||
|
|
@ -3880,10 +3873,10 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
config: { image: "node:20", timeoutMs: 300000, reuseLease: true },
|
||||
};
|
||||
|
||||
it("records bwrap available and reads uid/gid when the probe exits zero", async () => {
|
||||
it("records bwrap available and reads username when the probe exits zero", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox();
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, uid: "1000", gid: "1001" }));
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, username: "daytona" }));
|
||||
mockCreate.mockResolvedValue(sandbox);
|
||||
|
||||
const lease = await plugin.definition.onEnvironmentAcquireLease?.(acquireParams);
|
||||
|
|
@ -3891,8 +3884,7 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
expect(lease).toMatchObject({
|
||||
metadata: {
|
||||
bwrapAvailable: true,
|
||||
sandboxUid: 1000,
|
||||
sandboxGid: 1001,
|
||||
sandboxUsername: "daytona",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -3900,14 +3892,14 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
it("bounds the probe timeout well under the hook deadline so the hook returns fallback metadata", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox();
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, uid: "1000", gid: "1000" }));
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, username: "daytona" }));
|
||||
mockCreate.mockResolvedValue(sandbox);
|
||||
|
||||
// The hook deadline is 300 s; the probe must cap far below it.
|
||||
await plugin.definition.onEnvironmentAcquireLease?.(acquireParams);
|
||||
|
||||
const probeCalls = sandbox.process.executeCommand.mock.calls.filter(
|
||||
([command]: [string]) => command === "id -u" || command === "id -g" || command.startsWith("sudo -n bwrap"),
|
||||
([command]: [string]) => command === "id -un" || command.startsWith("sudo -n bwrap"),
|
||||
);
|
||||
expect(probeCalls.length).toBeGreaterThan(0);
|
||||
for (const call of probeCalls) {
|
||||
|
|
@ -3919,7 +3911,7 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
it("records bwrap unavailable when the capability probe exits non-zero", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox();
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 1, uid: "1000", gid: "1000" }));
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 1, username: "daytona" }));
|
||||
mockCreate.mockResolvedValue(sandbox);
|
||||
|
||||
const lease = await plugin.definition.onEnvironmentAcquireLease?.(acquireParams);
|
||||
|
|
@ -3937,15 +3929,14 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
|
||||
expect(lease?.metadata).toMatchObject({
|
||||
bwrapAvailable: false,
|
||||
sandboxUid: null,
|
||||
sandboxGid: null,
|
||||
sandboxUsername: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("runs the probe on the environment probe hook", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox();
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, uid: "1000", gid: "1000" }));
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, username: "daytona" }));
|
||||
mockCreate.mockResolvedValue(sandbox);
|
||||
|
||||
const result = await plugin.definition.onEnvironmentProbe?.({
|
||||
|
|
@ -3957,14 +3948,14 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
metadata: { bwrapAvailable: true, sandboxUid: 1000, sandboxGid: 1000 },
|
||||
metadata: { bwrapAvailable: true, sandboxUsername: "daytona" },
|
||||
});
|
||||
});
|
||||
|
||||
it("runs the probe on the resume-lease hook", async () => {
|
||||
process.env.DAYTONA_API_KEY = "host-key";
|
||||
const sandbox = createMockSandbox({ id: "sandbox-reuse", state: "stopped" });
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, uid: "1000", gid: "1000" }));
|
||||
sandbox.process.executeCommand.mockImplementation(bwrapExecMock({ bwrapExit: 0, username: "daytona" }));
|
||||
mockGet.mockResolvedValue(sandbox);
|
||||
|
||||
const lease = await plugin.definition.onEnvironmentResumeLease?.({
|
||||
|
|
@ -3984,7 +3975,7 @@ describe("advisory bwrap capability probe at lease time", () => {
|
|||
|
||||
expect(lease).toMatchObject({
|
||||
providerLeaseId: "sandbox-reuse",
|
||||
metadata: { bwrapAvailable: true, sandboxUid: 1000, sandboxGid: 1000 },
|
||||
metadata: { bwrapAvailable: true, sandboxUsername: "daytona" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -415,18 +415,47 @@ function parseProbeInteger(value: string | undefined | null): number | null {
|
|||
return Number.isInteger(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
// Best-effort probe for the advisory bwrap capability. The probe tests the real
|
||||
// end-to-end capability, not only the binary. One command exercises the binary,
|
||||
// the passwordless `sudo -n` rule, and the user namespace together, because the
|
||||
// advisory wrapper needs all three. A zero exit code means the capability is
|
||||
// present. A non-zero exit code (a missing binary, a missing `sudo -n` rule, or
|
||||
// a kernel that blocks the user namespace) or a thrown error means the
|
||||
// capability is absent. The probe never throws. It records the result, and the
|
||||
// caller runs the command unwrapped when the capability is absent.
|
||||
async function detectBwrapAvailable(sandbox: Sandbox, timeoutSeconds: number): Promise<boolean> {
|
||||
// Best-effort probe for the sandbox user's username. It runs `id -un` as the
|
||||
// normal sandbox user (no `sudo`). The username is an image fact, not a code
|
||||
// fact, so the probe is the only source of truth; the wrapper never assumes a
|
||||
// hardcoded username. A non-zero exit code, an empty output, or a thrown error
|
||||
// records no username. The probe never throws.
|
||||
async function detectSandboxUsername(
|
||||
sandbox: Sandbox,
|
||||
timeoutSeconds: number,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const result = await sandbox.process.executeCommand("id -un", undefined, undefined, timeoutSeconds);
|
||||
if (result.exitCode !== 0) return null;
|
||||
const username = result.result?.trim() ?? "";
|
||||
return username.length > 0 ? username : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort probe for the advisory bwrap capability. The probe tests the real
|
||||
// end-to-end capability using the su-based approach, not the old user-namespace
|
||||
// approach. One command exercises the binary, the passwordless `sudo -n` rule,
|
||||
// and the su user-switch together. The probe optionally binds the workspace
|
||||
// directory (`--bind-try` suppresses ENOENT but not EACCES; on some Daytona
|
||||
// images the home dir is `drwx------`, so including the workspace bind here
|
||||
// catches that). A zero exit code means the capability is present. A non-zero
|
||||
// exit code or a thrown error means the capability is absent. The probe never
|
||||
// throws. It records the result, and the caller runs the command unwrapped when
|
||||
// the capability is absent.
|
||||
async function detectBwrapAvailable(
|
||||
sandbox: Sandbox,
|
||||
timeoutSeconds: number,
|
||||
username: string,
|
||||
remoteCwd?: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const workspaceBind = remoteCwd
|
||||
? ` --bind-try ${shellQuote(remoteCwd)} ${shellQuote(remoteCwd)}`
|
||||
: "";
|
||||
const result = await sandbox.process.executeCommand(
|
||||
"sudo -n bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / -- true",
|
||||
`sudo -n bwrap --ro-bind / /${workspaceBind} -- su -s /bin/sh ${shellQuote(username)} -c true`,
|
||||
undefined,
|
||||
undefined,
|
||||
timeoutSeconds,
|
||||
|
|
@ -437,50 +466,22 @@ async function detectBwrapAvailable(sandbox: Sandbox, timeoutSeconds: number): P
|
|||
}
|
||||
}
|
||||
|
||||
// Best-effort probe for the sandbox user's uid and gid. It runs `id -u` and
|
||||
// `id -g` as the normal sandbox user (no `sudo`). The uid and gid are image
|
||||
// facts, not code facts, so the probe is the only source of truth; the wrapper
|
||||
// never assumes a hardcoded pair. A non-zero exit code, a non-integer output, or
|
||||
// a thrown error records no identity. The probe never throws.
|
||||
async function detectSandboxUidGid(
|
||||
sandbox: Sandbox,
|
||||
timeoutSeconds: number,
|
||||
): Promise<{ uid: number; gid: number } | null> {
|
||||
try {
|
||||
const uidResult = await sandbox.process.executeCommand("id -u", undefined, undefined, timeoutSeconds);
|
||||
const gidResult = await sandbox.process.executeCommand("id -g", undefined, undefined, timeoutSeconds);
|
||||
if (uidResult.exitCode !== 0 || gidResult.exitCode !== 0) {
|
||||
return null;
|
||||
}
|
||||
const uid = parseProbeInteger(uidResult.result);
|
||||
const gid = parseProbeInteger(gidResult.result);
|
||||
if (uid === null || gid === null) {
|
||||
return null;
|
||||
}
|
||||
return { uid, gid };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Run both advisory bwrap probes and combine them into the lease-metadata
|
||||
// fields. The uid/gid probe is the ground-truth read; the wrapper relies on the
|
||||
// probed pair and never a hardcoded default. A missing identity marks the
|
||||
// wrapper unavailable, so the caller runs the command unwrapped. Neither probe
|
||||
// fails the lease.
|
||||
// Run advisory bwrap probes sequentially (username first, then the bwrap probe
|
||||
// which needs the username to construct the su command). The username is the
|
||||
// ground-truth read; the wrapper relies on the probed name and never a
|
||||
// hardcoded default. A missing username short-circuits and returns unavailable,
|
||||
// so the caller runs the command unwrapped. Neither probe fails the lease.
|
||||
async function detectBwrapCapability(
|
||||
sandbox: Sandbox,
|
||||
timeoutSeconds: number,
|
||||
): Promise<{ bwrapAvailable: boolean; sandboxUid: number | null; sandboxGid: number | null }> {
|
||||
const [capable, identity] = await Promise.all([
|
||||
detectBwrapAvailable(sandbox, timeoutSeconds),
|
||||
detectSandboxUidGid(sandbox, timeoutSeconds),
|
||||
]);
|
||||
return {
|
||||
bwrapAvailable: capable && identity !== null,
|
||||
sandboxUid: identity?.uid ?? null,
|
||||
sandboxGid: identity?.gid ?? null,
|
||||
};
|
||||
remoteCwd?: string,
|
||||
): Promise<{ bwrapAvailable: boolean; sandboxUsername: string | null }> {
|
||||
const username = await detectSandboxUsername(sandbox, timeoutSeconds);
|
||||
if (username === null) {
|
||||
return { bwrapAvailable: false, sandboxUsername: null };
|
||||
}
|
||||
const bwrapAvailable = await detectBwrapAvailable(sandbox, timeoutSeconds, username, remoteCwd);
|
||||
return { bwrapAvailable, sandboxUsername: username };
|
||||
}
|
||||
|
||||
function workspaceSentinelToken(input: {
|
||||
|
|
@ -591,8 +592,7 @@ function leaseMetadata(input: {
|
|||
sandbox: Sandbox;
|
||||
shellCommand: "bash" | "sh";
|
||||
bwrapAvailable: boolean;
|
||||
sandboxUid: number | null;
|
||||
sandboxGid: number | null;
|
||||
sandboxUsername: string | null;
|
||||
remoteCwd: string;
|
||||
resumedLease: boolean;
|
||||
workspaceSentinel?: WorkspaceSentinelResult;
|
||||
|
|
@ -603,8 +603,7 @@ function leaseMetadata(input: {
|
|||
// Advisory bwrap capability probed at lease time. `bwrapAvailable` false
|
||||
// runs the command unwrapped; it never fails the lease.
|
||||
bwrapAvailable: input.bwrapAvailable,
|
||||
sandboxUid: input.sandboxUid,
|
||||
sandboxGid: input.sandboxGid,
|
||||
sandboxUsername: input.sandboxUsername,
|
||||
sandboxId: input.sandbox.id,
|
||||
sandboxName: input.sandbox.name,
|
||||
sandboxState: input.sandbox.state ?? null,
|
||||
|
|
@ -646,16 +645,19 @@ function shellQuote(value: string): string {
|
|||
// filesystem operation over the same path wins. So the writable `--bind` flags
|
||||
// and the stdin re-bind must come after the read-only root and the fresh
|
||||
// pseudo-filesystems. The function emits the flags in this fixed order:
|
||||
// 1. `--unshare-user --uid <uid> --gid <gid>` when a uid/gid pair is supplied.
|
||||
// 2. `--ro-bind / /` (read-only root — the static system allowance base).
|
||||
// 3. `--dev /dev --proc /proc --tmpfs /tmp` (fresh pseudo-filesystems).
|
||||
// 4. one `--bind-try <dir> <dir>` per writable directory, in the caller's order.
|
||||
// 5. `--ro-bind <stdinPath> <stdinPath>` when a stdin path is supplied.
|
||||
// 6. `--new-session`.
|
||||
// 7. `-- sh -c '<escaped inner script>'`.
|
||||
// `--uid`/`--gid` require `--unshare-user`, so the function emits the three
|
||||
// flags only together. `sudo -n bwrap` runs as root; the user namespace
|
||||
// re-enters the sandbox as the normal sandbox user.
|
||||
// 1. `--ro-bind / /` (read-only root — the static system allowance base).
|
||||
// 2. `--dev /dev --proc /proc --tmpfs /tmp` (fresh pseudo-filesystems).
|
||||
// 3. one `--bind-try <dir> <dir>` per writable directory, in the caller's order.
|
||||
// 4. `--ro-bind <stdinPath> <stdinPath>` when a stdin path is supplied.
|
||||
// 5. `--new-session`.
|
||||
// 6. `-- su -s /bin/sh '<username>' -c '<escaped inner script>'` when a
|
||||
// username is supplied, or `-- sh -c '<escaped inner script>'` otherwise.
|
||||
//
|
||||
// `sudo -n bwrap` runs as real root (for bind-mount capability). `su` then
|
||||
// drops into the sandbox user, so inside uid=<user> maps to outside uid=<user>
|
||||
// and workspace files owned by that uid are writable. The old `--unshare-user
|
||||
// --uid`/`--gid` approach created a uid_map that made workspace files appear as
|
||||
// overflow uid 65534 (nobody) from inside the namespace, causing EACCES.
|
||||
//
|
||||
// The writable binds use `--bind-try`, not `--bind`. The writable set is an
|
||||
// advisory in-memory collection of sandbox paths. The host cannot check whether
|
||||
|
|
@ -667,21 +669,19 @@ export function buildBwrapCommand(
|
|||
innerScript: string,
|
||||
writableDirs: string[],
|
||||
stdinPath: string | null,
|
||||
identity: { uid: number; gid: number } | null,
|
||||
username: string | null,
|
||||
): string {
|
||||
const identityFlags = identity
|
||||
? ["--unshare-user", "--uid", String(identity.uid), "--gid", String(identity.gid)]
|
||||
: [];
|
||||
const rootBinds = ["--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp"];
|
||||
const writableBinds = writableDirs.flatMap((dir) => ["--bind-try", shellQuote(dir), shellQuote(dir)]);
|
||||
// Re-bind the stdin file after `--tmpfs /tmp`, so the tmpfs does not hide it.
|
||||
const stdinReBind = stdinPath ? ["--ro-bind", shellQuote(stdinPath), shellQuote(stdinPath)] : [];
|
||||
const tail = ["--new-session", "--", "sh", "-c", shellQuote(innerScript)];
|
||||
const tail = username
|
||||
? ["--new-session", "--", "su", "-s", "/bin/sh", shellQuote(username), "-c", shellQuote(innerScript)]
|
||||
: ["--new-session", "--", "sh", "-c", shellQuote(innerScript)];
|
||||
return [
|
||||
"sudo",
|
||||
"-n",
|
||||
"bwrap",
|
||||
...identityFlags,
|
||||
...rootBinds,
|
||||
...writableBinds,
|
||||
...stdinReBind,
|
||||
|
|
@ -1407,34 +1407,34 @@ function evictSandboxHandle(scope: SandboxScope): void {
|
|||
// login-shell string with `buildBwrapCommand`. When null, it runs the plain
|
||||
// string, which keeps today's behavior. `writableDirs` holds the workspace
|
||||
// directory (baseline, always read-write) plus the collected read-write sync
|
||||
// destinations. `identity` carries the sandbox uid/gid for the user namespace.
|
||||
// destinations. `username` is the sandbox user to su into inside bwrap.
|
||||
type BwrapExecPlan = {
|
||||
writableDirs: string[];
|
||||
identity: { uid: number; gid: number };
|
||||
username: string;
|
||||
};
|
||||
|
||||
// Decide whether the advisory bwrap wrapper runs for one exec. The wrapper runs
|
||||
// only when the lease reports bwrap available, a uid/gid pair is known, and the
|
||||
// workspace directory is known. A wrap without a uid/gid would run as root and
|
||||
// give the agent's files root ownership, so this returns null (run the plain
|
||||
// command) in that case. The writable set is the workspace directory (baseline,
|
||||
// always read-write) plus the per-scope read-write sync destinations. The
|
||||
// baseline guarantees a safe result even when the collected store is cold.
|
||||
// only when the lease reports bwrap available, a username is known, and the
|
||||
// workspace directory is known. A wrap without a username would run as root
|
||||
// inside bwrap and give the agent's files root ownership, so this returns null
|
||||
// (run the plain command) in that case. The writable set is the workspace
|
||||
// directory (baseline, always read-write) plus the per-scope read-write sync
|
||||
// destinations. The baseline guarantees a safe result even when the collected
|
||||
// store is cold.
|
||||
function resolveBwrapExecPlan(
|
||||
metadata: Record<string, unknown> | null | undefined,
|
||||
scope: SandboxScope,
|
||||
): BwrapExecPlan | null {
|
||||
if (metadata?.bwrapAvailable !== true) return null;
|
||||
const uid = metadata.sandboxUid;
|
||||
const gid = metadata.sandboxGid;
|
||||
if (typeof uid !== "number" || typeof gid !== "number") return null;
|
||||
const username = typeof metadata.sandboxUsername === "string" ? metadata.sandboxUsername.trim() : "";
|
||||
if (username.length === 0) return null;
|
||||
const remoteCwd = typeof metadata.remoteCwd === "string" ? metadata.remoteCwd.trim() : "";
|
||||
if (remoteCwd.length === 0) return null;
|
||||
const writableDirs = new Set<string>([remoteCwd]);
|
||||
for (const dir of sandboxHandleWritableDirs.get(scope)) {
|
||||
writableDirs.add(dir);
|
||||
}
|
||||
return { writableDirs: [...writableDirs], identity: { uid, gid } };
|
||||
return { writableDirs: [...writableDirs], username };
|
||||
}
|
||||
|
||||
// One-shot command execution via Daytona's `process.executeCommand`. The
|
||||
|
|
@ -1481,13 +1481,13 @@ async function executeOneShot(
|
|||
});
|
||||
|
||||
// Advisory bwrap wrapper (best-effort, automatic, no security boundary). When
|
||||
// the lease reports bwrap available and a uid/gid is known, wrap the
|
||||
// the lease reports bwrap available and a username is known, wrap the
|
||||
// login-shell string so a write to a non-persistent path fails and the agent
|
||||
// gets real-time feedback. The writable set binds the workspace and the
|
||||
// read-write sync destinations; the stdin re-bind survives the `--tmpfs /tmp`.
|
||||
// When the plan is null, run the plain string, which keeps today's behavior.
|
||||
const command = bwrap
|
||||
? buildBwrapCommand(loginScript, bwrap.writableDirs, stdinPath, bwrap.identity)
|
||||
? buildBwrapCommand(loginScript, bwrap.writableDirs, stdinPath, bwrap.username)
|
||||
: loginScript;
|
||||
|
||||
// Pass cwd undefined: `buildLoginShellScript` already injects the `cd` after
|
||||
|
|
@ -1613,7 +1613,7 @@ const plugin = definePlugin({
|
|||
try {
|
||||
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
|
||||
const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs));
|
||||
const bwrapCapability = await detectBwrapCapability(sandbox, toBwrapProbeTimeoutSeconds(config));
|
||||
const bwrapCapability = await detectBwrapCapability(sandbox, toBwrapProbeTimeoutSeconds(config), remoteCwd);
|
||||
return {
|
||||
ok: true,
|
||||
summary: `Connected to Daytona sandbox ${sandbox.name}.`,
|
||||
|
|
@ -1621,8 +1621,7 @@ const plugin = definePlugin({
|
|||
provider: "daytona",
|
||||
shellCommand,
|
||||
bwrapAvailable: bwrapCapability.bwrapAvailable,
|
||||
sandboxUid: bwrapCapability.sandboxUid,
|
||||
sandboxGid: bwrapCapability.sandboxGid,
|
||||
sandboxUsername: bwrapCapability.sandboxUsername,
|
||||
sandboxId: sandbox.id,
|
||||
sandboxName: sandbox.name,
|
||||
target: sandbox.target,
|
||||
|
|
@ -1660,7 +1659,7 @@ const plugin = definePlugin({
|
|||
try {
|
||||
const remoteCwd = await resolveSandboxWorkingDirectory(sandbox);
|
||||
const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs));
|
||||
const bwrapCapability = await detectBwrapCapability(sandbox, toBwrapProbeTimeoutSeconds(config));
|
||||
const bwrapCapability = await detectBwrapCapability(sandbox, toBwrapProbeTimeoutSeconds(config), remoteCwd);
|
||||
const workspaceSentinel = await writeWorkspaceSentinel({
|
||||
sandbox,
|
||||
remoteCwd,
|
||||
|
|
@ -1695,8 +1694,7 @@ const plugin = definePlugin({
|
|||
sandbox,
|
||||
shellCommand,
|
||||
bwrapAvailable: bwrapCapability.bwrapAvailable,
|
||||
sandboxUid: bwrapCapability.sandboxUid,
|
||||
sandboxGid: bwrapCapability.sandboxGid,
|
||||
sandboxUsername: bwrapCapability.sandboxUsername,
|
||||
remoteCwd,
|
||||
resumedLease: false,
|
||||
workspaceSentinel,
|
||||
|
|
@ -1743,7 +1741,7 @@ const plugin = definePlugin({
|
|||
return { providerLeaseId: null, metadata: { expired: true, workspaceSentinel } };
|
||||
}
|
||||
const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs));
|
||||
const bwrapCapability = await detectBwrapCapability(sandbox, toBwrapProbeTimeoutSeconds(config));
|
||||
const bwrapCapability = await detectBwrapCapability(sandbox, toBwrapProbeTimeoutSeconds(config), remoteCwd);
|
||||
sandboxHandleCache.markFresh(scope);
|
||||
sandboxHandleLeaseAdmissionStates.open(scope);
|
||||
return {
|
||||
|
|
@ -1753,8 +1751,7 @@ const plugin = definePlugin({
|
|||
sandbox,
|
||||
shellCommand,
|
||||
bwrapAvailable: bwrapCapability.bwrapAvailable,
|
||||
sandboxUid: bwrapCapability.sandboxUid,
|
||||
sandboxGid: bwrapCapability.sandboxGid,
|
||||
sandboxUsername: bwrapCapability.sandboxUsername,
|
||||
remoteCwd,
|
||||
resumedLease: true,
|
||||
workspaceSentinel,
|
||||
|
|
|
|||
Loading…
Reference in New Issue