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
54 changes: 54 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Dependencies — always reinstall inside the image
**/node_modules
**/backups

# Logs and package-manager noise
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Git and CI (not needed in the runtime image)
.git
.github

# Env and secrets — never bake local config into layers
.env
.env.*
!.env.example

# Build, test, and release artifacts
dist
coverage
.nyc_output
release
*.tsbuildinfo

# OS and editors
.DS_Store
Thumbs.db
.idea
.vscode
*.swp
*.swo

# Writable app layers and local runtime state (see repo `.gitignore`)
app/L1
app/L2
tmp

# Server-local temp and auth data (image should start clean)
server/tmp
server/data

# SQLite and local TLS experiments
*.sqlite
*.sqlite-shm
*.sqlite-wal
*.pem
*.csr

# Optional local tooling caches
.cache
.eslintcache
65 changes: 65 additions & 0 deletions .github/workflows/container-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Build the Space Agent image and push to GitHub Container Registry (ghcr.io).
# Image: ghcr.io/<owner>/<repo> with tags from branch, semver tag, and git SHA.
#
# After the first run, grant read access if needed: Package settings → Manage Actions access,
# or keep packages private and authenticate pulls with a PAT or `GITHUB_TOKEN` where supported.

name: Container Image

on:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read
packages: write

concurrency:
group: container-image-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Image name (GHCR requires lowercase)
run: echo "IMAGE_NAME=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v') }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,7 @@ Project concepts:
- the browser authenticates through the server and uses a server-issued `space_session` cookie for protected API, module, and app-file access; when the current login is allowed to auto-restore `userCrypto` on the same browser profile, the browser keeps one encrypted local blob in `localStorage`, and authenticated browser code fetches a session-derived wrapping key from the backend by hashing the current backend `sessionId` with the server-held session secret
- when `CUSTOMWARE_GIT_HISTORY` is enabled, writable `L1/<group>/` and `L2/<user>/` roots are treated as optional per-owner local Git repositories with adaptive-debounced server-side commits and rollback APIs
- `USER_FOLDER_SIZE_LIMIT_BYTES` optionally caps each `L2/<user>/` folder on disk; app-file mutations are checked against a cached per-user size total and only size-reducing mutations are allowed once a user folder is already over the limit
- runtime file discovery is backed by sharded `file_index` state; startup indexes `L0`, `L1`, and layer roots, auth first touch loads only the target user's auth files, and full `L2/<user>` shards are loaded only on demand for file/module access or active mutations rather than preloaded just because their folders exist on disk
- runtime parameters are defined in `commands/params.yaml`; `node space serve` resolves them in this order: launch arguments, stored `.env` params written by `node space set`, then process environment variables, then schema defaults; `node space supervise` accepts the same runtime parameters, owns the public `HOST` and `PORT`, requires `CUSTOMWARE_PATH`, enables source auto-update by default, and passes the remaining resolved params to private `space serve` children; `WORKERS` controls clustered HTTP worker count for `serve` and `supervise`; `CUSTOMWARE_PATH` is the parent directory for writable backend `L1/` and `L2/` storage when configured and also hosts backend-owned cloud-share archives under `share/spaces/` when that feature is enabled; `CUSTOMWARE_WATCHDOG` defaults to `true` and controls live backend customware watching, config watching, and the periodic reconcile backstop without disabling L0/L1 startup indexing, on-demand L2 loading, or explicit clustered mutation sync; `GIT_BACKEND` defaults to `auto` and may force `native` or `isomorphic` for server-owned Git flows such as local history and Git-backed module operations; `LOGIN_ALLOWED` gates the password-login endpoints and login-shell form, `CLOUD_SHARE_ALLOWED` gates hosted cloud-share uploads, `CLOUD_SHARE_URL` tells browser clients which hosted share receiver to use, and page shells receive only `frontend_exposed` values as injected meta tags
- runtime parameters are defined in `commands/params.yaml`; `node space serve` resolves them in this order: launch arguments, stored `.env` params written by `node space set`, then process environment variables, then schema defaults; `node space supervise` accepts the same runtime parameters, owns the public `HOST` and `PORT`, requires `CUSTOMWARE_PATH`, enables source auto-update by default, and passes the remaining resolved params to private `space serve` children; `WORKERS` controls clustered HTTP worker count for `serve` and `supervise`; `CUSTOMWARE_PATH` is the parent directory for writable backend `L1/` and `L2/` storage when configured and also hosts backend-owned cloud-share archives under `share/spaces/` when that feature is enabled; `CUSTOMWARE_WATCHDOG` defaults to `true` and controls live backend customware watching, config watching, and the periodic reconcile backstop without disabling startup indexing or explicit clustered mutation sync; `GIT_BACKEND` defaults to `auto` and may force `native` or `isomorphic` for server-owned Git flows such as local history and Git-backed module operations; `LOGIN_ALLOWED` gates the password-login endpoints and login-shell form, `CLOUD_SHARE_ALLOWED` gates hosted cloud-share uploads, `CLOUD_SHARE_URL` tells browser clients which hosted share receiver to use, and page shells receive only `frontend_exposed` values as injected meta tags
- app file APIs use logical app-rooted paths such as `L2/alice/user.yaml` or `/app/L2/alice/user.yaml`, and supported endpoints may also accept `~` or `~/...` for the authenticated user's `L2/<username>/...`; those logical paths do not change when `CUSTOMWARE_PATH` relocates the writable backend roots
- non-`/api` and non-`/mod` browser entry routes are served from `server/pages/`; `/login` and `/enter` are public and the protected page shells live behind the router-side session gate
- detailed browser-runtime rules live in `/app/AGENTS.md`
Expand Down Expand Up @@ -230,6 +229,7 @@ Project concepts:
- `.vscode/launch.json` provides a `Dev Server (npm run dev)` debugger entry that launches the local dev supervisor and auto-attaches to its spawned `node space serve` child so `server/` breakpoints keep working after watcher restarts
- `node space serve` to run the server directly
- `node space supervise CUSTOMWARE_PATH=<path>` to run the production-ready zero-downtime auto-update supervisor for source checkouts
- optional root `Dockerfile` plus `docker-entrypoint.sh`: image entrypoint uses `tini`, defaults `HOST`/`PORT`/`CUSTOMWARE_PATH`, persists `CUSTOMWARE_PATH` via `node space set`, optionally seeds `L2/admin` once when that tree is missing, then runs `node space supervise`; set image `CMD` or pass a command to `docker run` to replace the default process (see comments in `docker-entrypoint.sh` for `SPACE_DOCKER_*` bootstrap toggles); root `Justfile` exposes `just docker-build`, `just docker-run`, and `just docker-shell`, with optional env overrides `SPACE_DOCKER_IMAGE`, `SPACE_DOCKER_TAG`, `SPACE_DOCKER_VOLUME`, and `SPACE_DOCKER_PORT`; `.dockerignore` trims the build context for Node/npm layouts; root `docker-compose.yaml` runs `space-agent:local` with a named Docker volume mounted at `/srv/space/customware` (build the image first, e.g. `just docker-build`)
- `npm run install:packaging` to install packaging-only dependencies
- `npm run desktop:dev`, `npm run desktop:pack`, `npm run desktop:dist`, and `npm run package:desktop:macos:dev` for the Electron host and packaging flow, with the macOS dev build command producing the default unpacked local `.app` bundle
- `.github/workflows/release-desktop.yml` builds tagged desktop releases for Windows, macOS, and Linux on both x64 and arm64; automatic tag runs and manual `workflow_dispatch` reruns both require the selected `v*` tag to be on `main` history, and both skip only when a newer `v*` tag is already on `main` after it; every publish updates the GitHub Release for that tag before uploading clobbered artifacts selected by `packaging/release-asset-filters.yaml` with uniform `Space-Agent-<release version>-<platform>-<arch>.<extension>` asset names that collapse a redundant trailing `.0` patch to the project's normal two-segment release version
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# syntax=docker/dockerfile:1

FROM node:lts

ENV HOST=0.0.0.0
ENV PORT=3000

RUN apt-get update \
&& apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . /app
RUN npm install

COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

ENTRYPOINT ["/usr/bin/tini", "--", "/docker-entrypoint.sh"]
# Omit CMD so the entrypoint default path runs; set CMD or pass `docker run … cmd` to override.
28 changes: 28 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Space Agent — local task runner (https://github.com/casey/just)
# Docker: `just docker-build`, `just docker-run`, `just docker-shell`

# Image ref for `docker build` / `docker run` recipes below.
IMAGE := env_var_or_default("SPACE_DOCKER_IMAGE", "space-agent")
TAG := env_var_or_default("SPACE_DOCKER_TAG", "local")
FULL_IMAGE := IMAGE + ":" + TAG

# Named volume for writable L1/L2 (matches docker-entrypoint default CUSTOMWARE_PATH).
VOLUME := env_var_or_default("SPACE_DOCKER_VOLUME", "space-agent-customware")

# Host port published to container PORT (default 3000 in Dockerfile).
PORT := env_var_or_default("SPACE_DOCKER_PORT", "3000")

default:
@just --list

# Build the image from the repo root (honors `.dockerignore`).
docker-build:
docker build -t {{FULL_IMAGE}} .

# Run the default supervised stack: publish HOST:PORT → container :3000 and persist customware.
docker-run:
docker run --rm -p {{PORT}}:3000 -v {{VOLUME}}:/srv/space/customware {{FULL_IMAGE}}

# Interactive shell in the image (skips entrypoint bootstrap; no server started).
docker-shell:
docker run --rm -it -e SPACE_DOCKER_SKIP_INIT=1 -v {{VOLUME}}:/srv/space/customware {{FULL_IMAGE}} bash
Loading