Skip to content

feat(fleet): dsh as a sandboxed low-trust fleet adapter, pilot (#773) - #788

Merged
CSalcedoDataBI merged 5 commits into
mainfrom
issue-773-dsh-pilot
Oct 3, 2026
Merged

CSalcedoDataBI merged 5 commits into
mainfrom
issue-773-dsh-pilot

Conversation

@CSalcedoDataBI

Copy link
Copy Markdown
Owner

Refs #773. A pilot of dsh (DeepSeek Harness) as a sandboxed, low-cost fleet adapter, under the issue's security terms.

Security terms, how each is met

  • Container only. docker run --rm --pull=never:
    • The worktree is the only writable mount; the root filesystem is read-only and /tmp is a tmpfs.
    • No host home and no docker socket.
    • User node (uid 1000), all capabilities dropped, no-new-privileges.
    • Limits: 2 CPU, 2 GB, 512 processes.
    • Only DEEPSEEK_API_KEY is passed, by name, and there is no GitHub token.
  • Upload off. dsh 0.2.0-rc.2 uploads the whole session log inside every model request by default. Layers that stop it:
    • a config patch disables the session-log upload, telemetry and the package inventory;
    • env switches;
    • the telemetry hosts are pointed at 127.0.0.1;
    • the image build fails if any upload row is still enabled;
    • the entrypoint refuses to start (exit 78) on a worktree .env, on weakened switches, or on extra token-like variables.
  • Headless: --profile headless --json, not the SDK.
  • Route enforced in code, at planning and at launch: only Docs/Chore issues of PUBLIC repos. Anything else, or unknown visibility, falls back to claude with a one-line warning. A new registry field lowTrust keeps dsh out of the default and reviewer roles, and no override tier can clear it.
  • Pinned:
    • base image node:22.23.3-bookworm-slim@sha256:…;
    • exact @deepseek-ai/dsh@0.2.0-rc.2, installed with npm ci from a lockfile;
    • no @latest or curl. The container files live in containers/dsh/ at the repo root, because the lockfile is over the plugin's per-file size gate.
  • Edit-only. dsh gets a short briefing: the issue text, and the job of editing files in /work. It runs no git and opens no PR. After it exits, you are told to review git diff and open the PR yourself. Nothing is committed or pushed automatically.

Real pilot (Docker 29.8, real credential)

  • The image builds, and its check printed all three upload rows "disabled". The probe returns OK, a fake key classifies as AUTH, and a missing image as ERROR.
  • A real headless task on a throwaway repo, run 5 times, including through the exact launch script the registry generates:
    • Every run exited 0 and edited only README.md.
    • The folder outside the mount was hash-unchanged, and docker diff was empty.
    • Inside the container: no GitHub or other tokens (variable names checked); the only mounts were /work and /tmp.
    • The session log shows 0 accepted uploads.
  • About 6–14 s per task on deepseek-flash; one run used 675 input and 309 output tokens.

Known limits (pilot)

  • Network egress is open: dsh needs api.deepseek.com, and Docker has no per-host allow-list without a proxy. Only the telemetry hosts are blocked.
  • DeepSeek's auth error echoes the last 4 characters of the key.
  • A relaunch of a dsh session falls back to claude, because the session registry keeps no issue facts.
  • dsh only has pre-release versions.

Tests

Hermetic runs, all 0 failed:

Test file Passed
DshSandbox 57
AdapterRegistry 52
Fleet-Plan 20
Board-Work 341
CliProbeCodes 34
Get-ReviewerRoster 39
Install-Pinning 9
Board-Work.* + RunLedgerCheckpoint 258

Update-Docs -Check and claude plugin validate --strict pass. The directory checker still reports 0 blocking.

🤖 Generated with Claude Code

CSalcedoDataBI and others added 4 commits October 2, 2026 21:50
DeepSeek Harness (@deepseek-ai/dsh 0.2.0-rc.2, headless profile, --json)
joins the adapter registry as one JSON entry whose command is docker: it
runs only inside the agentic-board/dsh image (containers/dsh), never on
the host.

- Launch/probe: docker run --rm --pull=never, worktree as the only bind
  mount, read-only root + tmpfs /tmp (DSH_HOME), non-root, cap-drop ALL,
  no-new-privileges, cpu/memory/pid limits, only DEEPSEEK_API_KEY passed
  by name, telemetry collectors pinned to 127.0.0.1.
- Image: node base by tag+digest, exact dsh pin + lockfile via npm ci.
  The session-log upload (session-log-deepseek, on by default and carried
  inside the API request), OTLP telemetry and the package inventory are
  disabled by a --patch layer; the build fails if --dump-config still
  mounts them; the entrypoint refuses a workspace .env, weakened
  switches and stray credentials.
- lowTrust (new registry field): docs/chore routes only, never reviewer or
  default, sticky across tiers; offered only for PUBLIC repos (Fleet-Plan
  reads repository visibility) and re-checked at fleet launch, unknown
  visibility fails closed.
- {workPath} launch placeholder, refusing paths that could inject mount
  options. No bypass needed: workspace-write edits headless.
- Probe rules from dsh's real outputs (AUTH, MISSING_CREDENTIAL, QUOTA,
  RATE_LIMIT, CONTEXT_WINDOW_EXCEEDED, Docker errors); OK needs the
  final event to answer OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugin README "runs, sends and fetches": dsh runs only in the container,
is never installed for you, and sends its task and workspace reads to
DeepSeek's API under DeepSeek's terms with the session-log upload and
telemetry off. presets/README: the lowTrust field, the {workPath}
placeholder, dsh in the routes table. containers/dsh/README: the pins,
what the container gets, and how the upload is disabled and verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

Launch gate: Start-WorktreeSession is the one door every session goes
through (fleet picker, per-issue choice, relaunch). It now refuses a
lowTrust CLI unless the issue's route (Select-CliRoute on its labels,
Type and Size) is docs or chore AND its repo is PUBLIC; missing facts
(e.g. a relaunch) are refused. On refusal it starts claude and prints one
warning line. The issue facts come from the board item (Get-IssueLaunchFacts,
visibility now in the GraphQL read) and travel through Build-FleetPlan.
The fleet hook and relaunch record the CLI that actually started.

Briefing: dsh has no gh, pwsh, plugin scripts or token, so it gets
Get-ContainerEditBriefing (the issue text, capped, plus one job: edit the
files in /work; no git, no PR, no network) instead of the fleet briefing.
After the container exits, the launch script prints the human next step:
review git diff in the worktree, then commit and open the PR. Nothing is
committed or pushed automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026

Copy link
Copy Markdown

Review (sha d6c0348)

El diseño es sólido: el gate low-trust falla cerrado (visibilidad desconocida se rechaza, no se puede quitar lowTrust desde un tier posterior) y el launch vuelve a comprobarlo. La imagen tiene todo fijado por digest y lockfile, y el build falla si el upload sigue activo. Solo veo un punto de seguridad que vale la pena cubrir y algunos detalles menores. No revisé línea a línea DshSandbox.Tests.ps1 (410 líneas) ni el lockfile.

Seguridad (medio): exfiltración de DEEPSEEK_API_KEY por prompt injection.

  • El cuerpo del issue (Get-ContainerEditBriefing) es texto no confiable de un repo público y va directo al prompt.
  • El contenedor mantiene el egress abierto y la key está en su entorno. Si dsh puede ejecutar comandos en workspace-write, un issue malicioso podría hacer que mande la key a un servidor externo.
  • Las instrucciones "no uses la red" son solo texto. Los --add-host a 127.0.0.1 solo cubren los hosts de telemetría.
  • Acciones concretas:
    • (a) Confirmar si workspace-write permite ejecutar shell o red, y documentar la conclusión en el README.
    • (b) Si lo permite, usar una API key dedicada con límite de gasto, y avisarlo en el README.
    • (c) A futuro, un proxy con allow-list a api.deepseek.com.
  • El README ya admite que el egress queda abierto. Falta decir que la key es exfiltrable por esta vía.

Menores

  • entrypoint.sh: [ -e ./.env ] solo mira el cwd. Funciona porque WORKDIR /work es el mount, pero un docker run --workdir distinto (como el del probe) saltaría el chequeo. Mejor comprobarlo respecto de /work explícitamente.
  • entrypoint.sh: el filtro *KEY* rechazará variables inocuas (por ejemplo KEYBOARD). Es fail-closed y aceptable.
  • /tmp se monta con exec. Si dsh no lo necesita, quítalo para endurecer la imagen.
  • Get-CliRepoVisibility hace una llamada gh api por launch cuando falta la visibilidad. Es correcto, solo hay que tenerlo en cuenta con muchos launches en paralelo.

Tests: la cobertura es amplia (Dsh/AdapterRegistry/Board-Work). Faltan casos para Format-CliTemplateLaunch con {workPath} que contenga ,, comillas o saltos de línea (el throw nuevo), y para el chequeo de .env en el entrypoint si no hay un test de integración de la imagen.

The RawGh lint ratchet (#571) caught a bare gh call added for the dsh
visibility check. Invoke-Gh throws on failure instead of reading a 401 as an
empty answer; the catch still turns it into $null, which keeps dsh away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026

Copy link
Copy Markdown

Review (sha 166efc1)

El diseño es sólido: defensa en capas, falla cerrado, gate de low-trust en planner y en launch, imagen fijada por digest y lockfile. Hallazgos, de mayor a menor:

  1. Seguridad: la API key puede salir por inyección de prompt. Get-ContainerEditBriefing mete el cuerpo del issue en el prompt. En un repo público lo escribe cualquiera. El contenedor tiene DEEPSEEK_API_KEY en el entorno y la red abierta (el README lo admite). Un issue malicioso puede pedirle a dsh que ejecute un comando o una petición que envíe la key fuera.
    • Mitiga (a) con un proxy o red de egress restringida a api.deepseek.com, o (b) con una key dedicada al pilot, con límite de gasto y rotable.
    • Documenta el riesgo residual en el README. Ahora mismo "no hay token de GitHub" suena a que el riesgo está cubierto.
  2. Delimitadores falsificables. --- end of issue text --- puede aparecer dentro del body y cerrar el bloque antes de tiempo. Usa un delimitador aleatorio por launch, o neutraliza esa línea en el body. Es de severidad baja, pero es barato de arreglar.
  3. Tests de comportamiento ausentes. DshSandbox.Tests.ps1 comprueba entrypoint.sh y Dockerfile solo con regex sobre el texto (p. ej. [ -e ./.env ] && refuse). Nada ejecuta entrypoint.sh ni check-upload-off.js.
    • Añade un test que corra check-upload-off.js con un dump de ejemplo: ok, fila no deshabilitada, fila duplicada y fila ausente.
    • Añade otro que corra entrypoint.sh con sh con un .env y con un FOO_TOKEN, y compruebe el exit 78. Se puede stubear dsh en el PATH.
  4. Permisos del bind mount en hosts Linux. El contenedor corre como uid 1000 (node). En un host Linux cuyo usuario no sea 1000, el worktree montado no será escribible. Es seguro, pero el fallo será confuso. Menciónalo en el README o añade --user configurable.
  5. Detalle menor. check-upload-off.js queda dentro de la imagen final. Es inocuo, pero se puede evitar haciendo el check en la etapa build.

Sin bugs de lógica evidentes en el gate low-trust (visibilidad desconocida → rechazo, flag pegajoso entre tiers, {workPath} validado).

@CSalcedoDataBI
CSalcedoDataBI merged commit 3764347 into main Oct 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant