Skip to content

Commit 6f53291

Browse files
committed
feat(enclave): support relative mount paths, skip missing directories
1 parent cfdbedb commit 6f53291

7 files changed

Lines changed: 74 additions & 18 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"pi-enclave": minor
3+
---
4+
5+
Support relative mount paths and skip missing directories.
6+
7+
Mount paths in config are now resolved against the workspace directory, so project configs can use short relative paths like `mounts = [".jj", ".git"]` instead of hardcoded absolute paths. Missing mount paths are silently skipped at VM start, making optional mounts safe to declare unconditionally.

packages/enclave/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,24 @@ GraphQL policy parses the request body and checks actual field names (not the sp
130130

131131
### Mounts
132132

133-
Additional directories to mount in the VM (e.g. for jj workspaces):
133+
Additional directories to mount in the VM. Relative paths are resolved against the workspace directory. Missing paths are silently skipped, so you can list optional directories like `.jj` and `.git` without errors:
134134

135135
```toml
136-
mounts = [
137-
"~/dev/myproject/.jj",
138-
"~/dev/myproject/.git",
139-
]
136+
# In .pi/enclave.toml (project config)
137+
mounts = [".jj", ".git"]
138+
```
139+
140+
Absolute and `~`-prefixed paths also work (useful in global config):
141+
142+
```toml
143+
mounts = ["~/shared/data"]
140144
```
141145

142146
For read-only mounts, use the object form:
143147

144148
```toml
145149
mounts = [
146-
"~/dev/myproject/.jj",
150+
".jj",
147151
{ path = "~/shared/configs", readonly = true },
148152
]
149153
```

packages/enclave/src/config.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,17 @@ export function loadConfig(cwd: string): {
353353
const dropInPrefix = globalDropInDir();
354354
const dropIns = layers.filter((l) => l.path.startsWith(dropInPrefix)).map((l) => basename(l.path, ".toml"));
355355

356-
// Expand ~ in mount paths
356+
// Resolve mount paths: expand ~, resolve relative paths against cwd
357357
if (merged.mounts) {
358358
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
359-
merged.mounts = merged.mounts.map((m) => ({
360-
...m,
361-
path: m.path.startsWith("~/") ? join(home, m.path.slice(2)) : m.path === "~" ? home : m.path,
362-
}));
359+
const resolvedCwd = resolve(cwd);
360+
merged.mounts = merged.mounts.map((m) => {
361+
let p = m.path;
362+
if (p === "~") p = home;
363+
else if (p.startsWith("~/")) p = join(home, p.slice(2));
364+
else if (!p.startsWith("/")) p = join(resolvedCwd, p);
365+
return { ...m, path: p };
366+
});
363367
}
364368

365369
return { merged, policies, hasGlobalConfig, hasProjectConfig, dropIns };

packages/enclave/src/vm.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import { execSync } from "node:child_process";
11+
import { existsSync } from "node:fs";
1112
import {
1213
type CreateHttpHooksOptions,
1314
type ExecResult,
@@ -213,9 +214,11 @@ export class EnclaveVM {
213214
[workspaceDir]: shadowedFs,
214215
};
215216

216-
// Extra mounts (e.g. jj repo root, shared directories)
217+
// Extra mounts (e.g. jj repo root, shared directories).
218+
// Skip paths that don't exist on the host (common with optional mounts like .jj/.git).
217219
for (const extra of this.options.extraMounts) {
218220
if (mounts[extra.path]) continue; // workspace already mounted
221+
if (!existsSync(extra.path)) continue;
219222
const provider = new RealFSProvider(extra.path);
220223
if (extra.readonly) {
221224
mounts[extra.path] = new ShadowProvider(provider, {

packages/enclave/templates/pi-enclave.d/jj.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# Jujutsu (jj) support for pi-enclave
2-
# For jj workspaces, add a mount for the parent .jj directory in your
3-
# project config:
2+
# For jj workspaces, add a mount for the .jj directory in your project config:
43
#
5-
# mounts = ["~/dev/myproject/.jj"]
4+
# mounts = [".jj"]
65
packages = ["jujutsu"]
76
setup = """
87
jj config set --user user.name "$USER_NAME"

packages/enclave/templates/project.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
# Protects API keys and isolates file access.
33
enabled = true
44

5-
# Mount additional directories in the VM (supports ~):
6-
# mounts = ["~/dev/myproject/.jj"]
5+
# Mount additional directories (relative to workspace, supports ~).
6+
# Missing paths are silently skipped.
7+
# mounts = [".jj", ".git"]

packages/enclave/test/integration.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe("addPackageToConfig", () => {
153153
});
154154
});
155155

156-
describe("mount path ~ expansion", () => {
156+
describe("mount path resolution", () => {
157157
it("expands ~ to HOME in mount paths", () => {
158158
ensureGlobalConfig();
159159
const projectDir = join(tmpDir, "project");
@@ -173,4 +173,42 @@ describe("mount path ~ expansion", () => {
173173
expect(merged.mounts?.[0]?.path).toBe(join(tmpDir, "dev/.jj"));
174174
expect(merged.mounts?.[1]?.path).toBe(join(tmpDir, "dev/.git"));
175175
});
176+
177+
it("resolves relative paths against cwd", () => {
178+
const projectDir = join(tmpDir, "project");
179+
mkdirSync(join(projectDir, ".pi"), { recursive: true });
180+
writeFileSync(join(projectDir, ".pi", "enclave.toml"), 'enabled = true\nmounts = [".jj", ".git"]\n');
181+
const { merged } = loadConfig(projectDir);
182+
expect(merged.mounts).toHaveLength(2);
183+
expect(merged.mounts?.[0]?.path).toBe(join(projectDir, ".jj"));
184+
expect(merged.mounts?.[1]?.path).toBe(join(projectDir, ".git"));
185+
});
186+
187+
it("resolves nested relative paths against cwd", () => {
188+
const projectDir = join(tmpDir, "project");
189+
mkdirSync(join(projectDir, ".pi"), { recursive: true });
190+
writeFileSync(join(projectDir, ".pi", "enclave.toml"), 'enabled = true\nmounts = ["data/cache"]\n');
191+
const { merged } = loadConfig(projectDir);
192+
expect(merged.mounts?.[0]?.path).toBe(join(projectDir, "data/cache"));
193+
});
194+
195+
it("leaves absolute paths unchanged", () => {
196+
const projectDir = join(tmpDir, "project");
197+
mkdirSync(join(projectDir, ".pi"), { recursive: true });
198+
writeFileSync(join(projectDir, ".pi", "enclave.toml"), 'enabled = true\nmounts = ["/tmp/shared"]\n');
199+
const { merged } = loadConfig(projectDir);
200+
expect(merged.mounts?.[0]?.path).toBe("/tmp/shared");
201+
});
202+
203+
it("resolves relative paths in object mount syntax", () => {
204+
const projectDir = join(tmpDir, "project");
205+
mkdirSync(join(projectDir, ".pi"), { recursive: true });
206+
writeFileSync(
207+
join(projectDir, ".pi", "enclave.toml"),
208+
'enabled = true\n\n[[mounts]]\npath = ".jj"\nreadonly = true\n',
209+
);
210+
const { merged } = loadConfig(projectDir);
211+
expect(merged.mounts?.[0]?.path).toBe(join(projectDir, ".jj"));
212+
expect(merged.mounts?.[0]?.readonly).toBe(true);
213+
});
176214
});

0 commit comments

Comments
 (0)