Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
45f4f3d
Initial commit with task details
konard May 12, 2026
4ee4bb1
feat(core): use shared box image for project containers
konard May 12, 2026
aa8867f
fix(core): use public box image alias
konard May 12, 2026
393b70d
fix(core): use lightweight shared box image
konard May 12, 2026
bc9bf4c
fix(core): keep prompt inert for non-interactive shells
konard May 12, 2026
601164a
fix(core): normalize box image home environment
konard May 12, 2026
cb90cb1
fix(core): rewrite inherited box home references
konard May 12, 2026
bf18af7
fix(core): narrow clone cache refresh refs
konard May 12, 2026
0ab5a45
Revert "Initial commit with task details"
konard May 12, 2026
5a2ceec
Merge remote-tracking branch 'origin/main' into pr-281-local
skulidropek May 14, 2026
223ca99
fix(e2e): keep opencode temp state out of build context
skulidropek May 14, 2026
25b7283
fix(e2e): keep login context temp state out of checkout
skulidropek May 14, 2026
dd273e0
docs(core): document bun profile renderer
skulidropek May 14, 2026
ba0a297
fix(api): retry skiller install in controller image
skulidropek May 14, 2026
6f23fe2
fix(core): validate generated ssh user names
skulidropek May 14, 2026
8a99997
fix(e2e): avoid recursive home chown in image build
skulidropek May 14, 2026
107800a
test(core): update workspace dockerfile assertion
skulidropek May 14, 2026
c537c64
fix(core): address box review invariants
skulidropek May 14, 2026
ffa9f80
ci(e2e): free disk before docker builds
skulidropek May 14, 2026
578de65
test(core): assert rendered chown path
skulidropek May 14, 2026
5c2656b
fix(core): stabilize box project container startup
skulidropek May 14, 2026
73761c8
fix(core): address coderabbit box followups
skulidropek May 14, 2026
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
25 changes: 25 additions & 0 deletions .github/actions/free-docker-disk/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Free Docker disk
description: Remove unused hosted-runner toolchains before Docker image E2E builds.

runs:
using: composite
steps:
- name: Free disk for Docker builds
shell: bash
run: |
set -euxo pipefail

df -h
docker system df || true

sudo rm -rf \
/opt/ghc \
/opt/hostedtoolcache/CodeQL \
/usr/local/.ghcup \
/usr/local/lib/android \
/usr/share/dotnet

docker system prune -af --volumes || true

df -h
docker system df || true
12 changes: 12 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ jobs:
submodules: true
- name: Install dependencies
uses: ./.github/actions/setup
- name: Free Docker disk
uses: ./.github/actions/free-docker-disk
- name: Docker info
run: docker version && docker compose version
- name: Browser command startup
Expand All @@ -174,6 +176,8 @@ jobs:
submodules: true
- name: Install dependencies
uses: ./.github/actions/setup
- name: Free Docker disk
uses: ./.github/actions/free-docker-disk
- name: Docker info
run: docker version && docker compose version
- name: OpenCode autoconnect
Expand All @@ -189,6 +193,8 @@ jobs:
submodules: true
- name: Install dependencies
uses: ./.github/actions/setup
- name: Free Docker disk
uses: ./.github/actions/free-docker-disk
- name: Docker info
run: docker version && docker compose version
- name: Clone cache reuse
Expand All @@ -204,6 +210,8 @@ jobs:
submodules: true
- name: Install dependencies
uses: ./.github/actions/setup
- name: Free Docker disk
uses: ./.github/actions/free-docker-disk
- name: Docker info
run: docker version && docker compose version
- name: Login context notice
Expand All @@ -219,6 +227,8 @@ jobs:
submodules: true
- name: Install dependencies
uses: ./.github/actions/setup
- name: Free Docker disk
uses: ./.github/actions/free-docker-disk
- name: Docker info
run: docker version && docker compose version
- name: Runtime volumes + host SSH CLI
Expand All @@ -234,6 +244,8 @@ jobs:
submodules: true
- name: Install dependencies
uses: ./.github/actions/setup
- name: Free Docker disk
uses: ./.github/actions/free-docker-disk
- name: Docker info
run: docker version && docker compose version
- name: Clone auto-open SSH
Expand Down
13 changes: 12 additions & 1 deletion packages/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ RUN bun run --cwd packages/api build
RUN bun scripts/skiller-apply-docker-git-patches.mjs
RUN test -f third_party/skiller-desktop-skills-manager/package.json \
&& cd third_party/skiller-desktop-skills-manager \
&& bun install --frozen-lockfile --silent \
&& for attempt in 1 2 3 4 5; do \
if bun install --frozen-lockfile --silent; then \
break; \
fi; \
if [ "$attempt" = "5" ]; then \
echo "skiller bun install failed after retries" >&2; \
exit 1; \
fi; \
echo "skiller bun install attempt ${attempt} failed; retrying..." >&2; \
rm -rf /root/.bun/install/cache node_modules; \
sleep $((attempt * 2)); \
done \
&& bun run build \
&& touch out/.docker-git-browser-folder-picker.patch \
&& mkdir -p out/preload \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
/* jscpd:ignore-start */
import { Either } from "effect"

import { type CreateCommand, defaultTemplateConfig, isDockerNetworkMode, isGpuMode, type ParseError } from "./domain.js"
import {
type CreateCommand,
defaultTemplateConfig,
isDockerNetworkMode,
isGpuMode,
isUnixUserName,
type ParseError,
sshUserNamePatternDescription
} from "./domain.js"

const parsePort = (value: string): Either.Either<number, ParseError> => {
const parsed = Number(value)
Expand All @@ -22,8 +30,64 @@ const parsePort = (value: string): Either.Either<number, ParseError> => {
return Either.right(parsed)
}

/**
* Parses a raw SSH port value into the valid Docker host-port range.
*
* @param value - Raw textual value for `--ssh-port`.
* @returns Either a valid integer port or a typed parse error for `--ssh-port`.
* @pure true
* @effect none; CORE parser only evaluates the provided string.
* @invariant Right(port) implies Number.isInteger(port) and 1 <= port <= 65535.
* @precondition value is untrusted CLI or config text.
* @postcondition the function returns a typed Either and never throws.
* @complexity O(1) time / O(1) space.
*/
export const parseSshPort = (value: string): Either.Either<number, ParseError> => parsePort(value)

/**
* Parses and validates the SSH user used by generated Dockerfiles and entrypoints.
*
* @param value - Optional raw value for `--ssh-user`; undefined falls back to the default template user.
* @returns Either a Linux user name matching the docker-git invariant or a typed parse error.
* @pure true
* @effect none; CORE parser only trims and validates the candidate string.
* @invariant Right(user) implies user matches ^[a-z_][a-z0-9_-]{0,31}$.
* @precondition value is untrusted CLI or config text.
* @postcondition empty candidates fail as MissingRequiredOption; unsafe candidates fail as InvalidOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const parseSshUser = (
value: string | undefined
): Either.Either<string, ParseError> => {
const candidate = value?.trim() ?? defaultTemplateConfig.sshUser
if (candidate.length === 0) {
return Either.left({
_tag: "MissingRequiredOption",
option: "--ssh-user"
})
}
if (!isUnixUserName(candidate)) {
return Either.left({
_tag: "InvalidOption",
option: "--ssh-user",
reason: `expected Linux user name matching ${sshUserNamePatternDescription}`
})
}
return Either.right(candidate)
}

/**
* Parses the Docker network mode selector used by generated compose files.
*
* @param value - Optional raw value for `--network-mode`; undefined falls back to the template default.
* @returns Either a supported network mode or a typed parse error for `--network-mode`.
* @pure true
* @effect none; CORE parser only trims and checks a finite domain.
* @invariant Right(mode) implies mode is either "shared" or "project".
* @precondition value is untrusted CLI or config text.
* @postcondition unsupported modes fail as InvalidOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const parseDockerNetworkMode = (
value: string | undefined
): Either.Either<CreateCommand["config"]["dockerNetworkMode"], ParseError> => {
Expand All @@ -38,6 +102,18 @@ export const parseDockerNetworkMode = (
})
}

/**
* Parses the GPU mode selector used by generated compose files.
*
* @param value - Optional raw value for `--gpu`; undefined falls back to the template default.
* @returns Either a supported GPU mode or a typed parse error for `--gpu`.
* @pure true
* @effect none; CORE parser only trims and checks a finite domain.
* @invariant Right(mode) implies mode is either "none" or "all".
* @precondition value is untrusted CLI or config text.
* @postcondition unsupported modes fail as InvalidOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const parseGpuMode = (
value: string | undefined
): Either.Either<CreateCommand["config"]["gpu"], ParseError> => {
Expand All @@ -52,6 +128,20 @@ export const parseGpuMode = (
})
}

/**
* Parses a required non-empty string option with an optional fallback.
*
* @param option - CLI option name reported in typed parse errors.
* @param value - Optional raw value supplied by the user.
* @param fallback - Optional default used when value is undefined.
* @returns Either the trimmed non-empty candidate or a typed missing-option error.
* @pure true
* @effect none; CORE parser only trims and checks string length.
* @invariant Right(candidate) implies candidate.length > 0.
* @precondition option names the boundary field being decoded.
* @postcondition missing or empty candidates fail as MissingRequiredOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const nonEmpty = (
option: string,
value: string | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { Either } from "effect"

import { expandContainerHome } from "../usecases/scrap-path.js"
import { resolveAutoAgentFlags } from "./auto-agent-flags.js"
import { nonEmpty, parseDockerNetworkMode, parseGpuMode, parseSshPort } from "./command-builders-shared.js"
import {
nonEmpty,
parseDockerNetworkMode,
parseGpuMode,
parseSshPort,
parseSshUser
} from "./command-builders-shared.js"
import { type RawOptions } from "./command-options.js"
import {
type AgentMode,
Expand Down Expand Up @@ -46,7 +52,7 @@ const resolveRepoBasics = (raw: RawOptions): Either.Either<RepoBasics, ParseErro
const repoRef = yield* _(
nonEmpty("--repo-ref", raw.repoRef ?? resolvedRepo.repoRef, defaultTemplateConfig.repoRef)
)
const sshUser = yield* _(nonEmpty("--ssh-user", raw.sshUser, defaultTemplateConfig.sshUser))
const sshUser = yield* _(parseSshUser(raw.sshUser))
const rawTargetDir = yield* _(
nonEmpty("--target-dir", raw.targetDir, defaultTemplateConfig.targetDir)
)
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/docker-git/frontend-lib/core/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export type AgentMode = "claude" | "codex" | "gemini"
export type DockerNetworkMode = "shared" | "project"
export type GpuMode = "none" | "all"

const unixUserNamePattern = /^[a-z_][a-z0-9_-]{0,31}$/

export const sshUserNamePatternDescription = "^[a-z_][a-z0-9_-]{0,31}$"

// CHANGE: define the SSH user name invariant in the core domain
// WHY: generated Dockerfiles and entrypoints interpolate sshUser into shell-sensitive user commands
// QUOTE(ТЗ): n/a
// REF: PR-281-coderabbit-sshUser-validation
// SOURCE: n/a
// FORMAT THEOREM: forall u: isUnixUserName(u) -> not contains_shell_metacharacters(u)
// PURITY: CORE
// INVARIANT: accepted user names contain only lowercase Linux account-name characters
// COMPLEXITY: O(n)/O(1) where n = |value|
export const isUnixUserName = (value: string): boolean => unixUserNamePattern.test(value)

export interface TemplateConfig {
readonly containerName: string
readonly serviceName: string
Expand Down
92 changes: 91 additions & 1 deletion packages/app/src/lib/core/command-builders-shared.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
/* jscpd:ignore-start */
import { Either } from "effect"

import { type CreateCommand, defaultTemplateConfig, isDockerNetworkMode, isGpuMode, type ParseError } from "./domain.js"
import {
type CreateCommand,
defaultTemplateConfig,
isDockerNetworkMode,
isGpuMode,
isUnixUserName,
type ParseError,
sshUserNamePatternDescription
} from "./domain.js"

const parsePort = (value: string): Either.Either<number, ParseError> => {
const parsed = Number(value)
Expand All @@ -22,8 +30,64 @@ const parsePort = (value: string): Either.Either<number, ParseError> => {
return Either.right(parsed)
}

/**
* Parses a raw SSH port value into the valid Docker host-port range.
*
* @param value - Raw textual value for `--ssh-port`.
* @returns Either a valid integer port or a typed parse error for `--ssh-port`.
* @pure true
* @effect none; CORE parser only evaluates the provided string.
* @invariant Right(port) implies Number.isInteger(port) and 1 <= port <= 65535.
* @precondition value is untrusted CLI or config text.
* @postcondition the function returns a typed Either and never throws.
* @complexity O(1) time / O(1) space.
*/
export const parseSshPort = (value: string): Either.Either<number, ParseError> => parsePort(value)

/**
* Parses and validates the SSH user used by generated Dockerfiles and entrypoints.
*
* @param value - Optional raw value for `--ssh-user`; undefined falls back to the default template user.
* @returns Either a Linux user name matching the docker-git invariant or a typed parse error.
* @pure true
* @effect none; CORE parser only trims and validates the candidate string.
* @invariant Right(user) implies user matches ^[a-z_][a-z0-9_-]{0,31}$.
* @precondition value is untrusted CLI or config text.
* @postcondition empty candidates fail as MissingRequiredOption; unsafe candidates fail as InvalidOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const parseSshUser = (
value: string | undefined
): Either.Either<string, ParseError> => {
const candidate = value?.trim() ?? defaultTemplateConfig.sshUser
if (candidate.length === 0) {
return Either.left({
_tag: "MissingRequiredOption",
option: "--ssh-user"
})
}
if (!isUnixUserName(candidate)) {
return Either.left({
_tag: "InvalidOption",
option: "--ssh-user",
reason: `expected Linux user name matching ${sshUserNamePatternDescription}`
})
}
return Either.right(candidate)
}

/**
* Parses the Docker network mode selector used by generated compose files.
*
* @param value - Optional raw value for `--network-mode`; undefined falls back to the template default.
* @returns Either a supported network mode or a typed parse error for `--network-mode`.
* @pure true
* @effect none; CORE parser only trims and checks a finite domain.
* @invariant Right(mode) implies mode is either "shared" or "project".
* @precondition value is untrusted CLI or config text.
* @postcondition unsupported modes fail as InvalidOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const parseDockerNetworkMode = (
value: string | undefined
): Either.Either<CreateCommand["config"]["dockerNetworkMode"], ParseError> => {
Expand All @@ -38,6 +102,18 @@ export const parseDockerNetworkMode = (
})
}

/**
* Parses the GPU mode selector used by generated compose files.
*
* @param value - Optional raw value for `--gpu`; undefined falls back to the template default.
* @returns Either a supported GPU mode or a typed parse error for `--gpu`.
* @pure true
* @effect none; CORE parser only trims and checks a finite domain.
* @invariant Right(mode) implies mode is either "none" or "all".
* @precondition value is untrusted CLI or config text.
* @postcondition unsupported modes fail as InvalidOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const parseGpuMode = (
value: string | undefined
): Either.Either<CreateCommand["config"]["gpu"], ParseError> => {
Expand All @@ -52,6 +128,20 @@ export const parseGpuMode = (
})
}

/**
* Parses a required non-empty string option with an optional fallback.
*
* @param option - CLI option name reported in typed parse errors.
* @param value - Optional raw value supplied by the user.
* @param fallback - Optional default used when value is undefined.
* @returns Either the trimmed non-empty candidate or a typed missing-option error.
* @pure true
* @effect none; CORE parser only trims and checks string length.
* @invariant Right(candidate) implies candidate.length > 0.
* @precondition option names the boundary field being decoded.
* @postcondition missing or empty candidates fail as MissingRequiredOption.
* @complexity O(n) time / O(1) space where n = |value|.
*/
export const nonEmpty = (
option: string,
value: string | undefined,
Expand Down
Loading
Loading