mirror of
https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git
synced 2026-11-03 01:13:24 +00:00
* fix(example): keep agents usable when DSH's inner sandbox has no backend DSH confines bash commands itself (bubblewrap, else Landlock) and fails closed with SANDBOX_UNAVAILABLE when no runner is usable. In this hardened pod - non-root, capabilities dropped, seccomp RuntimeDefault - bubblewrap cannot start (it needs CAP_SYS_ADMIN) and the pod exposes no Landlock runner, so the shipped workspace-write default made every command fail: sandbox mode "workspace-write" is requested but no sandbox backend is usable The DSH base bundle at the pinned ref documents DSH_PERMISSION_MODE as the explicit deployment override, so set it to danger-full-access in both sandbox templates. The per-user pod is the isolation boundary in this example (its own pod, PVC and Service; non-root; all capabilities dropped), so a working agent is worth more than an inner file policy that cannot be enforced. Note that danger-full-access also turns approval prompts off. Trade-off documented rather than hidden: an agent command can now read its own DSH_HOME (including .credentials.yaml) and write anywhere in the container. Hosts where a runner works should keep workspace-write or read-only. - main-image docs: DSH_PERMISSION_MODE row plus a note on the runners and the fail-closed behavior - example README: limits bullet and a troubleshooting row with the in-pod backend check and how to restore a confined mode - architecture: tenant-isolation bullet extended - skill runbook/troubleshooting: operation guidance; never grant CAP_SYS_ADMIN or seccompProfile: Unconfined to "fix" this without an explicit decision Verified: make check (14 manifests), make lint, YAML assertions on both templates, and the override expression at the pinned dsh-v0.2.1-rc.5 base bundle (mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'). * fix(image): install bubblewrap for DSH's command sandbox DSH's Linux sandbox runner chain is bubblewrap, then Landlock, and it fails closed with SANDBOX_UNAVAILABLE when neither works. The image shipped neither, so a sandboxed agent had no way to execute confined commands. Add bubblewrap to the runtime stage. Debian ships it unsetuid (0.12.0), so it still needs unprivileged user namespaces plus the namespace/mount syscalls: in a hardened pod (all capabilities dropped, RuntimeDefault seccomp) it usually cannot create one, and DSH then falls back to Landlock or, failing that, to the DSH_PERMISSION_MODE=danger-full-access override the templates already set. Where the host allows an unprivileged user namespace, the first-choice runner now exists, so the confined modes can be restored. Docs corrected to match: bubblewrap is no longer "absent"; the blocker is the unsetuid binary's namespace requirement, not a missing CAP_SYS_ADMIN grant. Verified: make check, make lint; the package exists in the pinned base (node:24-trixie-slim, bubblewrap 0.12.0-1~deb13u1); a throwaway container with the pod's flags (--user 1000 --cap-drop ALL --security-opt no-new-privileges) still cannot create a namespace, which is why the override stays. No local image build was run. * ci(image): build and smoke-test the main image on PRs that touch it The Dockerfile and the pins in versions.env could only be validated by a merge to main or by running `make image` by hand; neither gives a PR-time signal. Add ci-image.yaml: on pull requests that change Dockerfile, .dockerignore or versions.env (and on manual dispatch) it builds linux/amd64 natively with the pins sourced from versions.env - no registry login, no push - loads the image locally and smoke-tests it: - the packaged CLI runs and reports the pinned version - the container is the non-root node user (1000) and owns /workspace + DSH_HOME - the bubblewrap command-sandbox runner is present - the OCI version label matches versions.env - a container serves /favicon.svg (the same unauthenticated probe the shipped manifests use), with logs filtered so the launch token is never printed Path-filtered like the other on-demand checks, so it is not a required status check; the published multi-arch build stays in build-main.yaml and release.yaml. Docs updated in the same change: AGENTS.md repository map and testing table, docs/gitflow.md checks section, docs/main-image.md build section. Verified: actionlint 1.7.12 and yamllint 1.38.0 (CI versions) clean over the whole repository, plus make check and make lint. --------- Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
156 lines
5.6 KiB
Docker
156 lines
5.6 KiB
Docker
# syntax=docker/dockerfile:1.7
|
|
#
|
|
# The primary artifact of this repository: DeepSeek Harness Web (dsh web),
|
|
# packaged as a slim, non-root, multi-arch runtime image.
|
|
#
|
|
# Build args (all pinned in versions.env, verified by scripts/check-versions.sh):
|
|
# DSH_REPO upstream git remote to package
|
|
# DSH_VERSION upstream git ref to build (tag or branch)
|
|
# RELEASE_REVISION optional repository revision label (r2, r3, ...)
|
|
# PNPM_VERSION pnpm used to build DSH
|
|
# VCS_REF this repository's commit, recorded in OCI labels
|
|
# NODE_BUILD_IMAGE node image used to build DSH
|
|
# NODE_RUNTIME_IMAGE node image used at runtime
|
|
ARG NODE_BUILD_IMAGE=docker.io/node:24-trixie
|
|
ARG NODE_RUNTIME_IMAGE=docker.io/node:24-trixie-slim
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build stage: clone DSH at the pinned ref, build, and package the CLI.
|
|
# ---------------------------------------------------------------------------
|
|
FROM $NODE_BUILD_IMAGE AS build
|
|
|
|
ARG DSH_REPO=https://github.com/deepseek-ai/deepseek-harness.git
|
|
ARG DSH_VERSION=dsh-v0.2.1-rc.5
|
|
ARG PNPM_VERSION=11.28.5
|
|
|
|
ENV CI=true \
|
|
NODE_OPTIONS=--max-old-space-size=4096
|
|
|
|
RUN corepack enable \
|
|
&& corepack prepare "pnpm@$PNPM_VERSION" --activate
|
|
|
|
WORKDIR /src
|
|
|
|
RUN git clone --depth 1 --branch "$DSH_VERSION" "$DSH_REPO" .
|
|
|
|
RUN pnpm install --frozen-lockfile
|
|
|
|
RUN pnpm run build
|
|
|
|
# ── Package the CLI with pnpm pack ───────────────────────────────────────
|
|
RUN mkdir -p /opt/dsh \
|
|
&& pnpm --filter @deepseek-ai/dsh pack \
|
|
--pack-destination /opt \
|
|
--config.ignore-scripts=true \
|
|
&& mv /opt/deepseek-ai-dsh-*.tgz /opt/dsh.tgz \
|
|
&& tar -xzf /opt/dsh.tgz -C /opt/dsh --strip-components=1 \
|
|
&& rm /opt/dsh.tgz
|
|
|
|
# ── Production install with npm ──────────────────────────────────────────
|
|
RUN cd /opt/dsh \
|
|
&& npm install --omit=dev --no-audit --no-fund \
|
|
&& test -f lib/bin.js \
|
|
&& node lib/bin.js web --help >/dev/null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Runtime stage
|
|
# ---------------------------------------------------------------------------
|
|
FROM $NODE_RUNTIME_IMAGE AS runtime
|
|
|
|
ARG DSH_VERSION=dsh-v0.2.1-rc.5
|
|
ARG RELEASE_REVISION=
|
|
ARG PNPM_VERSION=11.28.5
|
|
ARG VCS_REF=unknown
|
|
|
|
LABEL org.opencontainers.image.title="DeepSeek Harness Web packaged by SREDevOps.org" \
|
|
org.opencontainers.image.description="DeepSeek Harness Web (dsh web) as a slim, non-root, Kubernetes-ready image" \
|
|
org.opencontainers.image.source="https://github.com/sredevopsorg/deepseek-kubernetes-sandbox" \
|
|
org.opencontainers.image.version="$DSH_VERSION" \
|
|
org.opencontainers.image.revision="$VCS_REF" \
|
|
org.opencontainers.image.licenses="MIT" \
|
|
io.sredevopsorg.dsh.version="$DSH_VERSION" \
|
|
io.sredevopsorg.release.revision="$RELEASE_REVISION"
|
|
|
|
ENV NODE_ENV=production \
|
|
HOME=/home/node \
|
|
DSH_HOME=/home/node/.dsh \
|
|
DSH_PORT=3080 \
|
|
PATH="/app/node_modules/.bin:$PATH"
|
|
|
|
# git/ripgrep/jq/yq/curl/gh are deliberate agent tooling parity: a sandboxed DSH
|
|
# instance expects the usual developer CLI. tini reaps signals for a clean stop.
|
|
# bubblewrap is DSH's first-choice command-sandbox runner on Linux. It ships
|
|
# unsetuid, so it needs unprivileged user namespaces plus the namespace/mount
|
|
# syscalls; a hardened pod (all capabilities dropped, RuntimeDefault seccomp)
|
|
# usually cannot create one, which is why DSH then falls back to Landlock and,
|
|
# when that is unavailable too, to DSH_PERMISSION_MODE=danger-full-access.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
git \
|
|
ripgrep \
|
|
jq \
|
|
yq \
|
|
curl \
|
|
wget \
|
|
tini \
|
|
gh \
|
|
bubblewrap \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
|
|
&& install -d -o node -g node -m 0755 /workspace "$DSH_HOME" \
|
|
&& corepack enable \
|
|
&& corepack prepare "pnpm@$PNPM_VERSION" --activate \
|
|
&& pnpm --version
|
|
|
|
COPY --from=build --chown=node:node /opt/dsh /app
|
|
|
|
COPY <<'EOF' /usr/local/bin/dsh-entrypoint
|
|
#!/bin/sh
|
|
set -eu
|
|
|
|
: "${DSH_PORT:=3080}"
|
|
: "${DSH_HOST:=}"
|
|
: "${DSH_TRUSTED_HOSTS:=}"
|
|
DSH_PUBLIC_URL="${DSH_PUBLIC_URL-http://localhost:${DSH_PORT}}"
|
|
|
|
# DSH rejects wildcard hosts. Pick the container/pod's first non-loopback IPv4.
|
|
if [ -z "$DSH_HOST" ]; then
|
|
DSH_HOST="$(node -e 'const os=require("os"); const ifaces=Object.values(os.networkInterfaces()).flat(); const addr=ifaces.find(v=>v&&v.family==="IPv4"&&!v.internal); if(!addr){process.exit(1)} process.stdout.write(addr.address)')"
|
|
fi
|
|
|
|
if [ -n "$DSH_PUBLIC_URL" ]; then
|
|
set -- --public-url "$DSH_PUBLIC_URL" "$@"
|
|
fi
|
|
|
|
# Additional trusted hosts (e.g. the public gateway host behind a proxy).
|
|
# DSH Host/Origin-fences /api: without this, requests forwarded with the
|
|
# browser's Host header are rejected. Accepts comma- or space-separated names;
|
|
# localhost/127.0.0.1 are always trusted below.
|
|
old_ifs="$IFS"
|
|
IFS=', '
|
|
for trusted_host in $DSH_TRUSTED_HOSTS; do
|
|
[ -n "$trusted_host" ] || continue
|
|
set -- "$@" --trusted-host "$trusted_host"
|
|
done
|
|
IFS="$old_ifs"
|
|
|
|
exec node /app/lib/bin.js web \
|
|
--no-open \
|
|
--host "$DSH_HOST" \
|
|
--port "$DSH_PORT" \
|
|
--trusted-host localhost \
|
|
--trusted-host 127.0.0.1 \
|
|
"$@"
|
|
EOF
|
|
|
|
RUN chmod 0755 /usr/local/bin/dsh-entrypoint
|
|
|
|
WORKDIR /workspace
|
|
USER node
|
|
|
|
EXPOSE 3080
|
|
VOLUME ["/home/node/.dsh"]
|
|
STOPSIGNAL SIGTERM
|
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/dsh-entrypoint"]
|