@opencode-cloud/core
Advanced tools
| //! Docker registry API helpers. | ||
| //! | ||
| //! Provides lightweight helpers for querying registry manifests and configs | ||
| //! without pulling images. | ||
| use super::DockerError; | ||
| use reqwest::header::ACCEPT; | ||
| use serde::Deserialize; | ||
| use std::collections::HashMap; | ||
| #[derive(Deserialize)] | ||
| struct TokenResponse { | ||
| token: Option<String>, | ||
| access_token: Option<String>, | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct ManifestConfig { | ||
| digest: String, | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct Manifest { | ||
| config: ManifestConfig, | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct ManifestList { | ||
| manifests: Vec<ManifestDescriptor>, | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct ManifestDescriptor { | ||
| digest: String, | ||
| platform: Option<ManifestPlatform>, | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct ManifestPlatform { | ||
| architecture: Option<String>, | ||
| os: Option<String>, | ||
| } | ||
| #[derive(Deserialize)] | ||
| #[serde(untagged)] | ||
| enum ManifestResponse { | ||
| Single(Manifest), | ||
| List(ManifestList), | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct ImageConfig { | ||
| config: Option<ImageConfigDetails>, | ||
| } | ||
| #[derive(Deserialize)] | ||
| struct ImageConfigDetails { | ||
| #[serde(rename = "Labels")] | ||
| labels: Option<HashMap<String, String>>, | ||
| } | ||
| pub async fn fetch_registry_version( | ||
| registry_base: &str, | ||
| token_url: &str, | ||
| repo: &str, | ||
| tag: &str, | ||
| maybe_manifest_digest: Option<&str>, | ||
| label_key: &str, | ||
| ) -> Result<Option<String>, DockerError> { | ||
| let client = reqwest::Client::new(); | ||
| let token = fetch_registry_token(&client, token_url).await?; | ||
| let manifest = if let Some(digest) = maybe_manifest_digest { | ||
| match fetch_registry_manifest(&client, registry_base, repo, digest, &token).await { | ||
| Ok(manifest) => manifest, | ||
| Err(digest_err) => fetch_registry_manifest(&client, registry_base, repo, tag, &token) | ||
| .await | ||
| .map_err(|tag_err| { | ||
| DockerError::Connection(format!( | ||
| "Failed to fetch registry manifest. Digest error: {digest_err}. Tag error: {tag_err}" | ||
| )) | ||
| })?, | ||
| } | ||
| } else { | ||
| fetch_registry_manifest(&client, registry_base, repo, tag, &token).await? | ||
| }; | ||
| let image_config = fetch_registry_image_config( | ||
| &client, | ||
| registry_base, | ||
| repo, | ||
| &manifest.config.digest, | ||
| &token, | ||
| ) | ||
| .await?; | ||
| let maybe_version = image_config | ||
| .config | ||
| .and_then(|details| details.labels) | ||
| .and_then(|labels| labels.get(label_key).cloned()); | ||
| Ok(maybe_version) | ||
| } | ||
| async fn fetch_registry_token( | ||
| client: &reqwest::Client, | ||
| token_url: &str, | ||
| ) -> Result<String, DockerError> { | ||
| let response = client | ||
| .get(token_url) | ||
| .send() | ||
| .await | ||
| .map_err(|e| DockerError::Connection(format!("Failed to fetch registry token: {e}")))?; | ||
| let token_response: TokenResponse = response | ||
| .json() | ||
| .await | ||
| .map_err(|e| DockerError::Connection(format!("Failed to decode registry token: {e}")))?; | ||
| let token = token_response | ||
| .token | ||
| .or(token_response.access_token) | ||
| .ok_or_else(|| DockerError::Connection("Registry token missing".to_string()))?; | ||
| Ok(token) | ||
| } | ||
| async fn fetch_registry_manifest( | ||
| client: &reqwest::Client, | ||
| registry_base: &str, | ||
| repo: &str, | ||
| reference: &str, | ||
| token: &str, | ||
| ) -> Result<Manifest, DockerError> { | ||
| let mut current_reference = reference.to_string(); | ||
| loop { | ||
| let manifest_url = format!("{registry_base}/v2/{repo}/manifests/{current_reference}"); | ||
| let response = client | ||
| .get(&manifest_url) | ||
| .header( | ||
| ACCEPT, | ||
| "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json", | ||
| ) | ||
| .bearer_auth(token) | ||
| .send() | ||
| .await | ||
| .map_err(|e| { | ||
| DockerError::Connection(format!("Failed to fetch registry manifest: {e}")) | ||
| })?; | ||
| if response.status() == reqwest::StatusCode::NOT_FOUND { | ||
| return Err(DockerError::Connection(format!( | ||
| "Manifest not found for {repo}:{current_reference}" | ||
| ))); | ||
| } | ||
| let manifest_response: ManifestResponse = response | ||
| .json() | ||
| .await | ||
| .map_err(|e| DockerError::Connection(format!("Failed to decode manifest: {e}")))?; | ||
| match manifest_response { | ||
| ManifestResponse::Single(manifest) => return Ok(manifest), | ||
| ManifestResponse::List(list) => { | ||
| let digest = select_manifest_digest(&list).ok_or_else(|| { | ||
| DockerError::Connection(format!( | ||
| "No manifests available for {repo}:{current_reference}" | ||
| )) | ||
| })?; | ||
| current_reference = digest; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| fn select_manifest_digest(list: &ManifestList) -> Option<String> { | ||
| let preferred = list.manifests.iter().find(|manifest| { | ||
| let Some(platform) = &manifest.platform else { | ||
| return false; | ||
| }; | ||
| platform.os.as_deref() == Some("linux") && platform.architecture.as_deref() == Some("amd64") | ||
| }); | ||
| preferred | ||
| .or_else(|| list.manifests.first()) | ||
| .map(|manifest| manifest.digest.clone()) | ||
| } | ||
| async fn fetch_registry_image_config( | ||
| client: &reqwest::Client, | ||
| registry_base: &str, | ||
| repo: &str, | ||
| digest: &str, | ||
| token: &str, | ||
| ) -> Result<ImageConfig, DockerError> { | ||
| let config_url = format!("{registry_base}/v2/{repo}/blobs/{digest}"); | ||
| let response = client | ||
| .get(&config_url) | ||
| .bearer_auth(token) | ||
| .send() | ||
| .await | ||
| .map_err(|e| DockerError::Connection(format!("Failed to fetch image config: {e}")))?; | ||
| if !response.status().is_success() { | ||
| return Err(DockerError::Connection(format!( | ||
| "Failed to fetch image config: HTTP {}", | ||
| response.status() | ||
| ))); | ||
| } | ||
| let config = response | ||
| .json() | ||
| .await | ||
| .map_err(|e| DockerError::Connection(format!("Failed to decode image config: {e}")))?; | ||
| Ok(config) | ||
| } |
+1
-1
| [package] | ||
| name = "opencode-cloud-core" | ||
| version = "10.0.0" | ||
| version = "10.1.0" | ||
| edition = "2024" | ||
@@ -5,0 +5,0 @@ rust-version = "1.88" |
+1
-1
| { | ||
| "name": "@opencode-cloud/core", | ||
| "version": "10.0.0", | ||
| "version": "10.1.0", | ||
| "description": "Core NAPI bindings for opencode-cloud (internal package)", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
+3
-0
@@ -199,2 +199,5 @@ # opencode-cloud | ||
| # Check for updates and choose what to update | ||
| occ update | ||
| # Update the opencode-cloud CLI binary | ||
@@ -201,0 +204,0 @@ occ update cli |
+55
-46
@@ -33,5 +33,5 @@ # ============================================================================= | ||
| # ----------------------------------------------------------------------------- | ||
| # Stage 1: Runtime | ||
| # Stage 1: Base | ||
| # ----------------------------------------------------------------------------- | ||
| FROM ubuntu:24.04 AS runtime | ||
| FROM ubuntu:24.04 AS base | ||
@@ -240,3 +240,8 @@ # OCI Labels for image metadata | ||
| # bun - self-managing installer, trusted to handle versions | ||
| RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \ | ||
| && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun | ||
| ENV PATH="/home/opencode/.bun/bin:${PATH}" | ||
| # uv - self-managing installer, trusted to handle versions (fast Python package manager) | ||
@@ -501,2 +506,7 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh | ||
| # ----------------------------------------------------------------------------- | ||
| # Stage 2: opencode build | ||
| # ----------------------------------------------------------------------------- | ||
| FROM base AS opencode-build | ||
| # ----------------------------------------------------------------------------- | ||
| # opencode Setup (Fork + Broker + Proxy) | ||
@@ -510,7 +520,5 @@ # ----------------------------------------------------------------------------- | ||
| # - opencode-broker build | ||
| # - opencode web build + runtime | ||
| # - PAM configuration + systemd services | ||
| # - opencode config file | ||
| # | ||
| # NOTE: This section switches between opencode and root users as needed. | ||
| # NOTE: This stage uses opencode user + sudo for privileged installs. | ||
| USER opencode | ||
@@ -529,8 +537,5 @@ # ----------------------------------------------------------------------------- | ||
| && git checkout "${OPENCODE_COMMIT}" \ | ||
| && curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \ | ||
| && export PATH="/home/opencode/.bun/bin:${PATH}" \ | ||
| && bun install --frozen-lockfile \ | ||
| && cd packages/opencode \ | ||
| && bun run build-single-ui \ | ||
| && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \ | ||
| && cd /tmp/opencode-repo \ | ||
@@ -544,26 +549,17 @@ && sudo mkdir -p /opt/opencode/bin /opt/opencode/ui \ | ||
| && sudo chmod +x /opt/opencode/bin/opencode \ | ||
| && /opt/opencode/bin/opencode --version | ||
| && cd /tmp/opencode-repo/packages/opencode-broker \ | ||
| && cargo build --release \ | ||
| && sudo mkdir -p /usr/local/bin \ | ||
| && sudo cp target/release/opencode-broker /usr/local/bin/opencode-broker \ | ||
| && sudo chmod 4755 /usr/local/bin/opencode-broker \ | ||
| && rm -rf /tmp/opencode-repo \ | ||
| && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun | ||
| # Add opencode to PATH | ||
| ENV PATH="/opt/opencode/bin:${PATH}" | ||
| # ----------------------------------------------------------------------------- | ||
| # opencode-broker Installation | ||
| # Stage 3: Runtime | ||
| # ----------------------------------------------------------------------------- | ||
| # Build opencode-broker from source (Rust service for PAM authentication) | ||
| # The broker handles PAM authentication and user process spawning | ||
| # NOTE: Requires root privileges for setuid bit (chmod 4755) to allow broker | ||
| # to run with elevated privileges for PAM authentication | ||
| USER root | ||
| RUN cd /tmp/opencode-repo/packages/opencode-broker \ | ||
| && runuser -u opencode -- bash -c '. /home/opencode/.cargo/env && cargo build --release' \ | ||
| && mkdir -p /usr/local/bin \ | ||
| && cp target/release/opencode-broker /usr/local/bin/opencode-broker \ | ||
| && chmod 4755 /usr/local/bin/opencode-broker \ | ||
| && rm -rf /tmp/opencode-repo | ||
| FROM base AS runtime | ||
| # Verify broker binary exists and is executable | ||
| RUN ls -la /usr/local/bin/opencode-broker \ | ||
| && test -x /usr/local/bin/opencode-broker \ | ||
| && echo "Broker installed" | ||
| # Add opencode to PATH | ||
| ENV PATH="/opt/opencode/bin:${PATH}" | ||
@@ -576,2 +572,3 @@ # ----------------------------------------------------------------------------- | ||
| # NOTE: Requires root privileges to write to /etc/pam.d/ | ||
| USER root | ||
| RUN printf '%s\n' \ | ||
@@ -635,15 +632,3 @@ '# PAM configuration for OpenCode authentication' \ | ||
| USER opencode | ||
| # ----------------------------------------------------------------------------- | ||
| # GSD Plugin Installation | ||
| # ----------------------------------------------------------------------------- | ||
| # Install the GSD (Get Shit Done) plugin for opencode | ||
| # Note: If this fails in container builds due to "~" path resolution, retry with | ||
| # OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode set explicitly. | ||
| RUN mkdir -p /home/opencode/.npm \ | ||
| && npx --yes get-shit-done-cc --opencode --global \ | ||
| && rm -rf /home/opencode/.npm/_cacache /home/opencode/.npm/_npx | ||
| # ----------------------------------------------------------------------------- | ||
| # opencode systemd Service (2026-01-22) | ||
@@ -653,3 +638,2 @@ # ----------------------------------------------------------------------------- | ||
| # NOTE: Requires root privileges to write to /etc/systemd/system/ | ||
| USER root | ||
| RUN printf '%s\n' \ | ||
@@ -667,3 +651,3 @@ '[Unit]' \ | ||
| 'RestartSec=5' \ | ||
| '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' \ | ||
| 'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ | ||
| '' \ | ||
@@ -696,4 +680,2 @@ '[Install]' \ | ||
| USER opencode | ||
| # ----------------------------------------------------------------------------- | ||
@@ -705,3 +687,2 @@ # Entrypoint Script (Hybrid Init Support) | ||
| # Note: Entrypoint runs as root to support both modes; tini mode drops to opencode user | ||
| USER root | ||
| RUN printf '%s\n' \ | ||
@@ -732,2 +713,30 @@ '#!/bin/bash' \ | ||
| # ----------------------------------------------------------------------------- | ||
| # opencode Artifacts | ||
| # ----------------------------------------------------------------------------- | ||
| COPY --from=opencode-build /opt/opencode /opt/opencode | ||
| COPY --from=opencode-build /usr/local/bin/opencode-broker /usr/local/bin/opencode-broker | ||
| RUN chown -R opencode:opencode /opt/opencode \ | ||
| && chmod +x /opt/opencode/bin/opencode \ | ||
| && chmod 4755 /usr/local/bin/opencode-broker | ||
| # Verify broker binary exists and is executable | ||
| RUN ls -la /usr/local/bin/opencode-broker \ | ||
| && test -x /usr/local/bin/opencode-broker \ | ||
| && echo "Broker installed" | ||
| USER opencode | ||
| RUN /opt/opencode/bin/opencode --version | ||
| # ----------------------------------------------------------------------------- | ||
| # GSD Plugin Installation | ||
| # ----------------------------------------------------------------------------- | ||
| # Install the GSD (Get Shit Done) plugin for opencode | ||
| # Note: If this fails in container builds due to "~" path resolution, retry with | ||
| # OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode set explicitly. | ||
| RUN mkdir -p /home/opencode/.npm \ | ||
| && npx --yes get-shit-done-cc --opencode --global \ | ||
| && rm -rf /home/opencode/.npm/_cacache /home/opencode/.npm/_npx | ||
| # ----------------------------------------------------------------------------- | ||
| # Version File | ||
@@ -734,0 +743,0 @@ # ----------------------------------------------------------------------------- |
@@ -24,2 +24,3 @@ //! Docker operations module | ||
| pub mod progress; | ||
| mod registry; | ||
| pub mod state; | ||
@@ -51,3 +52,6 @@ pub mod update; | ||
| // Version detection | ||
| pub use version::{VERSION_LABEL, get_cli_version, get_image_version, versions_compatible}; | ||
| pub use version::{ | ||
| VERSION_LABEL, get_cli_version, get_image_version, get_registry_latest_version, | ||
| versions_compatible, | ||
| }; | ||
@@ -54,0 +58,0 @@ // Container exec operations |
@@ -5,3 +5,4 @@ //! Docker image version detection | ||
| use super::{DockerClient, DockerError}; | ||
| use super::registry::fetch_registry_version; | ||
| use super::{DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT}; | ||
@@ -42,2 +43,60 @@ /// Version label key in Docker image | ||
| pub async fn get_registry_latest_version( | ||
| client: &DockerClient, | ||
| ) -> Result<Option<String>, DockerError> { | ||
| match fetch_ghcr_registry_version(client).await { | ||
| Ok(version) => Ok(version), | ||
| Err(ghcr_err) => fetch_dockerhub_registry_version(client).await.map_err(|dockerhub_err| { | ||
| DockerError::Connection(format!( | ||
| "Failed to fetch registry version. GHCR: {ghcr_err}. Docker Hub: {dockerhub_err}" | ||
| )) | ||
| }), | ||
| } | ||
| } | ||
| async fn fetch_ghcr_registry_version(client: &DockerClient) -> Result<Option<String>, DockerError> { | ||
| let repo = IMAGE_NAME_GHCR | ||
| .strip_prefix("ghcr.io/") | ||
| .unwrap_or(IMAGE_NAME_GHCR); | ||
| let reference = format!("{IMAGE_NAME_GHCR}:{IMAGE_TAG_DEFAULT}"); | ||
| let digest = fetch_registry_digest(client, &reference).await; | ||
| fetch_registry_version( | ||
| "https://ghcr.io", | ||
| &format!("https://ghcr.io/token?scope=repository:{repo}:pull"), | ||
| repo, | ||
| IMAGE_TAG_DEFAULT, | ||
| digest.as_deref(), | ||
| VERSION_LABEL, | ||
| ) | ||
| .await | ||
| } | ||
| async fn fetch_dockerhub_registry_version( | ||
| client: &DockerClient, | ||
| ) -> Result<Option<String>, DockerError> { | ||
| let repo = IMAGE_NAME_DOCKERHUB; | ||
| let reference = format!("{IMAGE_NAME_DOCKERHUB}:{IMAGE_TAG_DEFAULT}"); | ||
| let digest = fetch_registry_digest(client, &reference).await; | ||
| fetch_registry_version( | ||
| "https://registry-1.docker.io", | ||
| &format!( | ||
| "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{repo}:pull" | ||
| ), | ||
| repo, | ||
| IMAGE_TAG_DEFAULT, | ||
| digest.as_deref(), | ||
| VERSION_LABEL, | ||
| ) | ||
| .await | ||
| } | ||
| async fn fetch_registry_digest(client: &DockerClient, reference: &str) -> Option<String> { | ||
| client | ||
| .inner() | ||
| .inspect_registry_image(reference, None) | ||
| .await | ||
| .ok() | ||
| .and_then(|info| info.descriptor.digest) | ||
| } | ||
| /// CLI version from Cargo.toml | ||
@@ -44,0 +103,0 @@ pub fn get_cli_version() -> &'static str { |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
335981
2.66%44
2.33%380
0.8%