Back to home@CJackHwang

Succinix

Succinix — browser-native Linux (real Node + Unix userland inside WebContainer)

Stars
5
Language
TypeScript
Created
Aug 6, 2026
Updated
Aug 15, 2026

Introduction

Succinix

License: MIT Version CI PRs Welcome

A browser-native Linux: a full-screen Unix terminal powered by WebContainer + Lifo, with a unified TerminalExecutor that routes node|npm|npx to a real Node.js runtime and everything else to a Lifo Unix userland — sharing one filesystem.

Language: English | 简体中文

Open a browser tab, boot into a Linux-like environment, and use Unix tools, Node.js, process management, port forwarding, and a Postgres database (tinbase) without installing anything.


Features

  • Full-screen terminal experience — a centered DOM boot splash with system self-checks and graceful environment-exit (shows a professional error page instead of degrading), then an interactive shell (guest@succinix:~$).
  • Interactive terminal keys (REPL)Ctrl+C interrupts a running command and discards queued commands (a node/npm/npx run is killed via the interrupt protocol; pure Lifo commands and background services are not touched), Up/Down arrows browse command history (session memory), Tab completes built-in command names and file paths (multiple candidates are listed), and Ctrl+L clears the screen. The prompt follows the session cwd: cd /workspace/proj turns guest@succinix:~$ into guest@succinix:~/proj$ (~ = the workspace root).
  • Unified command execution — one terminal entry point:
    • node, npm, npx and project binaries run on a real Node.js process (WebContainer).
    • python / python3 / pip / pip3 run on a built-in Pyodide runtime (Python 3.14.2, Pyodide 314.0.4) — a resident daemon packaged as a system asset (zero install, cannot be broken by user npm install), injected lazily on first use. python -c "<code>" and python <script.py> are supported; pip maps to Pyodide's micropip (pure-Python wheels persist across refresh via /.pyodide/site-packages); the interactive REPL is not (WebContainer stdin boundary).
    • Everything else (grep, sed, awk, cat, tar, curl, pipes, redirects, ...) runs on Lifo, a clean-room TypeScript implementation of Unix.
  • Session working directory (fusion)cd in the Lifo sandbox now drives a session cwd that the host persists (/etc/succinix.cwd, survives refresh) and applies to every real Node/Python child process (spawn cwd). pwd shows the session cwd, node/python see the same directory — no more cd /ws/proj && npm install installing into the container root. cd / returns to the workspace root (~); cd to a missing directory keeps the session cwd unchanged. lang lists the built-in runtimes and versions.
  • Shared filesystem — the browser (wc.fs) and Lifo commands operate on the same files. No bridge code; WebContainer virtualizes node:fs for processes, and Lifo consumes it via NativeFsProvider.
  • Process managementps / kill over a unified process table (real child processes + tracked state), including background spawn. Each ps entry carries a scope field (system / container / unknown, plus containerId for container) — a heuristic derived from the command string and the process launch cwd (cd /workspace/c-<id> && ... prefix), meant for UI display and query filtering only, not a security boundary: a user process whose command merely looks like a system process (e.g. node /usr/lib/succinix/fake.js) is classified system. Do not rely on it for permission / isolation / kill-interception decisions (see docs/PROTOCOL.md).
  • Port management — services are detected via WebContainer server-ready events and listed by ports with their preview URLs.
  • Databasedb start boots a real Postgres (tinbase, PGlite/WASM engine) inside the container; db status / db stop manage it.
  • Persistence — the workspace (files, config, env, settings, workspaces) is snapshotted to IndexedDB and restored on boot; refresh never loses user files. snapshot command for status / manual save / reset. Snapshots are text-focused: binary/unreadable files are skipped (counted and reported in the save log), and a snapshot whose collected size exceeds ~50 MB is skipped with a warning rather than written (snapshot now reports skipped (over 50MB limit)). The tinbase database store (.tinbase, PGlite/WASM) is excluded entirely — it is binary and a text-only partial restore would corrupt it, so tinbase data persists across db stop/db start in a session but not across a browser refresh (refresh recreates a fresh store).
  • Multi-instance embedding (0.6.0+)?instance=<id> starts the app as a named instance: per-instance state files, snapshots, services/ports views and process views (ps filtering, cross-instance kill rejected). Two tabs with different ids are fully isolated (separate hosts + IndexedDB keys).
  • Multi-user semantics (0.6.0+)?user=<id> (alias of ?instance=<id>) additionally seeds a per-user home (/workspace/users/<id>): the session starts in the home, the prompt renders it as ~, whoami shows the user, and state/snapshots/process views are per-user. Organizational isolation, not a security boundary — there is no real kernel or permission model; the standalone app stays guest-only (see AGENTS.md).
  • Memory managementfree / top give a memory overview (device + JS heap; sandbox estimates are honestly labeled), reboot restarts the system with a browser reload (persisted data survives), shutdown powers off, and cache / cache clear report and clean rebuildable caches without touching /workspace.
  • Workspace splitworkspace manages multiple isolated workspaces: each lives in its own /ws/<name> directory with its own files and state; create / switch / rm manage them, and the current workspace is recorded in /ws/.current (persists across refreshes). The default main workspace is initialized on first boot.
  • System configurationenv manages persistent environment variables (/etc/succinix.env, merged into real Node child processes at spawn time) and settings manages persistent system settings (/etc/succinix.settings): the tinbase port (preview-port, default 3001), the initial workspace (default-workspace, default main), and the terminal font size (font-size, applied live). Both files ride the snapshot so they survive refreshes.
  • Service managementservice manages named background services declaratively on top of spawn/ps/kill and the port registry: definitions live in /etc/succinix.services (name|command|port, # comments, ${PORT} placeholder resolved from preview-port), with start/stop/status/enable/disable. enable records the service in /etc/succinix.autostart and boot pulls it up declaratively — a declarative restart, not a daemon (no crash self-healing).
  • System log (journald-style) — a persistent log written to /var/log/succinix.log on the container FS (rides the snapshot, so it survives refreshes), formatted 2026-08-05T04:00:00Z [level] message. It captures boot events (BOOT), command executions (INFO with cmd/exit/runtime), service events (INFO/WARN), snapshot events (INFO) and errors (ERROR). log reads it (log last 20, log -n <count>, log boot BOOT-only, log clear); the file auto-truncates to a ~200 KB tail when oversized. Interactive log -f (tail -f) is intentionally not implemented (POC).
  • Package managementpkg unifies the two real package channels behind one apt-style interface: lifo (lifo list / lifo install / lifo remove / lifo search — Lifo extension packages such as lifo-pkg-git, lifo-pkg-ffmpeg) and npm (real Node npm for the full ecosystem). Source is auto-detected: a package whose lifo-pkg-<name> exists on npm installs via lifo, otherwise via npm; on a name conflict lifo wins (tool packages). pkg list merges both channels with a SOURCE column, pkg search merges both searches, pkg install/remove echo the real command output and never swallow failures. The npm installed list is read from the node_modules top-level directories only (a "top-level direct-install" simplification — the container's preinstalled runtime dependencies appear too, and the dependency tree is not parsed).
  • Virtual network viewnetstat renders the port registry as a virtual listening-port table (Proto Local Address State, tcp 127.0.0.1:<port> LISTEN; netstat -p adds the associated process, matched by port number in the process command, - when unmatched) and ip addr shows the browser's virtual network identity (lo: virtual loopback, eth0: <preview-domain> (virtual)). Everything is honestly labeled virtual — no fabricated interfaces, IPs, or connections.
  • System information & login banneruname reports the honest browser-native system identity (Succinix 0.6.0 js-runtime+webcontainer <api-version> <arch>; kernel identified as js-runtime+webcontainer, never impersonating a Linux kernel; -a adds hostname/OS, -r is the @webcontainer/api runtime version, -m is the UA-derived architecture) and motd shows/edits the login banner at /etc/succinix.motd (persisted with snapshots; the default welcome line is printed on every boot and restored by motd reset).
  • Self-test mode?test=1 runs a system-diagnostics self-check in the browser.

Architecture

flowchart TD
    subgraph Browser["Browser tab"]
        XT["xterm.js (JetBrains Mono, dark-amber theme)"]
        TC["TerminalClient — file RPC over the shared filesystem<br/>/cmd.json { id, cmd, opts }<br/>/result-&lt;id&gt;.json { id, ok, exitCode, stdout, stderr, runtime }"]
        XT -- "terminal(command)" --> TC
    end

    WC["WebContainer (COOP/COEP, virtualized node:fs)"]

    subgraph Host["node host.js — TerminalExecutor (persistent daemon, PID 1)"]
        RT["prefix dispatch"]
        NODE["node | npm | npx → child_process.spawn (real Node.js)"]
        PY["python | python3 | pip | pip3 → resident Pyodide daemon (python-daemon.js)"]
        LIFO["everything else → Lifo sandbox.commands.run (Unix tools)"]
        PS["ps / kill — unified process registry"]
        CWD["cwd / setCwd — session cwd (cd-synced, persisted)"]
        SP["spawn — background long-running processes"]
    end

    TC -- "file RPC" --> WC
    WC -- "shared node:fs" --> Host

    RT --> NODE
    RT --> PY
    RT --> LIFO
    RT --> PS
    RT --> CWD
    RT --> SP

Key design decision: the filesystem is the single source of truth. Because WebContainer exposes the container filesystem to processes via node:fs, and Lifo mounts process.cwd() through NativeFsProvider, browser, Node processes and Lifo all see one filesystem. There is no filesystem bridge to maintain.

Quick Start

Requirements: a modern Chromium-based browser (Chrome/Edge) with cross-origin isolation (COOP/COEP headers) and SharedArrayBuffer support. No server-side infrastructure needed for local development.

npm install          # install dependencies
npm run dev          # start Vite dev server (COOP/COEP headers preconfigured)
# open http://localhost:7892

The page boots Succinix: system self-checks, then a shell prompt. Type help for available commands.

Build & checks

npx tsc -p tsconfig.json --noEmit   # type check (0 errors required)
node scripts/build-host.mjs         # bundle the in-container host (host.js + lifo-core.js + python Pyodide daemon)
npm run build                       # production build
node scripts/verify-deploy.mjs      # deploy-readiness gate (build + preview + COOP/COEP + ?test=1)

Testing

Succinix has a layered test setup that runs locally and in CI (GitHub Actions). No new runtime dependencies were added for testing — e2e reuses the existing CDP scripts (verify-deploy / bench / scenarios), and unit tests use mock filesystem / IndexedDB / network.

  • Lintnpm run lint (ESLint flat config in eslint.config.js). typescript-eslint recommended + project rules: no-explicit-any (error), no leftover console.log (warn; console.warn/error allowed for the degradation-log convention, host-side files exempt), no unused vars/imports. Gate: 0 errors.
  • Typechecknpm run typecheck (tsc -p tsconfig.json --noEmit). Gate: 0 errors.
  • Unit testsnpm run test (Vitest, node environment) covers the pure-logic modules src/log.ts, src/persist/index.ts, src/services/index.ts, src/pkg/index.ts, src/motd.ts, src/config.ts, src/engine/host-route.ts, src/engine/client.ts against in-memory mocks (see tests/); src/commands/index.ts pure functions (workspace/uname/netstat/port-matching/label) are also unit-tested. npm run test:coverage adds the v8 coverage gate: ≥70% statements/branches/functions/lines on the coverage-included files.
  • Test-mode URLs are developer hooks (P6-19)?test=1, ?bench=1 and ?scenario=1 are test-only: they expose internal handles on window (__succinixResult / __succinixBench / __succinixScenario, the last being able to drive real commands) and must never appear in production links. Normal visits carry no query string and expose nothing.
  • e2enpm run test:e2e builds once, then runs the CDP scripts sequentially against vite preview in headless Chrome:
    1. scripts/verify-deploy.mjs — deploy-readiness gate + ?test=1 self-test (gate ≥71 passed, 0 failed);
    2. scripts/bench.mjs — performance benchmark (JSON output);
    3. scripts/scenarios.mjs — the 14 real-workflow scenario suite (S1–S14; definitions split across scripts/scenarios/);
    4. scripts/lang-verify.mjs — the language-ecosystem verification suite (TASK25);
    5. scripts/instance-demo.mjs — multi-instance + multi-user demo (dual-tab, R3);
    6. scripts/instance-routing.mjs — same-page instance routing (R5);
    7. scripts/cordis-app-e2e.mjs — an external @succinix/engine consumer verifies the published dsh-key contract. Playwright is intentionally not used: the CDP scripts keep the pipeline zero-dependency and identical to local runs.
  • CI.github/workflows/ci.yml runs lint → typecheck → unit tests (with coverage) → build → verify-deploy (headless self-test) on every push/PR; the full e2e gate lives in .github/workflows/e2e-full.yml (source/script changes, deploy gate retried once on the known scenario flake); a scheduled nightly job runs the heavy scenarios + lang-verify + instance-demo suite. See the CI badge at the top of this file.
  • pre-commit (optional, zero-dependency)npm run setup:hooks writes a .git/hooks/pre-commit that runs tsc --noEmit and ESLint on the changed files only (scripts/pre-commit.sh). It is not forced: skipping setup:hooks leaves the project fully commit-ready.

Dependencies & audit

Dependency policy: report-only, no automatic upgrades (upgrades are evaluated separately to avoid regressions). Audit results as of the TASK17 final round (2026-08-05):

  • npm audit0 vulnerabilities (all direct + transitive dependencies clean).
  • npm outdated → only @lifo-sh/core 0.10.8 → 0.10.9 has a newer release; everything else is current. Not upgraded (policy), pending separate evaluation.
  • public/host.js is esbuild-minified (minify: true in scripts/build-host.mjs); the host daemon stays small (~16.5 KB) while @lifo-sh/core is bundled separately into public/lifo-core.js (~1 MB) and lazy-imported on the first Lifo command. Plain minify is used because the full ?test=1 suite passes against the minified bundle (Lifo has no Function.name dependency that breaks under name-minification).

Self-test mode

# open http://localhost:7892/?test=1

Runs the full diagnostics suite (filesystem, routing, process lifecycle, ports, config, services, logs, packages, smoke) inside the centered boot-splash overlay, then prints the summary into the terminal and drops you into the shell.

Deployment (Vercel)

Succinix is a pure static site (Vite → dist/): no backend and no server-side state — workspaces, files, config and settings live in the browser's IndexedDB and ride the snapshot (the tinbase database store is excluded; see Persistence). It deploys to any static host that can send custom response headers; the one-click path is Vercel.

Why COOP/COEP matters. WebContainer requires cross-origin isolation. Without the Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: credentialless headers the page fails the boot environment check and shows the error page instead of the terminal. vercel.json ships these headers for every path (including assets/* and host.js), matching the dev and preview servers. Skipping them is the #1 cause of a "white screen + environment error page" on deployment.

One-click deploy (Vercel):

  1. Push this repository to GitHub / GitLab / Bitbucket.
  2. In the Vercel dashboard, Import Project → pick the repo. Vercel auto-detects Vite (framework: vite, buildCommand: npm run build, outputDirectory: dist from vercel.json).
  3. Deploy. Optionally add a custom domain, e.g. succinix.alibicore.com or a cjack.me subdomain.

CLI equivalent (requires a Vercel account/token):

npm i -g vercel
vercel login
vercel --prod

Local deploy-readiness verification (no Vercel token needed). vite preview serves the built dist/ the same way Vercel does, so this is the "static artifact is deployable" proof:

npm run build
node scripts/verify-deploy.mjs
# starts vite preview, asserts COOP/COEP on /, /host.js and the JS bundle,
# then runs ?test=1 in headless Chrome — PASSED requires >=71 passed and 0 failed

Data scoping. IndexedDB is isolated per origin. Changing the deployment domain = starting a fresh system: workspaces, files and database data do not migrate between domains. Refresh on the same domain is safe (the snapshot restores); only a domain change resets the system. This also applies to Vercel preview deployments: each preview gets its own unique URL (a distinct origin), so every preview environment has its own separately-scoped IndexedDB — data does not carry over between preview deployments either.

Usage

Built-in commands (handled in the browser)

CommandDescription
helpShow command help
clearClear the screen (Ctrl+L also works)
sysinfoShow browser-detected system information
portsList ready service ports with preview URLs
db startStart the tinbase database (auto-installs if missing)
db statusShow database status (port registry + process table)
db stopStop the database
versionShow version
whoamiShow current user (guest; the user id in ?user= mode)
snapshotPersistence status; snapshot now saves, snapshot clear --yes resets
freeShow memory overview (device + JS heap; sandbox estimates marked ~)
topLive process table — 3 snapshots 2s apart, then exits
rebootRestart Succinix (browser reload; persisted data survives)
shutdownPower off (you can close this tab)
cacheShow cache usage; cache clear cleans rebuildable caches
workspaceList workspaces; create / switch / rm manage isolated workspaces
envList / set (env KEY=value) / unset (env -u KEY) environment variables, persisted in /etc/succinix.env
settingsView / set / reset (settings reset KEY) system settings, persisted in /etc/succinix.settings
serviceList services (state + port); start / stop / status / enable / disable <name> manage them. Definitions in /etc/succinix.services, boot autostart in /etc/succinix.autostart (declarative restart, not a daemon)
logShow recent system-log entries (last 20) from /var/log/succinix.log; log -n <count> last N, log boot BOOT-only, log clear empties the file
pkgPackage management: pkg list (lifo + npm merged with SOURCE), pkg search <term> (both channels), pkg install <name> (lifo if lifo-pkg-<name> exists, else npm), pkg remove <name> (via the installed source), pkg info <name>
netstatList virtual listening ports (port registry as tcp 127.0.0.1:<port> LISTEN); netstat -p adds the associated process (matched by port number in the process command, - when unmatched)
ip addrShow virtual network identity — lo: virtual loopback, eth0: <preview-domain> (virtual); no fabricated interfaces or IPs
unameShow system identity: summary line (Succinix <version> js-runtime+webcontainer <api-version> <arch>); uname -a all fields, -r runtime version, -m architecture (from UA, unknown if absent)
motdView the login banner (/etc/succinix.motd); motd <text> sets it (persisted), motd reset restores the default
langList built-in language runtimes: lang (table), lang pythonPython 3.14.2 (Pyodide 314.0.4), lang node, lang typescript
pwdShow the session working directory (host-maintained, cd-synced, applied to node/python children)

Host commands (TerminalExecutor, unified routing)

CommandRouteDescription
node ... / npm ... / npx ...NodeReal Node.js child process; when the command contains shell metacharacters (&&, |, >, 2>&1, ...) the whole chain runs through the Lifo shell (pipes/chains/redirects parsed there; each node/npm/npx segment is forwarded back to the real binary), result runtime=lifo
python ... / python3 ... / pip ... / pip3 ...PythonBuilt-in resident Pyodide daemon (python -c "<code>" / python <script.py> / python -m pip <cmd>, pip maps to micropip); when the command contains shell metacharacters the whole chain runs through the Lifo shell (pipes/redirects parsed there; each python/pip segment is forwarded to the same resident daemon), result runtime=lifo
grep, cat, tar, curl, ...LifoUnix tools, pipes, redirects
psList the unified process table
kill <pid>Terminate a process (SIGTERM)
cwd / setCwd <dir> / ping / exitProtocol commands (cwd = session cwd)

Verified Behavior

Result of the browser runtime verification suite (see src/selftest/index.ts): 76 passed, 0 failed, 5 skipped (2026-08-10 run, against the minified host bundle; the Python runtime runs on the resident Pyodide 314.0.4 daemon — Python 3.14.2, pip via micropip, and the pip/pyparsing self-test checks are included). The skips are known boundaries (external network, symlink fallback, device-memory stats), never silent failures. In ?test=1 mode the summary line and any failure list are additionally printed to the terminal after the boot overlay fades (self-test results stay visible).

  • Shared filesystem: browser -> Lifo and Lifo -> browser reads/writes work.
  • Routing: node -e "console.log(21*2)" -> 42 (runtime=node); npm --version -> real npm version; grep/cat/wc -> runtime=lifo.
  • Shell fusion (TASK24): node-prefixed commands with shell metacharacters fall back to the Lifo shell — node -e "console.log(21*2)" | grep 42 -> 42 (runtime=lifo), node --version && npm --version -> both real versions on two lines; each node/npm/npx segment in the chain runs the real binary (forwarded from the Lifo shell, not the in-browser JS interpreter). Escaped quotes in node -e are preserved (node -e "console.log(\"hi\")" -> hi); an unterminated quote reports unterminated quote in command instead of silently truncating.
  • Python pipes (TASK24 复审): python commands with shell metacharacters are no longer silently truncated at the pipe — python -c "print(1)" | grep 2 -> empty (runtime=lifo), python -c "print(42)" | grep 42 -> 42; each python/pip segment in the chain runs the same resident Pyodide daemon.
  • Env merge (TASK24 复审): env FOO=bar truly reaches child processes — a node -e "console.log(process.env.FOO)" child reports bar (the env file lives under process.cwd()/etc/succinix.env, matching the browser write path).
  • Session cwd persistence (TASK24 复审): cd's session cwd is persisted to process.cwd()/etc/succinix.cwd (previously the read-only virtual root — lost on refresh) and restored on host start, so pwd / node / python cwd survive a refresh.
  • Process lifecycle: spawn a background service, ps shows it, kill transitions it to exited.
  • Port registry: server-ready events surface preview URLs.
  • Database: tinbase (PGlite/WASM) boots and serves.
  • Memory: device memory / JS heap stats reported by the browser (free can render).
  • Config: env set/get/delete lifecycle and settings write/reset persist to /etc/succinix.*.
  • Services: service lists the built-in tinbase definition; a temporary echo server can be started, observed running (process table + port registry), stopped, and removed with zero residue; service enable/disable write and remove the /etc/succinix.autostart file (deduped).
  • Logs: command executions are recorded with exit/runtime, boot events are recorded as BOOT entries, and log clear empties the log file (asserted by the self-test suite).
  • Packages: pkg list renders the two-channel table (NAME / SOURCE / VERSION); pkg search git hits lifo-pkg-git (network-dependent — skipped on failure, per the known-boundary convention).
  • Network view: netstat renders the port registry as a virtual listening-port table and netstat -p associates a spawned echo server (port 3456) with its process; after kill the port disappears from the table. ip addr prints the virtual loopback and preview domain, honestly labeled (virtual).
  • System info: uname renders the honest system line (Succinix <version> js-runtime+webcontainer <api-version> <arch>) and the -a/-r/-m forms; the -r/-m flag parsing is additionally asserted through the command-dispatch path (not just the builders). motd set → read-back → reset leaves /etc/succinix.motd at its default (zero residue).
  • Smoke: all 25 safe built-in commands (help/clear/sysinfo/version/whoami/ports/pwd/lang/db status/db stop/snapshot/free/top/cache/workspace/env/settings/service/log/pkg/netstat/ip addr/uname -a/motd/shutdown) dispatch through the browser handler without error; reboot and db start are excluded from the automated smoke (destructive/heavy side effects).
  • Languages (TASK27): python -c "print(6*7)" returns 42 via the built-in Pyodide 314.0.4 daemon (Python 3.14.2); the full stdlib import matrix (json/csv/re/math/os/sqlite3/subprocess/collections/datetime/hashlib/urllib) is green and extended-stdlib imports are self-tested; python3 --version reports Python 3.14.2; lang lists node/python/typescript and lang python reports the bundled version. Python reads/writes the shared FS (browser + node see the same file), python -m pip install pyparsing → import works (micropip), and the ?test=1 suite asserts all of it. See the Language Ecosystem — Verified Support Matrix for the authoritative, measurement-backed matrix.
  • Session cwd (TASK23): cd /workspace syncs the host session cwd and a node -e "console.log(process.cwd())" child follows it; cd into a missing directory keeps the session cwd unchanged. (TASK24: /workspace is the Lifo VFS view — real node/python subprocesses spawn in the mapped host directory, so process.cwd() inside a child reports the real path such as /home/<wc-id>/proj; pwd/cwd still report the Lifo view /workspace/....)
  • EACCES hint (TASK24): npm i -g hitting the read-only /usr/local appends an actionable hint (hint: /usr/local is read-only for guest. Install locally: npm i <pkg> (or set a user prefix: npm config set prefix ~/.npm-global)) to the error output; permission semantics are unchanged.
  • Language regression (TASK25, scenario S14): the 5 user-measured pits are locked against regression — node --version && npm --version chain, node -e nested-quote file writes (preserved through tsc), npm i -g EACCES + hint, cd-synced npm installs (packaged into the project dir, not the root), and python true pipes.
  • Stability: the RPC client serializes requests over the single-slot /cmd.json channel (no more parallel-channel race), retries read-only commands (ping/ps/cwd) once on transport failure, and the browser watchdog re-injects + respawns host.js after 2 consecutive failed pings.

Languages

Succinix ships two built-in language runtimes (system assets, zero user install) and can execute precompiled WASI modules; every claim below is measurement-backed by scripts/lang-verify.mjs (real browser execution) — see the authoritative docs/LANGUAGES.md matrix (中文: docs/LANGUAGES.zh-CN.md).

LanguageCommandStatusNotable facts (measured)
Pythonpython / python3 / pip / pip3✅ built-in3.14.2 Pyodide 314.0.4; 11/11 stdlib imports; sqlite3/json real; pip via micropip (pure-Python wheels persist across refresh; compiled wheels re-install after refresh), no REPL, subprocess imports but can't spawn
Node.jsnode✅ built-in22.22.3; real binaries; node -e quote preservation; full TS toolchain (typescript/tsx/vitest)
npmnpm✅ built-in10.8.2; local installs into session cwd; global → EACCES + hint
TypeScriptnpx tsc / tsx✅ via npmtsc → node → vitest full loop (S13/S14)
Ruby⚠️ probe only@ruby/wasm-wasi v2 runs in-container (6*7 → 42); not integrated
C / Rust / Go❌ absentno compilers (which gcc/rustc/go → not found)
WASInode:wasiprecompiled WASI modules run under node:wasi

The full matrix, ecosystem replacement-degree assessment, and every known boundary are in docs/LANGUAGES.md; the lang command lists the built-in runtimes and versions interactively.

Known Boundaries

These are environmental constraints, not bugs:

  • CORS: curl to sites without CORS headers fails (exit 7). Use a CORS-friendly proxy, e.g. curl https://r.jina.ai/<url>.
  • Symlinks: not supported by the Lifo VFS (ln reports the limitation).
  • No package manager / native binaries: there is no apt; native executables cannot run. Succinix is a browser-native Linux.
  • stdin for interactive processes: unreliable in the WebContainer environment; the design uses file-based RPC instead.
  • Streaming cross-runtime pipes: cross-runtime pipes are buffered (fine for agent-style "run then read" workflows).
  • /workspace is a Lifo VFS view; real node/python children see real paths: the browser filesystem root (wc.fs /) and Lifo's /workspace both map to the host process cwd (/home/<wc-id>), and the container root / is a read-only system view. pwd/cwd report the Lifo view (/workspace/...), while process.cwd() inside a node/python child reports the real mapped path (/home/<wc-id>/...). They point at the same directory.
  • Watchdog probe can be swallowed by a queued command: the host liveness watchdog writes a direct ping probe to the single-slot /cmd.json channel; if a user command is enqueued in the same ~120 ms host-poll window it overwrites the probe, so that probe times out and the watchdog skips the round (neutral, not counted as a failure). This only delays liveness detection by one 30 s cycle in the rare overlap case; it does not kill a healthy host.
  • Single-command output is capped at 1 MB: to bound container memory and result-file size, each command's stdout/stderr keeps at most the last ~1 MB of output (large dumps are truncated to their tail). Normal use (seq 1 5000, cat mid-size files, npm install logs) is far below the cap.
  • Snapshot dedup is blind to same-size content edits (bounded window): the auto-snapshot dedupes on directory structure + total bytes, so an edit that changes content but not size (e.g. sed -i 's/foo/bar/' with equal-length foo/bar, or vi overwriting in place) is not detected by the structure gate. Browser-side writes (env/settings/motd/workspace switch/service files) force a save immediately and are safe. Edits made through the shell (Lifo/node) are caught by the auto-snapshot's 30 s maximum-age force: even when the signature is unchanged, a full save is forced every ~30 s, so an equal-length edit survives a tab crash as long as it was made more than ~30 s before the crash. The residual loss window is an equal-length shell edit made in the last ~30 s immediately followed by a crash with no pagehide/beforeunload (OOM kill, OS reclaim).
  • Declarative autostart (not a daemon): service enable only records the service for a boot-time restart. There is no crash detection or self-healing — if a service exits after boot, restart it manually (service start <name>).
  • log -f (tail -f) not implemented: interactive streaming output is deferred (POC; interactive stdin is unreliable in WebContainer). Use log / log -n <count> instead. log clear wipes /var/log/succinix.log and is therefore not itself recorded in the log.
  • Log append is a read-modify-write (backlog): WebContainer's FileSystemAPI has no appendFile, so each log write reads the whole /var/log/succinix.log, appends one line, and writes it back — O(file size) per entry. Within the ~200 KB auto-truncation cap this is acceptable for a POC; the backlog item (P4-14) is to switch to true append when the API provides it, or to sharded files. This is the first part of the log system that would need work at high command volume.
  • Python REPL is not implemented: the built-in python runtime is command-oriented (python -c "<code>", python <script.py>, python -m pip <cmd>). An interactive >>> REPL needs persistent stdin, which is unreliable in WebContainer — use python -c instead. pip is available via Pyodide's micropip (pure-Python wheels persist across refresh via /.pyodide/site-packages; compiled wheels such as numpy need one pip install <pkg> after refresh because the text snapshot does not carry binary .so files). python -m <module> runs via runpy.run_module (only -m pip is special-cased). subprocess imports but cannot spawn — Pyodide raises OSError: [Errno 138] emscripten does not support processes (see docs/LANGUAGES.md).
  • First python command is slow: the Pyodide runtime (~13 MB of JS + wasm + stdlib) is lazily injected into the container on first use, and the resident daemon does a one-time loadPyodide, so the first python command can take a few seconds; subsequent commands reuse the instance and are fast. It never depends on a user npm install (system asset), so it cannot be broken by user actions.
  • External inbound networking: services are reachable via virtual preview URLs, not from the public internet.
  • Services claim processes by command string: service stop (and db stop) locate a service by matching its rendered command against the process table, not by PID lineage. A manually started process running the same command may be matched and killed. service start likewise reports "already running" if a process with that command is found.
  • Built-in tinbase service needs one install step: the preset service definition (tinbase) runs npx tinbase start --port ${PORT} --engine wasm, which requires tinbase to be installed in the container. Run db start once first to complete the in-container install before using service start tinbase.
  • lifo packages are session-scoped; npm packages persist: lifo install places packages in the Lifo runtime's in-memory global module directory, so they exist for the current host session and are recreated when the host restarts (a full refresh boots a fresh Lifo kernel). npm packages install into /node_modules on the shared filesystem and persist with the workspace snapshot. pkg list merges both; the source rule is "lifo if lifo-pkg-<name> exists on npm, otherwise npm; lifo wins on a name conflict".
  • pkg installs need registry access: pkg install/search/info hit the npm registry (via lifo search / real npm). When the registry is unreachable the command reports the reason and does not pretend to succeed.
  • Multi-user is organizational isolation, no permission bits: the standalone app stays single-user (guest is the only user; ?user=<id>/?instance=<id> embed mode partitions directories, state and process views per user/instance — not a security boundary, no real kernel or permission model). Permission-bit management (chmod semantics) is not simulated — simulated modes would add no real value.
  • Chromium-only: WebContainers requires a Chromium-based browser (Chrome/Edge). Firefox, Safari, and mobile browsers are not supported; the environment-check error page explains the requirements instead of degrading.
  • Deployment hosts must send custom response headers: WebContainer's cross-origin isolation requires the COOP/COEP headers configured in vercel.json. Hosts that cannot set custom response headers (e.g. some object-storage/CDN static hosting) cannot run Succinix. Vercel's free plan supports custom headers via vercel.json.
  • No Content-Security-Policy header (evaluated, deferred): a CSP is not currently sent. WebContainer's internals need worker-src blob: (worker bootstrap), script-src with wasm-unsafe-eval (Lifo/Pyodide), and connect-src to the npm registry / Pyodide CDN; a strict CSP risks breaking the runtime. It was evaluated and deliberately deferred rather than shipped unverified (P6-18) — revisit with a ?test=1 + verify-deploy pass before enabling.

Project Structure

src/
  main.ts            # entry: Cordis host assembly (xterm terminal, REPL, boot orchestration)
  boot-steps.ts      # boot sequence, system info detection, env pre-check
  boot-ui.ts         # centered DOM boot overlay renderer (splash/logs/env-fail page)
  app/               # xterm assembly, output, local commands, logging, snapshot, watchdog, dev hooks
  host/              # app-level Cordis plugins (terminal, commands, snapshot, watchdog, selftest, container)
  commands/          # browser-side commands (help/ports/db/free/top/cache/workspace/env/settings/service/log/pkg/netstat/ip/...)
  config.ts          # system configuration: /etc/succinix.env + /etc/succinix.settings I/O & defaults
  motd.ts            # login banner: /etc/succinix.motd I/O & default
  services/          # service management: /etc/succinix.services + /etc/succinix.autostart I/O, status/start/stop
  log.ts             # journald-style system log: /var/log/succinix.log append/read/clear/BOOT-filter
  pkg/               # package management: pkg list/search/install/remove/info over lifo + npm channels
  persist/           # snapshot persistence: exclusions/collect/signature/IndexedDB
  selftest/          # self-test suite (?test=1)
  engine/            # TerminalExecutor engine — decoupled, reusable (see Ecosystem)
    index.ts         # internal core barrel consumed by src/plugin (not a package export)
    client.ts        # file-RPC client, TerminalClient (was terminal-client.ts)
    host/            # TerminalExecutor daemon, runs inside WebContainer (config/rpc/run/spawn/ps-kill/main)
    host-route.ts    # host pure logic: routing / path mapping / per-instance filtering + kill authorization
    host-procs.ts    # unified process registry (was host-procs.ts)
    lifo-core.ts     # lazy @lifo-sh/core kernel entry (bundled to public/lifo-core.js)
    python-daemon/       # resident Pyodide 314.0.4 daemon CLI (loader/rpc/pip/main, bundled to public/pyodide/python-daemon.js)
    python-daemon-client.ts # host-side daemon lifecycle + JSON-line protocol client
    python-assets.ts    # lazy Pyodide asset injection (first-use, ~13 MB)
  terminal/          # terminal core consumed by the host terminal facade (no ./terminal export)
  instance/          # instance factory consumed by host.ensureInstance (no ./instance export)
  plugin/            # dsh Cordis plugin entry: services, lifecycle, events, capabilities, HostManager
scripts/
  build-host.mjs     # esbuild bundle of the in-container host (host.js + lazy lifo-core.js)
  build-engine-package.mjs  # build the publishable @succinix/engine package (packages/engine/, no publish)
  verify-deploy.mjs  # deploy-readiness gate: build + preview + COOP/COEP + ?test=1 self-test
  verify-bootgate.mjs  # boot-gate verification: no input during boot, step-counted boot log (CDP)
  bench.mjs          # headless-Chrome performance benchmark (JSON output)
  scenarios.mjs      # 14 real-workflow scenario suite (headless Chrome + CDP; S14 = language regression)
  scenarios/         # scenario definitions split by suite: smoke / services / filesystem / kernel / languages (O11)
  lang-verify.mjs    # language-ecosystem verification (TASK27; real browser execution)
  instance-demo.mjs  # multi-instance + multi-user demo (dual-tab, R3)
  instance-routing.mjs  # same-page instance routing (R5)
  cordis-app-e2e.mjs # external @succinix/engine consumer verifies the published contract
  run-e2e.mjs        # npm run test:e2e: build once + run the 7 CDP steps above sequentially
  check-plugin-boundaries.mjs  # plugin boundary gate: engine/terminal/instance stay Cordis-free
  check-dsh-shapes.mjs  # dsh shape gate: vendored dsh surface vs src/plugin/dsh-types.ts
  check-dsh-keys.mjs   # legacy-key gate: forbid stale ctx.succinix* tokens outside the allowlist
  pre-commit.sh      # optional pre-commit: tsc + eslint on changed files (zero-dependency)
  setup-hooks.mjs    # npm run setup:hooks: wire .git/hooks/pre-commit to pre-commit.sh
tests/
  log.test.ts        # Vitest unit tests for src/log.ts (mock FS)
  persist.test.ts    # ... src/persist/index.ts (exclusion/signature/force/empty-dirs, mock FS + fake IDB)
  services.test.ts   # ... src/services/index.ts (parse/port-render/state, mock client)
  pkg.test.ts        # ... src/pkg/index.ts (source detection/command construction, mock network)
  motd.test.ts       # ... src/motd.ts
  config.test.ts     # ... src/config.ts
  helpers/fakes.ts   # in-memory FileSystemAPI / fake IndexedDB / scriptable terminal client
eslint.config.js     # ESLint flat config (typescript-eslint recommended + project rules)
vitest.config.ts     # Vitest config + v8 coverage gate (>=70% on core pure-logic modules)
.github/workflows/
  ci.yml             # CI: lint → typecheck → unit tests (coverage) → build → verify-deploy; nightly scenarios
  e2e-full.yml       # full e2e gate: verify-deploy/bench/scenarios/lang-verify/instance-demo/instance-routing/cordis-app
public/
  host.js            # lightweight in-container host daemon (generated)
  lifo-core.js       # @lifo-sh/core kernel bundle, lazily imported by host.js (generated)

Ecosystem

Succinix's command-execution engine is decoupled from the Succinix app itself, and ships as @succinix/engine@0.6.0, a single Cordis plugin for @deepseek-ai/cordis@4.0.1. There is no standalone SDK API line: consumers apply the plugin, then use the dsh services under ctx.fs, ctx.sandbox, ctx.terminals, and ctx.sessionPersistence. A consumer's page boots a WebContainer and gets a shared-filesystem shell with a real Node runtime (node|npm|npx), a built-in Pyodide Python, and a Lifo Unix userland (everything else) — without building any of that itself.

import { Context } from '@deepseek-ai/cordis';
import engine from '@succinix/engine';

const ctx = new Context();
const fiber = ctx.plugin(engine, {
  container: { mode: 'external' },
  defaultInstance: { instanceId: 'default' },
});
await fiber;

const host = ctx.get('succinix-host', false)!;
await host.boot();
await host.ensureInstance('default', { executor: {} });

// ctx.fs, ctx.sandbox, ctx.terminals, and ctx.sessionPersistence are
// available; host.executor, host.terminal, host.snapshot, host.ports, and
// host.services sit behind the internal succinix-host seam.

dsh Plugin API

ServiceWhat it does
ctx.fsdsh file system: 12 primitives, 13 FS_* codes, sandboxMode
ctx.sandboxdsh sandbox provider: synchronous confine, node fail-closed
ctx.terminalsdsh owner-scoped PTY registry with exact Agent owners
ctx.sessionPersistencedsh event-sourced JSONL session log
host (succinix-host)Internal lifecycle seam: boot / attach / ensureInstance, executor, terminal, snapshot, persist, workspace, ports, services, capabilities, dispose / shutdown

Consumers declare inject: ['fs', 'sandbox', 'terminals', 'sessionPersistence'] or probe with ctx.get('fs', false). The published .d.ts augments Context['fs'], Context['sandbox'], Context['terminals'], Context['sessionPersistence'], and the succinix/* event map.

Protocol & integration docs

  • docs/PROTOCOL.md — the authoritative file-RPC wire contract: request/response shapes, command routing, process model, port events, timeouts.
  • docs/SDK.md — the 0.6.0 dsh Cordis plugin integration reference: install, config, dsh services, host seam, capabilities, lifecycle, hot reload, container modes.
  • docs/PLUGIN.md — how third-party Cordis plugins consume or extend Succinix.
  • docs/cordis-contract.md — the authoritative contract snapshot and its browser runner.
  • docs/MIGRATION.md — migration from the 0.4.0 standalone SDK and 0.5.0 single-key forms.
  • docs/LANGUAGES.md — the measurement-backed language support matrix.

Vision

The engine is the same code that powers the Succinix terminal, behind a clean Cordis boundary: core logic stays Cordis-free, the wire protocol is documented, and no app-layer dependency leaks into src/engine/. Any Chromium-based frontend that already boots a WebContainer can add a dsh execution world sharing its own files by applying @succinix/engine.

Development Archive

docs/tasks/TASK*.md document this project's incremental development history (each task's requirements, retention rules, and quality gates). They are kept in the repository as a historical development archive and are not part of the shipped product.

Documentation

Roadmap

  • POC: Lifo inside WebContainer with shared filesystem
  • TerminalExecutor v1: unified routing + process table
  • Product shell: full-screen terminal, boot sequence, ports, tinbase
  • Production-grade interface: English UI, dark-amber theme, JetBrains Mono, system self-checks
  • Boot splash: centered DOM overlay, responsive layout, graceful environment-exit
  • Persistence layer: files/state persisted to IndexedDB, restored on boot (no data loss on refresh)
  • Memory management: free/top-style commands, cache cleanup, reboot to reclaim memory
  • Workspace split: multiple virtual directories with isolated state (like Sunam workspaces)
  • Virtual network view: netstat virtual listening-port table + ip addr honest virtual identity
  • dsh single-track engine: @succinix/engine@0.6.0 with ctx.fs / ctx.sandbox / ctx.terminals / ctx.sessionPersistence
  • SunamAI integration: replace shell_run engine with TerminalExecutor — deferred (planned as TASK8; not scheduled)
  • Optional: WebSocket tunnel for external access

License

MIT © 2026 CJackHwang. See LICENSE.

Acknowledgements

  • Lifo — the TypeScript Unix userland (MIT).
  • WebContainers by StackBlitz — Node.js runtime in the browser.
  • xterm.js — terminal emulation (MIT).
  • tinbase — browser Postgres (PGlite/WASM).
  • Vite — build tooling (MIT).