Sign inSign up

sbx/opencode-kit:latest

Manifest digest

sha256:3760da77446a65866790164077b29191fd7dc175612ca48ef697b9463de44a2b

Last pushed

about 22 hours by sbx

Type

Sandbox Kit

Manifest digest

sha256:3760da77446a65866790164077b29191fd7dc175612ca48ef697b9463de44a2b

yaml
schemaVersion: "2"
kind: sandbox
name: opencode
version: 1.0.0
displayName: OpenCode
description: OpenCode, the open-source terminal coding agent, with every provider credential resolved by the sandbox proxy.
sandbox:
    image: docker.io/sbx/opencode-image:latest
    entrypoint:
        - opencode
agentInstructions:
    filename: AGENTS.md
permissions:
    network:
        allow:
            - api.anthropic.com
            - claude.ai
            - console.anthropic.com
            - api.business.githubcopilot.com
            - api.enterprise.githubcopilot.com
            - api.github.com
            - api.githubcopilot.com
            - api.individual.githubcopilot.com
            - copilot.github.com
            - github.com
            - raw.githubusercontent.com
            - aiplatform.googleapis.com
            - generativelanguage.googleapis.com
            - oauth2.googleapis.com
            - vertexai.googleapis.com
            - api.groq.com
            - api.openai.com
            - openai.com
            - openrouter.ai
            - api.x.ai
            - auth.openai.com
            - chatgpt.com
            - '**.githubusercontent.com'
            - codeload.github.com
            - registry.npmjs.org
            - opencode.ai
            - '*.opencode.ai'
            - archive.ubuntu.com
            - security.ubuntu.com
            - ports.ubuntu.com
            - download.docker.com
credentials:
    - service: anthropic
      apiKey:
        name: ANTHROPIC_API_KEY
        proxyManaged: true
        inject:
            - domain: api.anthropic.com
              header: x-api-key
              format: '%s'
            - domain: claude.ai
              header: x-api-key
              format: '%s'
            - domain: console.anthropic.com
              header: x-api-key
              format: '%s'
    - service: github
      apiKey:
        name: ""
        inject:
            - domain: api.business.githubcopilot.com
              header: Authorization
              format: Bearer %s
            - domain: api.enterprise.githubcopilot.com
              header: Authorization
              format: Bearer %s
            - domain: api.github.com
              header: Authorization
              format: Bearer %s
            - domain: api.githubcopilot.com
              header: Authorization
              format: Bearer %s
            - domain: api.individual.githubcopilot.com
              header: Authorization
              format: Bearer %s
            - domain: copilot.github.com
              header: Authorization
              format: Bearer %s
            - domain: github.com
              header: Authorization
              format: Bearer %s
            - domain: raw.githubusercontent.com
              header: Authorization
              format: Bearer %s
    - service: google
      apiKey:
        name: GOOGLE_GENERATIVE_AI_API_KEY
        proxyManaged: true
        inject:
            - domain: aiplatform.googleapis.com
              header: x-goog-api-key
              format: '%s'
            - domain: generativelanguage.googleapis.com
              header: x-goog-api-key
              format: '%s'
            - domain: oauth2.googleapis.com
              header: x-goog-api-key
              format: '%s'
            - domain: vertexai.googleapis.com
              header: x-goog-api-key
              format: '%s'
    - service: groq
      apiKey:
        name: GROQ_API_KEY
        proxyManaged: true
        inject:
            - domain: api.groq.com
              header: Authorization
              format: Bearer %s
    - service: openai
      apiKey:
        name: OPENAI_API_KEY
        proxyManaged: true
        inject:
            - domain: api.openai.com
              header: Authorization
              format: Bearer %s
            - domain: openai.com
              header: Authorization
              format: Bearer %s
      oauth:
        tokenEndpoint:
            host: auth.openai.com
            path: /oauth/token
        resourceHosts:
            - chatgpt.com
        sentinels:
            accessToken: oai-oat01-proxy-managed
            refreshToken: oai-ort01-proxy-managed
        credentialFile:
            path: ~/.local/share/opencode/auth.json
            structure:
                openai:
                    access: '{{.AccessToken}}'
                    expires: '{{.ExpiresAt}}'
                    refresh: '{{.RefreshToken}}'
                    type: oauth
        skipIfEnv:
            - OPENAI_API_KEY
    - service: openrouter
      apiKey:
        name: OPENROUTER_API_KEY
        proxyManaged: true
        inject:
            - domain: openrouter.ai
              header: Authorization
              format: Bearer %s
    - service: xai
      apiKey:
        name: XAI_API_KEY
        proxyManaged: true
        inject:
            - domain: api.x.ai
              header: Authorization
              format: Bearer %s
setup:
    startup:
        - command:
            - sh
            - -c
            - '{ command -v apt-get && apt-get update -qq -y || true; } >/dev/null 2>&1 </dev/null &'
          user: root
          description: Update apt package cache in background
        - command:
            - sh
            - -c
            - |
              set -e
              [ -n "$MCP_GATEWAY_URL" ] || exit 0
              cfg="$HOME/.config/opencode/opencode.json"
              if ! command -v jq >/dev/null 2>&1; then
                echo "jq is not installed; MCP gateway not registered in opencode.json" >&2
                exit 0
              fi
              gateway=$(cat <<EOF
              {
                "type": "remote",
                "url": "$MCP_GATEWAY_URL",
                "enabled": true,
                "headers": {
                  "Authorization": "Bearer $MCP_SENTINEL_TOKEN_NAME"
                }
              }
              EOF
              )
              if ! jq -n --argjson gateway "$gateway" 'empty'; then
                echo "MCP_GATEWAY_URL or MCP_SENTINEL_TOKEN_NAME does not fit in a JSON string; MCP gateway not registered" >&2
                exit 0
              fi
              # A symlink is resolved so the merge writes its target and the link stays.
              # mv would nest the config inside a directory and still exit 0, so only a
              # regular file (or no file at all) is merged into.
              real=$cfg
              if [ -L "$cfg" ]; then
                real=$(readlink -f "$cfg") || real=
                if [ -z "$real" ] || [ ! -f "$real" ]; then
                  echo "opencode.json is a symlink to a missing or non-regular file; MCP gateway not registered" >&2
                  exit 0
                fi
              elif [ -e "$cfg" ] && [ ! -f "$cfg" ]; then
                echo "opencode.json is not a regular file; MCP gateway not registered" >&2
                exit 0
              fi
              # jq -s reads an empty or blank file as no values, which becomes {}; a
              # missing file is read from /dev/null for the same result. So the mv
              # below is the only write to the config path.
              src=$real
              [ -e "$real" ] || src=/dev/null
              if ! merged=$(jq -s --argjson gateway "$gateway" '
                  (if length == 0 then {}
                   elif length == 1 and (.[0] | type) == "object" then .[0]
                   else error("not a single JSON object") end)
                  | ."$schema" //= "https://opencode.ai/config.json"
                  | .mcp["mcp-gateway"] = $gateway' "$src"); then
                echo "opencode.json is not a single JSON object with an object or null mcp; MCP gateway not registered" >&2
                exit 0
              fi
              # The temp name is unpredictable, so a stale or crashed leftover is never installed.
              if ! mkdir -p "$(dirname "$real")" || ! tmp=$(mktemp "$real.XXXXXX"); then
                echo "cannot create a temp file beside opencode.json; MCP gateway not registered" >&2
                exit 0
              fi
              trap 'rm -f "$tmp"' EXIT
              if ! printf '%s\n' "$merged" > "$tmp" || ! mv -f "$tmp" "$real"; then
                echo "cannot write opencode.json; MCP gateway not registered" >&2
                exit 0
              fi
          user: agent
          description: Register the sandbox MCP gateway in ~/.config/opencode/opencode.json
        - command:
            - sh
            - -c
            - |
              set -e
              umask 077
              # Local sandboxes always carry GH_TOKEN and say via the mode var whether a
              # credential backs it; cloud sets GH_TOKEN only when one does and no mode var.
              [ -n "${GH_TOKEN:-}" ] || exit 0
              [ "${SBX_CRED_GITHUB_MODE:-}" != none ] || exit 0
              auth="$HOME/.local/share/opencode/auth.json"
              mkdir -p "$(dirname "$auth")"
              # A canceled start can leave its seed running; retries must recheck auth.json after it finishes.
              exec 9>>"$(dirname "$auth")/.copilot-seed.lock"
              flock 9
              # mv would nest the seed inside a directory here and still exit 0.
              if [ -e "$auth" ] && [ ! -f "$auth" ]; then
                echo "auth.json is not a regular file; leaving the GitHub Copilot provider unseeded" >&2
                exit 0
              fi
              # Every start re-fires this seed. One pass keeps an entry that fits OpenCode's Oauth schema and still
              # carries the token as refresh (so an exchanged access survives), leaves a malformed file alone, and re-seeds the rest.
              if ! merged=$({ if [ -s "$auth" ]; then cat "$auth"; else echo '{}'; fi; } | jq -s --arg token "$GH_TOKEN" '
                  if length != 1 or (.[0] | type) != "object" then error("auth.json must hold exactly one JSON object")
                  elif (.[0]."github-copilot" | type == "object" and .type == "oauth" and .refresh == $token
                        and (.access | type) == "string" and (.expires | type == "number" and . >= 0 and . == floor)
                        and ((has("accountId") | not) or (.accountId | type) == "string")
                        and ((has("enterpriseUrl") | not) or (.enterpriseUrl | type) == "string")) then empty
                  else .[0] | ."github-copilot" = {type: "oauth", refresh: $token, access: $token, expires: 0} end'); then
                echo "auth.json is not a single JSON object; leaving the GitHub Copilot provider unseeded" >&2
                exit 0
              fi
              [ -n "$merged" ] || exit 0
              # The temp name is unpredictable, so a stale or crashed leftover is never installed. The
              # same uid owns this directory, so no write pattern here is a boundary against a co-resident process.
              tmp=$(mktemp "$(dirname "$auth")/auth.json.XXXXXX")
              trap 'rm -f "$tmp"' EXIT
              printf '%s\n' "$merged" > "$tmp"
              mv -f "$tmp" "$auth"
          user: agent
          description: Seed OpenCode's GitHub Copilot provider from the GH_TOKEN sentinel
        - command:
            - sh
            - -c
            - |
              set -e
              auth="$HOME/.local/share/opencode/auth.json"
              mkdir -p "$(dirname "$auth")"
              exec 9>>"$(dirname "$auth")/.credentials-sync.lock"
              flock 9
              if ! existing=$(opencode auth export --standalone); then
                echo "unable to read OpenCode v2 credentials; leaving them unchanged" >&2
                exit 0
              fi
              if [ -e "$auth" ] && [ ! -f "$auth" ]; then
                echo "auth.json is not a regular file; leaving v2 credentials unchanged" >&2
                exit 0
              fi
              if [ -f "$auth" ]; then
                if ! credentials=$(jq -c --arg github_token "${GH_TOKEN:-}" '
                def oauth($integration; $id; $method; $label; $access; $refresh):
                  .[$integration] as $credential |
                  if ($credential | type) == "object"
                    and ($credential.type == "oauth")
                    and ($access != "")
                    and ($credential.access == $access)
                    and ($credential.refresh == $refresh)
                    and (($credential.refresh | type) == "string")
                    and (($credential.access | type) == "string")
                    and ($credential.expires | type == "number" and . >= 0 and . == floor)
                  then {
                    id: $id, integrationID: $integration, label: $label, active: true,
                    value: ({type: "oauth", methodID: $method, refresh: $credential.refresh, access: $credential.access, expires: $credential.expires}
                      + (if (($credential.accountId | type) == "string") or (($credential.enterpriseUrl | type) == "string")
                         then {metadata: ({} + (if ($credential.accountId | type) == "string" then {accountID: $credential.accountId} else {} end)
                                               + (if ($credential.enterpriseUrl | type) == "string" then {enterpriseUrl: $credential.enterpriseUrl} else {} end))}
                         else {} end))
                  else empty end;
                [oauth("openai"; "sbx-openai"; "chatgpt-browser"; "Sandbox OpenAI"; "oai-oat01-proxy-managed"; "oai-ort01-proxy-managed"), oauth("github-copilot"; "sbx-github-copilot"; "device"; "Sandbox GitHub Copilot"; $github_token; $github_token)]' "$auth"); then
                  echo "auth.json is not valid OpenCode OAuth data; leaving v2 credentials unchanged" >&2
                  exit 0
                fi
              else
                credentials='[]'
              fi
              if ! sync=$(jq -cn --argjson existing "$existing" --argjson desired "$credentials" '
                def managed($wanted):
                  .id == $wanted.id
                  or ($wanted.desired != null and .label == "OAuth" and .integrationID == $wanted.integrationID and .value == $wanted.desired.value);
                ["openai", "github-copilot"] | map(
                  . as $integration |
                  {integrationID: $integration, id: ("sbx-" + $integration),
                   desired: ([$desired[] | select(.integrationID == $integration)][0])}
                  | . as $wanted
                   | ($existing | map(select(.integrationID == $wanted.integrationID))) as $records
                   | ($records | map(select(.active == true)) | .[0]) as $active
                   | {integrationID: $wanted.integrationID, id: $wanted.id, desired: $wanted.desired,
                      remove: [$records[] | select(managed($wanted)) | .id],
                     previousActiveID: (if $active != null and ($active | managed($wanted)) then $active.id else null end),
                     activate: ($wanted.desired != null and $active != null and ($active | managed($wanted)))}
                )'); then
                echo "unable to reconcile OpenCode v2 credentials; leaving them unchanged" >&2
                exit 0
              fi
              if ! removed=$(jq -cn --argjson existing "$existing" --argjson sync "$sync" '
                [$sync[] as $change | $existing[] |
                  select(.integrationID == $change.integrationID and (.id as $id | $change.remove | index($id)))]'); then
                echo "unable to save managed OpenCode credentials for rollback" >&2
                exit 0
              fi
              printf '%s\n' "$sync" | jq -r '.[] | .integrationID as $integration | .remove[]? | [$integration, .] | @tsv' |
                while IFS="$(printf '\t')" read -r integration id; do
                  opencode auth logout "$integration" "$id" --standalone >/dev/null 2>&1 || true
                done
              if [ "$credentials" != "[]" ] && ! printf '%s\n' "$credentials" | opencode auth import --standalone >/dev/null; then
                echo "unable to import sandbox OAuth credentials into OpenCode v2" >&2
                if [ "$removed" != "[]" ] && ! printf '%s\n' "$removed" | opencode auth import --standalone >/dev/null; then
                  echo "unable to restore managed OpenCode credentials after failed import" >&2
                  exit 1
                fi
                if ! printf '%s\n' "$sync" | jq -r '.[] | select(.previousActiveID != null) | [.integrationID, .previousActiveID] | @tsv' |
                  while IFS="$(printf '\t')" read -r integration id; do
                    opencode auth switch "$integration" "$id" --standalone >/dev/null
                  done; then
                  echo "unable to restore the managed OpenCode credential selection after failed import" >&2
                  exit 1
                fi
                echo "restored managed OpenCode credentials after failed import" >&2
                exit 0
              fi
              printf '%s\n' "$sync" | jq -r '.[] | select(.activate) | [.integrationID, .id] | @tsv' |
                while IFS="$(printf '\t')" read -r integration id; do
                  opencode auth switch "$integration" "$id" --standalone >/dev/null
                done
          user: agent
          description: Import sandbox OAuth credentials into OpenCode v2