@opencode-cloud/core 4.2.1 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "opencode-cloud-core"
3
- version = "4.2.1"
3
+ version = "4.2.2"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  license = "MIT"
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
 
11
11
  > [!WARNING]
12
- > This project is a work in progress and evolving rapidly. Use with caution.
12
+ > This tool is still a work in progress and is rapidly evolving. Expect frequent updates and breaking changes. Follow updates at https://github.com/pRizz/opencode-cloud. Stability will be announced at some point. Use with caution.
13
13
 
14
14
  A production-ready toolkit for deploying and managing [opencode](https://github.com/anomalyco/opencode) as a persistent cloud service, **sandboxed inside a Docker container** for isolation and security.
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cloud/core",
3
- "version": "4.2.1",
3
+ "version": "4.2.2",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/src/config/mod.rs CHANGED
@@ -14,6 +14,7 @@ use std::path::PathBuf;
14
14
  use anyhow::{Context, Result};
15
15
  use jsonc_parser::parse_to_serde_value;
16
16
 
17
+ use crate::docker::mount::ParsedMount;
17
18
  pub use paths::{get_config_dir, get_config_path, get_data_dir, get_hosts_path, get_pid_path};
18
19
  pub use schema::{Config, default_mounts, validate_bind_address};
19
20
  pub use validation::{
@@ -99,13 +100,36 @@ pub fn load_config() -> Result<Config> {
99
100
  }
100
101
 
101
102
  // Deserialize into Config struct (deny_unknown_fields will reject unknown keys)
102
- let config: Config = serde_json::from_value(parsed_value).with_context(|| {
103
+ let mut config: Config = serde_json::from_value(parsed_value).with_context(|| {
103
104
  format!(
104
105
  "Invalid configuration in {}. Check for unknown fields or invalid values.",
105
106
  config_path.display()
106
107
  )
107
108
  })?;
108
109
 
110
+ let mut removed_shadowing_mounts = false;
111
+ config.mounts.retain(|mount_str| {
112
+ let parsed = match ParsedMount::parse(mount_str) {
113
+ Ok(parsed) => parsed,
114
+ Err(_) => return true,
115
+ };
116
+ if parsed.container_path == "/opt/opencode"
117
+ || parsed.container_path.starts_with("/opt/opencode/")
118
+ {
119
+ removed_shadowing_mounts = true;
120
+ tracing::warn!(
121
+ "Skipping bind mount that overrides opencode binaries: {}",
122
+ mount_str
123
+ );
124
+ return false;
125
+ }
126
+ true
127
+ });
128
+
129
+ if removed_shadowing_mounts {
130
+ tracing::info!("Removed bind mounts that shadow /opt/opencode");
131
+ }
132
+
109
133
  ensure_default_mount_dirs(&config)?;
110
134
  Ok(config)
111
135
  }
@@ -2,8 +2,10 @@
2
2
  //!
3
3
  //! Defines the structure and defaults for the config.json file.
4
4
 
5
- use super::paths::{get_config_dir, get_data_dir};
6
- use crate::docker::volume::{MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION};
5
+ use crate::docker::volume::{
6
+ MOUNT_CACHE, MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, MOUNT_STATE,
7
+ };
8
+ use directories::BaseDirs;
7
9
  use serde::{Deserialize, Serialize};
8
10
  use std::net::{IpAddr, Ipv4Addr};
9
11
  /// Main configuration structure for opencode-cloud
@@ -176,18 +178,21 @@ fn default_update_check() -> String {
176
178
  }
177
179
 
178
180
  pub fn default_mounts() -> Vec<String> {
179
- let maybe_data_dir = get_data_dir();
180
- let maybe_config_dir = get_config_dir();
181
- let (Some(data_dir), Some(config_dir)) = (maybe_data_dir, maybe_config_dir) else {
181
+ let Some(base_dirs) = BaseDirs::new() else {
182
182
  return Vec::new();
183
183
  };
184
+ let home_dir = base_dirs.home_dir();
184
185
 
185
- let session_dir = data_dir.join("data");
186
+ let data_dir = home_dir.join(".local").join("share").join("opencode");
187
+ let state_dir = home_dir.join(".local").join("state").join("opencode");
188
+ let cache_dir = home_dir.join(".cache").join("opencode");
189
+ let config_dir = home_dir.join(".config").join("opencode");
186
190
  let workspace_dir = data_dir.join("workspace");
187
- let config_dir = config_dir.join("container");
188
191
 
189
192
  vec![
190
- format!("{}:{MOUNT_SESSION}", session_dir.display()),
193
+ format!("{}:{MOUNT_SESSION}", data_dir.display()),
194
+ format!("{}:{MOUNT_STATE}", state_dir.display()),
195
+ format!("{}:{MOUNT_CACHE}", cache_dir.display()),
191
196
  format!("{}:{MOUNT_PROJECTS}", workspace_dir.display()),
192
197
  format!("{}:{MOUNT_CONFIG}", config_dir.display()),
193
198
  ]
@@ -164,6 +164,7 @@ RUN mkdir -p \
164
164
  /home/opencode/.config \
165
165
  /home/opencode/.local/bin \
166
166
  /home/opencode/.local/share \
167
+ /home/opencode/.local/state \
167
168
  /home/opencode/.cache \
168
169
  /home/opencode/workspace
169
170
 
@@ -529,16 +530,15 @@ RUN OPENCODE_COMMIT="c7fb116c1cf59a76b184f842ed3eb0113d93196b" \
529
530
  && bun run build-single-ui \
530
531
  && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \
531
532
  && cd /tmp/opencode-repo \
532
- && mkdir -p /home/opencode/.local/share/opencode/bin \
533
- && mkdir -p /home/opencode/.local/share/opencode/ui \
534
- && cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/bin/opencode /home/opencode/.local/share/opencode/bin/opencode \
535
- && cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /home/opencode/.local/share/opencode/ui/ \
536
- && chown -R opencode:opencode /home/opencode/.local/share/opencode \
537
- && chmod +x /home/opencode/.local/share/opencode/bin/opencode \
538
- && /home/opencode/.local/share/opencode/bin/opencode --version
533
+ && sudo mkdir -p /opt/opencode/bin /opt/opencode/ui \
534
+ && sudo cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/bin/opencode /opt/opencode/bin/opencode \
535
+ && sudo cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /opt/opencode/ui/ \
536
+ && sudo chown -R opencode:opencode /opt/opencode \
537
+ && sudo chmod +x /opt/opencode/bin/opencode \
538
+ && /opt/opencode/bin/opencode --version
539
539
 
540
540
  # Add opencode to PATH
541
- ENV PATH="/home/opencode/.local/share/opencode/bin:${PATH}"
541
+ ENV PATH="/opt/opencode/bin:${PATH}"
542
542
 
543
543
  # -----------------------------------------------------------------------------
544
544
  # opencode-broker Installation
@@ -651,10 +651,10 @@ RUN printf '%s\n' \
651
651
  'Type=simple' \
652
652
  'User=opencode' \
653
653
  'WorkingDirectory=/home/opencode/workspace' \
654
- 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
654
+ 'ExecStart=/opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
655
655
  'Restart=always' \
656
656
  'RestartSec=5' \
657
- 'Environment=PATH=/home/opencode/.local/share/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
657
+ 'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
658
658
  '' \
659
659
  '[Install]' \
660
660
  'WantedBy=multi-user.target' \
@@ -700,7 +700,7 @@ RUN printf '%s\n' \
700
700
  ' install -d -m 0755 /run/opencode' \
701
701
  ' /usr/local/bin/opencode-broker &' \
702
702
  ' # Use runuser to switch to opencode user without password prompt' \
703
- ' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \
703
+ ' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \
704
704
  'fi' \
705
705
  > /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
706
706
 
@@ -6,7 +6,8 @@
6
6
  use super::dockerfile::{IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
7
7
  use super::mount::ParsedMount;
8
8
  use super::volume::{
9
- MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, VOLUME_CONFIG, VOLUME_PROJECTS, VOLUME_SESSION,
9
+ MOUNT_CACHE, MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, MOUNT_STATE, VOLUME_CACHE,
10
+ VOLUME_CONFIG, VOLUME_PROJECTS, VOLUME_SESSION, VOLUME_STATE,
10
11
  };
11
12
  use super::{DockerClient, DockerError};
12
13
  use bollard::container::{
@@ -16,7 +17,7 @@ use bollard::container::{
16
17
  use bollard::service::{
17
18
  HostConfig, Mount, MountPointTypeEnum, MountTypeEnum, PortBinding, PortMap,
18
19
  };
19
- use std::collections::HashMap;
20
+ use std::collections::{HashMap, HashSet};
20
21
  use tracing::debug;
21
22
 
22
23
  /// Default container name
@@ -25,6 +26,11 @@ pub const CONTAINER_NAME: &str = "opencode-cloud-sandbox";
25
26
  /// Default port for opencode web UI
26
27
  pub const OPENCODE_WEB_PORT: u16 = 3000;
27
28
 
29
+ fn has_env_key(env: &[String], key: &str) -> bool {
30
+ let prefix = format!("{key}=");
31
+ env.iter().any(|entry| entry.starts_with(&prefix))
32
+ }
33
+
28
34
  /// Create the opencode container with volume mounts
29
35
  ///
30
36
  /// Does not start the container - use start_container after creation.
@@ -85,30 +91,36 @@ pub async fn create_container(
85
91
  )));
86
92
  }
87
93
 
88
- // Create volume mounts
89
- let mut mounts = vec![
90
- Mount {
91
- target: Some(MOUNT_SESSION.to_string()),
92
- source: Some(VOLUME_SESSION.to_string()),
93
- typ: Some(MountTypeEnum::VOLUME),
94
- read_only: Some(false),
95
- ..Default::default()
96
- },
97
- Mount {
98
- target: Some(MOUNT_PROJECTS.to_string()),
99
- source: Some(VOLUME_PROJECTS.to_string()),
100
- typ: Some(MountTypeEnum::VOLUME),
101
- read_only: Some(false),
102
- ..Default::default()
103
- },
104
- Mount {
105
- target: Some(MOUNT_CONFIG.to_string()),
106
- source: Some(VOLUME_CONFIG.to_string()),
94
+ let mut bind_targets = HashSet::new();
95
+ if let Some(ref user_mounts) = bind_mounts {
96
+ for parsed in user_mounts {
97
+ bind_targets.insert(parsed.container_path.clone());
98
+ }
99
+ }
100
+
101
+ // Create volume mounts (skip if overridden by bind mounts)
102
+ let mut mounts = Vec::new();
103
+ let mut add_volume_mount = |target: &str, source: &str| {
104
+ if bind_targets.contains(target) {
105
+ tracing::trace!(
106
+ "Skipping volume mount for {} (overridden by bind mount)",
107
+ target
108
+ );
109
+ return;
110
+ }
111
+ mounts.push(Mount {
112
+ target: Some(target.to_string()),
113
+ source: Some(source.to_string()),
107
114
  typ: Some(MountTypeEnum::VOLUME),
108
115
  read_only: Some(false),
109
116
  ..Default::default()
110
- },
111
- ];
117
+ });
118
+ };
119
+ add_volume_mount(MOUNT_SESSION, VOLUME_SESSION);
120
+ add_volume_mount(MOUNT_STATE, VOLUME_STATE);
121
+ add_volume_mount(MOUNT_CACHE, VOLUME_CACHE);
122
+ add_volume_mount(MOUNT_PROJECTS, VOLUME_PROJECTS);
123
+ add_volume_mount(MOUNT_CONFIG, VOLUME_CONFIG);
112
124
 
113
125
  // Add user-defined bind mounts from config/CLI
114
126
  if let Some(ref user_mounts) = bind_mounts {
@@ -189,14 +201,24 @@ pub async fn create_container(
189
201
  };
190
202
 
191
203
  // Build environment variables
204
+ let mut env = env_vars.unwrap_or_default();
205
+ if !has_env_key(&env, "XDG_DATA_HOME") {
206
+ env.push("XDG_DATA_HOME=/home/opencode/.local/share".to_string());
207
+ }
208
+ if !has_env_key(&env, "XDG_STATE_HOME") {
209
+ env.push("XDG_STATE_HOME=/home/opencode/.local/state".to_string());
210
+ }
211
+ if !has_env_key(&env, "XDG_CONFIG_HOME") {
212
+ env.push("XDG_CONFIG_HOME=/home/opencode/.config".to_string());
213
+ }
214
+ if !has_env_key(&env, "XDG_CACHE_HOME") {
215
+ env.push("XDG_CACHE_HOME=/home/opencode/.cache".to_string());
216
+ }
192
217
  // Add USE_SYSTEMD=1 when Cockpit is enabled to tell entrypoint to use systemd
193
- let final_env = if cockpit_enabled_val {
194
- let mut env = env_vars.unwrap_or_default();
218
+ if cockpit_enabled_val && !has_env_key(&env, "USE_SYSTEMD") {
195
219
  env.push("USE_SYSTEMD=1".to_string());
196
- Some(env)
197
- } else {
198
- env_vars
199
- };
220
+ }
221
+ let final_env = if env.is_empty() { None } else { Some(env) };
200
222
 
201
223
  // Create container config
202
224
  let config = Config {
package/src/docker/mod.rs CHANGED
@@ -60,8 +60,9 @@ pub use users::{
60
60
 
61
61
  // Volume management
62
62
  pub use volume::{
63
- MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, VOLUME_CONFIG, VOLUME_NAMES, VOLUME_PROJECTS,
64
- VOLUME_SESSION, ensure_volumes_exist, remove_all_volumes, remove_volume, volume_exists,
63
+ MOUNT_CACHE, MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, MOUNT_STATE, VOLUME_CACHE,
64
+ VOLUME_CONFIG, VOLUME_NAMES, VOLUME_PROJECTS, VOLUME_SESSION, VOLUME_STATE,
65
+ ensure_volumes_exist, remove_all_volumes, remove_volume, volume_exists,
65
66
  };
66
67
 
67
68
  // Bind mount parsing and validation
@@ -11,6 +11,12 @@ use tracing::debug;
11
11
  /// Volume name for opencode data
12
12
  pub const VOLUME_SESSION: &str = "opencode-data";
13
13
 
14
+ /// Volume name for opencode state
15
+ pub const VOLUME_STATE: &str = "opencode-state";
16
+
17
+ /// Volume name for opencode cache
18
+ pub const VOLUME_CACHE: &str = "opencode-cache";
19
+
14
20
  /// Volume name for project files
15
21
  pub const VOLUME_PROJECTS: &str = "opencode-workspace";
16
22
 
@@ -18,16 +24,28 @@ pub const VOLUME_PROJECTS: &str = "opencode-workspace";
18
24
  pub const VOLUME_CONFIG: &str = "opencode-config";
19
25
 
20
26
  /// All volume names as array for iteration
21
- pub const VOLUME_NAMES: [&str; 3] = [VOLUME_SESSION, VOLUME_PROJECTS, VOLUME_CONFIG];
27
+ pub const VOLUME_NAMES: [&str; 5] = [
28
+ VOLUME_SESSION,
29
+ VOLUME_STATE,
30
+ VOLUME_CACHE,
31
+ VOLUME_PROJECTS,
32
+ VOLUME_CONFIG,
33
+ ];
22
34
 
23
35
  /// Mount point for opencode data inside container
24
- pub const MOUNT_SESSION: &str = "/home/opencode/.local/share";
36
+ pub const MOUNT_SESSION: &str = "/home/opencode/.local/share/opencode";
37
+
38
+ /// Mount point for opencode state inside container
39
+ pub const MOUNT_STATE: &str = "/home/opencode/.local/state/opencode";
40
+
41
+ /// Mount point for opencode cache inside container
42
+ pub const MOUNT_CACHE: &str = "/home/opencode/.cache/opencode";
25
43
 
26
44
  /// Mount point for project files inside container
27
45
  pub const MOUNT_PROJECTS: &str = "/home/opencode/workspace";
28
46
 
29
47
  /// Mount point for configuration inside container
30
- pub const MOUNT_CONFIG: &str = "/home/opencode/.config";
48
+ pub const MOUNT_CONFIG: &str = "/home/opencode/.config/opencode";
31
49
 
32
50
  /// Ensure all required volumes exist
33
51
  ///
@@ -123,22 +141,28 @@ mod tests {
123
141
  #[test]
124
142
  fn volume_constants_are_correct() {
125
143
  assert_eq!(VOLUME_SESSION, "opencode-data");
144
+ assert_eq!(VOLUME_STATE, "opencode-state");
145
+ assert_eq!(VOLUME_CACHE, "opencode-cache");
126
146
  assert_eq!(VOLUME_PROJECTS, "opencode-workspace");
127
147
  assert_eq!(VOLUME_CONFIG, "opencode-config");
128
148
  }
129
149
 
130
150
  #[test]
131
151
  fn volume_names_array_has_all_volumes() {
132
- assert_eq!(VOLUME_NAMES.len(), 3);
152
+ assert_eq!(VOLUME_NAMES.len(), 5);
133
153
  assert!(VOLUME_NAMES.contains(&VOLUME_SESSION));
154
+ assert!(VOLUME_NAMES.contains(&VOLUME_STATE));
155
+ assert!(VOLUME_NAMES.contains(&VOLUME_CACHE));
134
156
  assert!(VOLUME_NAMES.contains(&VOLUME_PROJECTS));
135
157
  assert!(VOLUME_NAMES.contains(&VOLUME_CONFIG));
136
158
  }
137
159
 
138
160
  #[test]
139
161
  fn mount_points_are_correct() {
140
- assert_eq!(MOUNT_SESSION, "/home/opencode/.local/share");
162
+ assert_eq!(MOUNT_SESSION, "/home/opencode/.local/share/opencode");
163
+ assert_eq!(MOUNT_STATE, "/home/opencode/.local/state/opencode");
164
+ assert_eq!(MOUNT_CACHE, "/home/opencode/.cache/opencode");
141
165
  assert_eq!(MOUNT_PROJECTS, "/home/opencode/workspace");
142
- assert_eq!(MOUNT_CONFIG, "/home/opencode/.config");
166
+ assert_eq!(MOUNT_CONFIG, "/home/opencode/.config/opencode");
143
167
  }
144
168
  }