@opencode-cloud/core 12.0.1 → 13.0.0

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 = "12.0.1"
3
+ version = "13.0.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.89"
6
6
  license = "MIT"
@@ -39,7 +39,7 @@ napi = { version = "2", features = ["tokio_rt", "napi9"], optional = true }
39
39
  napi-derive = { version = "2", optional = true }
40
40
 
41
41
  # Docker integration
42
- bollard = { version = "0.18", features = ["chrono", "buildkit"] }
42
+ bollard = { version = "0.20.1", features = ["chrono", "buildkit"] }
43
43
  futures-util = "0.3"
44
44
  tar = "0.4"
45
45
  flate2 = "1.0"
@@ -47,13 +47,13 @@ tokio-retry = "0.3"
47
47
  indicatif = { version = "0.17", features = ["tokio", "futures"] }
48
48
  http-body-util = "0.1"
49
49
  bytes = "1.9"
50
- reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
50
+ reqwest = { version = "0.13", default-features = false, features = ["rustls", "webpki-roots", "json"] }
51
51
 
52
52
  # Platform service management (macOS)
53
53
  plist = "1.8"
54
54
 
55
55
  # Host management
56
- whoami = "1.5"
56
+ whoami = "2.1"
57
57
  ssh2-config-rs = "0.7.2"
58
58
  dirs = "6"
59
59
 
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![crates.io](https://img.shields.io/crates/v/opencode-cloud.svg)](https://crates.io/crates/opencode-cloud)
6
6
  [![GHCR](https://img.shields.io/badge/ghcr.io-sandbox-blue?logo=github)](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
7
7
  [![Docker Hub](https://img.shields.io/docker/v/prizz/opencode-cloud-sandbox?label=docker&sort=semver)](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
8
+ [![Docker Pulls](https://img.shields.io/docker/pulls/prizz/opencode-cloud-sandbox)](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
8
9
  [![docs.rs](https://docs.rs/opencode-cloud/badge.svg)](https://docs.rs/opencode-cloud)
9
10
  [![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cloud/core",
3
- "version": "12.0.1",
3
+ "version": "13.0.0",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -71,7 +71,6 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
71
71
  systemd-sysv=257.* \
72
72
  dbus=1.16.* \
73
73
  # Shell and terminal
74
- zsh=5.9-* \
75
74
  tmux=3.5a-* \
76
75
  # Editors
77
76
  vim=2:9.1.* \
@@ -155,7 +154,7 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
155
154
  # Create Non-Root User
156
155
  # -----------------------------------------------------------------------------
157
156
  # Create 'opencode' user with passwordless sudo
158
- RUN useradd -m -s /bin/zsh -G sudo opencode \
157
+ RUN useradd -m -s /bin/bash -G sudo opencode \
159
158
  && echo "opencode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/opencode \
160
159
  && chmod 0440 /etc/sudoers.d/opencode
161
160
 
@@ -176,27 +175,23 @@ RUN mkdir -p \
176
175
  ENV PATH="/home/opencode/.local/bin:${PATH}"
177
176
 
178
177
  # -----------------------------------------------------------------------------
179
- # Shell Setup: Zsh + Oh My Zsh + Starship
178
+ # Shell Setup: Bash + Starship
180
179
  # -----------------------------------------------------------------------------
181
- # Oh My Zsh - self-managing installer, trusted to handle versions
182
- # Disabled temporarily to reduce Docker build time.
183
- # RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
184
-
185
180
  # Starship prompt - self-managing installer, trusted to handle versions
186
181
  # Disabled temporarily to reduce Docker build time.
187
182
  # RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /home/opencode/.local/bin
188
183
 
189
- # Configure zsh with starship
184
+ # Configure bash with starship
190
185
  # Disabled temporarily to reduce Docker build time.
191
- # RUN echo 'eval "$(starship init zsh)"' >> /home/opencode/.zshrc \
192
- # && echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshrc
186
+ # RUN echo 'eval "$(starship init bash)"' >> /home/opencode/.bashrc \
187
+ # && echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.bashrc
193
188
 
194
189
  # -----------------------------------------------------------------------------
195
190
  # mise: Universal Version Manager
196
191
  # -----------------------------------------------------------------------------
197
192
  # mise - self-managing installer, trusted to handle versions
198
193
  RUN curl https://mise.run | sh \
199
- && echo 'eval "$(/home/opencode/.local/bin/mise activate zsh)"' >> /home/opencode/.zshrc
194
+ && echo 'eval "$(/home/opencode/.local/bin/mise activate bash)"' >> /home/opencode/.bashrc
200
195
 
201
196
  # Install language runtimes via mise (2026-02-03)
202
197
  # - node@25: pinned to major version
@@ -306,7 +301,7 @@ RUN mkdir -p /home/opencode/.cargo/registry /home/opencode/.cargo/git \
306
301
  # RUN apt-get update && apt-get install -y --no-install-recommends direnv=2.32.* \
307
302
  # && rm -rf /var/lib/apt/lists/*
308
303
  # USER opencode
309
- # RUN echo 'eval "$(direnv hook zsh)"' >> /home/opencode/.zshrc
304
+ # RUN echo 'eval "$(direnv hook bash)"' >> /home/opencode/.bashrc
310
305
 
311
306
  # Install HTTPie
312
307
  # Disabled temporarily to reduce Docker build time.
@@ -508,10 +503,10 @@ RUN printf '%s\n' \
508
503
  'alias d="docker"' \
509
504
  'alias dc="docker compose"' \
510
505
  '' \
511
- >> /home/opencode/.zshrc
506
+ >> /home/opencode/.bashrc
512
507
 
513
508
  # Set up pipx path
514
- RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshrc
509
+ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.bashrc
515
510
 
516
511
  # -----------------------------------------------------------------------------
517
512
  # Stage 2: opencode build
@@ -540,7 +535,7 @@ USER opencode
540
535
  # commit on the main branch of https://github.com/pRizz/opencode.
541
536
  # Update it by running: ./scripts/update-opencode-commit.sh
542
537
  # Build opencode from source (BuildKit cache mounts disabled for now)
543
- RUN OPENCODE_COMMIT="cfe79755427fd8d3b94be76e1c7912bdc943a8ab" \
538
+ RUN OPENCODE_COMMIT="ce0a5f0f2a8abfb9a00e335a41e7cfd3487dbb18" \
544
539
  && rm -rf /tmp/opencode-repo \
545
540
  && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
546
541
  && cd /tmp/opencode-repo \
@@ -10,9 +10,9 @@ use super::volume::{
10
10
  VOLUME_CACHE, VOLUME_CONFIG, VOLUME_PROJECTS, VOLUME_SESSION, VOLUME_STATE, VOLUME_USERS,
11
11
  };
12
12
  use super::{DockerClient, DockerError};
13
- use bollard::container::{
14
- Config, CreateContainerOptions, RemoveContainerOptions, StartContainerOptions,
15
- StopContainerOptions,
13
+ use bollard::models::ContainerCreateBody;
14
+ use bollard::query_parameters::{
15
+ CreateContainerOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
16
16
  };
17
17
  use bollard::service::{
18
18
  HostConfig, Mount, MountPointTypeEnum, MountTypeEnum, PortBinding, PortMap,
@@ -155,11 +155,10 @@ pub async fn create_container(
155
155
  );
156
156
  }
157
157
 
158
- // Create exposed ports map
159
- let mut exposed_ports = HashMap::new();
160
- exposed_ports.insert("3000/tcp".to_string(), HashMap::new());
158
+ // Create exposed ports list (bollard v0.20+ uses Vec<String>)
159
+ let mut exposed_ports = vec!["3000/tcp".to_string()];
161
160
  if cockpit_enabled_val {
162
- exposed_ports.insert("9090/tcp".to_string(), HashMap::new());
161
+ exposed_ports.push("9090/tcp".to_string());
163
162
  }
164
163
 
165
164
  // Create host config
@@ -197,6 +196,9 @@ pub async fn create_container(
197
196
  mounts: Some(mounts),
198
197
  port_bindings: Some(port_bindings),
199
198
  auto_remove: Some(false),
199
+ // CAP_SETUID and CAP_SETGID required for opencode-broker to spawn
200
+ // PTY processes as different users via setuid/setgid syscalls
201
+ cap_add: Some(vec!["SETUID".to_string(), "SETGID".to_string()]),
200
202
  ..Default::default()
201
203
  }
202
204
  };
@@ -221,8 +223,8 @@ pub async fn create_container(
221
223
  }
222
224
  let final_env = if env.is_empty() { None } else { Some(env) };
223
225
 
224
- // Create container config
225
- let config = Config {
226
+ // Create container config (bollard v0.20+ uses ContainerCreateBody)
227
+ let config = ContainerCreateBody {
226
228
  image: Some(image_name.to_string()),
227
229
  hostname: Some(CONTAINER_NAME.to_string()),
228
230
  working_dir: Some("/home/opencode/workspace".to_string()),
@@ -234,8 +236,8 @@ pub async fn create_container(
234
236
 
235
237
  // Create container
236
238
  let options = CreateContainerOptions {
237
- name: container_name,
238
- platform: None,
239
+ name: Some(container_name.to_string()),
240
+ platform: String::new(),
239
241
  };
240
242
 
241
243
  let response = client
@@ -263,7 +265,7 @@ pub async fn start_container(client: &DockerClient, name: &str) -> Result<(), Do
263
265
 
264
266
  client
265
267
  .inner()
266
- .start_container(name, None::<StartContainerOptions<String>>)
268
+ .start_container(name, None::<StartContainerOptions>)
267
269
  .await
268
270
  .map_err(|e| DockerError::Container(format!("Failed to start container {name}: {e}")))?;
269
271
 
@@ -282,10 +284,13 @@ pub async fn stop_container(
282
284
  name: &str,
283
285
  timeout_secs: Option<i64>,
284
286
  ) -> Result<(), DockerError> {
285
- let timeout = timeout_secs.unwrap_or(10);
287
+ let timeout = timeout_secs.unwrap_or(10) as i32;
286
288
  debug!("Stopping container {} with {}s timeout", name, timeout);
287
289
 
288
- let options = StopContainerOptions { t: timeout };
290
+ let options = StopContainerOptions {
291
+ signal: None,
292
+ t: Some(timeout),
293
+ };
289
294
 
290
295
  client
291
296
  .inner()
@@ -34,6 +34,10 @@ pub enum DockerError {
34
34
  #[error("Docker pull failed: {0}")]
35
35
  Pull(String),
36
36
 
37
+ /// Image operation failed
38
+ #[error("Docker image operation failed: {0}")]
39
+ Image(String),
40
+
37
41
  /// Container operation failed
38
42
  #[error("Container operation failed: {0}")]
39
43
  Container(String),
@@ -7,14 +7,18 @@ use super::progress::ProgressReporter;
7
7
  use super::{
8
8
  DOCKERFILE, DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT,
9
9
  };
10
- use bollard::image::{BuildImageOptions, BuilderVersion, CreateImageOptions};
11
10
  use bollard::moby::buildkit::v1::StatusResponse as BuildkitStatusResponse;
12
11
  use bollard::models::BuildInfoAux;
12
+ use bollard::query_parameters::{
13
+ BuildImageOptions, BuilderVersion, CreateImageOptions, ListImagesOptionsBuilder,
14
+ RemoveImageOptionsBuilder,
15
+ };
13
16
  use bytes::Bytes;
14
17
  use flate2::Compression;
15
18
  use flate2::write::GzEncoder;
16
19
  use futures_util::StreamExt;
17
- use std::collections::{HashMap, VecDeque};
20
+ use http_body_util::{Either, Full};
21
+ use std::collections::{HashMap, HashSet, VecDeque};
18
22
  use std::env;
19
23
  use std::time::{SystemTime, UNIX_EPOCH};
20
24
  use tar::Builder as TarBuilder;
@@ -66,6 +70,80 @@ pub async fn image_exists(
66
70
  }
67
71
  }
68
72
 
73
+ /// Remove all images whose tags or digests contain the provided name fragment
74
+ ///
75
+ /// Returns the number of image references removed.
76
+ pub async fn remove_images_by_name(
77
+ client: &DockerClient,
78
+ name_fragment: &str,
79
+ force: bool,
80
+ ) -> Result<usize, DockerError> {
81
+ debug!("Removing Docker images matching '{name_fragment}'");
82
+
83
+ let images = list_docker_images(client).await?;
84
+
85
+ let references = collect_image_references(&images, name_fragment);
86
+ remove_image_references(client, references, force).await
87
+ }
88
+
89
+ /// List all local Docker images (including intermediate layers).
90
+ async fn list_docker_images(
91
+ client: &DockerClient,
92
+ ) -> Result<Vec<bollard::models::ImageSummary>, DockerError> {
93
+ let list_options = ListImagesOptionsBuilder::new().all(true).build();
94
+ client
95
+ .inner()
96
+ .list_images(Some(list_options))
97
+ .await
98
+ .map_err(|e| DockerError::Image(format!("Failed to list images: {e}")))
99
+ }
100
+
101
+ /// Collect tags and digests that contain the provided name fragment.
102
+ fn collect_image_references(
103
+ images: &[bollard::models::ImageSummary],
104
+ name_fragment: &str,
105
+ ) -> HashSet<String> {
106
+ let mut references = HashSet::new();
107
+ for image in images {
108
+ for tag in &image.repo_tags {
109
+ if tag != "<none>:<none>" && tag.contains(name_fragment) {
110
+ references.insert(tag.to_string());
111
+ }
112
+ }
113
+
114
+ for digest in &image.repo_digests {
115
+ if digest.contains(name_fragment) {
116
+ references.insert(digest.to_string());
117
+ }
118
+ }
119
+ }
120
+ references
121
+ }
122
+
123
+ /// Remove image references (tags/digests), returning the number removed.
124
+ async fn remove_image_references(
125
+ client: &DockerClient,
126
+ references: HashSet<String>,
127
+ force: bool,
128
+ ) -> Result<usize, DockerError> {
129
+ if references.is_empty() {
130
+ return Ok(0);
131
+ }
132
+
133
+ let remove_options = RemoveImageOptionsBuilder::new().force(force).build();
134
+ let mut removed = 0usize;
135
+ for reference in references {
136
+ client
137
+ .inner()
138
+ .remove_image(&reference, Some(remove_options.clone()), None)
139
+ .await
140
+ .map_err(|e| DockerError::Image(format!("Failed to remove image {reference}: {e}")))?;
141
+ removed += 1;
142
+ }
143
+
144
+ Ok(removed)
145
+ }
146
+
69
147
  /// Build the opencode image from embedded Dockerfile
70
148
  ///
71
149
  /// Shows real-time build progress with streaming output.
@@ -103,18 +181,20 @@ pub async fn build_image(
103
181
  );
104
182
  let build_args = build_args.unwrap_or_default();
105
183
  let options = BuildImageOptions {
106
- t: full_name.clone(),
184
+ t: Some(full_name.clone()),
107
185
  dockerfile: "Dockerfile".to_string(),
108
186
  version: BuilderVersion::BuilderBuildKit,
109
187
  session: Some(session_id),
110
188
  rm: true,
111
189
  nocache: no_cache,
112
- buildargs: build_args,
190
+ buildargs: Some(build_args),
191
+ platform: String::new(),
192
+ target: String::new(),
113
193
  ..Default::default()
114
194
  };
115
195
 
116
196
  // Create build body from context
117
- let body = Bytes::from(context);
197
+ let body: Either<Full<Bytes>, _> = Either::Left(Full::new(Bytes::from(context)));
118
198
 
119
199
  // Start build with streaming output
120
200
  let mut stream = client.inner().build_image(options, None, Some(body));
@@ -137,10 +217,12 @@ pub async fn build_image(
137
217
 
138
218
  handle_stream_message(&info, progress, &mut log_state);
139
219
 
140
- if let Some(error_msg) = info.error {
141
- progress.abandon_all(&error_msg);
220
+ if let Some(error_detail) = &info.error_detail
221
+ && let Some(error_msg) = &error_detail.message
222
+ {
223
+ progress.abandon_all(error_msg);
142
224
  let context = format_build_error_with_context(
143
- &error_msg,
225
+ error_msg,
144
226
  &log_state.recent_logs,
145
227
  &log_state.error_logs,
146
228
  &log_state.recent_buildkit_logs,
@@ -620,8 +702,9 @@ async fn do_pull(
620
702
  let full_name = format!("{image}:{tag}");
621
703
 
622
704
  let options = CreateImageOptions {
623
- from_image: image,
624
- tag,
705
+ from_image: Some(image.to_string()),
706
+ tag: Some(tag.to_string()),
707
+ platform: String::new(),
625
708
  ..Default::default()
626
709
  };
627
710
 
@@ -634,9 +717,11 @@ async fn do_pull(
634
717
  match result {
635
718
  Ok(info) => {
636
719
  // Handle errors from the stream
637
- if let Some(error_msg) = info.error {
638
- progress.abandon_all(&error_msg);
639
- return Err(DockerError::Pull(error_msg));
720
+ if let Some(error_detail) = &info.error_detail
721
+ && let Some(error_msg) = &error_detail.message
722
+ {
723
+ progress.abandon_all(error_msg);
724
+ return Err(DockerError::Pull(error_msg.to_string()));
640
725
  }
641
726
 
642
727
  // Handle layer progress
package/src/docker/mod.rs CHANGED
@@ -42,7 +42,7 @@ pub use health::{
42
42
  pub use dockerfile::{DOCKERFILE, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
43
43
 
44
44
  // Image operations
45
- pub use image::{build_image, image_exists, pull_image};
45
+ pub use image::{build_image, image_exists, pull_image, remove_images_by_name};
46
46
 
47
47
  // Update operations
48
48
  pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
@@ -6,7 +6,7 @@
6
6
  use super::image::{image_exists, pull_image};
7
7
  use super::progress::ProgressReporter;
8
8
  use super::{DockerClient, DockerError, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
9
- use bollard::image::TagImageOptions;
9
+ use bollard::query_parameters::TagImageOptions;
10
10
  use tracing::debug;
11
11
 
12
12
  /// Tag for the previous image version (used for rollback)
@@ -45,8 +45,8 @@ pub async fn tag_current_as_previous(client: &DockerClient) -> Result<(), Docker
45
45
 
46
46
  // Tag current as previous
47
47
  let options = TagImageOptions {
48
- repo: IMAGE_NAME_GHCR,
49
- tag: PREVIOUS_TAG,
48
+ repo: Some(IMAGE_NAME_GHCR.to_string()),
49
+ tag: Some(PREVIOUS_TAG.to_string()),
50
50
  };
51
51
 
52
52
  client
@@ -124,8 +124,8 @@ pub async fn rollback_image(client: &DockerClient) -> Result<(), DockerError> {
124
124
 
125
125
  // Re-tag previous as latest
126
126
  let options = TagImageOptions {
127
- repo: IMAGE_NAME_GHCR,
128
- tag: IMAGE_TAG_DEFAULT,
127
+ repo: Some(IMAGE_NAME_GHCR.to_string()),
128
+ tag: Some(IMAGE_TAG_DEFAULT.to_string()),
129
129
  };
130
130
 
131
131
  client
@@ -4,7 +4,8 @@
4
4
  //! for persistent storage across container restarts.
5
5
 
6
6
  use super::{DockerClient, DockerError};
7
- use bollard::volume::CreateVolumeOptions;
7
+ use bollard::models::VolumeCreateRequest;
8
+ use bollard::query_parameters::RemoveVolumeOptions;
8
9
  use std::collections::HashMap;
9
10
  use tracing::debug;
10
11
 
@@ -73,12 +74,16 @@ pub async fn ensure_volumes_exist(client: &DockerClient) -> Result<(), DockerErr
73
74
  async fn ensure_volume_exists(client: &DockerClient, name: &str) -> Result<(), DockerError> {
74
75
  debug!("Checking volume: {}", name);
75
76
 
76
- // Create volume options with default local driver
77
- let options = CreateVolumeOptions {
78
- name,
79
- driver: "local",
80
- driver_opts: HashMap::new(),
81
- labels: HashMap::from([("managed-by", "opencode-cloud")]),
77
+ // Create volume request with default local driver (bollard v0.20+ uses VolumeCreateRequest)
78
+ let options = VolumeCreateRequest {
79
+ name: Some(name.to_string()),
80
+ driver: Some("local".to_string()),
81
+ driver_opts: Some(HashMap::new()),
82
+ labels: Some(HashMap::from([(
83
+ "managed-by".to_string(),
84
+ "opencode-cloud".to_string(),
85
+ )])),
86
+ cluster_volume_spec: None,
82
87
  };
83
88
 
84
89
  // create_volume is idempotent - returns existing volume if it exists
@@ -116,7 +121,7 @@ pub async fn remove_volume(client: &DockerClient, name: &str) -> Result<(), Dock
116
121
 
117
122
  client
118
123
  .inner()
119
- .remove_volume(name, None)
124
+ .remove_volume(name, None::<RemoveVolumeOptions>)
120
125
  .await
121
126
  .map_err(|e| DockerError::Volume(format!("Failed to remove volume {name}: {e}")))?;
122
127
 
@@ -38,7 +38,7 @@ pub struct HostConfig {
38
38
  }
39
39
 
40
40
  fn default_user() -> String {
41
- whoami::username()
41
+ whoami::username().unwrap_or_else(|_| "user".to_string())
42
42
  }
43
43
 
44
44
  impl Default for HostConfig {