Skip to content

Commit 7759f43

Browse files
pRizzclaude
andcommitted
docs(17): create phase plan for custom bind mounts
Phase 17: Custom Bind Mounts - 3 plans in 3 waves - Plan 01: Core mount module (parsing, validation, error types) and config schema - Plan 02: CLI mount subcommand (add/remove/list) and start flags (--mount, --no-mounts) - Plan 03: Container creation with bind mounts and status display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8095bea commit 7759f43

4 files changed

Lines changed: 1026 additions & 7 deletions

File tree

.planning/ROADMAP.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,19 +329,19 @@ Plans:
329329
**Requirements**: None (enhancement)
330330
**Note**: Users often want to work with local project directories inside the container. This phase adds configuration and CLI options to specify bind mounts that map host paths to container paths.
331331
**Success Criteria** (what must be TRUE):
332-
1. User can add bind mounts via `occ config set mounts "/path/on/host:/path/in/container"`
332+
1. User can add bind mounts via `occ mount add /host/path:/container/path`
333333
2. User can add multiple mounts (array in config)
334334
3. Mounts are applied when container starts
335335
4. User can specify read-only mounts via `:ro` suffix
336336
5. Invalid paths (non-existent directories) are validated before container start
337337
6. `occ start --mount /path:/container/path` allows one-time mount without persisting to config
338338
7. `occ status` shows active bind mounts
339-
**Plans**: TBD
339+
**Plans**: 3 plans
340340

341341
Plans:
342-
- [ ] 17-01: TBD (config schema for mounts, validation)
343-
- [ ] 17-02: TBD (container creation with bind mounts)
344-
- [ ] 17-03: TBD (CLI commands and status display)
342+
- [ ] 17-01-PLAN.md — Core mount module (parsing, validation, error types) and config schema extension
343+
- [ ] 17-02-PLAN.md — CLI mount subcommand (add/remove/list) and start command flags (--mount, --no-mounts)
344+
- [ ] 17-03-PLAN.md — Container creation with bind mounts and status display of active mounts
345345

346346
### Phase 18: CLI Sync Strategy
347347
**Goal**: Develop and implement a strategy to ensure the Rust CLI and Node CLI remain feature-complete and behavior-consistent
@@ -559,7 +559,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> ... -> 22 -> 23 -> 24 -> 25 -> 2
559559
| 14. Versioning and Release Automation | 3/3 | ✓ Complete | 2026-01-23 |
560560
| 15. Prebuilt Image Option | 3/3 | ✓ Complete | 2026-01-24 |
561561
| 16. Code Quality Audit | 2/2 | ✓ Complete | 2026-01-25 |
562-
| 17. Custom Bind Mounts | 0/3 | Not started | - |
562+
| 17. Custom Bind Mounts | 0/3 | Planned | - |
563563
| 18. CLI Sync Strategy | 0/3 | Not started | - |
564564
| 19. CI/CD Automation | - | MERGED | - |
565565
| 20. One-Click Cloud Deploy | 0/3 | Not started | - |
@@ -574,4 +574,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> ... -> 22 -> 23 -> 24 -> 25 -> 2
574574

575575
---
576576
*Roadmap created: 2026-01-18*
577-
*Last updated: 2026-01-25 (Phase 16 complete)*
577+
*Last updated: 2026-01-25 (Phase 17 planned)*
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
phase: 17-custom-bind-mounts
3+
plan: 01
4+
type: execute
5+
wave: 1
6+
depends_on: []
7+
files_modified:
8+
- packages/core/src/config/schema.rs
9+
- packages/core/src/docker/mount.rs
10+
- packages/core/src/docker/mod.rs
11+
autonomous: true
12+
13+
must_haves:
14+
truths:
15+
- "Mount string can be parsed into host path, container path, and read-only flag"
16+
- "Invalid mount formats are rejected with clear error messages"
17+
- "Relative paths are rejected with actionable error"
18+
- "Non-existent paths are rejected with clear error"
19+
- "Symlinks are resolved to their target directories"
20+
- "Config stores mounts as array of strings"
21+
artifacts:
22+
- path: "packages/core/src/docker/mount.rs"
23+
provides: "Mount parsing, validation, and Bollard conversion"
24+
exports: ["ParsedMount", "MountError", "validate_mount_path", "check_container_path_warning"]
25+
- path: "packages/core/src/config/schema.rs"
26+
provides: "Config mounts field"
27+
contains: "pub mounts: Vec<String>"
28+
key_links:
29+
- from: "packages/core/src/docker/mount.rs"
30+
to: "bollard::service::Mount"
31+
via: "to_bollard_mount() method"
32+
pattern: "MountTypeEnum::BIND"
33+
---
34+
35+
<objective>
36+
Create the core mount parsing and validation module with config schema extension.
37+
38+
Purpose: Establish the foundation for bind mount support with proper validation before container integration.
39+
Output: `mount.rs` module with ParsedMount struct, MountError enum, validation functions, and config schema with `mounts: Vec<String>` field.
40+
</objective>
41+
42+
<execution_context>
43+
@~/.claude/get-shit-done/workflows/execute-plan.md
44+
@~/.claude/get-shit-done/templates/summary.md
45+
</execution_context>
46+
47+
<context>
48+
@.planning/PROJECT.md
49+
@.planning/ROADMAP.md
50+
@.planning/STATE.md
51+
@.planning/phases/17-custom-bind-mounts/17-CONTEXT.md
52+
@.planning/phases/17-custom-bind-mounts/17-RESEARCH.md
53+
@packages/core/src/config/schema.rs
54+
@packages/core/src/docker/mod.rs
55+
@packages/core/src/docker/error.rs
56+
</context>
57+
58+
<tasks>
59+
60+
<task type="auto">
61+
<name>Task 1: Create mount.rs module with parsing and validation</name>
62+
<files>packages/core/src/docker/mount.rs, packages/core/src/docker/mod.rs</files>
63+
<action>
64+
Create `packages/core/src/docker/mount.rs` with:
65+
66+
1. **MountError enum** using thiserror:
67+
- `RelativePath(String)` - "Mount paths must be absolute. Use: /full/path/to/dir"
68+
- `InvalidFormat(String)` - "Invalid mount format. Expected: /host/path:/container/path[:ro]"
69+
- `PathNotFound(String, String)` - path and underlying error
70+
- `NotADirectory(String)` - "Path is not a directory"
71+
- `PermissionDenied(String)` - "Cannot access path"
72+
73+
2. **ParsedMount struct**:
74+
```rust
75+
pub struct ParsedMount {
76+
pub host_path: PathBuf,
77+
pub container_path: String,
78+
pub read_only: bool,
79+
}
80+
```
81+
82+
3. **ParsedMount::parse()** method:
83+
- Split mount string by `:` (handle 2 or 3 parts)
84+
- Part 1: host path (must be absolute)
85+
- Part 2: container path
86+
- Part 3 (optional): "ro" or "rw" (default rw)
87+
- Return MountError::InvalidFormat for wrong number of parts
88+
- Return MountError::RelativePath if host path not absolute
89+
90+
4. **ParsedMount::to_bollard_mount()** method:
91+
```rust
92+
pub fn to_bollard_mount(&self) -> bollard::service::Mount {
93+
Mount {
94+
target: Some(self.container_path.clone()),
95+
source: Some(self.host_path.to_string_lossy().to_string()),
96+
typ: Some(MountTypeEnum::BIND),
97+
read_only: Some(self.read_only),
98+
..Default::default()
99+
}
100+
}
101+
```
102+
103+
5. **validate_mount_path()** function:
104+
- Check `is_absolute()` - return MountError::RelativePath
105+
- Call `std::fs::canonicalize()` - return MountError::PathNotFound on error
106+
- Call `std::fs::metadata()` - return MountError::PathNotFound or PermissionDenied
107+
- Check `metadata.is_dir()` - return MountError::NotADirectory
108+
- Return Ok(canonical_path)
109+
110+
6. **check_container_path_warning()** function:
111+
- SYSTEM_PATHS constant: `/etc`, `/usr`, `/bin`, `/sbin`, `/lib`, `/var`
112+
- Return Some(warning_string) if container_path starts with any system path
113+
- Return None otherwise
114+
115+
7. **Unit tests**:
116+
- `parse_valid_mount_rw` - "/a:/b" -> host=/a, container=/b, read_only=false
117+
- `parse_valid_mount_ro` - "/a:/b:ro" -> read_only=true
118+
- `parse_valid_mount_explicit_rw` - "/a:/b:rw" -> read_only=false
119+
- `parse_invalid_format_single_part` - "invalid" -> InvalidFormat
120+
- `parse_invalid_format_too_many_parts` - "/a:/b:ro:extra" -> InvalidFormat
121+
- `parse_relative_path_rejected` - "./rel:/b" -> RelativePath
122+
- `system_path_warning` - "/etc/passwd" triggers warning
123+
- `non_system_path_no_warning` - "/workspace/data" no warning
124+
125+
Update `packages/core/src/docker/mod.rs`:
126+
- Add `pub mod mount;`
127+
- Add `pub use mount::{MountError, ParsedMount, check_container_path_warning, validate_mount_path};`
128+
</action>
129+
<verify>
130+
Run `cargo build -p opencode-cloud-core` to verify compilation.
131+
Run `cargo test -p opencode-cloud-core mount::` to verify tests pass.
132+
</verify>
133+
<done>
134+
mount.rs exists with ParsedMount, MountError, validate_mount_path, check_container_path_warning. All unit tests pass. Module exported from docker/mod.rs.
135+
</done>
136+
</task>
137+
138+
<task type="auto">
139+
<name>Task 2: Add mounts field to config schema</name>
140+
<files>packages/core/src/config/schema.rs</files>
141+
<action>
142+
In `packages/core/src/config/schema.rs`:
143+
144+
1. Add `mounts` field to Config struct after `update_check`:
145+
```rust
146+
/// Bind mounts to apply when starting the container
147+
/// Format: ["/host/path:/container/path", "/host:/mnt:ro"]
148+
#[serde(default)]
149+
pub mounts: Vec<String>,
150+
```
151+
152+
2. Update `Default for Config` impl:
153+
- Add `mounts: Vec::new(),` in the default initialization
154+
155+
3. Add tests:
156+
- `test_default_config_mounts_field` - default config has empty mounts vec
157+
- `test_serialize_deserialize_with_mounts` - roundtrip with mounts array
158+
- `test_mounts_field_default_on_missing` - old configs without mounts get empty vec
159+
160+
Ensure existing tests still pass (the deny_unknown_fields attribute should not affect new fields we add).
161+
</action>
162+
<verify>
163+
Run `cargo test -p opencode-cloud-core config::` to verify all config tests pass.
164+
Run `cargo build -p opencode-cloud-core` to verify compilation.
165+
</verify>
166+
<done>
167+
Config struct has `mounts: Vec<String>` field with serde default. All existing and new config tests pass.
168+
</done>
169+
</task>
170+
171+
</tasks>
172+
173+
<verification>
174+
1. `cargo build --all-targets --all-features` passes
175+
2. `cargo test --all-features` passes
176+
3. `cargo clippy --all-targets --all-features -- -D warnings` passes
177+
4. ParsedMount can parse "/host:/container", "/host:/container:ro", "/host:/container:rw"
178+
5. Validation rejects relative paths, non-existent paths, non-directories
179+
6. Config loads and saves with mounts array
180+
</verification>
181+
182+
<success_criteria>
183+
- Mount parsing works for all valid formats (2-part rw, 3-part ro/rw)
184+
- Validation catches all error cases with clear messages
185+
- Config schema accepts mounts array and handles old configs gracefully
186+
- All tests pass, no clippy warnings
187+
</success_criteria>
188+
189+
<output>
190+
After completion, create `.planning/phases/17-custom-bind-mounts/17-01-SUMMARY.md`
191+
</output>

0 commit comments

Comments
 (0)