@opencode-cloud/core 4.2.1 → 4.2.3
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 +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/config/mod.rs +25 -1
- package/src/config/schema.rs +13 -8
- package/src/docker/Dockerfile +11 -11
- package/src/docker/container.rs +51 -29
- package/src/docker/mod.rs +3 -2
- package/src/docker/volume.rs +30 -6
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
|
|
11
11
|
> [!WARNING]
|
|
12
|
-
> This
|
|
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
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
|
}
|
package/src/config/schema.rs
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Defines the structure and defaults for the config.json file.
|
|
4
4
|
|
|
5
|
-
use
|
|
6
|
-
|
|
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
|
|
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
|
|
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}",
|
|
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
|
]
|
package/src/docker/Dockerfile
CHANGED
|
@@ -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 /
|
|
533
|
-
&&
|
|
534
|
-
&& cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/
|
|
535
|
-
&&
|
|
536
|
-
&&
|
|
537
|
-
&&
|
|
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="/
|
|
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=/
|
|
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=/
|
|
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 && /
|
|
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
|
|
package/src/docker/container.rs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
64
|
-
|
|
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
|
package/src/docker/volume.rs
CHANGED
|
@@ -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;
|
|
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(),
|
|
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
|
}
|