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
108 changes: 74 additions & 34 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,32 +300,37 @@ jobs:
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile

# Build the `codeg-mcp` companion binary for the matrix target and
# stage it as a Tauri sidecar at
# `src-tauri/binaries/codeg-mcp-<triple>{.exe}`. `tauri-action` (below)
# then bundles it via the `bundle.externalBin` entry in
# `tauri.conf.json` — Tauri installs it next to the main executable
# (Contents/MacOS on macOS, install root on Linux/Windows) where the
# runtime `locate_codeg_mcp_binary()` finds it via `current_exe`
# sibling lookup. The Linux arm64 cross env vars set above also
# apply to this cargo invocation.
- name: Stage codeg-mcp sidecar for Tauri bundle
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.22.x"

# Build both sidecars for the matrix target and stage them as Tauri
# externalBin companions:
# - codeg-mcp: stdio MCP companion
# - codeg-tsnet: pure-Go Tailscale Funnel control plane
# `tauri-action` (below) bundles them via `bundle.externalBin` in
# `tauri.conf.json` next to the main executable, where runtime
# sibling lookup finds them.
- name: Stage sidecars for Tauri bundle
shell: bash
run: pnpm tauri:prepare-sidecars --target ${{ matrix.target }}

- name: Verify codeg-mcp sidecar landed
- name: Verify sidecars landed
shell: bash
run: |
ext=""
case "${{ matrix.target }}" in
*windows*) ext=".exe" ;;
esac
file="src-tauri/binaries/codeg-mcp-${{ matrix.target }}${ext}"
if [ ! -f "$file" ]; then
echo "FATAL: sidecar $file missing after prepare-sidecars"
exit 1
fi
ls -la "$file"
for name in codeg-mcp codeg-tsnet; do
file="src-tauri/binaries/${name}-${{ matrix.target }}${ext}"
if [ ! -f "$file" ]; then
echo "FATAL: sidecar $file missing after prepare-sidecars"
exit 1
fi
ls -la "$file"
done

- name: Import Apple Developer ID certificate
if: contains(matrix.target, 'apple-darwin')
Expand Down Expand Up @@ -662,24 +667,48 @@ jobs:
pnpm install --frozen-lockfile
pnpm build

- name: Build server + codeg-mcp companion
working-directory: src-tauri
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.22.x"

- name: Build server + sidecars
shell: bash
# `codeg-mcp` is the stdio MCP companion the runtime injects per
# session (see acp/delegation/companion.rs). Built with the same
# `--no-default-features --target` flags as the server so it shares
# the cross-compile env (Linux arm64) without dragging in tauri
# runtime deps.
# session (see acp/delegation/companion.rs). `codeg-tsnet` is the
# pure-Go Tailscale Funnel control plane. Both ship next to
# codeg-server so sibling lookup works.
run: |
cargo build --release --bin codeg-server --no-default-features --target ${{ matrix.target }}
cargo build --release --bin codeg-mcp --no-default-features --target ${{ matrix.target }}
set -euo pipefail
cargo build --release --bin codeg-server --no-default-features --target ${{ matrix.target }} --manifest-path src-tauri/Cargo.toml
cargo build --release --bin codeg-mcp --no-default-features --target ${{ matrix.target }} --manifest-path src-tauri/Cargo.toml
# Map Rust triple -> Go env for cross builds.
case "${{ matrix.target }}" in
x86_64-apple-darwin) export GOOS=darwin GOARCH=amd64 ;;
aarch64-apple-darwin) export GOOS=darwin GOARCH=arm64 ;;
x86_64-unknown-linux-gnu) export GOOS=linux GOARCH=amd64 ;;
aarch64-unknown-linux-gnu) export GOOS=linux GOARCH=arm64 ;;
x86_64-pc-windows-msvc) export GOOS=windows GOARCH=amd64 ;;
aarch64-pc-windows-msvc) export GOOS=windows GOARCH=arm64 ;;
*) echo "unsupported target for codeg-tsnet: ${{ matrix.target }}"; exit 1 ;;
esac
export CGO_ENABLED=0
if [ "$GOOS" = "windows" ]; then
out="src-tauri/target/${{ matrix.target }}/release/codeg-tsnet.exe"
else
out="src-tauri/target/${{ matrix.target }}/release/codeg-tsnet"
fi
mkdir -p "$(dirname "$out")"
(cd codeg-tsnet && go build -o "../$out" .)

- name: Package (Unix)
if: runner.os != 'Windows'
run: |
mkdir -p dist/${{ matrix.artifact }}
cp src-tauri/target/${{ matrix.target }}/release/codeg-server dist/${{ matrix.artifact }}/
cp src-tauri/target/${{ matrix.target }}/release/codeg-mcp dist/${{ matrix.artifact }}/
chmod +x dist/${{ matrix.artifact }}/codeg-server dist/${{ matrix.artifact }}/codeg-mcp
cp src-tauri/target/${{ matrix.target }}/release/codeg-tsnet dist/${{ matrix.artifact }}/
chmod +x dist/${{ matrix.artifact }}/codeg-server dist/${{ matrix.artifact }}/codeg-mcp dist/${{ matrix.artifact }}/codeg-tsnet
cp -r out dist/${{ matrix.artifact }}/web
cd dist && tar czf ${{ matrix.artifact }}.tar.gz ${{ matrix.artifact }}

Expand All @@ -690,6 +719,7 @@ jobs:
New-Item -ItemType Directory -Force -Path dist/${{ matrix.artifact }}
Copy-Item src-tauri/target/${{ matrix.target }}/release/codeg-server.exe dist/${{ matrix.artifact }}/
Copy-Item src-tauri/target/${{ matrix.target }}/release/codeg-mcp.exe dist/${{ matrix.artifact }}/
Copy-Item src-tauri/target/${{ matrix.target }}/release/codeg-tsnet.exe dist/${{ matrix.artifact }}/
Copy-Item -Recurse out dist/${{ matrix.artifact }}/web
Compress-Archive -Path dist/${{ matrix.artifact }} -DestinationPath dist/${{ matrix.artifact }}.zip

Expand All @@ -699,15 +729,16 @@ jobs:
set -euo pipefail
test -x "dist/${{ matrix.artifact }}/codeg-server"
test -x "dist/${{ matrix.artifact }}/codeg-mcp"
# `codeg-mcp --help` exits 0 without flags and prints the usage
# line. Only run it for native targets; cross-compiled arm64
# binaries on x64 runners can't execute.
test -x "dist/${{ matrix.artifact }}/codeg-tsnet"
# Only execute native binaries; cross-compiled arm64 on x64
# runners cannot run.
if [ "${{ matrix.target }}" = "x86_64-unknown-linux-gnu" ] || \
[ "${{ matrix.target }}" = "x86_64-apple-darwin" ] || \
[ "${{ matrix.target }}" = "aarch64-apple-darwin" ]; then
"dist/${{ matrix.artifact }}/codeg-mcp" --help | grep -F 'codeg-mcp --parent-connection-id'
"dist/${{ matrix.artifact }}/codeg-tsnet" --help | grep -Ei 'control-addr|hostname|state-dir'
else
echo "skipping codeg-mcp --help on cross-target ${{ matrix.target }}"
echo "skipping sidecar --help on cross-target ${{ matrix.target }}"
fi

- name: Smoke test packaged artifact (Windows)
Expand All @@ -720,10 +751,17 @@ jobs:
if (-not (Test-Path "dist/${{ matrix.artifact }}/codeg-mcp.exe")) {
throw "codeg-mcp.exe missing from packaged artifact"
}
if (-not (Test-Path "dist/${{ matrix.artifact }}/codeg-tsnet.exe")) {
throw "codeg-tsnet.exe missing from packaged artifact"
}
$help = & "dist/${{ matrix.artifact }}/codeg-mcp.exe" --help
if ($help -notlike "*codeg-mcp --parent-connection-id*") {
throw "codeg-mcp --help output unexpected: $help"
}
$tsHelp = & "dist/${{ matrix.artifact }}/codeg-tsnet.exe" --help
if ($tsHelp -notmatch "control-addr|hostname|state-dir") {
throw "codeg-tsnet --help output unexpected: $tsHelp"
}

- name: Upload artifact for Docker build (Linux only)
if: startsWith(matrix.target, 'x86_64-unknown-linux') || startsWith(matrix.target, 'aarch64-unknown-linux')
Expand Down Expand Up @@ -809,14 +847,16 @@ jobs:
mkdir -p dist/amd64 dist/arm64
cp artifacts/linux-x64/codeg-server dist/amd64/codeg-server
cp artifacts/linux-x64/codeg-mcp dist/amd64/codeg-mcp
cp artifacts/linux-x64/codeg-tsnet dist/amd64/codeg-tsnet
cp artifacts/linux-arm64/codeg-server dist/arm64/codeg-server
cp artifacts/linux-arm64/codeg-mcp dist/arm64/codeg-mcp
cp artifacts/linux-arm64/codeg-tsnet dist/arm64/codeg-tsnet
cp -r artifacts/linux-x64/web dist/web
chmod +x dist/amd64/codeg-server dist/amd64/codeg-mcp \
dist/arm64/codeg-server dist/arm64/codeg-mcp
chmod +x dist/amd64/codeg-server dist/amd64/codeg-mcp dist/amd64/codeg-tsnet \
dist/arm64/codeg-server dist/arm64/codeg-mcp dist/arm64/codeg-tsnet
# Fail fast if any companion went missing — Docker would otherwise
# produce an image where delegation silently degrades.
for f in dist/amd64/codeg-mcp dist/arm64/codeg-mcp; do
# produce an image where delegation / Funnel silently degrades.
for f in dist/amd64/codeg-mcp dist/arm64/codeg-mcp dist/amd64/codeg-tsnet dist/arm64/codeg-tsnet; do
test -x "$f" || { echo "FATAL: $f missing or non-exec"; exit 1; }
done

Expand Down
21 changes: 15 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@ COPY public/ ./public/
COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./
RUN pnpm build

# Stage 2: Build Rust server binary + codeg-mcp companion
# Stage 2: Build Rust server binary + sidecars
FROM rust:slim-bookworm AS backend
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y pkg-config libssl-dev curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install Go for the pure-Go Tailscale Funnel sidecar (codeg-tsnet).
RUN curl -fsSL https://go.dev/dl/go1.22.12.linux-amd64.tar.gz | tar -C /usr/local -xz
ENV PATH="/usr/local/go/bin:${PATH}"
WORKDIR /app
COPY src-tauri/ ./src-tauri/
COPY codeg-tsnet/ ./codeg-tsnet/
WORKDIR /app/src-tauri
COPY src-tauri/ ./
# codeg-mcp is the stdio MCP companion the runtime injects per session
# (see acp/delegation/companion.rs). It must ship next to codeg-server so
# `locate_codeg_mcp_binary()` finds it via the exe-sibling lookup.
# (see acp/delegation/companion.rs). codeg-tsnet is the private Tailscale
# Funnel control plane. Both must ship next to codeg-server so sibling
# lookup works.
RUN cargo build --release --bin codeg-server --no-default-features \
&& cargo build --release --bin codeg-mcp --no-default-features
&& cargo build --release --bin codeg-mcp --no-default-features \
&& cd /app/codeg-tsnet && CGO_ENABLED=0 go build -o /app/src-tauri/target/release/codeg-tsnet .

# Stage 3: Runtime
FROM node:24-bookworm-slim
Expand All @@ -42,6 +50,7 @@ RUN apt-get update && apt-get install -y \

COPY --from=backend /app/src-tauri/target/release/codeg-server /usr/local/bin/codeg-server
COPY --from=backend /app/src-tauri/target/release/codeg-mcp /usr/local/bin/codeg-mcp
COPY --from=backend /app/src-tauri/target/release/codeg-tsnet /usr/local/bin/codeg-tsnet
COPY --from=frontend /app/out /app/web

ENV CODEG_STATIC_DIR=/app/web
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ COPY dist/${TARGETARCH}/codeg-server /usr/local/bin/codeg-server
# (see acp/delegation/companion.rs). It must ship next to codeg-server so
# `locate_codeg_mcp_binary()` finds it via the exe-sibling lookup.
COPY dist/${TARGETARCH}/codeg-mcp /usr/local/bin/codeg-mcp
# codeg-tsnet is the private Tailscale Funnel control plane.
COPY dist/${TARGETARCH}/codeg-tsnet /usr/local/bin/codeg-tsnet
COPY dist/web /app/web

ENV CODEG_STATIC_DIR=/app/web
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,12 @@ Environment variables:
| `CODEG_DATA_DIR` | `~/.local/share/codeg` | SQLite database directory (also roots `uploads/`, `pets/`) |
| `CODEG_STATIC_DIR` | `./web` or `./out` | Next.js static export directory |
| `CODEG_MCP_BIN` | _(unset)_ | Absolute path to the `codeg-mcp` companion. Overrides the default sibling-of-executable + `PATH` lookup. Use this for source builds or custom layouts where the companion lives outside the server's install directory. |
| `CODEG_SKIP_SIDECAR` | _(unset)_ | Frontend-only convenience for `pnpm tauri dev` / `pnpm tauri build` — when `1`, skips building the `codeg-mcp` sidecar. Delegation is disabled in that build; ship-quality artifacts must leave it unset. |
| `CODEG_TSNET_BIN` | _(unset)_ | Absolute path to the `codeg-tsnet` Funnel sidecar. Overrides sibling-of-executable + `PATH` lookup. |
| `CODEG_TS_FUNNEL` | _(unset)_ | When `1` / `true` / `yes`, enable Tailscale Funnel on `codeg-server` after bind. Failures never take down local web. Headless mode requires `CODEG_TS_AUTHKEY`. |
| `CODEG_TS_AUTHKEY` | _(unset)_ | Tailscale auth key for headless Funnel login (required for server/Docker when Funnel is enabled). Never logged. |
| `CODEG_TS_STATE_DIR` | `<CODEG_DATA_DIR>/tailscale` | Independent userspace Tailscale state directory. Codeg never manages system Tailscale. |
| `CODEG_TS_HOSTNAME` | `codeg-<8hex>` | Hostname for Codeg's private Tailscale node (stable hash of data dir by default). |
| `CODEG_SKIP_SIDECAR` | _(unset)_ | Frontend-only convenience for `pnpm tauri dev` / `pnpm tauri build` — when `1`, skips building both `codeg-mcp` and `codeg-tsnet` sidecars. Delegation/Funnel are disabled in that build; ship-quality artifacts must leave it unset. |
| `CODEG_UPLOAD_MAX_TOTAL_BYTES` | _(unset)_ | Hard cap on total bytes resident under `<data dir>/uploads/`. Plain decimal byte count (e.g. `10737418240` for 10 GiB). Unset, `0`, or an unparseable value disables the cap and prints a startup line so the posture is visible. The cap is enforced within a single `codeg-server` process — horizontally-scaled deployments sharing one `uploads/` volume need external coordination (file lock, Redis, reverse-proxy quota). |
| `CODEG_UPLOAD_QUOTA_STRICT` | _(unset)_ | When truthy (`1` / `true` / `yes` / `on`), abort startup with exit code 2 if `CODEG_UPLOAD_MAX_TOTAL_BYTES` is set to an unparseable value, instead of fail-open with a WARN. Use this when your security policy requires "configured quota must be effective". |

Expand Down
5 changes: 5 additions & 0 deletions codeg-tsnet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/codeg-tsnet
/codeg-tsnet.exe
*.test
*.out

Loading
Loading