Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f74a315
feat: launch isolated source dev stack with full lifecycle proof
pythonyx135793 Oct 17, 2026
3d31cc8
fix: initialize proof path in runner step context
pythonyx135793 Oct 17, 2026
2fb7c2e
fix: provision ripgrep on hosted Ubuntu before stock gates
pythonyx135793 Oct 17, 2026
95e6714
test: register dev stack coverage and normalize portable fixture counts
pythonyx135793 Oct 17, 2026
39dc9df
fix(dev): confine instance storage and retain failure diagnostics
pythonyx135793 Oct 17, 2026
4233164
ci(dev): canonicalize hosted temporary root
pythonyx135793 Oct 17, 2026
3dca961
ci(dev): match platform validation to contributor scope
pythonyx135793 Oct 17, 2026
31a0643
fix(dev): bind Linux process ownership to kernel boot
pythonyx135793 Oct 17, 2026
3a45269
fix(dev): prove exact published loopback ports
pythonyx135793 Oct 17, 2026
e28ae75
Fix pinned CLI execution and prove live interactive terminal
pythonyx135793 Oct 17, 2026
02806e9
Bound browser input events and verify real CDP typing early
pythonyx135793 Oct 17, 2026
88d9e1b
Keep capability browser state short and retain startup diagnostics
pythonyx135793 Oct 17, 2026
46e34e5
fix(dev): use native Darwin stty and retain browser diagnostics
pythonyx135793 Oct 17, 2026
fafa02e
test(dev): verify current ANSI screens and early cookie capability
pythonyx135793 Oct 17, 2026
19c1286
test(dev): isolate the Darwin CI browser keychain
pythonyx135793 Oct 17, 2026
aa03e60
docs: document full source stack startup in contributing
pythonyx135793 Oct 17, 2026
be84251
Wait for browser-owned terminal geometry before UI proof
pythonyx135793 Oct 17, 2026
a4476b7
fix(dev): scope readiness probes to service generations
pythonyx135793 Oct 17, 2026
4d5bda9
fix: limit hot reload builds to source services
pythonyx135793 Oct 17, 2026
c6f7fa1
docs: document source development stack in changelog
pythonyx135793 Oct 17, 2026
ec57e6b
fix: address source development stack review findings
pythonyx135793 Oct 18, 2026
90502fa
test: retain worktree provisioning errors in source validation
pythonyx135793 Oct 18, 2026
1855198
fix: await restored source generations before lifecycle proof
pythonyx135793 Oct 18, 2026
1169096
Merge main into source development stack
pythonyx135793 Oct 19, 2026
884d001
fix: coordinate Git worktree metadata operations
pythonyx135793 Oct 19, 2026
8a11a04
test: make browser privacy and watchdog fixtures deterministic
pythonyx135793 Oct 19, 2026
b0ce62f
fix(web): cancel live debug notice timer on unmount
pythonyx135793 Oct 19, 2026
7448752
test(web): assert live debug timer is removed on unmount
pythonyx135793 Oct 19, 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
173 changes: 173 additions & 0 deletions .github/workflows/dev-stack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
name: Source development stack

on:
pull_request:
paths:
- "scripts/dev-stack*"
- "scripts/dev-stack/**"
- "scripts/tests/dev-stack.test.mjs"
- "apps/web/vite.config.ts"
- "apps/web/**"
- ".nvmrc"
- "packages/**"
- "apps/api/**"
- "apps/worker/**"
- "package*.json"
- "Dockerfile"
- ".github/workflows/dev-stack.yml"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
push:
branches:
- "codex/spaceapp65-*"
workflow_dispatch:
inputs:
expected_source:
description: "Optional exact 40-character reviewed source SHA; mismatch fails before executing project code"
type: string
required: false

permissions:
contents: read

concurrency:
group: dev-stack-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
full-stack:
# Standard public runners only. No private, self-hosted, larger or paid fallback.
if: github.event.repository.private == false
name: Real source stack / ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-15-intel]
runs-on: ${{ matrix.os }}
timeout-minutes: 120
defaults:
run:
shell: bash
env:
CI: "true"
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
SPACE_DEV_REDACT_SETUP: "true"
SPACE_DEV_CANDIDATE: ${{ github.event.pull_request.head.sha || github.sha }}
EXPECTED_SOURCE: ${{ inputs.expected_source }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
cache: npm

- name: Verify exact public candidate
run: |
printf 'SPACE_DEV_PROOF_DIR=%s/spaceapp-dev-stack-proof\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { appendFileSync, realpathSync } from 'node:fs';
assert.match(process.env.SPACE_DEV_CANDIDATE, /^[a-f0-9]{40}$/);
const actual = execFileSync('git', ['rev-parse', 'HEAD'], {encoding:'utf8'}).trim();
assert.equal(actual, process.env.SPACE_DEV_CANDIDATE);
if (process.env.EXPECTED_SOURCE) {
assert.match(process.env.EXPECTED_SOURCE, /^[a-f0-9]{40}$/);
assert.equal(actual, process.env.EXPECTED_SOURCE);
}
appendFileSync(process.env.GITHUB_ENV, `TMPDIR=${realpathSync(process.env.RUNNER_TEMP)}\n`);
console.log(`Exact source ${actual}; Node ${process.version}; ${process.platform}/${process.arch}`);
NODE
git diff --exit-code

- name: Prepare ephemeral Intel-macOS Docker
if: runner.os == 'macOS'
run: |
test "$(uname -m)" = x86_64
brew install colima docker docker-compose ripgrep
mkdir -p "$HOME/.docker/cli-plugins"
ln -sfn "$(brew --prefix)/opt/docker-compose/bin/docker-compose" "$HOME/.docker/cli-plugins/docker-compose"
colima start --profile spaceapp-dev-proof --cpu 3 --memory 6 --disk 30
docker context use colima-spaceapp-dev-proof

- name: Prepare hosted Linux test prerequisites
if: runner.os == 'Linux'
run: sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep ffmpeg

- name: Verify existing Docker, Compose, browser and search tools
run: |
docker version
docker compose version
command -v rg
if [[ "$RUNNER_OS" == macOS ]]; then
test -x '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
else
test -x /usr/bin/google-chrome
test -x /usr/bin/ffmpeg
fi

- name: Install locked workspace dependencies
run: npm ci --no-audit --no-fund

- name: Early actual pinned CLI and zero-provider PTY capability
run: node scripts/dev-stack/capability-proof.mjs

- name: Daemon-free isolation and cleanup regressions
run: npm run test:dev-stack

- name: Full stock check
run: npm run check

- name: Full stock tests (Linux reference platform)
if: runner.os == 'Linux'
run: npm test

- name: Native macOS private-instance regressions
if: runner.os == 'macOS'
run: >-
node node_modules/vitest/vitest.mjs run --config apps/api/vitest.config.ts
apps/api/tests/instance-storage.test.ts apps/api/tests/dev-oauth-storage.test.ts
apps/api/tests/dev-docx-isolation.test.ts apps/api/tests/dev-incidents-storage.test.ts
apps/api/tests/space-control-governance.test.ts
apps/api/tests/room-pane-worktrees.test.ts

- name: Native macOS CLI host regressions
if: runner.os == 'macOS'
run: npm test --workspace @space/cli-host

- name: Full stock build
run: npm run build

- name: Full stock hygiene
run: npm run hygiene:preflight

- name: Real HTTP, owner, browser, PTY, HMR, isolation and failure cleanup
run: node scripts/dev-stack/lifecycle-proof.mjs

- name: Final clean exact-source guard
if: always()
run: |
test "$(git rev-parse HEAD)" = "$SPACE_DEV_CANDIDATE"
git diff --exit-code
test -z "$(git status --porcelain --untracked-files=all)"

- name: Remove ephemeral hosted Colima profile
if: always() && runner.os == 'macOS'
run: |
if colima status --profile spaceapp-dev-proof >/dev/null 2>&1; then
colima stop --profile spaceapp-dev-proof
fi
if colima list --json | grep -q 'spaceapp-dev-proof'; then
colima delete --profile spaceapp-dev-proof --force
fi

- name: Preserve redacted evidence only
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dev-stack-${{ matrix.os }}-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ runner.temp }}/spaceapp-dev-stack-proof
if-no-files-found: warn
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ projects/
.local/

run-spaceapp-*.tgz

# Isolated from-source contributor runtime (contains secrets).
/.dev-stack*/
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Semantic Versioning.

## [Unreleased]

### Fixed

- Serialize Git worktree metadata operations for each repository during concurrent
pane setup.
- Preserve package reload priority and recovery before Compose setup in the source
development stack.
- Validate isolated storage roots and preserve typed DOCX errors and MCP diagnostics
in new development instances.

### Added

- A source development stack (`npm run dev:stack`) with isolated local state,
service hot reload, and documented setup, stop, and reset commands.

## [1.1.6] - 2026-10-18

Source: sanitized Space app @ f8803d5d96 (4595 files). Adds install telemetry
Expand Down
45 changes: 45 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,51 @@ 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 the Node/npm prerequisites above and a working Docker Engine or Docker
Desktop with Compose V2. On macOS, Docker Desktop or an existing Colima context
must already work. On Windows 11, run these commands inside an existing WSL2
Linux environment with Docker integration. The launcher does not install these
tools or change OS settings.

From the checkout root:

```sh
ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm ci
npm run dev:stack
```

The command starts the isolated PostgreSQL/Temporal containers, migrations,
source API, Vite web app, worker and CLI host. Open the printed web URL, create
your local owner with the one-time setup token, then sign in and open Getting
Started and an OpenCode pane. No provider account or API key is needed to reach
the terminal UI. Web changes hot-reload; API changes restart the API; changes
to worker/shared packages rebuild and restart the dependent source services.

Defaults are API `4810`, web `4811`, PostgreSQL `54329` and Temporal `47233`,
all bound to `127.0.0.1`. Use `SPACE_DEV_API_PORT`, `SPACE_DEV_WEB_PORT`,
`SPACE_DEV_POSTGRES_PORT` and `SPACE_DEV_TEMPORAL_PORT` to select unused ports.
Choose `SPACE_DEV_STATE_DIR=.dev-stack-<name>` for a separate instance; the
default is `.dev-stack/`. Installed ports `4910`/`4911` are rejected.

Keep the same state/port environment when stopping or resetting:

```sh
npm run dev:stack -- down
npm run dev:stack -- reset
```

`down` preserves the private database/state; `reset` also removes only that
state and its owned resources. For an occupied port, choose an unused port
before first startup. If Docker is unavailable or cleanup fails, restore the
original Docker context and retry `down` or `reset`; retain the ownership
manifest instead of deleting it. A required process exit is reported by name
and stops the stack. Never share the setup token or generated state directory.

See the [development stack guide](docs/development-stack.md) for alternate-port
examples, private-state details, troubleshooting and the Linux/Intel-macOS proof.

## First executed task, without a provider

```sh
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { createSpaceControl, supportsControlRuntime } from "./space-control.js";
import { createControlScreenshotCapture } from "./space-control-capture.js";
import { registerSpaceControlRoutes } from "./space-control-routes.js";
import { pluginsService } from "./plugins-service.js";
import { instanceStorageDirectory } from "./instance-storage.js";
import { InMemorySourceControlRepository, PostgresSourceControlRepository } from "@space/db";
import { SourceControlService } from "./source-control/service.js";
import { registerSourceControlRoutes } from "./source-control/routes.js";
Expand Down Expand Up @@ -1442,7 +1443,7 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Fastify
}
const sourceControlPublishingManager =
options.sourceControlPublishingManager ?? new SourceControlPublishingManager({ store });
const personalGitRoot = process.env.SPACE_PERSONAL_GIT_ROOT || "/opt/spaceapp/var/personal-source-control";
const personalGitRoot = instanceStorageDirectory("personal-source-control", process.env.SPACE_PERSONAL_GIT_ROOT);
const sourceControlService = options.sourceControlService ?? new SourceControlService({
repository: config.runtimeStore === "postgres" && config.databaseUrl
? PostgresSourceControlRepository.fromConnectionString(config.databaseUrl)
Expand Down Expand Up @@ -4359,7 +4360,7 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Fastify
if(action.operation==="panes.open"){
const recordDebugIncident = async (incident: Record<string, unknown>) => {
try {
const dir = "/opt/spaceapp/var/live-voice-logs";
const dir = instanceStorageDirectory("live-voice-logs");
await mkdir(dir, { recursive: true });
await appendFile(`${dir}/debug-incidents.jsonl`, JSON.stringify(incident) + "\n", "utf8");
} catch (err) {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/auth-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { sendApiError } from "./http-errors.js";
import { createDesktopAuthHandoffStore, isValidDesktopAuthHandoffId } from "./desktop-auth-handoffs.js";
import { recordAudit } from "./request-audit.js";
import { idParamSchema, parseBody, parseQuery } from "./route-parsing.js";
import { instanceSecretDirectories } from "./instance-storage.js";

/**
* Operator login/logout and Google sign-in (OAuth, linked accounts, desktop handoff); the parent retains
Expand Down Expand Up @@ -229,7 +230,7 @@ export function registerAuthRoutes(app: FastifyInstance, options: {
auth.googleClientId = clientId;
auth.googleClientSecret = clientSecret;
let saved = false;
for (const dir of ["/opt/spaceapp/secrets", "/var/lib/spaceapp"]) {
for (const dir of instanceSecretDirectories()) {
try {
await mkdir(dir, { recursive: true, mode: 0o700 });
await writeFile(
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 @@ -3,6 +3,7 @@ import { createHash, createHmac, randomBytes, scrypt as scryptCallback, timingSa
import { authUserSchema, resolveSpaceInstance, sessionCookieNameFor, type AuthUser, type LoginInput } from "@space/contracts";
import { persistentOperatorSessionTtlSeconds, signPersistentOperatorSessionToken, signSessionTokenWithTtl } from "@space/runtime";
import { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_CLIENT_SECRET } from "./antigravity-usage-services.js";
import { instanceSecretDirectories } from "./instance-storage.js";

// A dev instance on another port of the same host must not overwrite the stable session cookie.
const SESSION_COOKIE = sessionCookieNameFor(resolveSpaceInstance(process.env));
Expand Down Expand Up @@ -55,7 +56,7 @@ export function getAuthConfig(env: NodeJS.ProcessEnv): AuthConfig {
let googleClientSecret = env.SPACE_GOOGLE_CLIENT_SECRET || env.GOOGLE_CLIENT_SECRET || undefined;
if (!googleClientId) {
try {
const candidates = ["/opt/spaceapp/secrets/google-oauth.json", "/var/lib/spaceapp/google-oauth.json"];
const candidates = instanceSecretDirectories(env).map(dir => `${dir}/google-oauth.json`);
for (const p of candidates) {
if (existsSync(p)) {
const raw = JSON.parse(readFileSync(p, "utf8"));
Expand Down
22 changes: 19 additions & 3 deletions apps/api/src/cli-agent-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { basename, dirname, extname, isAbsolute, join, relative, sep } from "node:path";
import { promisify } from "node:util";
import sanitizeHtml from "sanitize-html";
import { idSchema, isAgentFileArtifact, type Artifact } from "@space/contracts";
import { idSchema, isAgentFileArtifact, resolveSpaceInstance, type Artifact } from "@space/contracts";
import { makeSpaceId, type SpaceStore } from "@space/runtime";
import { z } from "zod";
import type { SpaceApiConfig } from "./config.js";
import { recordUploadedArtifact, UploadRecordUncertainError } from "./upload-record.js";
import { childProcessEnv } from "./child-process-env.js";
import { instanceStorageDirectory } from "./instance-storage.js";

export const cliAgentFilesTokenHeader = "x-space-cli-agent-files-token";
export const agentFileMaxBytes = 100 * 1024 * 1024;
Expand Down Expand Up @@ -200,6 +201,11 @@ export function resolveAgentFileMimeType(originalFilename: string, declaredMimeT
return "application/octet-stream";
}

/**
* Rebuild a DOCX package through an explicit direct command or the stable installed broker.
* Dev work-root rejection becomes a typed normalization error before writes or child execution;
* conversion uses a private request directory, which is removed after success or failure.
*/
export async function normalizeAgentFileDocx(
input: AgentFileDocxNormalizerInput,
options: NormalizeAgentFileDocxOptions = {}
Expand All @@ -208,8 +214,18 @@ export async function normalizeAgentFileDocx(
throw new AgentFileDocxNormalizationError("The DOCX source is not a valid Office package.");
}
const directCommand = options.command ?? process.env.SPACE_LIBREOFFICE_PATH;
const temporaryRoot = options.temporaryRoot ??
(directCommand ? tmpdir() : process.env.SPACE_AGENT_FILE_DOCX_WORK_ROOT ?? "/opt/spaceapp/var/agent-docx-work");
const dev = resolveSpaceInstance(process.env) === "dev";
if (dev && !directCommand) {
throw new AgentFileDocxNormalizationError("Dev DOCX normalization requires an explicit direct LibreOffice command; the installed privileged broker is not used.");
}
let temporaryRoot: string;
try {
temporaryRoot = dev
? instanceStorageDirectory("agent-docx-work", options.temporaryRoot ?? process.env.SPACE_AGENT_FILE_DOCX_WORK_ROOT)
: options.temporaryRoot ?? (directCommand ? tmpdir() : process.env.SPACE_AGENT_FILE_DOCX_WORK_ROOT ?? "/opt/spaceapp/var/agent-docx-work");
} catch (error) {
throw new AgentFileDocxNormalizationError(error instanceof Error ? error.message : undefined);
}
await mkdir(temporaryRoot, { recursive: true, mode: 0o700 });
const workingRoot = await mkdtemp(join(temporaryRoot, "request-"));
const inputDirectory = join(workingRoot, "input");
Expand Down
Loading