Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docker-compose.api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ services:
environment:
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-host}
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}
DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE: ${DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE:-host}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-}
DOCKER_GIT_PROJECT_SSH_BIND_HOST: ${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-0.0.0.0}
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
Expand All @@ -36,7 +36,8 @@ services:
volumes:
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
- docker_git_docker_data:/var/lib/docker
privileged: true
- /var/run/docker.sock:/var/run/docker.sock
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-false}
cgroup: host
init: true
restart: unless-stopped
Expand Down
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ services:
environment:
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-host}
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}
DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE: ${DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE:-host}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-}
DOCKER_GIT_PROJECT_SSH_BIND_HOST: ${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-0.0.0.0}
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
Expand All @@ -36,7 +36,8 @@ services:
volumes:
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
- docker_git_docker_data:/var/lib/docker
privileged: true
- /var/run/docker.sock:/var/run/docker.sock
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-false}
cgroup: host
init: true
restart: unless-stopped
Expand Down
2 changes: 1 addition & 1 deletion packages/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ LABEL io.prover-coder-ai.docker-git.controller-build-skiller=$DOCKER_GIT_CONTROL
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
ENV DOCKER_GIT_CONTROLLER_BUILD_SKILLER=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
ENV DOCKER_GIT_API_PORT=3334
ENV DOCKER_GIT_DOCKER_RUNTIME=isolated
ENV DOCKER_GIT_DOCKER_RUNTIME=host
ENV DOCKER_HOST=unix:///var/run/docker.sock
EXPOSE 3334

Expand Down
28 changes: 18 additions & 10 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ HTTP API for docker-git orchestration (projects, agents, logs/events, federation
This is now the intended controller plane:
- the API runs inside `docker-git-api`
- `.docker-git` state lives in the Docker volume `docker-git-projects`
- the API starts an isolated Docker daemon inside the controller by default
- child project containers no longer depend on host bind mounts for bootstrap auth/env
- the host `/var/run/docker.sock` is not mounted into the controller or project containers
- the API uses the host Docker daemon by default via `/var/run/docker.sock`
- child project containers use host-backed Docker unless an explicit
`DOCKER_GIT_PROJECT_DOCKER_HOST` is provided

## Runtime contract: host-Docker-backed

`docker-git` is host-Docker-backed, not isolated. The controller container
created from this package binds the host socket
`docker-git` is host-Docker-backed by default. The primary controller
container created from this package binds the host socket
(`/var/run/docker.sock:/var/run/docker.sock`, see `docker-compose.yml`) and
uses it to spawn per-project containers. There is no Docker-in-Docker
runtime; the daemon is always the host's daemon.
uses it to spawn per-project containers. `DOCKER_GIT_DOCKER_RUNTIME=isolated`
is an opt-in fallback for environments that explicitly require an embedded
controller daemon.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Security note: binding `/var/run/docker.sock` gives the controller container
root-equivalent control over the host Docker daemon, including the ability to
create containers and mount host paths. This is an intended trade-off for the
host-backed architecture; run the controller only in trusted environments and
review the threat model before exposing the API.

The host CLI (`packages/app`) also talks to that same daemon directly when
it bootstraps the controller. Three failure modes look identical at first
Expand Down Expand Up @@ -61,12 +68,13 @@ Optional env:

- `DOCKER_GIT_API_BIND_HOST` (default: `127.0.0.1`)
- `DOCKER_GIT_API_PORT` (default: `3334`)
- `DOCKER_GIT_DOCKER_RUNTIME` (default: `isolated`; starts a managed Docker daemon in `docker-git-api`)
- `DOCKER_GIT_DOCKER_RUNTIME` (default: `host`; set to `isolated` as an optional fallback to use an embedded controller daemon)
- `DOCKER_GIT_CONTROLLER_DOCKER_HOST` (default: `unix:///var/run/docker.sock`; socket path inside the controller)
- `DOCKER_GIT_CONTROLLER_PRIVILEGED` (default: `false`; set to `true` only when using `DOCKER_GIT_DOCKER_RUNTIME=isolated`)
- `DOCKER_GIT_DOCKERD_TCP_HOST` (default: `tcp://0.0.0.0:2375`; reachable only inside Docker networks unless explicitly published)
- `DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE` (default: `host`; keeps nested project containers compatible with cgroup v2 DinD)
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: `tcp://host.docker.internal:2375`; lets project containers use the isolated daemon)
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0` in controller mode; project SSH binds inside the isolated controller runtime)
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: empty; unset uses host socket in project containers when mounted)
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0`)
- `DOCKER_GIT_PROJECTS_ROOT` (container path, default: `/home/dev/.docker-git`)
- `DOCKER_GIT_PROJECTS_ROOT_VOLUME` (Docker volume name for controller state, default: `docker-git-projects`)
- `DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN` (optional public ActivityPub origin)
Expand Down
2 changes: 1 addition & 1 deletion packages/api/scripts/start-controller.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

runtime="${DOCKER_GIT_DOCKER_RUNTIME:-isolated}"
runtime="${DOCKER_GIT_DOCKER_RUNTIME:-host}"
docker_host="${DOCKER_HOST:-unix:///var/run/docker.sock}"
dockerd_pid=""

Expand Down
122 changes: 115 additions & 7 deletions packages/app/src/docker-git/controller-compose.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
import type { PlatformError } from "@effect/platform/Error"
import * as FileSystem from "@effect/platform/FileSystem"
import * as Path from "@effect/platform/Path"
import { Effect } from "effect"
import { Duration, Effect } from "effect"

import { computeLocalControllerRevision, controllerRevisionEnvKey } from "./controller-revision.js"
import { runCommandWithCapturedOutput } from "./frontend-lib/shell/command-runner.js"
import { findExistingUpwards } from "./frontend-lib/usecases/path-helpers.js"
import type { ControllerBootstrapError } from "./host-errors.js"

Expand All @@ -18,6 +20,9 @@ export type ControllerComposeFiles = {
readonly gpuOverlayPath: string | null
}

const skillerSubmodulePath = "third_party/skiller-desktop-skills-manager"
const skillerPackagePath = `${skillerSubmodulePath}/package.json`

const controllerBootstrapError = (message: string): ControllerBootstrapError => ({
_tag: "ControllerBootstrapError",
message
Expand Down Expand Up @@ -82,9 +87,104 @@ const composeFilePath = (): Effect.Effect<string, PlatformError, FileSystem.File
const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
controllerBootstrapError(`Failed to resolve docker-compose.yml path.\nDetails: ${String(error)}`)

const mapSkillerPathError = (error: PlatformError): ControllerBootstrapError =>
controllerBootstrapError(`Failed to check Skiller submodule path.\nDetails: ${String(error)}`)

const mapControllerRevisionError = (error: PlatformError): ControllerBootstrapError =>
controllerBootstrapError(`Failed to compute docker-git controller revision.\nDetails: ${String(error)}`)

const skillerSubmoduleCommand = [
"submodule",
"update",
"--init",
"--checkout",
skillerSubmodulePath
]
const skillerSubmoduleInitTimeout = Duration.seconds(60)

const formatSkillerSubmoduleFailure = (rootDir: string, exitCode: number, output: string): ControllerBootstrapError =>
controllerBootstrapError(
[
"Failed to initialize Skiller submodule before building docker-git controller.",
`Command: git ${skillerSubmoduleCommand.join(" ")}`,
`Working directory: ${rootDir}`,
`Exit code: ${exitCode}`,
output.trim().length > 0 ? `Output:\n${output.trim()}` : "Output: n/a"
].join("\n")
)

const runSkillerSubmoduleInit = (
rootDir: string
): Effect.Effect<void, ControllerBootstrapError, CommandExecutor.CommandExecutor> =>
runCommandWithCapturedOutput(
{
cwd: rootDir,
command: "git",
args: skillerSubmoduleCommand
},
[0],
(exitCode, output) => formatSkillerSubmoduleFailure(rootDir, exitCode, output)
).pipe(
Effect.timeoutFail({
duration: skillerSubmoduleInitTimeout,
onTimeout: () =>
controllerBootstrapError(
[
"Timed out while initializing Skiller submodule before building docker-git controller.",
`Command: git ${skillerSubmoduleCommand.join(" ")}`,
`Working directory: ${rootDir}`,
`Timeout: ${Duration.toSeconds(skillerSubmoduleInitTimeout)} seconds`
].join("\n")
)
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Effect.mapError((error): ControllerBootstrapError =>
error._tag === "ControllerBootstrapError"
? error
: controllerBootstrapError(
`Failed to initialize Skiller submodule before building docker-git controller.\nDetails: ${String(error)}`
)
)
)

// CHANGE: initialize the pinned Skiller submodule before controller Docker builds
// WHY: the API image copies `third_party`, so an empty submodule makes the patch/build step fail
// QUOTE(ТЗ): "исправь проблему"
// REF: user-message-2026-05-24-controller-skiller-submodule
// SOURCE: n/a
// FORMAT THEOREM: forall root: missing(root/skillerPackagePath) -> init(root) -> exists(root/skillerPackagePath) or typed error
// PURITY: SHELL
// EFFECT: Effect<void, ControllerBootstrapError, FileSystem | Path | CommandExecutor>
// INVARIANT: controller revision and Docker build context are computed only after Skiller source exists
// COMPLEXITY: O(1) filesystem probes plus O(git submodule update)
export const ensureSkillerSubmoduleInitialized = (
rootDir: string
): Effect.Effect<void, ControllerBootstrapError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const path = yield* _(Path.Path)
const packagePath = path.join(rootDir, skillerPackagePath)
const existsBeforeInit = yield* _(fs.exists(packagePath).pipe(Effect.mapError(mapSkillerPathError)))
if (existsBeforeInit) {
return
}

yield* _(Effect.log("Initializing Skiller submodule for docker-git controller build."))
yield* _(runSkillerSubmoduleInit(rootDir))

const existsAfterInit = yield* _(fs.exists(packagePath).pipe(Effect.mapError(mapSkillerPathError)))
if (existsAfterInit) {
return
}

return yield* _(
Effect.fail(
controllerBootstrapError(
`Skiller submodule initialization completed but ${packagePath} was not found.`
)
)
)
})

export const composeFilesForMode = (
composePath: string,
gpuOverlayPath: string | null
Expand Down Expand Up @@ -126,13 +226,13 @@ type ComposePathAndGpuMode = {
readonly buildSkillerMode: ControllerBuildSkillerMode
}

const withComposePathAndGpuMode = <A>(
const withComposePathAndGpuMode = <A, R>(
effect: (input: ComposePathAndGpuMode) => Effect.Effect<
A,
ControllerBootstrapError,
FileSystem.FileSystem | Path.Path
R
>
): Effect.Effect<A, ControllerBootstrapError, FileSystem.FileSystem | Path.Path> =>
): Effect.Effect<A, ControllerBootstrapError, FileSystem.FileSystem | Path.Path | R> =>
composeFilePath().pipe(
Effect.mapError(mapComposePathError),
Effect.flatMap((composePath) =>
Expand Down Expand Up @@ -170,8 +270,16 @@ const persistControllerRevision = (revision: string): Effect.Effect<void> =>
export const prepareControllerRevision = (): Effect.Effect<
string,
ControllerBootstrapError,
FileSystem.FileSystem | Path.Path
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
> =>
withComposePathAndGpuMode(({ buildSkillerMode, composePath, gpuMode }) =>
computeControllerRevision(composePath, gpuMode, buildSkillerMode)
).pipe(Effect.tap((revision) => persistControllerRevision(revision)))
Effect.gen(function*(_) {
const path = yield* _(Path.Path)
if (buildSkillerMode === "1") {
yield* _(ensureSkillerSubmoduleInitialized(path.dirname(composePath)))
}
return yield* _(computeControllerRevision(composePath, gpuMode, buildSkillerMode))
})
).pipe(
Effect.tap((revision) => persistControllerRevision(revision))
)
1 change: 1 addition & 0 deletions packages/app/src/docker-git/controller-revision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const controllerRevisionInputs: ReadonlyArray<string> = [
"docker-compose.yml",
"docker-compose.api.yml",
"docker-compose.gpu.yml",
".gitmodules",
"package.json",
"bun.lock",
"bunfig.toml",
Expand Down
Loading
Loading