Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
23e95f8
feat: launch isolated source dev stack with full lifecycle proof
pythonyx135793 Oct 17, 2026
6a9d5c4
fix: initialize proof path in runner step context
pythonyx135793 Oct 17, 2026
0ad5896
fix: provision ripgrep on hosted Ubuntu before stock gates
pythonyx135793 Oct 17, 2026
3614eae
test: register dev stack coverage and normalize portable fixture counts
pythonyx135793 Oct 17, 2026
1fd0801
fix(dev): confine instance storage and retain failure diagnostics
pythonyx135793 Oct 17, 2026
9dbfeb7
ci(dev): canonicalize hosted temporary root
pythonyx135793 Oct 17, 2026
e884bbd
ci(dev): match platform validation to contributor scope
pythonyx135793 Oct 17, 2026
43276ae
fix(dev): bind Linux process ownership to kernel boot
pythonyx135793 Oct 17, 2026
1ec7c99
fix(dev): prove exact published loopback ports
pythonyx135793 Oct 17, 2026
bf7d4f8
Fix pinned CLI execution and prove live interactive terminal
pythonyx135793 Oct 17, 2026
370b1d8
Bound browser input events and verify real CDP typing early
pythonyx135793 Oct 17, 2026
3107ea1
Keep capability browser state short and retain startup diagnostics
pythonyx135793 Oct 17, 2026
2cdc44a
fix(dev): use native Darwin stty and retain browser diagnostics
pythonyx135793 Oct 17, 2026
b9aa882
test(dev): verify current ANSI screens and early cookie capability
pythonyx135793 Oct 17, 2026
211e646
test(dev): isolate the Darwin CI browser keychain
pythonyx135793 Oct 17, 2026
a210959
docs: document full source stack startup in contributing
pythonyx135793 Oct 17, 2026
49b8339
Wait for browser-owned terminal geometry before UI proof
pythonyx135793 Oct 17, 2026
38f88b9
fix(dev): scope readiness probes to service generations
pythonyx135793 Oct 17, 2026
36a673b
fix: limit hot reload builds to source services
pythonyx135793 Oct 17, 2026
80fd1d9
docs: document source development stack in changelog
pythonyx135793 Oct 17, 2026
6f78605
fix: address source development stack review findings
pythonyx135793 Oct 18, 2026
dd3be64
test: retain worktree provisioning errors in source validation
pythonyx135793 Oct 18, 2026
fe81b4f
fix: await restored source generations before lifecycle proof
pythonyx135793 Oct 18, 2026
4724a3a
Merge main into source development stack
pythonyx135793 Oct 19, 2026
ca1f85e
fix: coordinate Git worktree metadata operations
pythonyx135793 Oct 19, 2026
34fb3cc
test: make browser privacy and watchdog fixtures deterministic
pythonyx135793 Oct 19, 2026
52399a5
fix(web): cancel live debug notice timer on unmount
pythonyx135793 Oct 19, 2026
ddb7ad9
test(web): assert live debug timer is removed on unmount
pythonyx135793 Oct 19, 2026
7ef250e
Merge current main while preserving source stack isolation and accept…
pythonyx135793 Oct 22, 2026
11a22e5
fix(dev): follow current scoped OpenCode CLI package and native layout
pythonyx135793 Oct 22, 2026
3832e61
fix(dev): verify the exact pinned OpenCode 2 version line
pythonyx135793 Oct 22, 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*/
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ 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.7] - 2026-10-21

Source: sanitized Space app @ 7619ef9523 (4729 files: SpaceApp 77d00f6895 with
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 @@ -103,6 +103,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 @@ -1432,7 +1433,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 @@ -4324,7 +4325,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, decodeDesktopAuthHandoffCookie, desktopAuthHandoffRedirect, encodeDesktopAuthHandoffCookie, parseDesktopAuthHandoffBinding } 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, createPublicKey, randomBytes, scrypt as scryptC
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 @@ -62,7 +63,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";
import { agentTokenLive, agentTokenSecret, agentTokenTtlMs } from "./cli-browser-bridge.js";

export const cliAgentFilesTokenHeader = "x-space-cli-agent-files-token";
Expand Down Expand Up @@ -202,6 +203,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 @@ -210,8 +216,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