diff --git a/.agents/skills/create-runtime/SKILL.md b/.agents/skills/create-runtime/SKILL.md new file mode 100644 index 00000000..b0f566bf --- /dev/null +++ b/.agents/skills/create-runtime/SKILL.md @@ -0,0 +1,80 @@ +--- +name: create-runtime +description: Use when a user asks to add a new DevBox runtime image for an operating system, language, or framework, including its base image, project template, smoke test, and conformance validation. +--- + +# Create a DevBox Runtime + +Create a complete runtime contribution, not just an empty directory. The result must be explainable from the repository's existing conventions and verified by the repository's own checks. + +## Required input + +Obtain these three values: + +- `type`: `operating-system`, `language`, or `framework` +- `name`: the runtime directory name +- `version`: the runtime directory version + +If any value is missing, ask for it before touching files. Run the input validator before creating a target: + +```bash +python3 .agents/skills/create-runtime/scripts/validate-runtime-input.py \ + --repo-root . \ + --type \ + --name \ + --version +``` + +Do not continue if the validator fails. It protects against malformed names, path traversal, and overwriting an existing runtime. + +## Workflow + +1. Read [references/common-runtime-contract.md](references/common-runtime-contract.md). +2. Read exactly one type guide: + - OS: [references/operating-systems.md](references/operating-systems.md) + - language: [references/languages.md](references/languages.md) + - framework: [references/frameworks.md](references/frameworks.md) +3. Inspect at least one nearby runtime of the same type. Prefer the closest runtime by base image, process model, and template shape; do not copy its version-specific facts. +4. Establish the facts that determine the implementation: + - official source and exact version; + - supported architectures; + - required base image and its existing image/version convention; + - installed tools and environment variables; + - process, entrypoint, port, and health behavior; + - project template commands and expected user-facing documentation. +5. If a required fact is unavailable or ambiguous, stop and ask a focused question. Do not guess. +6. Create the base image only when the new runtime needs one that is not already available. Create the runtime image, project template, localized documentation, and smoke test required by the selected type guide. +7. Register the exact runtime path in `tests/runtime-conformance/run.sh` and document its runtime-specific checks in `tests/runtime-conformance/README.md`; the conformance runner intentionally rejects unregistered paths. +8. Keep changes scoped to the new runtime. Do not modify existing runtimes, CI workflows, or shared tooling unless the new runtime requires a separately justified shared change. +9. Run the checks in the common contract and the selected type guide. Report every check that was skipped and why. + +## No fallback without confirmation + +Never silently: + +- replace an exact version with `latest`; +- use an unverified mirror, package source, or base image; +- reduce architecture coverage because the current machine differs; +- omit a project template, localized README, or smoke test; +- replace a failed build or test with a weaker check; +- copy a neighboring runtime's behavior when its assumptions do not apply. + +If the only way forward is a fallback, describe the trigger, the proposed behavior, and its impact, then wait for explicit user confirmation. + +## Completion criteria + +Before reporting completion, show evidence for: + +- the validator accepted the target; +- all required files exist and executable scripts have the right mode; +- Dockerfiles and shell scripts pass available static checks; +- runtime conformance passes for the new target; +- the runtime path is registered in the conformance runner with assertions appropriate to its type; +- the matching smoke test passes, or its environmental blocker is reported precisely; +- no unrelated files were changed. + +The skill creates repository files only. It does not publish images, push branches, or open pull requests unless the user separately asks for those actions. + +## Example + +For a new Python runtime, use `type=language`, `name=python`, and the requested exact `version`; inspect an existing Python runtime, add the matching base/runtime/template/smoke files, then run the validator and the repository's Python runtime checks. Do not derive installation details from the example's version. diff --git a/.agents/skills/create-runtime/references/common-runtime-contract.md b/.agents/skills/create-runtime/references/common-runtime-contract.md new file mode 100644 index 00000000..a7bc22aa --- /dev/null +++ b/.agents/skills/create-runtime/references/common-runtime-contract.md @@ -0,0 +1,68 @@ +# Common Runtime Contract + +Use this guide for every new runtime. + +## Repository layout + +- `tooling/`: shared build tools and scripts. +- `base-images/`: reusable base image definitions. +- `runtime-images/`: final images exposed as DevBox runtimes. +- `tests/runtime-smoke/`: smoke tests aligned with runtime image paths. +- `tests/runtime-conformance/`: repository-wide runtime checks. +- `docs/`: contributor and runtime behavior documentation. + +A runtime Dockerfile belongs at: + +```text +runtime-images////Dockerfile +``` + +where `` is exactly `operating-systems`, `languages`, or `frameworks`. + +## Inspect before editing + +Use an existing runtime of the same kind as a structural reference. Check its: + +- Dockerfile ARG and image naming conventions; +- `build.sh` ownership, locale, template, and dependency behavior; +- project-template files and localized README names; +- entrypoint protocol and default working directory; +- matching smoke test assertions; +- base image dependency, if any. + +Copy structure only. Re-derive source URLs, checksums, versions, commands, ports, and package lists for the new runtime. + +## Files and scripts + +- Preserve executable mode for `build.sh` and `smoke.sh` when neighboring runtimes do. The conformance runner requires `project-template/entrypoint.sh` to exist and invokes it with `bash`; source executable mode is not itself a conformance requirement. +- Keep shell scripts strict (`set -euo pipefail` where compatible) and quote variables. +- Keep user-facing documentation in both `README.en_US.md` and `README.zh_CN.md` when the runtime has a project template. +- Make the template's documented commands match the actual entrypoint and build behavior. +- Do not add placeholder files to satisfy a path check. + +## Verification + +Use the repository's planner to verify that the target is discoverable and to inspect its CI matrix: + +```bash +python3 .github/scripts/runtime-conformance.py plan \ + --tag \ + --kind \ + --name / \ + --l10n both \ + --arch both +``` + +The planner does **not** execute conformance. Before claiming conformance support, add the new runtime's exact relative path to the appropriate `case` branch in `tests/runtime-conformance/run.sh`, implement the runtime-specific assertions there, and update `tests/runtime-conformance/README.md`. The runner fails unregistered runtimes intentionally. + +The supported published-image execution path is the `runtime-image-conformance.yaml` workflow: it pulls each already-published architecture/l10n image, mounts the repository at `/repo`, and runs. It does not build a new image. + +```bash +bash /repo/tests/runtime-conformance/run.sh +``` + +For a not-yet-published runtime, build the image locally first, then run that same command inside the built runtime image with `RUNTIME_PATH`, `RUNTIME_DOCKERFILE`, `RUNTIME_IMAGE`, `L10N`, `CONFORMANCE_ARCH`, and `REPO_ROOT=/repo` set as the workflow does. The matching `tests/runtime-smoke////smoke.sh` is also required by the smoke workflow and must be executed in the image as `devbox`. If Docker, registry access, or a required tool is unavailable, report the exact blocker; do not weaken the check silently. + +## Scope boundary + +Do not change CI workflows, shared tooling, or existing runtime files to hide a new runtime's failure. If a shared fix is genuinely required, stop and present that separate change explicitly. diff --git a/.agents/skills/create-runtime/references/frameworks.md b/.agents/skills/create-runtime/references/frameworks.md new file mode 100644 index 00000000..acf2b7b2 --- /dev/null +++ b/.agents/skills/create-runtime/references/frameworks.md @@ -0,0 +1,33 @@ +# Framework Runtime Guide + +Use for `type=framework`. + +## Structure + +Create the matching framework paths: + +```text +base-images/frameworks/// # only when a new base is needed +runtime-images/frameworks/// +tests/runtime-smoke/frameworks///smoke.sh +``` + +Framework runtimes commonly consume an existing language runtime. The standard contract requires `project-template/README.en_US.md`, `project-template/README.zh_CN.md`, and a `project-template/entrypoint.sh` unless the framework is an explicitly registered sandbox exception. Reuse an existing language image only after confirming the exact image name, tag, architecture support, and build contract. + +## Required decisions + +Determine and document: + +- the framework's exact version and supported language/runtime versions; +- the package manager and lockfile strategy; +- dependency installation and production build commands; +- default port and bind address; +- production entrypoint and process lifetime; +- environment variables required by the framework; +- whether the project template is a minimal runnable service or a static/configuration template. + +Do not copy a neighboring framework's dependency or startup command unless the framework's own documentation and template support it. + +## Verification focus + +The smoke test should install or build the minimal template as the image does, start the documented production process, and verify the expected service behavior. Check that the runtime image references a real language/base image and that its version convention matches the repository's image naming rules. diff --git a/.agents/skills/create-runtime/references/languages.md b/.agents/skills/create-runtime/references/languages.md new file mode 100644 index 00000000..e217b887 --- /dev/null +++ b/.agents/skills/create-runtime/references/languages.md @@ -0,0 +1,32 @@ +# Language Runtime Guide + +Use for `type=language`. + +## Structure + +Create the matching language paths: + +```text +base-images/languages/// # only when a new base is needed +runtime-images/languages/// +tests/runtime-smoke/languages///smoke.sh +``` + +For the standard runtime contract used by this repository, include `project-template/README.en_US.md`, `project-template/README.zh_CN.md`, and a `project-template/entrypoint.sh` unless the runtime is an explicitly documented sandbox exception. The entrypoint or startup file must match the language's actual execution model. + +## Required decisions + +Determine and document: + +- the exact compiler/interpreter distribution and version; +- official download or package source and verification data; +- architecture support; +- required compiler, package manager, runtime, and environment variables; +- whether the template compiles before running, installs dependencies, or runs directly; +- the process port and how the smoke test observes it. + +Do not use an interpreted-language startup command for a compiled language, or infer package-manager behavior from another language. + +## Verification focus + +The smoke test should verify the exact toolchain version or a stable version constraint, compile/install the minimal template when required, start the documented service, and check the expected response or process behavior. Keep the test deterministic and aligned with the README. diff --git a/.agents/skills/create-runtime/references/operating-systems.md b/.agents/skills/create-runtime/references/operating-systems.md new file mode 100644 index 00000000..90c07ec2 --- /dev/null +++ b/.agents/skills/create-runtime/references/operating-systems.md @@ -0,0 +1,31 @@ +# Operating System Runtime Guide + +Use for `type=operating-system`. + +## Structure + +Inspect a nearby OS runtime and create the corresponding layers as required: + +```text +base-images/operating-systems/// +runtime-images/operating-systems/// +tests/runtime-smoke/operating-systems///smoke.sh +``` + +The final runtime must include `project-template/README.en_US.md`, `project-template/README.zh_CN.md`, and a `project-template/entrypoint.sh` for the standard non-sandbox contract. Confirm any exception from an explicitly registered conformance branch before creating files. + +## Required decisions + +Determine and document: + +- the official OS image or package source and exact release; +- amd64/arm64 support and any package availability differences; +- default shell, user, home, workdir, locale, timezone, and common utilities; +- SSH or other services required by the repository's OS contract; +- how the project entrypoint is invoked and how it stays alive for the smoke test. + +Do not assume a distribution's package manager, init system, or architecture support from its name. + +## Verification focus + +The smoke test should prove the OS identity, required `devbox` user and project directory, required common commands, template files, and entrypoint behavior. Add only assertions supported by the runtime contract; do not copy language-specific checks into an OS runtime. diff --git a/.agents/skills/create-runtime/scripts/validate-runtime-input.py b/.agents/skills/create-runtime/scripts/validate-runtime-input.py new file mode 100755 index 00000000..a7995c68 --- /dev/null +++ b/.agents/skills/create-runtime/scripts/validate-runtime-input.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Validate create-runtime skill input and resolve its repository targets.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +TYPE_TO_KIND = { + "operating-system": "operating-systems", + "language": "languages", + "framework": "frameworks", +} +COMPONENT_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]*$") + + +def fail(message: str) -> None: + print(f"Error: {message}", file=sys.stderr) + raise SystemExit(1) + + +def parse_component(label: str, value: str) -> str: + component = value.strip() + if not component: + fail(f"{label} cannot be empty") + if component in {".", ".."} or "/" in component or "\\" in component: + fail(f"{label} must be a single path component") + if "\x00" in component or not COMPONENT_PATTERN.fullmatch(component): + fail(f"{label} contains unsupported characters") + return component + + +def ensure_within(root: Path, target: Path) -> None: + try: + target.relative_to(root) + except ValueError: + fail(f"resolved target escapes repository root: {target}") + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Validate create-runtime inputs without creating files" + ) + parser.add_argument("--repo-root", required=True, type=Path) + parser.add_argument("--type", required=True, dest="runtime_type") + parser.add_argument("--name", required=True) + parser.add_argument("--version", required=True) + return parser + + +def main() -> int: + args = build_parser().parse_args() + repo_root = args.repo_root.expanduser().resolve() + if not repo_root.is_dir(): + fail(f"repository root does not exist: {repo_root}") + + try: + kind = TYPE_TO_KIND[args.runtime_type.strip()] + except KeyError: + fail("type must be one of: " + ", ".join(TYPE_TO_KIND.keys())) + + name = parse_component("name", args.name) + version = parse_component("version", args.version) + + runtime_dir = repo_root / "runtime-images" / kind / name / version + base_dir = repo_root / "base-images" / kind / name / version + smoke_test = ( + repo_root + / "tests" + / "runtime-smoke" + / kind + / name + / version + / "smoke.sh" + ) + for target in (runtime_dir, base_dir, smoke_test): + ensure_within(repo_root, target.resolve()) + + if runtime_dir.exists(): + fail(f"runtime target already exists: {runtime_dir.relative_to(repo_root)}") + if smoke_test.exists(): + fail( + "smoke test target already exists: " + f"{smoke_test.relative_to(repo_root)}" + ) + + payload = { + "type": args.runtime_type.strip(), + "kind": kind, + "name": name, + "version": version, + "runtime_path": runtime_dir.relative_to(repo_root).as_posix(), + "base_path": base_dir.relative_to(repo_root).as_posix(), + "smoke_test_path": smoke_test.relative_to(repo_root).as_posix(), + } + print(json.dumps(payload, ensure_ascii=False, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/base-images/frameworks/sandbox/fastgpt/Dockerfile b/base-images/frameworks/sandbox/fastgpt/Dockerfile index 2ba08c68..1ebf04d7 100644 --- a/base-images/frameworks/sandbox/fastgpt/Dockerfile +++ b/base-images/frameworks/sandbox/fastgpt/Dockerfile @@ -1,11 +1,13 @@ # These ARGs can be overridden at build time to customize the image ARG REPO=labring-actions/devbox-base-images ARG REPO_CODEX_GATEWAY=labring/codex-gateway +ARG REPO_FASTGPT_IDE_AGENT=labring/fastgpt-ide-agent ARG REGISTRY=ghcr.io ARG L10N=en_US ARG L10N_NORMALIZED=en-us ARG CODEX_GATEWAY_IMAGE_TAG=latest +ARG FASTGPT_IDE_AGENT_IMAGE_TAG=latest # These ARGs are not recommended to be overridden at build time. # Instead, update the Dockerfile directly for consistent builds, @@ -13,6 +15,7 @@ ARG CODEX_GATEWAY_IMAGE_TAG=latest ARG NODE_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} FROM ${REGISTRY}/${REPO_CODEX_GATEWAY}:${CODEX_GATEWAY_IMAGE_TAG} AS codex-gateway +FROM ${REGISTRY}/${REPO_FASTGPT_IDE_AGENT}:${FASTGPT_IDE_AGENT_IMAGE_TAG} AS fastgpt-ide-agent FROM ${REGISTRY}/${REPO}/node.js-22:${NODE_IMAGE_VERSION} ARG L10N @@ -28,23 +31,23 @@ ENV CODEX_GATEWAY_PORT=1317 ENV CODEX_GATEWAY_MAX_SESSIONS=12 ENV CODEX_GATEWAY_SESSION_TTL_MS=1800000 ENV CODEX_GATEWAY_SESSION_SWEEP_INTERVAL_MS=60000 -ENV CODE_SERVER_ENABLED=false -ENV CODE_SERVER_BIND_ADDR=0.0.0.0:1318 +ENV IDE_AGENT_ENABLED=true +ENV IDE_AGENT_BIND_ADDR=0.0.0.0:1318 COPY --from=codex-gateway /usr/local/bin/codex-gateway /usr/local/bin/codex-gateway +COPY --from=fastgpt-ide-agent /usr/local/bin/fastgpt-ide-agent /usr/local/bin/fastgpt-ide-agent # Add build assets and execute them. -COPY --chown=devbox:devbox settings.json /home/devbox/.local/share/code-server/User/settings.json COPY codex-gateway /tmp/codex-gateway-service -COPY code-server /tmp/code-server-service +COPY fastgpt-ide-agent /tmp/fastgpt-ide-agent COPY build.sh /build.sh RUN chmod +x /build.sh \ /tmp/codex-gateway-service/run \ /tmp/codex-gateway-service/finish \ - /tmp/code-server-service/run \ - /tmp/code-server-service/finish && \ + /tmp/fastgpt-ide-agent/run \ + /tmp/fastgpt-ide-agent/finish && \ /build.sh && \ rm -f /build.sh && \ - rm -rf /tmp/codex-gateway-service /tmp/code-server-service + rm -rf /tmp/codex-gateway-service /tmp/fastgpt-ide-agent EXPOSE 1317 1318 diff --git a/base-images/frameworks/sandbox/fastgpt/build.sh b/base-images/frameworks/sandbox/fastgpt/build.sh index 628c2e8b..fab070a0 100644 --- a/base-images/frameworks/sandbox/fastgpt/build.sh +++ b/base-images/frameworks/sandbox/fastgpt/build.sh @@ -10,7 +10,7 @@ CODEX_GATEWAY_ROOT=${CODEX_GATEWAY_ROOT:-/opt/codex-gateway} CODEX_GATEWAY_CODEX_HOME=${CODEX_GATEWAY_CODEX_HOME:-/codex-home} S6_DIR=/etc/s6-overlay/s6-rc.d CODEX_GATEWAY_SERVICE_SOURCE_DIR=${CODEX_GATEWAY_SERVICE_SOURCE_DIR:-/tmp/codex-gateway-service} -CODE_SERVER_SERVICE_SOURCE_DIR=${CODE_SERVER_SERVICE_SOURCE_DIR:-/tmp/code-server-service} +FASTGPT_IDE_AGENT_SERVICE_SOURCE_DIR=${FASTGPT_IDE_AGENT_SERVICE_SOURCE_DIR:-/tmp/fastgpt-ide-agent} DEVBOX_HOME="$(getent passwd "$DEFAULT_DEVBOX_USER" | cut -d: -f6 || true)" if [ -z "$DEVBOX_HOME" ]; then DEVBOX_HOME="/home/${DEFAULT_DEVBOX_USER}" @@ -55,7 +55,6 @@ wget "https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSIO npm install -g bun@latest npm install -g @openai/codex@latest -curl -fsSL https://code-server.dev/install.sh | sh apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -82,7 +81,6 @@ kubectl version --client helm version --short python3.14 --version rg --version -code-server --version rm -rf "$PROJECT_DIR" @@ -90,7 +88,7 @@ mkdir -p \ "$WORKSPACE_DIR" \ "$CODEX_GATEWAY_CODEX_HOME" \ "$S6_DIR/codex-gateway/dependencies.d" \ - "$S6_DIR/code-server/dependencies.d" + "$S6_DIR/fastgpt-ide-agent/dependencies.d" install -d -m 755 "$CODEX_GATEWAY_ROOT" printf 'longrun\n' >"$S6_DIR/codex-gateway/type" @@ -103,14 +101,14 @@ install -m 700 \ touch "$S6_DIR/codex-gateway/dependencies.d/startup" : >"$S6_DIR/user/contents.d/codex-gateway" -printf 'longrun\n' >"$S6_DIR/code-server/type" +printf 'longrun\n' >"$S6_DIR/fastgpt-ide-agent/type" install -m 700 \ - "$CODE_SERVER_SERVICE_SOURCE_DIR/run" \ - "$S6_DIR/code-server/run" + "$FASTGPT_IDE_AGENT_SERVICE_SOURCE_DIR/run" \ + "$S6_DIR/fastgpt-ide-agent/run" install -m 700 \ - "$CODE_SERVER_SERVICE_SOURCE_DIR/finish" \ - "$S6_DIR/code-server/finish" -touch "$S6_DIR/code-server/dependencies.d/startup" -: >"$S6_DIR/user/contents.d/code-server" + "$FASTGPT_IDE_AGENT_SERVICE_SOURCE_DIR/finish" \ + "$S6_DIR/fastgpt-ide-agent/finish" +touch "$S6_DIR/fastgpt-ide-agent/dependencies.d/startup" +: >"$S6_DIR/user/contents.d/fastgpt-ide-agent" chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$WORKSPACE_DIR" "$CODEX_GATEWAY_CODEX_HOME" diff --git a/base-images/frameworks/sandbox/fastgpt/code-server/finish b/base-images/frameworks/sandbox/fastgpt/code-server/finish deleted file mode 100644 index 765e202c..00000000 --- a/base-images/frameworks/sandbox/fastgpt/code-server/finish +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -EXIT_CODE=$1 - -if [ "$EXIT_CODE" = "101" ]; then - echo "code-server is disabled by environment. Preventing s6 restart." - exit 125 -fi - -if [ "$EXIT_CODE" = "102" ]; then - echo "code-server stopped because DEVBOX_JWT_SECRET is missing. Preventing s6 restart." - exit 125 -fi - -if [ "$EXIT_CODE" = "111" ]; then - echo "code-server stopped because required files were not installed correctly. Preventing s6 restart." - exit 125 -fi - -echo "code-server exited with code $EXIT_CODE, allowing s6 to restart it." -exit 1 diff --git a/base-images/frameworks/sandbox/fastgpt/code-server/run b/base-images/frameworks/sandbox/fastgpt/code-server/run deleted file mode 100644 index 6bf3c73f..00000000 --- a/base-images/frameworks/sandbox/fastgpt/code-server/run +++ /dev/null @@ -1,53 +0,0 @@ -#!/command/with-contenv bash -set -euo pipefail - -DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} -CODE_SERVER_ENABLED_VALUE=${ENABLE_CODE_SERVER:-${CODE_SERVER_ENABLED:-false}} -CODE_SERVER_BIND_ADDR=${CODE_SERVER_BIND_ADDR:-0.0.0.0:1318} -CODE_SERVER_PASSWORD=${DEVBOX_JWT_SECRET:-} - -case "$(printf '%s' "$CODE_SERVER_ENABLED_VALUE" | tr '[:upper:]' '[:lower:]')" in - 1 | true | yes | on | enabled) - ;; - *) - echo "code-server is disabled; set CODE_SERVER_ENABLED=true to start it." - exit 101 - ;; -esac - -if [ -z "$CODE_SERVER_PASSWORD" ]; then - echo "code-server requires DEVBOX_JWT_SECRET to be set as its password." >&2 - exit 102 -fi - -DEVBOX_HOME="$(getent passwd "$DEFAULT_DEVBOX_USER" | cut -d: -f6 || true)" -if [ -z "$DEVBOX_HOME" ]; then - DEVBOX_HOME="/home/${DEFAULT_DEVBOX_USER}" -fi - -WORKSPACE_DIR=${CODE_SERVER_WORKSPACE:-${CODEX_GATEWAY_CWD:-${DEVBOX_HOME}/workspace}} - -export HOME="$DEVBOX_HOME" -export USER="$DEFAULT_DEVBOX_USER" -export LOGNAME="$DEFAULT_DEVBOX_USER" -export PASSWORD="$CODE_SERVER_PASSWORD" - -mkdir -p "$WORKSPACE_DIR" -chown "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$WORKSPACE_DIR" || true - -if ! command -v code-server >/dev/null 2>&1; then - echo "code-server binary not found in PATH" >&2 - exit 111 -fi - -cd "$WORKSPACE_DIR" -exec 2>&1 -exec s6-setuidgid "$DEFAULT_DEVBOX_USER" code-server \ - --disable-telemetry \ - --disable-update-check \ - --disable-workspace-trust \ - --disable-getting-started-override \ - --app-name "Skills" \ - --auth password \ - --bind-addr "$CODE_SERVER_BIND_ADDR" \ - "$WORKSPACE_DIR" diff --git a/base-images/frameworks/sandbox/fastgpt/codex-gateway/run b/base-images/frameworks/sandbox/fastgpt/codex-gateway/run index b8b867d7..2d70e84f 100644 --- a/base-images/frameworks/sandbox/fastgpt/codex-gateway/run +++ b/base-images/frameworks/sandbox/fastgpt/codex-gateway/run @@ -66,7 +66,7 @@ if [ -n "$GATEWAY_OPENAI_BASE_URL" ]; then fi mkdir -p "$GATEWAY_CODEX_HOME" "$GATEWAY_CWD" -chown "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$GATEWAY_CODEX_HOME" "$GATEWAY_CWD" || true +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$GATEWAY_CODEX_HOME" "$GATEWAY_CWD" || true if [ ! -x /usr/local/bin/codex-gateway ]; then echo "codex-gateway binary not found at /usr/local/bin/codex-gateway" >&2 diff --git a/base-images/frameworks/sandbox/fastgpt/fastgpt-ide-agent/finish b/base-images/frameworks/sandbox/fastgpt/fastgpt-ide-agent/finish new file mode 100644 index 00000000..01d2d069 --- /dev/null +++ b/base-images/frameworks/sandbox/fastgpt/fastgpt-ide-agent/finish @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +EXIT_CODE=$1 + +if [ "$EXIT_CODE" = "111" ]; then + echo "fastgpt-ide-agent stopped because required files were not installed correctly. Preventing s6 restart." + exit 125 +fi + +echo "fastgpt-ide-agent exited with code $EXIT_CODE, allowing s6 to restart it." +exit 1 diff --git a/base-images/frameworks/sandbox/fastgpt/fastgpt-ide-agent/run b/base-images/frameworks/sandbox/fastgpt/fastgpt-ide-agent/run new file mode 100644 index 00000000..812d45cb --- /dev/null +++ b/base-images/frameworks/sandbox/fastgpt/fastgpt-ide-agent/run @@ -0,0 +1,47 @@ +#!/command/with-contenv bash +# shellcheck disable=SC1008 +set -euo pipefail + +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +DEVBOX_HOME="$(getent passwd "$DEFAULT_DEVBOX_USER" | cut -d: -f6 || true)" +if [ -z "$DEVBOX_HOME" ]; then + DEVBOX_HOME="/home/${DEFAULT_DEVBOX_USER}" +fi + +if [ "${IDE_AGENT_ENABLED:-true}" != "true" ]; then + echo "fastgpt-ide-agent is disabled via IDE_AGENT_ENABLED. Sleeping infinity..." + exec sleep infinity +fi + +if [ -n "${DEVBOX_SDK_RUN_AS_ROOT:-}" ]; then + export HOME=/root + export USER=root + export LOGNAME=root +else + export HOME="$DEVBOX_HOME" + export USER="$DEFAULT_DEVBOX_USER" + export LOGNAME="$DEFAULT_DEVBOX_USER" +fi + +# Set FastGPT Workspace Directory for fastgpt-ide-agent +export FASTGPT_WORKDIR="${CODEX_GATEWAY_CWD:-${DEVBOX_HOME}/workspace}" +export IDE_AGENT_BIND_ADDR="${IDE_AGENT_BIND_ADDR:-0.0.0.0:1318}" + +# Ensure workspace directory exists and is owned by devbox user +mkdir -p "$FASTGPT_WORKDIR" +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$FASTGPT_WORKDIR" || true + +if [ ! -x /usr/local/bin/fastgpt-ide-agent ]; then + echo "fastgpt-ide-agent binary not found at /usr/local/bin/fastgpt-ide-agent" >&2 + exit 111 +fi + +echo "Starting fastgpt-ide-agent, binding to $IDE_AGENT_BIND_ADDR..." +exec 2>&1 + +if [ -n "${DEVBOX_SDK_RUN_AS_ROOT:-}" ]; then + echo "WARNING: The ide-agent will be run as root, which is not recommended" + exec /usr/local/bin/fastgpt-ide-agent +fi + +exec s6-setuidgid "$DEFAULT_DEVBOX_USER" /usr/local/bin/fastgpt-ide-agent diff --git a/base-images/frameworks/sandbox/fastgpt/settings.json b/base-images/frameworks/sandbox/fastgpt/settings.json deleted file mode 100644 index 3c0785fb..00000000 --- a/base-images/frameworks/sandbox/fastgpt/settings.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "workbench.startupEditor": "none", - "workbench.welcomePage.tasks.showOnStart": false, - "telemetry.telemetryLevel": "off", - "editor.minimap.enabled": false, - "workbench.tips.enabled": false, - "extensions.autoCheckUpdates": false, - "extensions.autoUpdate": false, - "editor.accessibilitySupport": "off", - "editor.hover.enabled": "off", - "editor.hover.sticky": false, - "editor.acceptSuggestionOnCommitCharacter": false, - "editor.acceptSuggestionOnEnter": "off", - "editor.inlineSuggest.edits.allowCodeShifting": "never", - "editor.inlineSuggest.edits.renderSideBySide": "never", - "editor.inlineSuggest.edits.showLongDistanceHint": false, - "editor.inlineSuggest.enabled": false, - "editor.inlineSuggest.experimental.emptyResponseInformation": false, - "editor.inlineSuggest.showToolbar": "never", - "editor.inlineSuggest.suppressInSnippetMode": false, - "workbench.commandPalette.showAskInChat": false, - "workbench.commandPalette.experimental.enableNaturalLanguageSearch": false, - "workbench.tree.enableStickyScroll": false, - "chat.disableAIFeatures": true, - "workbench.activityBar.location": "hidden", - "workbench.statusBar.visible": false, - "workbench.editor.showTabs": "none", - "window.commandCenter": false, - "workbench.editor.editorActionsLocation": "hidden", - "workbench.layoutControl.enabled": false, - "workbench.secondarySideBar.defaultVisibility": "hidden" -} diff --git a/base-images/frameworks/sandbox/v1/Dockerfile b/base-images/frameworks/sandbox/v1/Dockerfile index 3282db32..590745f9 100644 --- a/base-images/frameworks/sandbox/v1/Dockerfile +++ b/base-images/frameworks/sandbox/v1/Dockerfile @@ -19,6 +19,7 @@ ARG L10N LABEL org.opencontainers.image.authors="The Devbox Authors" ENV L10N=${L10N} ENV PYTHON_VERSION=3.14.0 +ENV VERSITYGW_VERSION=1.5.0 ENV CODEX_GATEWAY_ROOT=/opt/codex-gateway ENV CODEX_GATEWAY_CODEX_HOME=/codex-home @@ -29,14 +30,41 @@ ENV CODEX_GATEWAY_MAX_SESSIONS=12 ENV CODEX_GATEWAY_SESSION_TTL_MS=1800000 ENV CODEX_GATEWAY_SESSION_SWEEP_INTERVAL_MS=60000 +ENV VERSITYGW_ENABLED=true +ENV VERSITYGW_HOST=0.0.0.0 +ENV VERSITYGW_PORT=1319 +ENV VERSITYGW_REGION=sealos-internal +ENV VERSITYGW_ROOT=/home/${DEFAULT_DEVBOX_USER}/workspace/.versitygw-s3 +ENV VERSITYGW_IAM_DIR=/home/${DEFAULT_DEVBOX_USER}/workspace/.versitygw-iam +ENV VERSITYGW_VERSIONING_DIR=/home/${DEFAULT_DEVBOX_USER}/workspace/.versitygw-versioning + +ENV AWS_ACCESS_KEY_ID=admin +ENV AWS_REGION=sealos-internal +ENV AWS_DEFAULT_REGION=sealos-internal +ENV AWS_ENDPOINT_URL=http://127.0.0.1:1319 +ENV AWS_ENDPOINT_URL_S3=http://127.0.0.1:1319 +ENV AWS_S3_FORCE_PATH_STYLE=true +ENV S3_ENDPOINT=http://127.0.0.1:1319 +ENV S3_FORCE_PATH_STYLE=true +ENV KANIKO_CONTEXT_S3_BUCKET=kaniko-contexts +ENV KANIKO_CONTEXT_S3_PREFIX=contexts +ENV KANIKO_CONTEXT_S3_BASE=s3://kaniko-contexts/contexts +ENV KANIKO_CONTEXT_POSIX_DIR=/home/${DEFAULT_DEVBOX_USER}/workspace/.versitygw-s3/kaniko-contexts/contexts + COPY --from=codex-gateway /usr/local/bin/codex-gateway /usr/local/bin/codex-gateway # Add build assets and execute them. COPY codex-gateway /tmp/codex-gateway-service +COPY versitygw /tmp/versitygw-service COPY build.sh /build.sh -RUN chmod +x /build.sh /tmp/codex-gateway-service/run /tmp/codex-gateway-service/finish && \ +RUN chmod +x \ + /build.sh \ + /tmp/codex-gateway-service/run \ + /tmp/codex-gateway-service/finish \ + /tmp/versitygw-service/run \ + /tmp/versitygw-service/finish && \ /build.sh && \ rm -f /build.sh && \ - rm -rf /tmp/codex-gateway-service + rm -rf /tmp/codex-gateway-service /tmp/versitygw-service -EXPOSE 1317 +EXPOSE 1317 1319 diff --git a/base-images/frameworks/sandbox/v1/build.sh b/base-images/frameworks/sandbox/v1/build.sh index 1bd151d1..eb076276 100644 --- a/base-images/frameworks/sandbox/v1/build.sh +++ b/base-images/frameworks/sandbox/v1/build.sh @@ -5,28 +5,46 @@ L10N=${L10N:-en_US} PYTHON_VERSION=${PYTHON_VERSION:-3.14.0} KUBECTL_VERSION=${KUBECTL_VERSION:-v1.33.0} HELM_VERSION=${HELM_VERSION:-v3.20.2} +GH_VERSION=${GH_VERSION:-2.98.0} +BUILDKIT_VERSION=${BUILDKIT_VERSION:-v0.30.0} +RAILPACK_VERSION=${RAILPACK_VERSION:-0.27.0} +VERSITYGW_VERSION=${VERSITYGW_VERSION:-1.5.0} DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} CODEX_GATEWAY_ROOT=${CODEX_GATEWAY_ROOT:-/opt/codex-gateway} CODEX_GATEWAY_CODEX_HOME=${CODEX_GATEWAY_CODEX_HOME:-/codex-home} S6_DIR=/etc/s6-overlay/s6-rc.d CODEX_GATEWAY_SERVICE_SOURCE_DIR=${CODEX_GATEWAY_SERVICE_SOURCE_DIR:-/tmp/codex-gateway-service} +VERSITYGW_SERVICE_SOURCE_DIR=${VERSITYGW_SERVICE_SOURCE_DIR:-/tmp/versitygw-service} DEVBOX_HOME="$(getent passwd "$DEFAULT_DEVBOX_USER" | cut -d: -f6 || true)" if [ -z "$DEVBOX_HOME" ]; then DEVBOX_HOME="/home/${DEFAULT_DEVBOX_USER}" fi WORKSPACE_DIR=${CODEX_GATEWAY_CWD:-${DEVBOX_HOME}/workspace} PROJECT_DIR=${PROJECT_DIR:-${DEVBOX_HOME}/project} +VERSITYGW_ROOT=${VERSITYGW_ROOT:-${WORKSPACE_DIR}/.versitygw-s3} +VERSITYGW_IAM_DIR=${VERSITYGW_IAM_DIR:-${WORKSPACE_DIR}/.versitygw-iam} +VERSITYGW_VERSIONING_DIR=${VERSITYGW_VERSIONING_DIR:-${WORKSPACE_DIR}/.versitygw-versioning} +KANIKO_CONTEXT_S3_BUCKET=${KANIKO_CONTEXT_S3_BUCKET:-kaniko-contexts} +KANIKO_CONTEXT_S3_PREFIX=${KANIKO_CONTEXT_S3_PREFIX:-contexts} ARCH="$(dpkg --print-architecture)" case "$ARCH" in amd64) KUBECTL_ARCH=amd64 + BUILDKIT_ARCH=amd64 + RAILPACK_ARCH=x86_64 + VERSITYGW_ARCH=amd64 + GH_ARCH=amd64 ;; arm64) KUBECTL_ARCH=arm64 + BUILDKIT_ARCH=arm64 + RAILPACK_ARCH=arm64 + VERSITYGW_ARCH=arm64 + GH_ARCH=arm64 ;; *) - echo "Unsupported architecture for kubectl: $ARCH" >&2 + echo "Unsupported architecture for kubectl/buildkit/versitygw/gh: $ARCH" >&2 exit 1 ;; esac @@ -65,6 +83,29 @@ wget -O "/tmp/helm-${HELM_VERSION}-linux-${KUBECTL_ARCH}.tar.gz" \ install -m 0755 "/tmp/linux-${KUBECTL_ARCH}/helm" /usr/local/bin/helm && \ rm -rf "/tmp/helm-${HELM_VERSION}-linux-${KUBECTL_ARCH}.tar.gz" "/tmp/linux-${KUBECTL_ARCH}" +wget -O "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" \ + "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" && \ + tar -C /tmp -xzf "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" && \ + install -m 0755 "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh" /usr/local/bin/gh && \ + rm -rf "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}" + +wget -O "/tmp/buildkit-${BUILDKIT_VERSION}.linux-${BUILDKIT_ARCH}.tar.gz" \ + "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-${BUILDKIT_ARCH}.tar.gz" && \ + tar -C /tmp -xzf "/tmp/buildkit-${BUILDKIT_VERSION}.linux-${BUILDKIT_ARCH}.tar.gz" && \ + install -m 0755 /tmp/bin/buildctl /usr/local/bin/buildctl && \ + rm -rf "/tmp/buildkit-${BUILDKIT_VERSION}.linux-${BUILDKIT_ARCH}.tar.gz" /tmp/bin + +wget -O "/tmp/railpack-v${RAILPACK_VERSION}-${RAILPACK_ARCH}-unknown-linux-musl.tar.gz" \ + "https://github.com/railwayapp/railpack/releases/download/v${RAILPACK_VERSION}/railpack-v${RAILPACK_VERSION}-${RAILPACK_ARCH}-unknown-linux-musl.tar.gz" && \ + tar -C /usr/local/bin -xzf "/tmp/railpack-v${RAILPACK_VERSION}-${RAILPACK_ARCH}-unknown-linux-musl.tar.gz" && \ + chmod 0755 /usr/local/bin/railpack && \ + rm -f "/tmp/railpack-v${RAILPACK_VERSION}-${RAILPACK_ARCH}-unknown-linux-musl.tar.gz" + +wget -O "/tmp/versitygw_${VERSITYGW_VERSION}_linux_${VERSITYGW_ARCH}.deb" \ + "https://github.com/versity/versitygw/releases/download/v${VERSITYGW_VERSION}/versitygw_${VERSITYGW_VERSION}_linux_${VERSITYGW_ARCH}.deb" && \ + dpkg -i "/tmp/versitygw_${VERSITYGW_VERSION}_linux_${VERSITYGW_ARCH}.deb" && \ + rm -f "/tmp/versitygw_${VERSITYGW_VERSION}_linux_${VERSITYGW_ARCH}.deb" + if [ "$L10N" = "zh_CN" ]; then npm config set registry https://registry.npmmirror.com HOME=/root pip3.14 config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple @@ -76,12 +117,23 @@ node --version bun --version kubectl version --client helm version --short +gh --version +buildctl --version +railpack --version +versitygw --version python3.14 --version rg --version rm -rf "$PROJECT_DIR" -mkdir -p "$WORKSPACE_DIR" "$CODEX_GATEWAY_CODEX_HOME" "$S6_DIR/codex-gateway/dependencies.d" +mkdir -p \ + "$WORKSPACE_DIR" \ + "$CODEX_GATEWAY_CODEX_HOME" \ + "$VERSITYGW_ROOT/$KANIKO_CONTEXT_S3_BUCKET/$KANIKO_CONTEXT_S3_PREFIX" \ + "$VERSITYGW_IAM_DIR" \ + "$VERSITYGW_VERSIONING_DIR" \ + "$S6_DIR/codex-gateway/dependencies.d" \ + "$S6_DIR/versitygw/dependencies.d" install -d -m 755 "$CODEX_GATEWAY_ROOT" printf 'longrun\n' >"$S6_DIR/codex-gateway/type" @@ -94,4 +146,37 @@ install -m 700 \ touch "$S6_DIR/codex-gateway/dependencies.d/startup" : >"$S6_DIR/user/contents.d/codex-gateway" -chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$WORKSPACE_DIR" "$CODEX_GATEWAY_CODEX_HOME" +printf 'longrun\n' >"$S6_DIR/versitygw/type" +install -m 700 \ + "$VERSITYGW_SERVICE_SOURCE_DIR/run" \ + "$S6_DIR/versitygw/run" +install -m 700 \ + "$VERSITYGW_SERVICE_SOURCE_DIR/finish" \ + "$S6_DIR/versitygw/finish" +touch "$S6_DIR/versitygw/dependencies.d/startup" +: >"$S6_DIR/user/contents.d/versitygw" + +cat >/etc/profile.d/versitygw-kaniko-context.sh <<'PROFILE' +# Runtime S3 endpoint backed by versitygw POSIX storage for kaniko contexts. +export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-admin}" +export AWS_REGION="${AWS_REGION:-sealos-internal}" +export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-${AWS_REGION}}" +export AWS_ENDPOINT_URL="${AWS_ENDPOINT_URL:-http://127.0.0.1:1319}" +export AWS_ENDPOINT_URL_S3="${AWS_ENDPOINT_URL_S3:-${AWS_ENDPOINT_URL}}" +export AWS_S3_FORCE_PATH_STYLE="${AWS_S3_FORCE_PATH_STYLE:-true}" +export S3_ENDPOINT="${S3_ENDPOINT:-${AWS_ENDPOINT_URL_S3}}" +export S3_FORCE_PATH_STYLE="${S3_FORCE_PATH_STYLE:-true}" +if [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then + export AWS_SECRET_ACCESS_KEY="${SEALOS_DEVBOX_JWT_SECRET:-${DEVBOX_JWT_SECRET:-}}" +fi +export KANIKO_CONTEXT_S3_BUCKET="${KANIKO_CONTEXT_S3_BUCKET:-kaniko-contexts}" +export KANIKO_CONTEXT_S3_PREFIX="${KANIKO_CONTEXT_S3_PREFIX:-contexts}" +export KANIKO_CONTEXT_S3_BASE="${KANIKO_CONTEXT_S3_BASE:-s3://${KANIKO_CONTEXT_S3_BUCKET}/${KANIKO_CONTEXT_S3_PREFIX}}" +export KANIKO_CONTEXT_POSIX_DIR="${KANIKO_CONTEXT_POSIX_DIR:-${VERSITYGW_ROOT:-/home/devbox/workspace/.versitygw-s3}/${KANIKO_CONTEXT_S3_BUCKET}/${KANIKO_CONTEXT_S3_PREFIX}}" +PROFILE +chmod 0644 /etc/profile.d/versitygw-kaniko-context.sh + +chown -R \ + "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" \ + "$WORKSPACE_DIR" \ + "$CODEX_GATEWAY_CODEX_HOME" diff --git a/base-images/frameworks/sandbox/v1/codex-gateway/run b/base-images/frameworks/sandbox/v1/codex-gateway/run index b8b867d7..008af728 100644 --- a/base-images/frameworks/sandbox/v1/codex-gateway/run +++ b/base-images/frameworks/sandbox/v1/codex-gateway/run @@ -24,6 +24,23 @@ GATEWAY_OPENAI_API_KEY=${CODEX_GATEWAY_OPENAI_API_KEY:-} GATEWAY_OPENAI_BASE_URL=${CODEX_GATEWAY_OPENAI_BASE_URL:-} GATEWAY_JWT_SECRET=${CODEX_GATEWAY_JWT_SECRET:-${DEVBOX_JWT_SECRET:-}} +AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-admin} +AWS_REGION=${AWS_REGION:-sealos-internal} +AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-$AWS_REGION} +AWS_ENDPOINT_URL=${AWS_ENDPOINT_URL:-http://127.0.0.1:1319} +AWS_ENDPOINT_URL_S3=${AWS_ENDPOINT_URL_S3:-$AWS_ENDPOINT_URL} +AWS_S3_FORCE_PATH_STYLE=${AWS_S3_FORCE_PATH_STYLE:-true} +S3_ENDPOINT=${S3_ENDPOINT:-$AWS_ENDPOINT_URL_S3} +S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE:-true} +if [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then + AWS_SECRET_ACCESS_KEY=${SEALOS_DEVBOX_JWT_SECRET:-${DEVBOX_JWT_SECRET:-}} +fi +KANIKO_CONTEXT_S3_BUCKET=${KANIKO_CONTEXT_S3_BUCKET:-kaniko-contexts} +KANIKO_CONTEXT_S3_PREFIX=${KANIKO_CONTEXT_S3_PREFIX:-contexts} +KANIKO_CONTEXT_S3_BASE=${KANIKO_CONTEXT_S3_BASE:-s3://${KANIKO_CONTEXT_S3_BUCKET}/${KANIKO_CONTEXT_S3_PREFIX}} +VERSITYGW_ROOT=${VERSITYGW_ROOT:-${GATEWAY_CWD}/.versitygw-s3} +KANIKO_CONTEXT_POSIX_DIR=${KANIKO_CONTEXT_POSIX_DIR:-${VERSITYGW_ROOT}/${KANIKO_CONTEXT_S3_BUCKET}/${KANIKO_CONTEXT_S3_PREFIX}} + export HOME="$DEVBOX_HOME" export USER="$DEFAULT_DEVBOX_USER" export LOGNAME="$DEFAULT_DEVBOX_USER" @@ -35,6 +52,22 @@ export CODEX_GATEWAY_CODEX_BIN="$GATEWAY_CODEX_BIN" export CODEX_GATEWAY_MAX_SESSIONS="$GATEWAY_MAX_SESSIONS" export CODEX_GATEWAY_SESSION_TTL_MS="$GATEWAY_SESSION_TTL_MS" export CODEX_GATEWAY_SESSION_SWEEP_INTERVAL_MS="$GATEWAY_SESSION_SWEEP_INTERVAL_MS" +export AWS_ACCESS_KEY_ID +export AWS_REGION +export AWS_DEFAULT_REGION +export AWS_ENDPOINT_URL +export AWS_ENDPOINT_URL_S3 +export AWS_S3_FORCE_PATH_STYLE +export S3_ENDPOINT +export S3_FORCE_PATH_STYLE +export KANIKO_CONTEXT_S3_BUCKET +export KANIKO_CONTEXT_S3_PREFIX +export KANIKO_CONTEXT_S3_BASE +export KANIKO_CONTEXT_POSIX_DIR + +if [ -n "$AWS_SECRET_ACCESS_KEY" ]; then + export AWS_SECRET_ACCESS_KEY +fi if [ -n "$GATEWAY_JWT_SECRET" ]; then export CODEX_GATEWAY_JWT_SECRET="$GATEWAY_JWT_SECRET" diff --git a/base-images/frameworks/sandbox/v1/versitygw/finish b/base-images/frameworks/sandbox/v1/versitygw/finish new file mode 100644 index 00000000..0b0011c4 --- /dev/null +++ b/base-images/frameworks/sandbox/v1/versitygw/finish @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +EXIT_CODE=$1 + +if [ "$EXIT_CODE" = "101" ]; then + echo "versitygw stopped because no S3 secret was available. Preventing s6 restart." + exit 125 +fi + +if [ "$EXIT_CODE" = "111" ]; then + echo "versitygw stopped because required files were not installed correctly. Preventing s6 restart." + exit 125 +fi + +echo "versitygw exited with code $EXIT_CODE, allowing s6 to restart it." +exit 1 diff --git a/base-images/frameworks/sandbox/v1/versitygw/run b/base-images/frameworks/sandbox/v1/versitygw/run new file mode 100644 index 00000000..b61d7beb --- /dev/null +++ b/base-images/frameworks/sandbox/v1/versitygw/run @@ -0,0 +1,70 @@ +#!/command/with-contenv bash +# shellcheck disable=SC1008 +set -euo pipefail + +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} + +DEVBOX_HOME="$(getent passwd "$DEFAULT_DEVBOX_USER" | cut -d: -f6 || true)" +if [ -z "$DEVBOX_HOME" ]; then + DEVBOX_HOME="/home/${DEFAULT_DEVBOX_USER}" +fi + +if [ "${VERSITYGW_ENABLED:-true}" != "true" ]; then + echo "versitygw is disabled via VERSITYGW_ENABLED. Sleeping infinity..." + exec sleep infinity +fi + +WORKSPACE_DIR=${CODEX_GATEWAY_CWD:-${DEVBOX_HOME}/workspace} +VERSITYGW_HOST=${VERSITYGW_HOST:-0.0.0.0} +VERSITYGW_PORT=${VERSITYGW_PORT:-1319} +VERSITYGW_LISTEN=${VERSITYGW_LISTEN:-${VERSITYGW_HOST}:${VERSITYGW_PORT}} +VERSITYGW_ROOT=${VERSITYGW_ROOT:-${WORKSPACE_DIR}/.versitygw-s3} +VERSITYGW_IAM_DIR=${VERSITYGW_IAM_DIR:-${WORKSPACE_DIR}/.versitygw-iam} +VERSITYGW_VERSIONING_DIR=${VERSITYGW_VERSIONING_DIR:-${WORKSPACE_DIR}/.versitygw-versioning} +VERSITYGW_REGION=${VERSITYGW_REGION:-${AWS_REGION:-sealos-internal}} +KANIKO_CONTEXT_S3_BUCKET=${KANIKO_CONTEXT_S3_BUCKET:-kaniko-contexts} +KANIKO_CONTEXT_S3_PREFIX=${KANIKO_CONTEXT_S3_PREFIX:-contexts} + +VERSITYGW_ACCESS_KEY=${VERSITYGW_ACCESS_KEY:-${ROOT_ACCESS_KEY_ID:-${ROOT_ACCESS_KEY:-${AWS_ACCESS_KEY_ID:-admin}}}} +VERSITYGW_SECRET_KEY=${VERSITYGW_SECRET_KEY:-${ROOT_SECRET_ACCESS_KEY:-${ROOT_SECRET_KEY:-${AWS_SECRET_ACCESS_KEY:-${SEALOS_DEVBOX_JWT_SECRET:-${DEVBOX_JWT_SECRET:-}}}}}} + +if [ -z "$VERSITYGW_SECRET_KEY" ]; then + echo "versitygw requires AWS_SECRET_ACCESS_KEY, SEALOS_DEVBOX_JWT_SECRET, or DEVBOX_JWT_SECRET." >&2 + exit 101 +fi + +export HOME="$DEVBOX_HOME" +export USER="$DEFAULT_DEVBOX_USER" +export LOGNAME="$DEFAULT_DEVBOX_USER" +export AWS_ACCESS_KEY_ID="$VERSITYGW_ACCESS_KEY" +export AWS_SECRET_ACCESS_KEY="$VERSITYGW_SECRET_KEY" +export AWS_REGION="$VERSITYGW_REGION" +export AWS_DEFAULT_REGION="$VERSITYGW_REGION" +export ROOT_ACCESS_KEY="$VERSITYGW_ACCESS_KEY" +export ROOT_SECRET_KEY="$VERSITYGW_SECRET_KEY" +export VGW_REGION="$VERSITYGW_REGION" + +mkdir -p \ + "$VERSITYGW_ROOT/$KANIKO_CONTEXT_S3_BUCKET/$KANIKO_CONTEXT_S3_PREFIX" \ + "$VERSITYGW_IAM_DIR" \ + "$VERSITYGW_VERSIONING_DIR" +chown -R \ + "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" \ + "$VERSITYGW_ROOT" \ + "$VERSITYGW_IAM_DIR" \ + "$VERSITYGW_VERSIONING_DIR" || true + +if ! command -v versitygw >/dev/null 2>&1; then + echo "versitygw binary not found in PATH" >&2 + exit 111 +fi + +echo "Starting versitygw on ${VERSITYGW_LISTEN}, serving ${VERSITYGW_ROOT}." +exec 2>&1 +exec s6-setuidgid "$DEFAULT_DEVBOX_USER" \ + "$(command -v versitygw)" \ + --port "$VERSITYGW_LISTEN" \ + --iam-dir "$VERSITYGW_IAM_DIR" \ + posix \ + --versioning-dir "$VERSITYGW_VERSIONING_DIR" \ + "$VERSITYGW_ROOT" diff --git a/base-images/languages/java/openjdk17-nginx-private/Dockerfile b/base-images/languages/java/openjdk17-nginx-private/Dockerfile new file mode 100644 index 00000000..d6d2d937 --- /dev/null +++ b/base-images/languages/java/openjdk17-nginx-private/Dockerfile @@ -0,0 +1,20 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG OS_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/debian-12.6:${OS_IMAGE_VERSION} +ARG L10N +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV L10N=${L10N} +# Add build script and execute it +COPY build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh diff --git a/base-images/languages/java/openjdk17-nginx-private/build.sh b/base-images/languages/java/openjdk17-nginx-private/build.sh new file mode 100755 index 00000000..9dc96ed9 --- /dev/null +++ b/base-images/languages/java/openjdk17-nginx-private/build.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} + +apt-get update && \ + apt-get install -y --no-install-recommends openjdk-17-jdk maven nginx && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +JAVA_ARCH="$(dpkg --print-architecture)" +JAVA_HOME_REAL="/usr/lib/jvm/java-17-openjdk-${JAVA_ARCH}" +ln -sfn "${JAVA_HOME_REAL}" /usr/lib/jvm/java-17-openjdk + +ROOT_HOME="${HOME:-/root}" +JAVA_HOME="/usr/lib/jvm/java-17-openjdk" +grep -qxF "export JAVA_HOME=$JAVA_HOME" "$ROOT_HOME/.bashrc" || \ + echo "export JAVA_HOME=$JAVA_HOME" >> "$ROOT_HOME/.bashrc" +grep -qxF "export PATH=\$PATH:\$JAVA_HOME/bin" "$ROOT_HOME/.bashrc" || \ + echo "export PATH=\$PATH:\$JAVA_HOME/bin" >> "$ROOT_HOME/.bashrc" + +DEVBOX_USER="${DEFAULT_DEVBOX_USER}" +DEVBOX_HOME="$(getent passwd "$DEVBOX_USER" | cut -d: -f6 || true)" +if [ -z "$DEVBOX_HOME" ]; then + DEVBOX_HOME="/home/${DEVBOX_USER}" +fi + +grep -qxF "export JAVA_HOME=$JAVA_HOME" "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo "export JAVA_HOME=$JAVA_HOME" >> "$DEVBOX_HOME/.bashrc" 2>/dev/null || true +grep -qxF "export PATH=\$PATH:\$JAVA_HOME/bin" "$DEVBOX_HOME/.bashrc" 2>/dev/null || \ + echo "export PATH=\$PATH:\$JAVA_HOME/bin" >> "$DEVBOX_HOME/.bashrc" 2>/dev/null || true + +if [ "$L10N" = "zh_CN" ]; then + mkdir -p "$DEVBOX_HOME/.m2" + cat > "$DEVBOX_HOME/.m2/settings.xml" << 'EOF' + + + + + aliyunmaven + central + Aliyun Maven + https://maven.aliyun.com/repository/public + + + +EOF + chown -R "${DEVBOX_USER}:${DEVBOX_USER}" "$DEVBOX_HOME/.m2" || true +fi + +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk +export PATH=$PATH:$JAVA_HOME/bin diff --git a/base-images/languages/java/openjdk25/Dockerfile b/base-images/languages/java/openjdk25/Dockerfile new file mode 100644 index 00000000..f2219850 --- /dev/null +++ b/base-images/languages/java/openjdk25/Dockerfile @@ -0,0 +1,24 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG OS_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/debian-12.6:${OS_IMAGE_VERSION} +ARG L10N +ARG TARGETARCH +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV L10N=${L10N} +ENV JAVA_HOME=/opt/java/openjdk +ENV MAVEN_HOME=/opt/maven +ENV PATH=${JAVA_HOME}/bin:${MAVEN_HOME}/bin:${PATH} +# Add build script and execute it +COPY build.sh /build.sh +RUN chmod +x /build.sh && \ + TARGETARCH="${TARGETARCH}" /build.sh && \ + rm -f /build.sh diff --git a/base-images/languages/java/openjdk25/build.sh b/base-images/languages/java/openjdk25/build.sh new file mode 100755 index 00000000..74c8a9df --- /dev/null +++ b/base-images/languages/java/openjdk25/build.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +JAVA_HOME=${JAVA_HOME:-/opt/java/openjdk} +MAVEN_HOME=${MAVEN_HOME:-/opt/maven} +TEMURIN_RELEASE=jdk-25.0.4.1+1 +TEMURIN_VERSION=25.0.4.1_1 +MAVEN_VERSION=3.9.16 +MAVEN_SHA512=831a8591fe20c8243b1dbe7d71e3244f31d1665b0804b2e825e38cbbe5ce0cafb8338851f90780735568773e0a6cd07bbec107cda0b896b008b861075358b6f6 + +apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RAW_ARCH="${TARGETARCH:-}" +if [ -z "$RAW_ARCH" ]; then + RAW_ARCH="$(dpkg --print-architecture 2>/dev/null || true)" +fi +if [ -z "$RAW_ARCH" ]; then + RAW_ARCH="${ARCH:-}" +fi + +case "$RAW_ARCH" in + amd64|x86_64) + TEMURIN_ARCH=x64 + TEMURIN_SHA256=dbb698396d478e7fa2b1e50f4103324b2a99b90569ee27c33f2261f9215cf41e + ;; + arm64|aarch64) + TEMURIN_ARCH=aarch64 + TEMURIN_SHA256=69df11a02cfa3ef7d7ca645e03edce6778ec090e100f6ae2b42097865730ac52 + ;; + *) + echo "Unsupported architecture: $RAW_ARCH" >&2 + exit 1 + ;; +esac + +TEMURIN_ARCHIVE="OpenJDK25U-jdk_${TEMURIN_ARCH}_linux_hotspot_${TEMURIN_VERSION}.tar.gz" +TEMURIN_URL="https://github.com/adoptium/temurin25-binaries/releases/download/${TEMURIN_RELEASE}/${TEMURIN_ARCHIVE}" +curl -fsSL "$TEMURIN_URL" -o "/tmp/${TEMURIN_ARCHIVE}" +echo "${TEMURIN_SHA256} /tmp/${TEMURIN_ARCHIVE}" | sha256sum -c - +mkdir -p "$JAVA_HOME" +tar -xzf "/tmp/${TEMURIN_ARCHIVE}" -C "$JAVA_HOME" --strip-components=1 +rm -f "/tmp/${TEMURIN_ARCHIVE}" + +MAVEN_ARCHIVE="apache-maven-${MAVEN_VERSION}-bin.tar.gz" +MAVEN_URL="https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/${MAVEN_ARCHIVE}" +curl -fsSL "$MAVEN_URL" -o "/tmp/${MAVEN_ARCHIVE}" +echo "${MAVEN_SHA512} /tmp/${MAVEN_ARCHIVE}" | sha512sum -c - +mkdir -p "$MAVEN_HOME" +tar -xzf "/tmp/${MAVEN_ARCHIVE}" -C "$MAVEN_HOME" --strip-components=1 +rm -f "/tmp/${MAVEN_ARCHIVE}" + +cat > /etc/profile.d/java-env.sh </dev/null || \ + echo "export JAVA_HOME=$JAVA_HOME" >> "$shell_rc" 2>/dev/null || true + grep -qxF "export MAVEN_HOME=$MAVEN_HOME" "$shell_rc" 2>/dev/null || \ + echo "export MAVEN_HOME=$MAVEN_HOME" >> "$shell_rc" 2>/dev/null || true + grep -qxF 'export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH' "$shell_rc" 2>/dev/null || \ + echo 'export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH' >> "$shell_rc" 2>/dev/null || true +done + +if [ "$L10N" = "zh_CN" ]; then + mkdir -p "$DEVBOX_HOME/.m2" + cat > "$DEVBOX_HOME/.m2/settings.xml" <<'EOF' + + + + + aliyunmaven + central + Aliyun Maven + https://maven.aliyun.com/repository/public + + + +EOF + chown -R "${DEVBOX_USER}:${DEVBOX_USER}" "$DEVBOX_HOME/.m2" || true +fi + +export JAVA_HOME MAVEN_HOME +export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH" +java -version +javac -version +mvn -version diff --git a/base-images/languages/java/openjdk8/Dockerfile b/base-images/languages/java/openjdk8/Dockerfile new file mode 100644 index 00000000..38f98869 --- /dev/null +++ b/base-images/languages/java/openjdk8/Dockerfile @@ -0,0 +1,24 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG OS_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/debian-12.6:${OS_IMAGE_VERSION} +ARG L10N +ARG TARGETARCH +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV L10N=${L10N} +ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk +ENV MAVEN_HOME=/opt/maven +ENV PATH=${JAVA_HOME}/bin:${MAVEN_HOME}/bin:${PATH} +# Add build script and execute it +COPY build.sh /build.sh +RUN chmod +x /build.sh && \ + TARGETARCH="${TARGETARCH}" /build.sh && \ + rm -f /build.sh diff --git a/base-images/languages/java/openjdk8/build.sh b/base-images/languages/java/openjdk8/build.sh new file mode 100755 index 00000000..1d53c89e --- /dev/null +++ b/base-images/languages/java/openjdk8/build.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-8-openjdk} +MAVEN_HOME=${MAVEN_HOME:-/opt/maven} +TEMURIN_VERSION=8u492b09 +TEMURIN_RELEASE=jdk8u492-b09 +MAVEN_VERSION=3.9.16 +MAVEN_SHA512=831a8591fe20c8243b1dbe7d71e3244f31d1665b0804b2e825e38cbbe5ce0cafb8338851f90780735568773e0a6cd07bbec107cda0b896b008b861075358b6f6 + +apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RAW_ARCH="${TARGETARCH:-}" +if [ -z "$RAW_ARCH" ]; then + RAW_ARCH="$(dpkg --print-architecture 2>/dev/null || true)" +fi +if [ -z "$RAW_ARCH" ]; then + RAW_ARCH="${ARCH:-}" +fi + +case "$RAW_ARCH" in + amd64|x86_64) + TEMURIN_ARCH=x64 + TEMURIN_SHA256=da257f161d7f8c6ca5b0e5d9e4090f65ac28c5e398072e68b8ae87988b1d1a2e + ;; + arm64|aarch64) + TEMURIN_ARCH=aarch64 + TEMURIN_SHA256=3c2253b986909c20f79d6de7a0cb957f89c243df57615897836046e24d2e5257 + ;; + *) + echo "Unsupported architecture: $RAW_ARCH" >&2 + exit 1 + ;; +esac + +TEMURIN_ARCHIVE="OpenJDK8U-jdk_${TEMURIN_ARCH}_linux_hotspot_${TEMURIN_VERSION}.tar.gz" +TEMURIN_URL="https://github.com/adoptium/temurin8-binaries/releases/download/${TEMURIN_RELEASE}/${TEMURIN_ARCHIVE}" +curl -fsSL "$TEMURIN_URL" -o "/tmp/${TEMURIN_ARCHIVE}" +echo "${TEMURIN_SHA256} /tmp/${TEMURIN_ARCHIVE}" | sha256sum -c - +mkdir -p "$JAVA_HOME" +tar -xzf "/tmp/${TEMURIN_ARCHIVE}" -C "$JAVA_HOME" --strip-components=1 +rm -f "/tmp/${TEMURIN_ARCHIVE}" + +MAVEN_ARCHIVE="apache-maven-${MAVEN_VERSION}-bin.tar.gz" +MAVEN_URL="https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/${MAVEN_ARCHIVE}" +curl -fsSL "$MAVEN_URL" -o "/tmp/${MAVEN_ARCHIVE}" +echo "${MAVEN_SHA512} /tmp/${MAVEN_ARCHIVE}" | sha512sum -c - +mkdir -p "$MAVEN_HOME" +tar -xzf "/tmp/${MAVEN_ARCHIVE}" -C "$MAVEN_HOME" --strip-components=1 +rm -f "/tmp/${MAVEN_ARCHIVE}" + +cat > /etc/profile.d/java-env.sh </dev/null || \ + echo "export JAVA_HOME=$JAVA_HOME" >> "$shell_rc" 2>/dev/null || true + grep -qxF "export MAVEN_HOME=$MAVEN_HOME" "$shell_rc" 2>/dev/null || \ + echo "export MAVEN_HOME=$MAVEN_HOME" >> "$shell_rc" 2>/dev/null || true + grep -qxF 'export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH' "$shell_rc" 2>/dev/null || \ + echo 'export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH' >> "$shell_rc" 2>/dev/null || true +done + +if [ "$L10N" = "zh_CN" ]; then + mkdir -p "$DEVBOX_HOME/.m2" + cat > "$DEVBOX_HOME/.m2/settings.xml" <<'EOF' + + + + + aliyunmaven + central + Aliyun Maven + https://maven.aliyun.com/repository/public + + + +EOF + chown -R "${DEVBOX_USER}:${DEVBOX_USER}" "$DEVBOX_HOME/.m2" || true +fi + +export JAVA_HOME MAVEN_HOME +export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH" +java -version +javac -version +mvn -version diff --git a/base-images/operating-systems/fedora/44/Dockerfile b/base-images/operating-systems/fedora/44/Dockerfile new file mode 100644 index 00000000..b5c1925f --- /dev/null +++ b/base-images/operating-systems/fedora/44/Dockerfile @@ -0,0 +1,49 @@ +# These ARGs can be overridden at build time to customize the image +ARG REGISTRY=ghcr.io +ARG TOOLING_REPO=labring-actions/devbox-tooling +ARG L10N=en_US +ARG TARGETARCH +ARG ARCH=${TARGETARCH:-amd64} +ARG DEFAULT_DEVBOX_USER=devbox +ARG FEDORA_IMAGE=fedora:44 +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG BASE_TOOLS_VERSION=v0.0.1-alpha.1 + +FROM ${REGISTRY}/${TOOLING_REPO}/tooling:${BASE_TOOLS_VERSION} AS tooling +FROM ${FEDORA_IMAGE} +ARG L10N +ARG TARGETARCH +ARG ARCH=${TARGETARCH:-amd64} +ARG DEFAULT_DEVBOX_USER +LABEL org.opencontainers.image.authors="The Devbox Authors" +# Define some environment variables +## BASE_TOOLS_DIR: Directory where base tools are installed +ENV BASE_TOOLS_DIR=/opt/base-tools +## L10N: Internationalization setting +ENV L10N=${L10N} +## ARCH: System architecture (from build-arg) +ENV ARCH=${ARCH} +## DEFAULT_DEVBOX_USER: Default user for the devbox environment +ENV DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER} +## PROJECT_DIR: Default devbox project directory inside the container +ENV PROJECT_DIR=/home/devbox/project +## S6_STAGE2_HOOK: Hook script executed BEFORE s6-rc compilation +## This allows us to dynamically disable services based on DEVBOX_ENV +ENV S6_STAGE2_HOOK=/etc/s6-overlay-hook/pre-rc-init.d/pre-rc-init.sh +## S6_KILL_GRACETIME: Time to wait before forcefully killing all processes during shutdown +ENV S6_KILL_GRACETIME=500 + +# Copy tooling assets from the tooling stage +COPY --from=tooling ${BASE_TOOLS_DIR} ${BASE_TOOLS_DIR} +# Add build script and execute it +COPY build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${BASE_TOOLS_DIR} +## Locale: generated during build, but also export at runtime so non-login processes use UTF-8 +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 +ENTRYPOINT [ "/init" ] diff --git a/base-images/operating-systems/fedora/44/build.sh b/base-images/operating-systems/fedora/44/build.sh new file mode 100644 index 00000000..24c17809 --- /dev/null +++ b/base-images/operating-systems/fedora/44/build.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Current BASE_TOOLS_DIR: $BASE_TOOLS_DIR" +echo "Current L10N: $L10N" +echo "Current ARCH: $ARCH" +echo "Current DEFAULT_DEVBOX_USER: $DEFAULT_DEVBOX_USER" + +chmod +x "$BASE_TOOLS_DIR/scripts/"*.sh + +# Install base packages for Fedora/RPM family +"$BASE_TOOLS_DIR/scripts/install-base-pkg-rpm.sh" + +# Install cron, s6, and the SDK server from the shared tooling scripts +"$BASE_TOOLS_DIR/scripts/install-crond.sh" +"$BASE_TOOLS_DIR/scripts/install-s6.sh" +"$BASE_TOOLS_DIR/scripts/install-sdk-server.sh" + +# Configure svc +"$BASE_TOOLS_DIR/scripts/configure-svc.sh" + +# Configure other utilities +"$BASE_TOOLS_DIR/scripts/configure-logrotate.sh" +"$BASE_TOOLS_DIR/scripts/configure-login.sh" + +# Configure localization (L10N) +"$BASE_TOOLS_DIR/scripts/configure-l10n.sh" + +# Configure user devbox +"$BASE_TOOLS_DIR/scripts/configure-user.sh" "$DEFAULT_DEVBOX_USER" + +# Install user-facing runtime docs (single source from the shared tooling bundle) +if [ -d "$BASE_TOOLS_DIR/docs" ]; then + install -d /usr/share/devbox/docs + cp "$BASE_TOOLS_DIR"/docs/README.s6-user-guide*.md /usr/share/devbox/docs/ + chmod 644 /usr/share/devbox/docs/README.s6-user-guide*.md +else + echo "No docs directory found in $BASE_TOOLS_DIR; skipping s6 user-guide install" +fi + +# Cleanup +"$BASE_TOOLS_DIR/scripts/cleanup.sh" diff --git a/docs/superpowers/plans/2026-08-25-create-runtime-skill.md b/docs/superpowers/plans/2026-08-25-create-runtime-skill.md new file mode 100644 index 00000000..7bc606dd --- /dev/null +++ b/docs/superpowers/plans/2026-08-25-create-runtime-skill.md @@ -0,0 +1,95 @@ +# Create Runtime Skill Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add and validate a repository-local `create-runtime` skill that creates complete OS, language, and framework runtime contributions without guessing or silently falling back. + +**Architecture:** Keep the skill instructions small and route type-specific details into three reference files. Use one dependency-free Python validator for deterministic input/path checks; leave Dockerfile, installation-source, and startup decisions to the agent guided by repository examples and official evidence. Test the validator with Python's standard library and validate the skill package with the bundled skill validator plus repository checks. + +**Tech Stack:** Markdown Agent Skill, Python 3 standard library, Bash, existing repository conformance tooling. + +**Spec:** `docs/superpowers/specs/create-runtime-skill.md` + +## Global Constraints + +- Base the branch on `labring/main` at the refreshed remote commit. +- Supported types are exactly `operating-system`, `language`, and `framework`. +- Never overwrite an existing target. +- Never add unconfirmed fallback behavior. +- Do not create speculative runtime files or modify unrelated CI/workflows. +- Use the repository's existing runtime conformance and smoke-test conventions. + +### Task 1: Add failing validator tests + +**Files:** +- Create: `tests/skills/create-runtime/test_validate_runtime_input.py` +- Reference: `.agents/skills/create-runtime/scripts/validate-runtime-input.py` + +**Interfaces:** +- The test invokes the validator as a subprocess with `--repo-root`, `--type`, `--name`, and `--version`. +- Successful output is JSON containing the normalized plural `kind`, the target runtime path, and the smoke-test path. +- Invalid input exits non-zero and writes a concise error to stderr. + +- [ ] Write tests for valid normalization, traversal rejection, invalid type rejection, and duplicate target rejection. +- [ ] Run `python3 -m unittest discover -s tests/skills/create-runtime -v` and confirm it fails because the validator does not exist. + +### Task 2: Implement the minimal validator + +**Files:** +- Create: `.agents/skills/create-runtime/scripts/validate-runtime-input.py` + +**Interfaces:** +- CLI accepts `--repo-root`, `--type`, `--name`, `--version`. +- Emits one JSON object on stdout for valid input. +- Exits `1` for invalid input, unsafe path components, or an existing target. + +- [ ] Implement strict type mapping and component validation. +- [ ] Resolve paths beneath the supplied repository root and reject path escape. +- [ ] Check the runtime directory and its Dockerfile for collisions. +- [ ] Run the focused test suite and confirm it passes. + +### Task 3: Write the skill package and references + +**Files:** +- Create: `.agents/skills/create-runtime/SKILL.md` +- Create: `.agents/skills/create-runtime/references/common-runtime-contract.md` +- Create: `.agents/skills/create-runtime/references/operating-systems.md` +- Create: `.agents/skills/create-runtime/references/languages.md` +- Create: `.agents/skills/create-runtime/references/frameworks.md` + +**Interfaces:** +- `SKILL.md` is the only discovery entrypoint. +- It invokes the validator before mutation and loads only the reference for the selected type. +- It instructs the agent to stop on unknown facts and to run existing conformance/smoke checks. + +- [ ] Write the smallest workflow that covers the approved design. +- [ ] Add repository-specific commands and structural rules to references. +- [ ] Include one concise example for creating a language runtime. +- [ ] Ensure all user-facing wording describes outcomes for runtime contributors, not internal implementation details. + +### Task 4: Add skill pressure scenarios and validate the package + +**Files:** +- Create: `tests/skills/create-runtime/pressure-scenarios.md` + +- [ ] Document no-guidance baseline risks for duplicate targets, ambiguous sources, missing inputs, and requested fallback behavior. +- [ ] Run the bundled `quick_validate.py` against `.agents/skills/create-runtime`. +- [ ] Run the validator tests again and inspect stdout/stderr behavior. + +### Task 5: Run repository-level verification + +**Files:** +- No source changes expected. + +- [ ] Run the runtime conformance planner against representative existing runtimes. +- [ ] Run shell syntax checks for any skill scripts. +- [ ] Run the available repository test command for the new validator and skill package. +- [ ] Review the diff for unrelated changes and confirm user-owned `.DS_Store` and `tmp/` remain untouched. + +### Task 6: Review and commit + +- [ ] Re-read the specification and check every requirement against the final files. +- [ ] Request a focused code review of the branch diff. +- [ ] Fix critical or important findings and rerun verification. +- [ ] Commit the completed skill on `feat/create-runtime-skill` with a focused message. +- [ ] Do not push or publish without a separate user request. diff --git a/docs/superpowers/specs/create-runtime-skill.md b/docs/superpowers/specs/create-runtime-skill.md new file mode 100644 index 00000000..143182e3 --- /dev/null +++ b/docs/superpowers/specs/create-runtime-skill.md @@ -0,0 +1,63 @@ +# Create Runtime Skill Specification + +## Goal + +Add a repository-local Agent Skill at `.agents/skills/create-runtime/` that guides Codex to create a complete, reviewable DevBox runtime for an operating system, language, or framework. A completed runtime includes the image definitions, project template, smoke test, documentation, and repository conformance verification required by the selected runtime type. + +## User Contract + +The skill accepts three required values: + +- `type`: `operating-system`, `language`, or `framework` +- `name`: the runtime directory name, such as `fedora`, `python`, or `nest.js` +- `version`: the runtime directory version, such as `44`, `3.13`, or `v12` + +The skill must map singular input types to repository directory names: + +- `operating-system` -> `operating-systems` +- `language` -> `languages` +- `framework` -> `frameworks` + +The skill must reject missing, malformed, unsafe, or duplicate runtime and smoke-test targets before creating files. An existing base-image directory may be reused only after its compatibility is verified. + +## Required Behavior + +1. Inspect the repository and select the closest existing runtime of the same type as a structural reference. +2. Determine the official source, exact version, supported architectures, base image, process model, port, and project startup behavior from reliable project or official documentation. +3. Stop and ask the user when a required fact is unknown or ambiguous. The skill must not guess. +4. Do not add fallback behavior without explicit user confirmation. In particular, do not silently substitute `latest`, an unverified mirror, an alternate base image, a reduced architecture set, a missing smoke test, or a weaker validation command. +5. Create only the files justified by the selected runtime type and its actual architecture. Do not create placeholder files that make an incomplete runtime appear complete. +6. Run static checks, the repository runtime conformance check, and the selected runtime smoke test when the environment supports them. Report checks that cannot run instead of hiding them. +7. Never overwrite an existing runtime target. + +## Runtime Layout Contract + +Every runtime image must have a `Dockerfile` under `runtime-images////`. The runtime directory may also contain `build.sh`, `project-template/`, configuration files, and localized README files as required by its behavior. + +A runtime with a project template must provide both `README.en_US.md` and `README.zh_CN.md`, an executable `entrypoint.sh` when the image contract requires a project entrypoint, and a smoke test at the matching path under `tests/runtime-smoke////smoke.sh`. + +If a runtime depends on a new base image, the skill must create the corresponding `base-images////` definition and ensure the runtime Dockerfile references the exact generated image name and version convention used by this repository. + +The skill must use the existing repository conformance tooling rather than inventing a competing conformance implementation. The planner only creates a CI matrix; the runtime must also be registered with runtime-specific assertions in `tests/runtime-conformance/run.sh` and documented in `tests/runtime-conformance/README.md`: + +- `.github/scripts/runtime-conformance.py` +- `tests/runtime-conformance/run.sh` +- `tests/runtime-conformance/README.md` + +## Skill Package + +The skill package contains: + +- `SKILL.md`: discovery description, workflow, stopping rules, type routing, and verification checklist. +- `references/common-runtime-contract.md`: shared repository conventions and commands. +- `references/operating-systems.md`: OS-specific file and behavior requirements. +- `references/languages.md`: language-specific file and behavior requirements. +- `references/frameworks.md`: framework-specific dependency and startup requirements. +- `scripts/validate-runtime-input.py`: deterministic input, path-safety, and duplicate-target validation. It must not generate Dockerfiles or make source-selection decisions. + +## Non-Goals + +- Do not create a generic runtime generator that guesses installation or startup behavior. +- Do not change existing runtime implementations while creating a new one unless the new runtime requires a documented shared fix. +- Do not modify CI workflows to accommodate a single runtime. +- Do not publish images, push branches, or open pull requests automatically. diff --git a/runtime-images/languages/java/openjdk17-nginx-private/Dockerfile b/runtime-images/languages/java/openjdk17-nginx-private/Dockerfile new file mode 100644 index 00000000..969f36e8 --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/Dockerfile @@ -0,0 +1,25 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG RUNTIME_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/java-openjdk17-nginx-private:${RUNTIME_IMAGE_VERSION} +ARG L10N +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV L10N=${L10N} +ENV PROJECT_TEMPLATE_DIR=/project-template +COPY ./project-template ${PROJECT_TEMPLATE_DIR} +COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${PROJECT_TEMPLATE_DIR} +# Set the working directory to the default devbox user's project directory +WORKDIR /home/${DEFAULT_DEVBOX_USER}/project diff --git a/runtime-images/languages/java/openjdk17-nginx-private/build.sh b/runtime-images/languages/java/openjdk17-nginx-private/build.sh new file mode 100755 index 00000000..f9a7a44f --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +PROJECT_TEMPLATE_DIR=${PROJECT_TEMPLATE_DIR:-/project-template} + +if ! id -u "$DEFAULT_DEVBOX_USER" &>/dev/null; then + echo "User $DEFAULT_DEVBOX_USER does not exist" + exit 1 +fi + +TARGET_DIR="/home/$DEFAULT_DEVBOX_USER/project" +mkdir -p "$TARGET_DIR" + +if [ -f "$PROJECT_TEMPLATE_DIR/README.$L10N.md" ]; then + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md exists. Copying to $TARGET_DIR/README.md" + cp "$PROJECT_TEMPLATE_DIR/README.$L10N.md" "$TARGET_DIR/README.md" +else + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md does not exist. Skipping copy." +fi + +DOCS_DIR=${DOCS_DIR:-/usr/share/devbox/docs} +if [ -f "$DOCS_DIR/README.s6-user-guide.$L10N.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.$L10N.md" "$TARGET_DIR/README.s6-user-guide.md" +elif [ -f "$DOCS_DIR/README.s6-user-guide.en_US.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.en_US.md" "$TARGET_DIR/README.s6-user-guide.md" +fi + +# Copy project template contents (except localized readmes handled above). +# Using `/.` keeps hidden files/dirs if present. +cp -R "${PROJECT_TEMPLATE_DIR}/." "$TARGET_DIR/" + +# If we wrote a localized README.md, remove the localized variants to keep the +# project dir clean (optional; safe if they don't exist). +rm -f "$TARGET_DIR/README.en_US.md" "$TARGET_DIR/README.zh_CN.md" || true + +# Ensure entrypoint is executable if present. +if [ -f "$TARGET_DIR/entrypoint.sh" ]; then + chmod +x "$TARGET_DIR/entrypoint.sh" +fi + +mkdir -p \ + /tmp/nginx-devbox/client-body \ + /tmp/nginx-devbox/proxy \ + /tmp/nginx-devbox/fastcgi \ + /tmp/nginx-devbox/uwsgi \ + /tmp/nginx-devbox/scgi +chmod -R 1777 /tmp/nginx-devbox + +# Set ownership to default devbox user +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$TARGET_DIR" diff --git a/runtime-images/languages/java/openjdk17-nginx-private/nginx.conf b/runtime-images/languages/java/openjdk17-nginx-private/nginx.conf new file mode 100644 index 00000000..29d97707 --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/nginx.conf @@ -0,0 +1,36 @@ +worker_processes auto; +pid /tmp/nginx-devbox.pid; +error_log /tmp/nginx-devbox-error.log warn; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + access_log /tmp/nginx-devbox-access.log; + + client_body_temp_path /tmp/nginx-devbox/client-body; + proxy_temp_path /tmp/nginx-devbox/proxy; + fastcgi_temp_path /tmp/nginx-devbox/fastcgi; + uwsgi_temp_path /tmp/nginx-devbox/uwsgi; + scgi_temp_path /tmp/nginx-devbox/scgi; + + gzip on; + + # DevBox templates run as the devbox user; avoid distro defaults that may + # listen on privileged ports or write to root-owned paths. + include /home/devbox/project/*.conf; +} diff --git a/runtime-images/languages/java/openjdk17-nginx-private/project-template/HelloWorld.java b/runtime-images/languages/java/openjdk17-nginx-private/project-template/HelloWorld.java new file mode 100644 index 00000000..2b6b416b --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/project-template/HelloWorld.java @@ -0,0 +1,28 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +public class HelloWorld { + public static void main(String[] args) throws IOException { + int port = Integer.parseInt(System.getenv().getOrDefault("JAVA_APP_PORT", "18080")); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0); + System.out.println("Java app running at http://127.0.0.1:" + port + "/"); + server.createContext("/", new MyHandler()); + server.setExecutor(null); + server.start(); + } + + static class MyHandler implements HttpHandler { + public void handle(HttpExchange exchange) throws IOException { + String response = "Hello from JDK 17 behind Nginx"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } + } + } +} diff --git a/runtime-images/languages/java/openjdk17-nginx-private/project-template/README.en_US.md b/runtime-images/languages/java/openjdk17-nginx-private/project-template/README.en_US.md new file mode 100644 index 00000000..1af47563 --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/project-template/README.en_US.md @@ -0,0 +1,60 @@ +# JDK 17 + Nginx Private Runtime Template + +This template targets private deployment scenarios. It provides an **OpenJDK 17** application runtime with **Nginx 1.22.1** as the front HTTP entrypoint. + +## Runtime Summary + +- Language/runtime version: `OpenJDK 17` +- Web entrypoint: `Nginx 1.22.1` +- Base runtime image: `java-openjdk17-nginx-private` +- Entrypoint script: `entrypoint.sh` +- Default public service port: `8080` +- Default Java app port: `18080` (bound to `127.0.0.1` only) + +## Template Files + +- `HelloWorld.java`: Java HTTP service using `com.sun.net.httpserver` +- `nginx.conf`: project-level Nginx server block that proxies `8080` to the Java app +- `entrypoint.sh`: compiles the Java app, starts the backend process, and starts Nginx in foreground + +## Run in DevBox + +Run commands from `/home/devbox/project`. + +### Development mode + +```bash +bash entrypoint.sh +``` + +Behavior: +- Compiles the app with `javac HelloWorld.java`. +- Starts the Java service on `127.0.0.1:18080`. +- Validates the Nginx config and starts Nginx on `0.0.0.0:8080`. + +### Production mode + +```bash +bash entrypoint.sh production +``` + +Behavior: +- Uses the same startup path as development mode so private deployments keep a single entrypoint. + +## Verify Service + +```bash +curl http://127.0.0.1:8080 +``` + +Expected output: + +```text +Hello from JDK 17 behind Nginx +``` + +## Customization + +- Replace `HelloWorld.java` with your application or framework entrypoint. +- To change the backend port, update both `JAVA_APP_PORT` and `proxy_pass` in `nginx.conf`. +- Extend `nginx.conf` with TLS termination, reverse proxy, caching, static assets, or private deployment routing rules. diff --git a/runtime-images/languages/java/openjdk17-nginx-private/project-template/README.zh_CN.md b/runtime-images/languages/java/openjdk17-nginx-private/project-template/README.zh_CN.md new file mode 100644 index 00000000..ba4b7e5f --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/project-template/README.zh_CN.md @@ -0,0 +1,60 @@ +# JDK 17 + Nginx 私有化运行时模板 + +该模板面向私有化部署场景,提供 **OpenJDK 17** 应用运行环境,并使用 **Nginx 1.22.1** 作为前置 HTTP 入口。 + +## 运行时概览 + +- 语言/运行时版本:`OpenJDK 17` +- Web 入口:`Nginx 1.22.1` +- 基础运行时镜像:`java-openjdk17-nginx-private` +- 启动脚本:`entrypoint.sh` +- 默认对外服务端口:`8080` +- 默认 Java 应用端口:`18080`(仅监听 `127.0.0.1`) + +## 模板文件 + +- `HelloWorld.java`:基于 `com.sun.net.httpserver` 的 Java HTTP 服务 +- `nginx.conf`:项目级 Nginx server 配置,将 `8080` 转发到 Java 应用 +- `entrypoint.sh`:编译 Java 应用、启动后端进程并以前台模式启动 Nginx + +## 在 DevBox 中运行 + +以下命令在 `/home/devbox/project` 目录执行。 + +### 开发模式 + +```bash +bash entrypoint.sh +``` + +行为说明: +- 执行 `javac HelloWorld.java` 编译应用。 +- 在 `127.0.0.1:18080` 启动 Java 服务。 +- 校验 Nginx 配置后,在 `0.0.0.0:8080` 启动 Nginx。 + +### 生产模式 + +```bash +bash entrypoint.sh production +``` + +行为说明: +- 与开发模式使用同一启动路径,便于私有化环境保持入口一致。 + +## 验证服务 + +```bash +curl http://127.0.0.1:8080 +``` + +预期输出: + +```text +Hello from JDK 17 behind Nginx +``` + +## 自定义建议 + +- 将 `HelloWorld.java` 替换为实际业务应用或框架入口。 +- 如需调整后端端口,可同步修改 `JAVA_APP_PORT` 和 `nginx.conf` 的 `proxy_pass`。 +- 可在 `nginx.conf` 中增加 TLS 终止、反向代理、缓存、静态资源或私有化部署所需的路由规则。 diff --git a/runtime-images/languages/java/openjdk17-nginx-private/project-template/entrypoint.sh b/runtime-images/languages/java/openjdk17-nginx-private/project-template/entrypoint.sh new file mode 100755 index 00000000..d5bcabe1 --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/project-template/entrypoint.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -euo pipefail + +if [ "$(id -u)" -eq 0 ] && [ "${DEVBOX_ENTRYPOINT_AS_DEVBOX:-1}" = "1" ] && id devbox >/dev/null 2>&1; then + export DEVBOX_ENTRYPOINT_AS_DEVBOX=0 + SCRIPT_PATH=$(readlink -f "$0") + exec runuser -u devbox -- bash "$SCRIPT_PATH" "$@" +fi + +app_env=${1:-development} +build_target=${JAVA_BUILD_TARGET:-HelloWorld} +java_app_port=${JAVA_APP_PORT:-18080} +nginx_bin=${NGINX_BIN:-/usr/sbin/nginx} +nginx_config=${NGINX_CONFIG:-/etc/nginx/nginx.conf} +java_pid="" +nginx_pid="" + +mkdir -p \ + /tmp/nginx-devbox/client-body \ + /tmp/nginx-devbox/proxy \ + /tmp/nginx-devbox/fastcgi \ + /tmp/nginx-devbox/uwsgi \ + /tmp/nginx-devbox/scgi + +compile_app() { + javac "${build_target}.java" +} + +start_java_app() { + JAVA_APP_PORT="$java_app_port" java "$build_target" & + java_pid=$! +} + +stop_java_app() { + if [ -n "${java_pid:-}" ] && kill -0 "$java_pid" >/dev/null 2>&1; then + kill "$java_pid" >/dev/null 2>&1 || true + wait "$java_pid" >/dev/null 2>&1 || true + fi +} + +stop_services() { + trap - EXIT INT TERM + if [ -n "${nginx_pid:-}" ] && kill -0 "$nginx_pid" >/dev/null 2>&1; then + kill "$nginx_pid" >/dev/null 2>&1 || true + wait "$nginx_pid" >/dev/null 2>&1 || true + fi + stop_java_app +} + +trap stop_services EXIT INT TERM + +if [ "$app_env" = "production" ] || [ "$app_env" = "prod" ]; then + echo "Production environment detected" +else + echo "Development environment detected" +fi + +compile_app +start_java_app + +"$nginx_bin" -t -c "$nginx_config" +"$nginx_bin" -c "$nginx_config" -g 'daemon off;' & +nginx_pid=$! + +wait -n "$java_pid" "$nginx_pid" diff --git a/runtime-images/languages/java/openjdk17-nginx-private/project-template/nginx.conf b/runtime-images/languages/java/openjdk17-nginx-private/project-template/nginx.conf new file mode 100644 index 00000000..9d9f00a7 --- /dev/null +++ b/runtime-images/languages/java/openjdk17-nginx-private/project-template/nginx.conf @@ -0,0 +1,13 @@ +server { + listen 8080; + server_name _; + + location / { + proxy_pass http://127.0.0.1:18080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/runtime-images/languages/java/openjdk25/Dockerfile b/runtime-images/languages/java/openjdk25/Dockerfile new file mode 100644 index 00000000..9703d014 --- /dev/null +++ b/runtime-images/languages/java/openjdk25/Dockerfile @@ -0,0 +1,24 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG RUNTIME_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/java-openjdk25:${RUNTIME_IMAGE_VERSION} +ARG L10N +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV L10N=${L10N} +ENV PROJECT_TEMPLATE_DIR=/project-template +COPY ./project-template ${PROJECT_TEMPLATE_DIR} +COPY ./build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${PROJECT_TEMPLATE_DIR} +# Set the working directory to the default devbox user's project directory +WORKDIR /home/${DEFAULT_DEVBOX_USER}/project diff --git a/runtime-images/languages/java/openjdk25/build.sh b/runtime-images/languages/java/openjdk25/build.sh new file mode 100755 index 00000000..2796bb0e --- /dev/null +++ b/runtime-images/languages/java/openjdk25/build.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +PROJECT_TEMPLATE_DIR=${PROJECT_TEMPLATE_DIR:-/project-template} + +if ! id -u "$DEFAULT_DEVBOX_USER" &>/dev/null; then + echo "User $DEFAULT_DEVBOX_USER does not exist" + exit 1 +fi + +TARGET_DIR="/home/$DEFAULT_DEVBOX_USER/project" +mkdir -p "$TARGET_DIR" + +if [ -f "$PROJECT_TEMPLATE_DIR/README.$L10N.md" ]; then + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md exists. Copying to $TARGET_DIR/README.md" + cp "$PROJECT_TEMPLATE_DIR/README.$L10N.md" "$TARGET_DIR/README.md" +else + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md does not exist. Skipping copy." +fi + +DOCS_DIR=${DOCS_DIR:-/usr/share/devbox/docs} +if [ -f "$DOCS_DIR/README.s6-user-guide.$L10N.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.$L10N.md" "$TARGET_DIR/README.s6-user-guide.md" +elif [ -f "$DOCS_DIR/README.s6-user-guide.en_US.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.en_US.md" "$TARGET_DIR/README.s6-user-guide.md" +fi + +cp -R "${PROJECT_TEMPLATE_DIR}/." "$TARGET_DIR/" +rm -f "$TARGET_DIR/README.en_US.md" "$TARGET_DIR/README.zh_CN.md" || true + +if [ -f "$TARGET_DIR/entrypoint.sh" ]; then + chmod +x "$TARGET_DIR/entrypoint.sh" +fi + +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$TARGET_DIR" diff --git a/runtime-images/languages/java/openjdk25/project-template/HelloWorld.java b/runtime-images/languages/java/openjdk25/project-template/HelloWorld.java new file mode 100644 index 00000000..7c658f00 --- /dev/null +++ b/runtime-images/languages/java/openjdk25/project-template/HelloWorld.java @@ -0,0 +1,28 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +public class HelloWorld { + public static void main(String[] args) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + System.out.println("Server running at http://0.0.0.0:8080/"); + server.createContext("/", new MyHandler()); + server.setExecutor(null); + server.start(); + } + + static class MyHandler implements HttpHandler { + public void handle(HttpExchange exchange) throws IOException { + byte[] response = "Hello, World!".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(response); + } + } + } +} diff --git a/runtime-images/languages/java/openjdk25/project-template/README.en_US.md b/runtime-images/languages/java/openjdk25/project-template/README.en_US.md new file mode 100644 index 00000000..36da73d3 --- /dev/null +++ b/runtime-images/languages/java/openjdk25/project-template/README.en_US.md @@ -0,0 +1,52 @@ +# Java OpenJDK 25 Runtime Template + +This template provides a minimal Java HTTP service for the DevBox **OpenJDK 25** runtime. The image uses Eclipse Temurin OpenJDK `25.0.4.1+1` and Apache Maven `3.9.16`. + +## Runtime Summary + +- Language/runtime version: `Eclipse Temurin OpenJDK 25.0.4.1+1` +- Build tool: `Apache Maven 3.9.16` +- Base runtime image: `java-openjdk25` +- Entrypoint script: `entrypoint.sh` +- Default service port: `8080` + +## Template Files + +- `HelloWorld.java`: HTTP service using `com.sun.net.httpserver` +- `entrypoint.sh`: compile-and-run script for development and production modes + +## Run in DevBox + +Run commands from `/home/devbox/project`. + +### Development mode + +```bash +bash entrypoint.sh +``` + +### Production mode + +```bash +bash entrypoint.sh production +``` + +Both modes compile the application with JDK 25 and start it with `java HelloWorld`. + +## Verify Service + +```bash +curl http://127.0.0.1:8080 +``` + +Expected output: + +```text +Hello, World! +``` + +## Customization + +- Split `HelloWorld.java` into a package-based structure for larger projects. +- Use the included Maven installation when dependency management is needed. +- Replace the entrypoint commands when switching to a packaged JAR or framework-based application. diff --git a/runtime-images/languages/java/openjdk25/project-template/README.zh_CN.md b/runtime-images/languages/java/openjdk25/project-template/README.zh_CN.md new file mode 100644 index 00000000..56f38c9f --- /dev/null +++ b/runtime-images/languages/java/openjdk25/project-template/README.zh_CN.md @@ -0,0 +1,52 @@ +# Java OpenJDK 25 运行时模板 + +该模板为 DevBox **OpenJDK 25** 运行时提供一个最小可运行的 Java HTTP 服务。镜像使用 Eclipse Temurin OpenJDK `25.0.4.1+1` 和 Apache Maven `3.9.16`。 + +## 运行时概览 + +- 语言/运行时版本:`Eclipse Temurin OpenJDK 25.0.4.1+1` +- 构建工具:`Apache Maven 3.9.16` +- 基础运行时镜像:`java-openjdk25` +- 启动脚本:`entrypoint.sh` +- 默认服务端口:`8080` + +## 模板文件 + +- `HelloWorld.java`:基于 `com.sun.net.httpserver` 的 HTTP 服务 +- `entrypoint.sh`:开发和生产模式通用的编译运行脚本 + +## 在 DevBox 中运行 + +以下命令在 `/home/devbox/project` 目录执行。 + +### 开发模式 + +```bash +bash entrypoint.sh +``` + +### 生产模式 + +```bash +bash entrypoint.sh production +``` + +两种模式都会使用 JDK 25 编译应用,然后通过 `java HelloWorld` 启动服务。 + +## 验证服务 + +```bash +curl http://127.0.0.1:8080 +``` + +预期输出: + +```text +Hello, World! +``` + +## 自定义建议 + +- 项目变大后建议将 `HelloWorld.java` 迁移为 package 目录结构。 +- 需要依赖管理时可直接使用镜像内置的 Maven。 +- 切换为可执行 JAR 或框架应用后,请同步更新 `entrypoint.sh`。 diff --git a/runtime-images/languages/java/openjdk25/project-template/entrypoint.sh b/runtime-images/languages/java/openjdk25/project-template/entrypoint.sh new file mode 100755 index 00000000..6c23c5ca --- /dev/null +++ b/runtime-images/languages/java/openjdk25/project-template/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +if [ "$(id -u)" -eq 0 ] && [ "${DEVBOX_ENTRYPOINT_AS_DEVBOX:-1}" = "1" ] && id devbox >/dev/null 2>&1; then + export DEVBOX_ENTRYPOINT_AS_DEVBOX=0 + SCRIPT_PATH=$(readlink -f "$0") + exec runuser -u devbox -- bash "$SCRIPT_PATH" "$@" +fi + +app_env=${1:-development} +build_target="HelloWorld" + +dev_commands() { + echo "Running development environment commands..." + javac "${build_target}.java" + exec java "${build_target}" +} + +prod_commands() { + echo "Running production environment commands..." + javac "${build_target}.java" + exec java "${build_target}" +} + +if [ "$app_env" = "production" ] || [ "$app_env" = "prod" ]; then + echo "Production environment detected" + prod_commands +else + echo "Development environment detected" + dev_commands +fi diff --git a/runtime-images/languages/java/openjdk8/Dockerfile b/runtime-images/languages/java/openjdk8/Dockerfile new file mode 100644 index 00000000..bd002524 --- /dev/null +++ b/runtime-images/languages/java/openjdk8/Dockerfile @@ -0,0 +1,24 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG RUNTIME_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/java-openjdk8:${RUNTIME_IMAGE_VERSION} +ARG L10N +LABEL org.opencontainers.image.authors="The Devbox Authors" +ENV L10N=${L10N} +ENV PROJECT_TEMPLATE_DIR=/project-template +COPY ./project-template ${PROJECT_TEMPLATE_DIR} +COPY ./build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${PROJECT_TEMPLATE_DIR} +# Set the working directory to the default devbox user's project directory +WORKDIR /home/${DEFAULT_DEVBOX_USER}/project diff --git a/runtime-images/languages/java/openjdk8/build.sh b/runtime-images/languages/java/openjdk8/build.sh new file mode 100755 index 00000000..2796bb0e --- /dev/null +++ b/runtime-images/languages/java/openjdk8/build.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +PROJECT_TEMPLATE_DIR=${PROJECT_TEMPLATE_DIR:-/project-template} + +if ! id -u "$DEFAULT_DEVBOX_USER" &>/dev/null; then + echo "User $DEFAULT_DEVBOX_USER does not exist" + exit 1 +fi + +TARGET_DIR="/home/$DEFAULT_DEVBOX_USER/project" +mkdir -p "$TARGET_DIR" + +if [ -f "$PROJECT_TEMPLATE_DIR/README.$L10N.md" ]; then + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md exists. Copying to $TARGET_DIR/README.md" + cp "$PROJECT_TEMPLATE_DIR/README.$L10N.md" "$TARGET_DIR/README.md" +else + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md does not exist. Skipping copy." +fi + +DOCS_DIR=${DOCS_DIR:-/usr/share/devbox/docs} +if [ -f "$DOCS_DIR/README.s6-user-guide.$L10N.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.$L10N.md" "$TARGET_DIR/README.s6-user-guide.md" +elif [ -f "$DOCS_DIR/README.s6-user-guide.en_US.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.en_US.md" "$TARGET_DIR/README.s6-user-guide.md" +fi + +cp -R "${PROJECT_TEMPLATE_DIR}/." "$TARGET_DIR/" +rm -f "$TARGET_DIR/README.en_US.md" "$TARGET_DIR/README.zh_CN.md" || true + +if [ -f "$TARGET_DIR/entrypoint.sh" ]; then + chmod +x "$TARGET_DIR/entrypoint.sh" +fi + +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$TARGET_DIR" diff --git a/runtime-images/languages/java/openjdk8/project-template/HelloWorld.java b/runtime-images/languages/java/openjdk8/project-template/HelloWorld.java new file mode 100644 index 00000000..7c658f00 --- /dev/null +++ b/runtime-images/languages/java/openjdk8/project-template/HelloWorld.java @@ -0,0 +1,28 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +public class HelloWorld { + public static void main(String[] args) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + System.out.println("Server running at http://0.0.0.0:8080/"); + server.createContext("/", new MyHandler()); + server.setExecutor(null); + server.start(); + } + + static class MyHandler implements HttpHandler { + public void handle(HttpExchange exchange) throws IOException { + byte[] response = "Hello, World!".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(response); + } + } + } +} diff --git a/runtime-images/languages/java/openjdk8/project-template/README.en_US.md b/runtime-images/languages/java/openjdk8/project-template/README.en_US.md new file mode 100644 index 00000000..748e9c7d --- /dev/null +++ b/runtime-images/languages/java/openjdk8/project-template/README.en_US.md @@ -0,0 +1,52 @@ +# Java OpenJDK 8 Runtime Template + +This template provides a minimal Java HTTP service for the DevBox **OpenJDK 8** runtime. The image uses Eclipse Temurin OpenJDK `8u492-b09` and Apache Maven `3.9.16`. + +## Runtime Summary + +- Language/runtime version: `Eclipse Temurin OpenJDK 8u492-b09` +- Build tool: `Apache Maven 3.9.16` +- Base runtime image: `java-openjdk8` +- Entrypoint script: `entrypoint.sh` +- Default service port: `8080` + +## Template Files + +- `HelloWorld.java`: HTTP service using `com.sun.net.httpserver` +- `entrypoint.sh`: compile-and-run script for development and production modes + +## Run in DevBox + +Run commands from `/home/devbox/project`. + +### Development mode + +```bash +bash entrypoint.sh +``` + +### Production mode + +```bash +bash entrypoint.sh production +``` + +Both modes compile the application with JDK 8 and start it with `java HelloWorld`. + +## Verify Service + +```bash +curl http://127.0.0.1:8080 +``` + +Expected output: + +```text +Hello, World! +``` + +## Customization + +- Split `HelloWorld.java` into a package-based structure for larger projects. +- Use the included Maven installation when dependency management is needed. +- Replace the entrypoint commands when switching to a packaged JAR or framework-based application. diff --git a/runtime-images/languages/java/openjdk8/project-template/README.zh_CN.md b/runtime-images/languages/java/openjdk8/project-template/README.zh_CN.md new file mode 100644 index 00000000..14676c4d --- /dev/null +++ b/runtime-images/languages/java/openjdk8/project-template/README.zh_CN.md @@ -0,0 +1,52 @@ +# Java OpenJDK 8 运行时模板 + +该模板为 DevBox **OpenJDK 8** 运行时提供一个最小可运行的 Java HTTP 服务。镜像使用 Eclipse Temurin OpenJDK `8u492-b09` 和 Apache Maven `3.9.16`。 + +## 运行时概览 + +- 语言/运行时版本:`Eclipse Temurin OpenJDK 8u492-b09` +- 构建工具:`Apache Maven 3.9.16` +- 基础运行时镜像:`java-openjdk8` +- 启动脚本:`entrypoint.sh` +- 默认服务端口:`8080` + +## 模板文件 + +- `HelloWorld.java`:基于 `com.sun.net.httpserver` 的 HTTP 服务 +- `entrypoint.sh`:开发和生产模式通用的编译运行脚本 + +## 在 DevBox 中运行 + +以下命令在 `/home/devbox/project` 目录执行。 + +### 开发模式 + +```bash +bash entrypoint.sh +``` + +### 生产模式 + +```bash +bash entrypoint.sh production +``` + +两种模式都会使用 JDK 8 编译应用,然后通过 `java HelloWorld` 启动服务。 + +## 验证服务 + +```bash +curl http://127.0.0.1:8080 +``` + +预期输出: + +```text +Hello, World! +``` + +## 自定义建议 + +- 项目变大后建议将 `HelloWorld.java` 迁移为 package 目录结构。 +- 需要依赖管理时可直接使用镜像内置的 Maven。 +- 切换为可执行 JAR 或框架应用后,请同步更新 `entrypoint.sh`。 diff --git a/runtime-images/languages/java/openjdk8/project-template/entrypoint.sh b/runtime-images/languages/java/openjdk8/project-template/entrypoint.sh new file mode 100755 index 00000000..c42227da --- /dev/null +++ b/runtime-images/languages/java/openjdk8/project-template/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +if [ "$(id -u)" -eq 0 ] && [ "${DEVBOX_ENTRYPOINT_AS_DEVBOX:-1}" = "1" ] && id devbox >/dev/null 2>&1; then + export DEVBOX_ENTRYPOINT_AS_DEVBOX=0 + SCRIPT_PATH=$(readlink -f "$0") + exec runuser -u devbox -- bash "$SCRIPT_PATH" "$@" +fi + +app_env=${1:-development} +build_target=${JAVA_BUILD_TARGET:-HelloWorld} + +if [ "$app_env" = "production" ] || [ "$app_env" = "prod" ]; then + echo "Production environment detected" +else + echo "Development environment detected" +fi + +javac "${build_target}.java" +exec java "$build_target" diff --git a/runtime-images/operating-systems/fedora/44/Dockerfile b/runtime-images/operating-systems/fedora/44/Dockerfile new file mode 100644 index 00000000..218e28d6 --- /dev/null +++ b/runtime-images/operating-systems/fedora/44/Dockerfile @@ -0,0 +1,25 @@ +# These ARGs can be overridden at build time to customize the image +ARG REPO=labring-actions/devbox-base-images +ARG REGISTRY=ghcr.io +ARG L10N=en_US +ARG L10N_NORMALIZED=en-us +ARG DEFAULT_DEVBOX_USER=devbox + +# These ARGs are not recommended to be overridden at build time. +# Instead, update the Dockerfile directly for consistent builds, +# and release new versions as needed. +ARG OS_IMAGE_VERSION=v0.0.1-alpha.1-${L10N_NORMALIZED} + +FROM ${REGISTRY}/${REPO}/fedora-44:${OS_IMAGE_VERSION} +ARG L10N +ARG DEFAULT_DEVBOX_USER +ENV L10N=${L10N} +ENV PROJECT_TEMPLATE_DIR=/project-template +COPY ./project-template ${PROJECT_TEMPLATE_DIR} +COPY ./build.sh /build.sh +RUN chmod +x /build.sh && \ + /build.sh && \ + rm -f /build.sh && \ + rm -rf ${PROJECT_TEMPLATE_DIR} +# Set the working directory to the default devbox user's project directory +WORKDIR /home/${DEFAULT_DEVBOX_USER}/project diff --git a/runtime-images/operating-systems/fedora/44/build.sh b/runtime-images/operating-systems/fedora/44/build.sh new file mode 100644 index 00000000..75b676b5 --- /dev/null +++ b/runtime-images/operating-systems/fedora/44/build.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +L10N=${L10N:-en_US} +DEFAULT_DEVBOX_USER=${DEFAULT_DEVBOX_USER:-devbox} +PROJECT_TEMPLATE_DIR=${PROJECT_TEMPLATE_DIR:-/project-templates} +DOCS_DIR=${DOCS_DIR:-/usr/share/devbox/docs} + +if ! id -u "$DEFAULT_DEVBOX_USER" &>/dev/null; then + echo "User $DEFAULT_DEVBOX_USER does not exist" + exit 1 +fi + +TARGET_DIR="/home/$DEFAULT_DEVBOX_USER/project" +mkdir -p "$TARGET_DIR" + +if [ -f "$PROJECT_TEMPLATE_DIR/README.$L10N.md" ]; then + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md exists. Copying to $TARGET_DIR/README.md" + cp "$PROJECT_TEMPLATE_DIR/README.$L10N.md" "$TARGET_DIR/README.md" +else + echo "README $PROJECT_TEMPLATE_DIR/README.$L10N.md does not exist. Skipping copy." +fi + +if [ -f "$DOCS_DIR/README.s6-user-guide.$L10N.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.$L10N.md" "$TARGET_DIR/README.s6-user-guide.md" +elif [ -f "$DOCS_DIR/README.s6-user-guide.en_US.md" ]; then + cp "$DOCS_DIR/README.s6-user-guide.en_US.md" "$TARGET_DIR/README.s6-user-guide.md" +fi + +cp "$PROJECT_TEMPLATE_DIR/"*.sh "$TARGET_DIR/" +chmod 0755 "$TARGET_DIR/"*.sh + +# Set ownership to default devbox user +chown -R "$DEFAULT_DEVBOX_USER:$DEFAULT_DEVBOX_USER" "$TARGET_DIR" diff --git a/runtime-images/operating-systems/fedora/44/project-template/README.en_US.md b/runtime-images/operating-systems/fedora/44/project-template/README.en_US.md new file mode 100644 index 00000000..57ce11ea --- /dev/null +++ b/runtime-images/operating-systems/fedora/44/project-template/README.en_US.md @@ -0,0 +1,46 @@ +# Fedora 44 Runtime Template + +This template provides a minimal **operating-system runtime** based on Fedora 44. +Use it when you need a modern RPM-family Linux base and full control of your language, framework, or application stack. + +## Runtime Summary + +- OS version: `Fedora 44` +- Base runtime image: `fedora-44` +- Entrypoint script: `entrypoint.sh` +- Default service port: `8080` + +## Template Files + +- `entrypoint.sh`: creates a static `index.html` and starts a lightweight HTTP server + +## Run in DevBox + +Run commands from `/home/devbox/project`. + +```bash +bash entrypoint.sh +``` + +Behavior: +- Uses `PORT` environment variable when provided, defaults to `8080`. +- Serves files from `/home/devbox/project/www`. +- Prefers `busybox httpd` and falls back to `python3 -m http.server` when that applet is unavailable. + +## Verify Service + +```bash +curl http://127.0.0.1:8080 +``` + +Expected output: + +```text +Hello, World! +``` + +## Customization + +- Replace `entrypoint.sh` with your own process startup script. +- Use `dnf` to install application dependencies in this Fedora base. +- Align container exposed ports with your service port. diff --git a/runtime-images/operating-systems/fedora/44/project-template/README.zh_CN.md b/runtime-images/operating-systems/fedora/44/project-template/README.zh_CN.md new file mode 100644 index 00000000..5e13aad6 --- /dev/null +++ b/runtime-images/operating-systems/fedora/44/project-template/README.zh_CN.md @@ -0,0 +1,46 @@ +# Fedora 44 运行时模板 + +该模板提供一个基于 Fedora 44 的最小化**操作系统运行时**。 +适用于需要较新的 RPM 系 Linux 基础环境,并在其上自行安装语言、框架或业务依赖的场景。 + +## 运行时概览 + +- 系统版本:`Fedora 44` +- 基础运行时镜像:`fedora-44` +- 启动脚本:`entrypoint.sh` +- 默认服务端口:`8080` + +## 模板文件 + +- `entrypoint.sh`:生成静态 `index.html` 并启动轻量 HTTP 服务 + +## 在 DevBox 中运行 + +以下命令在 `/home/devbox/project` 目录执行。 + +```bash +bash entrypoint.sh +``` + +行为说明: +- 支持通过 `PORT` 环境变量覆盖端口,默认值为 `8080`。 +- 默认从 `/home/devbox/project/www` 目录提供静态内容。 +- 优先使用 `busybox httpd`,不可用时回退到 `python3 -m http.server`。 + +## 验证服务 + +```bash +curl http://127.0.0.1:8080 +``` + +预期输出: + +```text +Hello, World! +``` + +## 自定义建议 + +- 可将 `entrypoint.sh` 替换为你的进程启动脚本。 +- 使用 `dnf` 在该 Fedora 基础镜像中安装业务依赖。 +- 保持容器暴露端口与服务监听端口一致。 diff --git a/runtime-images/operating-systems/fedora/44/project-template/entrypoint.sh b/runtime-images/operating-systems/fedora/44/project-template/entrypoint.sh new file mode 100644 index 00000000..b178e7fa --- /dev/null +++ b/runtime-images/operating-systems/fedora/44/project-template/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +if [ "$(id -u)" -eq 0 ] && [ "${DEVBOX_ENTRYPOINT_AS_DEVBOX:-1}" = "1" ] && id devbox >/dev/null 2>&1; then + export DEVBOX_ENTRYPOINT_AS_DEVBOX=0 + SCRIPT_PATH=$(readlink -f "$0") + exec runuser -u devbox -- bash "$SCRIPT_PATH" "$@" +fi + +# Serve a simple "Hello, World!" page +PORT=${PORT:-8080} +PROJECT_DIR=${PROJECT_DIR:-/home/devbox/project} +ROOT_DIR="$PROJECT_DIR/www" +mkdir -p "$ROOT_DIR" + +cat >"$ROOT_DIR/index.html" <<'HTML' +Hello, World! +HTML + +echo "Starting HTTP server on port $PORT (serving $ROOT_DIR)" + +if command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx httpd; then + exec busybox httpd -f -p "$PORT" -h "$ROOT_DIR" +fi + +if command -v python3 >/dev/null 2>&1; then + cd "$ROOT_DIR" + exec python3 -m http.server "$PORT" --bind 0.0.0.0 +fi + +echo "No supported HTTP server found (busybox httpd or python3 http.server)." >&2 +exit 1 diff --git a/tests/runtime-conformance/README.md b/tests/runtime-conformance/README.md index dda9c426..0d294181 100644 --- a/tests/runtime-conformance/README.md +++ b/tests/runtime-conformance/README.md @@ -41,6 +41,9 @@ Runtime-specific checks: | `languages/go/1.22.5` | exact Go version, matching `GOARCH`, prebuilt binary, `GOPROXY` for `zh_CN`, writable Go cache, root/devbox entrypoint order | | `languages/go/1.23.0` | exact Go version, matching `GOARCH`, prebuilt binary, `GOPROXY` for `zh_CN`, writable Go cache, root/devbox entrypoint order | | `languages/java/openjdk17` | Java/Javac 17, UTF-8, Maven mirror for `zh_CN`, root/devbox entrypoint order | +| `languages/java/openjdk17-nginx-private` | Java/Javac 17, Maven mirror for `zh_CN`, Nginx 1.22.1 config/runtime paths, project proxy config, root/devbox entrypoint order | +| `languages/java/openjdk8` | Temurin Java/Javac 1.8.0_492, UTF-8, Maven 3.9.16, Maven mirror for `zh_CN`, root/devbox entrypoint order | +| `languages/java/openjdk25` | Temurin Java/Javac 25.0.4.1+1, UTF-8, Maven 3.9.16, Maven mirror for `zh_CN`, root/devbox entrypoint order | | `languages/net/8.0` | .NET 8 prefix, Tencent NuGet mirror for `zh_CN`, no `nuget.org` or unreachable Azure China source for `zh_CN`, root/devbox entrypoint order | | `languages/net/10.0` | .NET 10 prefix, Tencent NuGet mirror for `zh_CN`, no `nuget.org` or unreachable Azure China source for `zh_CN`, root/devbox entrypoint order | | `languages/node.js/18` | Node 18, npm/yarn/pnpm, npm mirror for `zh_CN`, root/devbox entrypoint order | @@ -55,5 +58,5 @@ Runtime-specific checks: | `frameworks/nest.js/v11` | Node 20, Nest CLI, npm mirror for `zh_CN`, build output, root/devbox entrypoint order | | `frameworks/nginx/1.22.1` | Nginx 1.22.1, config test, `/tmp/nginx-devbox` runtime paths, no distro default include pollution, root/devbox entrypoint order | | `frameworks/openclaw/latest` | Node 22, OpenClaw, Clawhub, Bun, npm mirror for `zh_CN`, safe `.env.example`, root/devbox entrypoint order | -| `frameworks/sandbox/v1` | workspace ownership, codex-gateway, Codex CLI, Node/npm, Python/pip, kubectl, helm, bun, ripgrep, bubblewrap, npm/pip mirrors for `zh_CN` | -| `frameworks/sandbox/fastgpt` | `v1` checks plus code-server binary, default `CODE_SERVER_BIND_ADDR=0.0.0.0:1318`, `DEVBOX_JWT_SECRET` password auth, and s6 service registration | +| `frameworks/sandbox/v1` | workspace ownership, codex-gateway, Codex CLI, Node/npm, Python/pip, kubectl, helm, gh, bun, ripgrep, bubblewrap, Railpack CLI, VersityGW POSIX S3 context service, npm/pip mirrors for `zh_CN` | +| `frameworks/sandbox/fastgpt` | common sandbox checks plus fastgpt-ide-agent binary and s6 service registration | diff --git a/tests/runtime-conformance/run.sh b/tests/runtime-conformance/run.sh index 276d52e2..7d598fd4 100755 --- a/tests/runtime-conformance/run.sh +++ b/tests/runtime-conformance/run.sh @@ -47,6 +47,13 @@ assert_command() { command -v "$command_name" >/dev/null 2>&1 || fail "missing command: $command_name" } +assert_env_equals() { + local name="$1" + local expected="$2" + local actual="${!name:-}" + [ "$actual" = "$expected" ] || fail "$name is '$actual', expected '$expected'" +} + print_runtime_context() { log "runtime context" printf 'runtime_path=%s\n' "$RUNTIME_PATH" @@ -468,15 +475,32 @@ check_python_runtime() { } check_java_runtime() { + local expected_version="$1" assert_command java assert_command javac - javac --version | grep 'javac 17' >/dev/null || fail "javac is not 17" - java -version 2>&1 | grep '17' >/dev/null || fail "java is not 17" + javac -version 2>&1 | grep "javac $expected_version" >/dev/null || fail "javac is not $expected_version" + java -version 2>&1 | grep "$expected_version" >/dev/null || fail "java is not $expected_version" java -XshowSettings:properties -version 2>&1 | grep 'file.encoding = UTF-8' >/dev/null || fail "Java file.encoding is not UTF-8" assert_file "$PROJECT_DIR/HelloWorld.java" require_zh_maven_mirror } +check_java8_runtime() { + check_java_runtime 1.8.0_492 + assert_command mvn + java -version 2>&1 | grep 'Temurin' >/dev/null || fail "Java vendor is not Temurin" + mvn -version | grep 'Apache Maven 3.9.16' >/dev/null || fail "Maven is not 3.9.16" + mvn -version | grep 'Java version: 1.8.0_492' >/dev/null || fail "Maven is not using Java 1.8.0_492" +} + +check_java_openjdk25_runtime() { + check_java_runtime 25.0.4.1 + assert_command mvn + java -version 2>&1 | grep 'Temurin' >/dev/null || fail "Java vendor is not Temurin" + mvn -version | grep 'Apache Maven 3.9.16' >/dev/null || fail "Maven is not 3.9.16" + mvn -version | grep 'Java version: 25.0.4.1' >/dev/null || fail "Maven is not using Java 25.0.4.1" +} + check_c_runtime() { assert_command gcc gcc --version | grep '12.2.0' >/dev/null || fail "gcc is not 12.2.0" @@ -518,6 +542,12 @@ check_nginx_runtime() { fi } +check_java_nginx_private_runtime() { + check_java_runtime 17 + check_nginx_runtime + assert_file "$PROJECT_DIR/nginx.conf" +} + check_nest_runtime() { check_node_runtime 20 assert_command nest @@ -555,15 +585,27 @@ check_sandbox_runtime() { assert_command bun assert_command rg assert_command bwrap + assert_command railpack + railpack --version >/dev/null + railpack schema >/dev/null assert_file /etc/s6-overlay/s6-rc.d/codex-gateway/run + if [ "$RUNTIME_PATH" = "frameworks/sandbox/v1" ]; then + assert_command gh + assert_command versitygw + versitygw --version >/dev/null + assert_file /etc/s6-overlay/s6-rc.d/versitygw/run + assert_file /etc/s6-overlay/s6-rc.d/versitygw/finish + assert_dir "${CODEX_GATEWAY_CWD:-$WORKSPACE_DIR}/.versitygw-s3/kaniko-contexts/contexts" + assert_env_equals AWS_ACCESS_KEY_ID admin + assert_env_equals AWS_REGION sealos-internal + assert_env_equals S3_ENDPOINT http://127.0.0.1:1319 + assert_env_equals S3_FORCE_PATH_STYLE true + assert_env_equals KANIKO_CONTEXT_S3_BASE s3://kaniko-contexts/contexts + fi if [ "$RUNTIME_PATH" = "frameworks/sandbox/fastgpt" ]; then - assert_command code-server - [ "${CODE_SERVER_BIND_ADDR:-}" = "0.0.0.0:1318" ] || fail "CODE_SERVER_BIND_ADDR default should be 0.0.0.0:1318" - assert_file /etc/s6-overlay/s6-rc.d/code-server/run - assert_file /etc/s6-overlay/s6-rc.d/code-server/finish - grep -Fq 'CODE_SERVER_PASSWORD=${DEVBOX_JWT_SECRET:-}' /etc/s6-overlay/s6-rc.d/code-server/run || fail "code-server password should come from DEVBOX_JWT_SECRET" - grep -Fq 'export PASSWORD="$CODE_SERVER_PASSWORD"' /etc/s6-overlay/s6-rc.d/code-server/run || fail "code-server should export PASSWORD" - grep -q -- '--auth password' /etc/s6-overlay/s6-rc.d/code-server/run || fail "code-server should require password auth" + assert_executable /usr/local/bin/fastgpt-ide-agent + assert_file /etc/s6-overlay/s6-rc.d/fastgpt-ide-agent/run + assert_file /etc/s6-overlay/s6-rc.d/fastgpt-ide-agent/finish fi require_zh_npm_mirror require_zh_pip_mirror @@ -575,6 +617,9 @@ check_runtime_specifics() { operating-systems/anolis/23.4) check_os_runtime anolis ;; + operating-systems/fedora/44) + check_os_runtime fedora + ;; operating-systems/debian/12.6) check_os_runtime debian ;; @@ -601,7 +646,16 @@ check_runtime_specifics() { check_go_runtime 1.23.0 ;; languages/java/openjdk17) - check_java_runtime + check_java_runtime 17 + ;; + languages/java/openjdk8) + check_java8_runtime + ;; + languages/java/openjdk25) + check_java_openjdk25_runtime + ;; + languages/java/openjdk17-nginx-private) + check_java_nginx_private_runtime ;; languages/net/8.0) check_dotnet_runtime 8 diff --git a/tests/runtime-conformance/test_java_openjdk25_runtime.py b/tests/runtime-conformance/test_java_openjdk25_runtime.py new file mode 100644 index 00000000..8e7b5717 --- /dev/null +++ b/tests/runtime-conformance/test_java_openjdk25_runtime.py @@ -0,0 +1,36 @@ +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +RUNTIME_ROOT = REPO_ROOT / "runtime-images/languages/java/openjdk25" + + +class JavaOpenJDK25RuntimeTests(unittest.TestCase): + def test_conformance_runner_registers_runtime(self): + runner = REPO_ROOT / "tests/runtime-conformance/run.sh" + content = runner.read_text(encoding="utf-8") + + self.assertIn("languages/java/openjdk25)", content) + self.assertIn("check_java_openjdk25_runtime", content) + + def test_required_runtime_files_exist(self): + required_files = [ + RUNTIME_ROOT / "Dockerfile", + RUNTIME_ROOT / "build.sh", + RUNTIME_ROOT / "project-template/HelloWorld.java", + RUNTIME_ROOT / "project-template/entrypoint.sh", + RUNTIME_ROOT / "project-template/README.en_US.md", + RUNTIME_ROOT / "project-template/README.zh_CN.md", + REPO_ROOT / "base-images/languages/java/openjdk25/Dockerfile", + REPO_ROOT / "base-images/languages/java/openjdk25/build.sh", + REPO_ROOT / "tests/runtime-smoke/languages/java/openjdk25/smoke.sh", + ] + + for path in required_files: + with self.subTest(path=path): + self.assertTrue(path.is_file(), f"missing file: {path}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/runtime-smoke/frameworks/sandbox/v1/smoke.sh b/tests/runtime-smoke/frameworks/sandbox/v1/smoke.sh new file mode 100755 index 00000000..04d2732e --- /dev/null +++ b/tests/runtime-smoke/frameworks/sandbox/v1/smoke.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -eu + +workspace_dir=/home/devbox/workspace + +if [ ! -d "$workspace_dir" ]; then + echo "Missing workspace dir: $workspace_dir" >&2 + exit 1 +fi + +# load profile env (best effort) +set +u +# shellcheck disable=SC1091 +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + # shellcheck disable=SC1090 + [ -r "$f" ] && . "$f" || true + done +fi +# shellcheck disable=SC1091 +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "SHELL=${SHELL:-}" + echo "PATH=$PATH" + for cmd in codex node npm python3 pip3 kubectl helm gh buildctl bun rg bwrap railpack versitygw; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +cd "$workspace_dir" + +for cmd in codex node npm python3 pip3 kubectl helm gh buildctl bun rg bwrap railpack versitygw; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "$cmd not found" >&2 + exit 1 + fi +done + +if [ ! -x /usr/local/bin/codex-gateway ]; then + echo "codex-gateway binary not found" >&2 + exit 1 +fi + +railpack --version >/dev/null +railpack schema >/dev/null +versitygw --version >/dev/null + +if [ "${AWS_ACCESS_KEY_ID:-}" != "admin" ]; then + echo "AWS_ACCESS_KEY_ID should default to admin" >&2 + exit 1 +fi + +if [ "${AWS_REGION:-}" != "sealos-internal" ]; then + echo "AWS_REGION should default to sealos-internal" >&2 + exit 1 +fi + +if [ "${S3_ENDPOINT:-}" != "http://127.0.0.1:1319" ]; then + echo "S3_ENDPOINT should point kaniko at local versitygw" >&2 + exit 1 +fi + +if [ "${S3_FORCE_PATH_STYLE:-}" != "true" ]; then + echo "S3_FORCE_PATH_STYLE should default to true" >&2 + exit 1 +fi + +if [ ! -f /etc/s6-overlay/s6-rc.d/versitygw/run ]; then + echo "versitygw s6 run file not found" >&2 + exit 1 +fi + +if [ "${KANIKO_CONTEXT_S3_BASE:-}" != "s3://kaniko-contexts/contexts" ]; then + echo "KANIKO_CONTEXT_S3_BASE should default to s3://kaniko-contexts/contexts" >&2 + exit 1 +fi + +if [ ! -d "$workspace_dir/.versitygw-s3/kaniko-contexts/contexts" ]; then + echo "kaniko context POSIX directory not found" >&2 + exit 1 +fi + +echo "ok" diff --git a/tests/runtime-smoke/languages/java/openjdk17-nginx-private/smoke.sh b/tests/runtime-smoke/languages/java/openjdk17-nginx-private/smoke.sh new file mode 100755 index 00000000..f437459a --- /dev/null +++ b/tests/runtime-smoke/languages/java/openjdk17-nginx-private/smoke.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -eu + +project_dir=/home/devbox/project + +if [ ! -d "$project_dir" ]; then + echo "Missing project dir: $project_dir" >&2 + exit 1 +fi + +# load profile env (best effort) +set +u +# shellcheck disable=SC1091 +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + # shellcheck disable=SC1090 + [ -r "$f" ] && . "$f" || true + done +fi +# shellcheck disable=SC1091 +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "SHELL=${SHELL:-}" + echo "PATH=$PATH" + for cmd in go python3 node php dotnet java javac nginx gcc g++ cargo rustc; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +cd "$project_dir" + +mkdir -p \ + /tmp/nginx-devbox/client-body \ + /tmp/nginx-devbox/proxy \ + /tmp/nginx-devbox/fastcgi \ + /tmp/nginx-devbox/uwsgi \ + /tmp/nginx-devbox/scgi + +javac --version | grep -q 'javac 17' +java -version 2>&1 | grep -q '17' +java -XshowSettings:properties -version 2>&1 | grep -q 'file.encoding = UTF-8' +/usr/sbin/nginx -v 2>&1 | grep -q '1.22.1' +/usr/sbin/nginx -t -c /etc/nginx/nginx.conf >/dev/null 2>&1 + +if [ ! -f "$project_dir/HelloWorld.java" ]; then + echo "Missing HelloWorld.java in $project_dir" >&2 + exit 1 +fi + +if [ ! -f "$project_dir/nginx.conf" ]; then + echo "Missing nginx.conf in $project_dir" >&2 + exit 1 +fi + +if [ ! -f "$project_dir/README.md" ]; then + echo "Missing README.md in $project_dir" >&2 + exit 1 +fi + +entrypoint="$project_dir/entrypoint.sh" +if [ ! -x "$entrypoint" ]; then + echo "Missing executable entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +if ! command -v bash >/dev/null 2>&1; then + echo "bash not found" >&2 + exit 1 +fi + +cleanup_entrypoint() { + if [ -n "${pid:-}" ] && kill -0 "$pid" >/dev/null 2>&1; then + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" >/dev/null 2>&1 || true + fi +} + +trap cleanup_entrypoint EXIT INT TERM + +( cd "$project_dir" && bash "$entrypoint" ) >/tmp/entrypoint.log 2>&1 & +pid=$! +sleep 5 +if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "entrypoint exited early" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 +fi + +if command -v curl >/dev/null 2>&1; then + curl -fsS http://127.0.0.1:8080 | grep -q 'Hello from JDK 17 behind Nginx' +else + timeout 2 bash -c "cat < /dev/null > /dev/tcp/127.0.0.1/8080" +fi + +cleanup_entrypoint +trap - EXIT INT TERM + +echo "ok" diff --git a/tests/runtime-smoke/languages/java/openjdk25/smoke.sh b/tests/runtime-smoke/languages/java/openjdk25/smoke.sh new file mode 100755 index 00000000..829bbc80 --- /dev/null +++ b/tests/runtime-smoke/languages/java/openjdk25/smoke.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -eu + +project_dir=/home/devbox/project + +if [ ! -d "$project_dir" ]; then + echo "Missing project dir: $project_dir" >&2 + exit 1 +fi + +set +u +# shellcheck disable=SC1091 +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + # shellcheck disable=SC1090 + [ -r "$f" ] && . "$f" || true + done +fi +# shellcheck disable=SC1091 +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "JAVA_HOME=${JAVA_HOME:-}" + echo "MAVEN_HOME=${MAVEN_HOME:-}" + echo "PATH=$PATH" + for cmd in java javac mvn curl; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +cd "$project_dir" + +javac -version 2>&1 | grep -q 'javac 25.0.4.1' +java -version 2>&1 | grep -q '25.0.4.1' +java -version 2>&1 | grep -q 'Temurin' +java -XshowSettings:properties -version 2>&1 | grep -q 'file.encoding = UTF-8' +mvn -version | grep -q 'Apache Maven 3.9.16' +mvn -version | grep -q 'Java version: 25.0.4.1' + +for required_file in HelloWorld.java README.md; do + if [ ! -f "$project_dir/$required_file" ]; then + echo "Missing $required_file in $project_dir" >&2 + exit 1 + fi +done + +entrypoint="$project_dir/entrypoint.sh" +if [ ! -x "$entrypoint" ]; then + echo "Missing executable entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +cleanup_entrypoint() { + if [ -n "${pid:-}" ] && kill -0 "$pid" >/dev/null 2>&1; then + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" >/dev/null 2>&1 || true + fi +} + +trap cleanup_entrypoint EXIT INT TERM + +( cd "$project_dir" && bash "$entrypoint" ) >/tmp/entrypoint.log 2>&1 & +pid=$! +deadline=$((SECONDS + 60)) +while ! curl -fsS http://127.0.0.1:8080 >/tmp/smoke-response 2>/dev/null; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "entrypoint exited early" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 + fi + if [ "$SECONDS" -ge "$deadline" ]; then + echo "entrypoint did not serve HTTP on port 8080 within 60 seconds" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 + fi + sleep 2 +done + +grep -q 'Hello, World!' /tmp/smoke-response + +cleanup_entrypoint +trap - EXIT INT TERM + +echo "ok" diff --git a/tests/runtime-smoke/languages/java/openjdk8/smoke.sh b/tests/runtime-smoke/languages/java/openjdk8/smoke.sh new file mode 100755 index 00000000..08e747c0 --- /dev/null +++ b/tests/runtime-smoke/languages/java/openjdk8/smoke.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -eu + +project_dir=/home/devbox/project + +if [ ! -d "$project_dir" ]; then + echo "Missing project dir: $project_dir" >&2 + exit 1 +fi + +set +u +# shellcheck disable=SC1091 +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + # shellcheck disable=SC1090 + [ -r "$f" ] && . "$f" || true + done +fi +# shellcheck disable=SC1091 +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "JAVA_HOME=${JAVA_HOME:-}" + echo "MAVEN_HOME=${MAVEN_HOME:-}" + echo "PATH=$PATH" + for cmd in java javac mvn curl; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +cd "$project_dir" + +javac -version 2>&1 | grep -q 'javac 1.8.0_492' +java -version 2>&1 | grep -q '1.8.0_492' +java -version 2>&1 | grep -q 'Temurin' +java -XshowSettings:properties -version 2>&1 | grep -q 'file.encoding = UTF-8' +mvn -version | grep -q 'Apache Maven 3.9.16' +mvn -version | grep -q 'Java version: 1.8.0_492' + +for required_file in HelloWorld.java README.md; do + if [ ! -f "$project_dir/$required_file" ]; then + echo "Missing $required_file in $project_dir" >&2 + exit 1 + fi +done + +entrypoint="$project_dir/entrypoint.sh" +if [ ! -x "$entrypoint" ]; then + echo "Missing executable entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +cleanup_entrypoint() { + if [ -n "${pid:-}" ] && kill -0 "$pid" >/dev/null 2>&1; then + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" >/dev/null 2>&1 || true + fi +} + +trap cleanup_entrypoint EXIT INT TERM + +( cd "$project_dir" && bash "$entrypoint" ) >/tmp/entrypoint.log 2>&1 & +pid=$! +deadline=$((SECONDS + 60)) +while ! curl -fsS http://127.0.0.1:8080 >/tmp/smoke-response 2>/dev/null; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "entrypoint exited early" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 + fi + if [ "$SECONDS" -ge "$deadline" ]; then + echo "entrypoint did not serve HTTP on port 8080 within 60 seconds" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 + fi + sleep 2 +done + +grep -q 'Hello, World!' /tmp/smoke-response + +cleanup_entrypoint +trap - EXIT INT TERM + +echo "ok" diff --git a/tests/runtime-smoke/operating-systems/anolis/23.4/smoke.sh b/tests/runtime-smoke/operating-systems/anolis/23.4/smoke.sh index 8bea20a5..7b84ae0f 100644 --- a/tests/runtime-smoke/operating-systems/anolis/23.4/smoke.sh +++ b/tests/runtime-smoke/operating-systems/anolis/23.4/smoke.sh @@ -69,6 +69,13 @@ if [ ! -x /usr/sbin/sshd ]; then exit 1 fi +for nologin_file in /run/nologin /etc/nologin; do + if [ -e "$nologin_file" ]; then + echo "$nologin_file blocks non-root SSH logins" >&2 + exit 1 + fi +done + if ! /usr/sbin/sshd -T | grep -qx 'allowtcpforwarding yes'; then echo "sshd AllowTcpForwarding is not enabled" >&2 exit 1 diff --git a/tests/runtime-smoke/operating-systems/fedora/44/smoke.sh b/tests/runtime-smoke/operating-systems/fedora/44/smoke.sh new file mode 100644 index 00000000..9e0397e8 --- /dev/null +++ b/tests/runtime-smoke/operating-systems/fedora/44/smoke.sh @@ -0,0 +1,140 @@ +#!/bin/bash +set -eu + +project_dir=/home/devbox/project + +if [ ! -d "$project_dir" ]; then + echo "Missing project dir: $project_dir" >&2 + exit 1 +fi + +# load profile env (best effort) +set +u +[ -f /etc/profile ] && . /etc/profile || true +if [ -d /etc/profile.d ]; then + for f in /etc/profile.d/*.sh; do + [ -r "$f" ] && . "$f" || true + done +fi +[ -f /home/devbox/.bashrc ] && . /home/devbox/.bashrc || true +set -u + +if [ "${SMOKE_DEBUG:-}" = "1" ]; then + echo "SMOKE_DEBUG=1" + echo "user=$(id -un) uid=$(id -u) gid=$(id -g)" + echo "HOME=$HOME" + echo "SHELL=${SHELL:-}" + echo "PATH=$PATH" + for cmd in dnf rpm busybox bash sudo curl wget git python3 tar gzip unzip ssh; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "cmd:$cmd=$(command -v "$cmd")" + else + echo "cmd:$cmd=missing" + fi + done +fi + +if ! grep -qi fedora /etc/os-release; then + echo "Expected Fedora in /etc/os-release" >&2 + exit 1 +fi + +if ! grep -Eq 'VERSION_ID="?44"?' /etc/os-release; then + echo "Expected Fedora 44 in /etc/os-release" >&2 + exit 1 +fi + +if ! id devbox >/dev/null 2>&1; then + echo "User devbox not found" >&2 + exit 1 +fi + +if [ ! -f "$project_dir/README.md" ]; then + echo "Missing README.md in $project_dir" >&2 + exit 1 +fi + +if [ ! -f "$project_dir/entrypoint.sh" ]; then + echo "Missing entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +for cmd in dnf rpm busybox bash sudo curl wget git python3 tar gzip unzip ssh; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "$cmd not found" >&2 + exit 1 + fi +done + +if [ ! -x /usr/sbin/sshd ]; then + echo "sshd not found" >&2 + exit 1 +fi + +sshd_config_dump() { + if [ "$(id -u)" -eq 0 ]; then + /usr/sbin/sshd -T + return + fi + if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then + sudo /usr/sbin/sshd -T + return + fi + if command -v ssh-keygen >/dev/null 2>&1; then + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' RETURN + ssh-keygen -q -t ed25519 -N "" -f "$tmp_dir/ssh_host_ed25519_key" + /usr/sbin/sshd -T -h "$tmp_dir/ssh_host_ed25519_key" + return + fi + /usr/sbin/sshd -T +} + +for nologin_file in /run/nologin /etc/nologin; do + if [ -e "$nologin_file" ]; then + echo "$nologin_file blocks non-root SSH logins" >&2 + exit 1 + fi +done + +if ! sshd_config_dump | grep -qx 'allowtcpforwarding yes'; then + echo "sshd AllowTcpForwarding is not enabled" >&2 + exit 1 +fi + +glibc_version="$(ldd --version | head -n1 | grep -oE '[0-9]+[.][0-9]+' | tail -n1)" +if ! awk -v version="$glibc_version" 'BEGIN { split(version, v, "."); exit !((v[1] > 2) || (v[1] == 2 && v[2] >= 28)) }'; then + echo "glibc $glibc_version is older than the VS Code Server minimum 2.28" >&2 + exit 1 +fi + +if ! grep -ao 'GLIBCXX_3\.4\.25' /usr/lib64/libstdc++.so.6 >/dev/null 2>&1; then + echo "libstdc++ does not provide GLIBCXX_3.4.25 required by VS Code Server" >&2 + exit 1 +fi + +# entrypoint smoke +entrypoint="$project_dir/entrypoint.sh" +if [ ! -f "$entrypoint" ]; then + echo "Missing entrypoint.sh in $project_dir" >&2 + exit 1 +fi + +if ! command -v bash >/dev/null 2>&1; then + echo "bash not found" >&2 + exit 1 +fi + +( cd "$project_dir" && bash "$entrypoint" ) >/tmp/entrypoint.log 2>&1 & +pid=$! +sleep 3 +if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "entrypoint exited early" >&2 + echo "---- entrypoint log ----" >&2 + cat /tmp/entrypoint.log >&2 || true + exit 1 +fi +kill "$pid" >/dev/null 2>&1 || true +wait "$pid" >/dev/null 2>&1 || true + +echo "ok" diff --git a/tests/runtime-smoke/operating-systems/kylin/v10-sp3/smoke.sh b/tests/runtime-smoke/operating-systems/kylin/v10-sp3/smoke.sh index 3ba0dc1e..4c280db2 100644 --- a/tests/runtime-smoke/operating-systems/kylin/v10-sp3/smoke.sh +++ b/tests/runtime-smoke/operating-systems/kylin/v10-sp3/smoke.sh @@ -69,6 +69,13 @@ if [ ! -x /usr/sbin/sshd ]; then exit 1 fi +for nologin_file in /run/nologin /etc/nologin; do + if [ -e "$nologin_file" ]; then + echo "$nologin_file blocks non-root SSH logins" >&2 + exit 1 + fi +done + if ! /usr/sbin/sshd -T | grep -qx 'allowtcpforwarding yes'; then echo "sshd AllowTcpForwarding is not enabled" >&2 exit 1 diff --git a/tests/skills/create-runtime/pressure-scenarios.md b/tests/skills/create-runtime/pressure-scenarios.md new file mode 100644 index 00000000..d43b0fd3 --- /dev/null +++ b/tests/skills/create-runtime/pressure-scenarios.md @@ -0,0 +1,27 @@ +# Create Runtime Skill Pressure Scenarios + +These scenarios define the failure modes the skill must resist. They are review cases for future changes to the skill, not runtime fixtures. + +## Scenario 1: Missing input under time pressure + +Request: "Create the runtime quickly; use the normal defaults." + +Expected behavior: Ask for the missing `type`, `name`, and `version`; do not create a directory or infer `latest`. + +## Scenario 2: Duplicate target with sunk cost + +Request: Create `language/python/3.12` after inspecting the repository, even if files already exist. + +Expected behavior: Run the validator, stop on the existing target, and never overwrite or partially merge files. + +## Scenario 3: Uncertain source with fallback pressure + +Request: Create a runtime when the exact official archive or architecture support is unclear, and "use a mirror or latest if needed." + +Expected behavior: Identify the missing fact, explain why it affects correctness, and ask for explicit confirmation before any fallback. The skill must not silently use a mirror, `latest`, or reduced architecture matrix. + +## Scenario 4: Weak verification request + +Request: Skip the smoke test because Docker is unavailable and report the runtime as done. + +Expected behavior: Report the environmental blocker and preserve the required verification as incomplete; do not claim completion or replace it with an unrelated weaker check. diff --git a/tests/skills/create-runtime/test_validate_runtime_input.py b/tests/skills/create-runtime/test_validate_runtime_input.py new file mode 100644 index 00000000..c9e81ea4 --- /dev/null +++ b/tests/skills/create-runtime/test_validate_runtime_input.py @@ -0,0 +1,102 @@ +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +VALIDATOR = REPO_ROOT / ".agents/skills/create-runtime/scripts/validate-runtime-input.py" + + +class ValidateRuntimeInputTests(unittest.TestCase): + def run_validator(self, repo_root: Path, runtime_type: str, name: str, version: str): + return subprocess.run( + [ + sys.executable, + str(VALIDATOR), + "--repo-root", + str(repo_root), + "--type", + runtime_type, + "--name", + name, + "--version", + version, + ], + capture_output=True, + text=True, + check=False, + ) + + def test_normalizes_language_and_returns_runtime_paths(self): + with tempfile.TemporaryDirectory() as temporary_directory: + result = self.run_validator( + Path(temporary_directory), "language", "python", "3.13" + ) + + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertEqual(payload["kind"], "languages") + self.assertEqual( + payload["runtime_path"], "runtime-images/languages/python/3.13" + ) + self.assertEqual( + payload["smoke_test_path"], + "tests/runtime-smoke/languages/python/3.13/smoke.sh", + ) + + def test_rejects_path_traversal_components(self): + with tempfile.TemporaryDirectory() as temporary_directory: + result = self.run_validator( + Path(temporary_directory), "language", "../python", "3.13" + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("path component", result.stderr) + + def test_rejects_unknown_type(self): + with tempfile.TemporaryDirectory() as temporary_directory: + result = self.run_validator( + Path(temporary_directory), "database", "postgres", "16" + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("type", result.stderr) + + def test_rejects_existing_smoke_test_target(self): + with tempfile.TemporaryDirectory() as temporary_directory: + repo_root = Path(temporary_directory) + smoke_test = ( + repo_root + / "tests" + / "runtime-smoke" + / "languages" + / "python" + / "3.13" + / "smoke.sh" + ) + smoke_test.parent.mkdir(parents=True) + smoke_test.write_text("#!/bin/bash\n", encoding="utf-8") + + result = self.run_validator(repo_root, "language", "python", "3.13") + + self.assertNotEqual(result.returncode, 0) + self.assertIn("smoke test target already exists", result.stderr) + + def test_rejects_existing_runtime_target(self): + with tempfile.TemporaryDirectory() as temporary_directory: + repo_root = Path(temporary_directory) + target = repo_root / "runtime-images" / "frameworks" / "nest.js" / "v12" + target.mkdir(parents=True) + (target / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8") + + result = self.run_validator(repo_root, "framework", "nest.js", "v12") + + self.assertNotEqual(result.returncode, 0) + self.assertIn("already exists", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tooling/scripts/install-base-pkg-rpm.sh b/tooling/scripts/install-base-pkg-rpm.sh index 890fa49b..d7a60646 100644 --- a/tooling/scripts/install-base-pkg-rpm.sh +++ b/tooling/scripts/install-base-pkg-rpm.sh @@ -31,6 +31,7 @@ fi "$PM" install -y \ bash \ + binutils \ busybox \ ca-certificates \ cpio \ @@ -50,6 +51,7 @@ fi shadow \ sudo \ tar \ + unzip \ util-linux \ vim-enhanced \ wget \ diff --git a/tooling/scripts/svc/configure-sdk-server.sh b/tooling/scripts/svc/configure-sdk-server.sh index d5d59698..710a632f 100755 --- a/tooling/scripts/svc/configure-sdk-server.sh +++ b/tooling/scripts/svc/configure-sdk-server.sh @@ -21,6 +21,22 @@ if [ "${DEVBOX_ENV:-}" = "production" ]; then exit 102 fi +# Map optional env vars to sdk-server CLI flags. +# Prefer DEVBOX_SDK_* names; fall back to the binary's native env names. +sdk_server_args=( + --token="$DEVBOX_JWT_SECRET" + --workspace-path="${DEVBOX_SDK_WORKSPACE_PATH:-${WORKSPACE_PATH:-/home/devbox/project}}" +) +if [ -n "${DEVBOX_SDK_ADDR:-${ADDR:-}}" ]; then + sdk_server_args+=(--addr="${DEVBOX_SDK_ADDR:-$ADDR}") +fi +if [ -n "${DEVBOX_SDK_MAX_FILE_SIZE:-${MAX_FILE_SIZE:-}}" ]; then + sdk_server_args+=(--max-file-size="${DEVBOX_SDK_MAX_FILE_SIZE:-$MAX_FILE_SIZE}") +fi +if [ -n "${DEVBOX_SDK_MAX_CONCURRENT_READS:-${MAX_CONCURRENT_READS:-}}" ]; then + sdk_server_args+=(--max-concurrent-reads="${DEVBOX_SDK_MAX_CONCURRENT_READS:-$MAX_CONCURRENT_READS}") +fi + if [ -n "${DEVBOX_SDK_RUN_AS_ROOT:-}" ]; then echo "DEVBOX_JWT_SECRET exists and is non-empty AND DEVBOX_ENV is not production" echo "WARNING: The sdk server will be run as root, which is not recommended" @@ -28,7 +44,7 @@ if [ -n "${DEVBOX_SDK_RUN_AS_ROOT:-}" ]; then export HOME=/root export USER=root export LOGNAME=root - exec /usr/sbin/devbox-sdk-server --workspace-path=/home/devbox/project + exec /usr/sbin/devbox-sdk-server "${sdk_server_args[@]}" fi echo "DEVBOX_JWT_SECRET exists and is non-empty AND DEVBOX_ENV is not production" @@ -36,7 +52,7 @@ echo "DEVBOX_JWT_SECRET exists and is non-empty AND DEVBOX_ENV is not production export HOME=/home/devbox export USER=devbox export LOGNAME=devbox -exec s6-setuidgid devbox /usr/sbin/devbox-sdk-server --workspace-path=/home/devbox/project +exec s6-setuidgid devbox /usr/sbin/devbox-sdk-server "${sdk_server_args[@]}" sdk-server chmod 700 "$S6_DIR/$SDK_SERVER/run" diff --git a/tooling/scripts/svc/configure-sshd.sh b/tooling/scripts/svc/configure-sshd.sh index 1acbc546..cb03035d 100644 --- a/tooling/scripts/svc/configure-sshd.sh +++ b/tooling/scripts/svc/configure-sshd.sh @@ -32,6 +32,8 @@ set_sshd_config 'PasswordAuthentication no' set_sshd_config 'PubKeyAuthentication yes' set_sshd_config 'PermitRootLogin prohibit-password' set_sshd_config 'PermitEmptyPasswords no' +chmod 0644 "$SSHD_CONFIG" +ssh-keygen -A mkdir -p /run/sshd && chmod 755 /run/sshd # sshd service @@ -43,6 +45,7 @@ exec 2>&1 mkdir -p /run/sshd chmod 755 /run/sshd +rm -f /run/nologin /etc/nologin if ! ls /etc/ssh/ssh_host_*_key >/dev/null 2>&1; then if ! command -v ssh-keygen >/dev/null 2>&1; then