Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d71c5c
docs: configure isolated source development stack
345942185-eng Oct 22, 2026
2c3ff8e
feat: add source stack supervisor and Docker acceptance runner
345942185-eng Oct 22, 2026
c0e7554
feat: isolate dev state and supervise source processes
345942185-eng Oct 22, 2026
d817259
test: cover dev stack isolation and terminal routing
345942185-eng Oct 22, 2026
8b1236e
fix: keep dev auth isolated from installed credentials
345942185-eng Oct 22, 2026
02979a1
test: reject installed OAuth discovery in source dev
345942185-eng Oct 22, 2026
2fb3cbb
fix: proxy source API and terminal WebSockets
345942185-eng Oct 22, 2026
a20d191
ci: validate source stack and cleanup on Ubuntu
345942185-eng Oct 22, 2026
a7a61aa
ci: verify source stack on Linux and macOS
345942185-eng Oct 22, 2026
0a02e25
fix: diagnose failed containers before cleanup
345942185-eng Oct 22, 2026
742284d
fix: use Temporal service address for health checks
345942185-eng Oct 22, 2026
40d0145
fix: verify original export snapshot and regenerated candidate
345942185-eng Oct 22, 2026
520c750
test: cover export snapshot verification after public edits
345942185-eng Oct 22, 2026
7c7daf1
fix: require database and enabled worker readiness
345942185-eng Oct 22, 2026
70bd8d1
fix: reset smoke resources after launcher failure
345942185-eng Oct 22, 2026
c7a4e0e
test: cover disabled workflows and cleanup after failed startup
345942185-eng Oct 22, 2026
49ecf95
ci: restore upstream provenance policy for outside contributions
345942185-eng Oct 22, 2026
8bff3d4
test: restore upstream workflow assertions
345942185-eng Oct 22, 2026
f7e1635
fix: resolve hoisted Vite from the checkout root
345942185-eng Oct 22, 2026
fb197f7
fix: launch the web service using the absolute Vite path
345942185-eng Oct 22, 2026
55b84a7
test: start the real Vite CLI from the launcher working directory
345942185-eng Oct 22, 2026
bf2291f
test: canonicalize temporary checkout paths on macOS
345942185-eng Oct 22, 2026
bc12e2a
fix: use valid UUID for source stack terminal acceptance
345942185-eng Oct 22, 2026
3541d69
fix(deps): patch Vite security advisories with 7.3.7
345942185-eng Oct 22, 2026
5abb4c7
fix(deps): patch Hono and sanitize-html security findings
345942185-eng Oct 22, 2026
19414e3
fix: prepare macOS PTY helper before source stack startup
345942185-eng Oct 23, 2026
ab8e149
fix: repair macOS node-pty helper execute bit
345942185-eng Oct 23, 2026
78539a0
test: cover macOS PTY helper permission repair
345942185-eng Oct 23, 2026
3e8e8f6
test: normalize CRLF in Codex mount assertions
345942185-eng Oct 23, 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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.git
.dev-stack
.github
.env
.env.*
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/dev-stack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Source development stack

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
source-stack:
name: Source stack / ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-15-intel]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version-file: .nvmrc
cache: npm
- name: Start Docker on macOS with Colima
if: runner.os == 'macOS'
run: |
brew install colima docker docker-compose
mkdir -p "$HOME/.docker/cli-plugins"
ln -sf "$(brew --prefix)/opt/docker-compose/bin/docker-compose" "$HOME/.docker/cli-plugins/docker-compose"
colima start --cpu 2 --memory 6 --disk 30 --runtime docker
docker info
- run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- run: node --test scripts/tests/dev-stack.test.mjs scripts/tests/dev-stack-vite.test.mjs
- name: Real source stack, owner setup, OpenCode PTY and cleanup
run: npm run smoke:dev-stack
- name: Stop test VM
if: always() && runner.os == 'macOS'
run: colima stop

source-quality:
name: Source quality gates
runs-on: ubuntu-latest
timeout-minutes: 75
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version-file: .nvmrc
cache: npm
- name: Install native test dependencies
run: sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep ffmpeg
- run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- run: npm run check
- run: npm test
- run: npm run build
- run: npm run hygiene:preflight
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Generated outputs and local tool reports. These are safe to regenerate; do
# not commit them, and clean them only with an allowlist.
node_modules/
.dev-stack/
dist/
dist-mock/
dist-types/
Expand Down
72 changes: 72 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,78 @@ packaging requires its normal toolchain and separate platform acceptance.
Dependency installation may use the network; the smoke command below does not
need a provider account, API key, Docker, PostgreSQL or Temporal.

## Run the full app from source

Use Node from `.nvmrc`, npm, a C++ build toolchain/Python for `node-pty`, and a
running Docker daemon with Compose v2 supporting `up --wait`. On macOS use
Docker Desktop or Colima (`colima start`); on Windows 11 run everything inside
WSL2 with Docker Desktop's WSL integration enabled. Native Windows is refused.

```sh
ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm ci
npm run dev:stack
# In another terminal:
npm run dev:stack -- down
npm run dev:stack -- reset
```

The foreground launcher builds the internal packages and starts PostgreSQL
(pgvector) and Temporal in Docker, then runs migrations and the CLI host,
Temporal worker, API watcher and Vite **from this checkout**. It installs the
exact OpenCode package pinned in `Dockerfile` under `.dev-stack/opencode`,
never globally. First start needs network access for images and that package.

Open the printed URL (`http://127.0.0.1:5911` by default) and use the printed
one-time token to create your owner account. Tokens expire after 15 minutes;
restart the stack if an unclaimed token has expired. Sign in normally and open
an OpenCode terminal in Getting Started; reaching its TUI needs no provider
account. Sending AI requests still requires a working model/provider.
Browser and optional companion services are disabled in this dev stack.

Each checkout has its own Compose project, volume, Unix sockets and ignored
`.dev-stack/` state. Secret files are mode 0600. No installed instance's
credentials, environment configuration or provider homes are reused. All
published ports bind to `127.0.0.1`:

| Service | Default | Override |
| --- | --- | --- |
| API | 5910 | `SPACE_DEV_API_PORT` |
| Vite | 5911 | `SPACE_DEV_WEB_PORT` |
| PostgreSQL | 55432 | `SPACE_DEV_POSTGRES_PORT` |
| Temporal | 57233 | `SPACE_DEV_TEMPORAL_PORT` |

Use distinct free ports and keep the same overrides for `up`, `down` and
`reset`. Ports 4910/4911 are refused to avoid collisions with installed SpaceApp.
Vite fails instead of silently choosing another port. Do not share these
development endpoints through a public proxy.

Edits in `apps/web/src` hot-reload; edits in `apps/api/src` restart the API.
After changes in `packages/*` or the worker, use **one command** in another
terminal: `npm run dev:stack -- rebuild`. It stops the old stack, rebuilds all
internal packages and starts a new foreground stack with the existing data.
Ctrl+C and `down` stop the source process groups and Docker services; `reset`
also removes only this checkout's volume and marked state directory. Unexpected
service exits fail the supervisor and stop the other services.

Troubleshooting:

- `docker info` / `docker compose version` must work before startup. No daemon
is installed or started for you. On macOS check Colima/Docker Desktop; in WSL2
check Docker integration and install/build on the Linux filesystem.
- A port conflict: change the corresponding override; do not stop an installed
SpaceApp to free 4910/4911. A second stack in the same checkout is refused.
- A readiness timeout or failed worker: inspect the named service output;
authentication shortcuts and disabled workflows are not readiness fixes.
- After an uncatchable supervisor kill, a lock may remain. Inspect its PID and
confirm no source processes from this checkout are alive before removing
`.dev-stack/lock`; rerun `reset` to remove its Docker resources. Do not remove
a lock belonging to a running stack.
- To validate real acceptance in a **fresh checkout without dev state**, run
`npm run smoke:dev-stack`. It creates an owner, signs in over HTTP, opens and
attaches a real OpenCode PTY, and asserts that reset leaves no processes,
containers or volumes. The test creates and destroys only its own data.
A passing unit test is not evidence of a passing Docker or macOS/WSL2 run.

## First executed task, without a provider

```sh
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export const csrfHeaderName = CSRF_HEADER;
export function getAuthConfig(env: NodeJS.ProcessEnv): AuthConfig {
let googleClientId = env.SPACE_GOOGLE_CLIENT_ID || env.GOOGLE_CLIENT_ID || undefined;
let googleClientSecret = env.SPACE_GOOGLE_CLIENT_SECRET || env.GOOGLE_CLIENT_SECRET || undefined;
if (!googleClientId) {
// A source dev stack must not discover an installed operator's OAuth secrets.
if (!googleClientId && env.SPACE_INSTANCE !== "dev" && env.SPACE_GOOGLE_AUTH_DISABLED !== "true") {
try {
const candidates = ["/opt/spaceapp/secrets/google-oauth.json", "/var/lib/spaceapp/google-oauth.json"];
for (const p of candidates) {
Expand Down
24 changes: 24 additions & 0 deletions apps/api/tests/auth-dev-isolation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it, vi } from "vitest";

const files = vi.hoisted(() => ({
existsSync: vi.fn(() => true),
readFileSync: vi.fn(() => JSON.stringify({ clientId: "installed-client", clientSecret: "installed-secret" }))
}));
vi.mock("node:fs", async (original) => ({ ...await original<typeof import("node:fs")>(), ...files }));
import { getAuthConfig } from "../src/auth.js";

describe("source development OAuth isolation", () => {
it("does not probe installed files in dev or when Google auth is disabled", () => {
for (const env of [{ SPACE_INSTANCE: "dev" }, { SPACE_GOOGLE_AUTH_DISABLED: "true" }]) {
files.existsSync.mockClear(); files.readFileSync.mockClear();
expect(getAuthConfig(env).googleClientId).toBeUndefined();
expect(files.existsSync).not.toHaveBeenCalled();
expect(files.readFileSync).not.toHaveBeenCalled();
}
});
it("retains installed-instance discovery and explicit dev configuration", () => {
expect(getAuthConfig({}).googleClientId).toBe("installed-client");
expect(getAuthConfig({ SPACE_INSTANCE: "dev", SPACE_GOOGLE_CLIENT_ID: "own-client", SPACE_GOOGLE_CLIENT_SECRET: "own-secret" }))
.toMatchObject({ googleClientId: "own-client", googleClientSecret: "own-secret" });
});
});
10 changes: 5 additions & 5 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export default defineConfig({
host: process.env.SPACE_WEB_HOST ?? "0.0.0.0",
port: Number.parseInt(process.env.SPACE_WEB_PORT ?? "4911", 10),
proxy: {
"/api": "http://127.0.0.1:4910",
"/plugins": "http://127.0.0.1:4910",
"/healthz": "http://127.0.0.1:4910",
"/readyz": "http://127.0.0.1:4910",
"/version": "http://127.0.0.1:4910"
"/api": { target: process.env.SPACE_API_ORIGIN ?? "http://127.0.0.1:4910", ws: true },
"/plugins": process.env.SPACE_API_ORIGIN ?? "http://127.0.0.1:4910",
"/healthz": process.env.SPACE_API_ORIGIN ?? "http://127.0.0.1:4910",
"/readyz": process.env.SPACE_API_ORIGIN ?? "http://127.0.0.1:4910",
"/version": process.env.SPACE_API_ORIGIN ?? "http://127.0.0.1:4910"
}
},
test: {
Expand Down
111 changes: 65 additions & 46 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading