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
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/npxrun is killed via theinterruptprotocol; 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/projturnsguest@succinix:~$intoguest@succinix:~/proj$(~= the workspace root). - Unified command execution — one terminal entry point:
node,npm,npxand project binaries run on a real Node.js process (WebContainer).python/python3/pip/pip3run 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 usernpm install), injected lazily on first use.python -c "<code>"andpython <script.py>are supported;pipmaps 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) —
cdin 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).pwdshows the session cwd,node/pythonsee the same directory — no morecd /ws/proj && npm installinstalling into the container root.cd /returns to the workspace root (~);cdto a missing directory keeps the session cwd unchanged.langlists the built-in runtimes and versions. - Shared filesystem — the browser (
wc.fs) and Lifo commands operate on the same files. No bridge code; WebContainer virtualizesnode:fsfor processes, and Lifo consumes it viaNativeFsProvider. - Process management —
ps/killover a unified process table (real child processes + tracked state), including backgroundspawn. Eachpsentry carries ascopefield (system/container/unknown, pluscontainerIdforcontainer) — 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 classifiedsystem. Do not rely on it for permission / isolation / kill-interception decisions (see docs/PROTOCOL.md). - Port management — services are detected via WebContainer
server-readyevents and listed byportswith their preview URLs. - Database —
db startboots a real Postgres (tinbase, PGlite/WASM engine) inside the container;db status/db stopmanage it. - Persistence — the workspace (files, config, env, settings, workspaces) is snapshotted to IndexedDB and restored on boot; refresh never loses user files.
snapshotcommand 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 nowreportsskipped (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 acrossdb stop/db startin 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 (psfiltering, cross-instancekillrejected). 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~,whoamishows 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 staysguest-only (see AGENTS.md). - Memory management —
free/topgive a memory overview (device + JS heap; sandbox estimates are honestly labeled),rebootrestarts the system with a browser reload (persisted data survives),shutdownpowers off, andcache/cache clearreport and clean rebuildable caches without touching/workspace. - Workspace split —
workspacemanages multiple isolated workspaces: each lives in its own/ws/<name>directory with its own files and state;create/switch/rmmanage them, and the current workspace is recorded in/ws/.current(persists across refreshes). The defaultmainworkspace is initialized on first boot. - System configuration —
envmanages persistent environment variables (/etc/succinix.env, merged into real Node child processes at spawn time) andsettingsmanages persistent system settings (/etc/succinix.settings): the tinbase port (preview-port, default 3001), the initial workspace (default-workspace, defaultmain), and the terminal font size (font-size, applied live). Both files ride the snapshot so they survive refreshes. - Service management —
servicemanages named background services declaratively on top ofspawn/ps/killand the port registry: definitions live in/etc/succinix.services(name|command|port,#comments,${PORT}placeholder resolved frompreview-port), withstart/stop/status/enable/disable.enablerecords the service in/etc/succinix.autostartand 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.logon the container FS (rides the snapshot, so it survives refreshes), formatted2026-08-05T04:00:00Z [level] message. It captures boot events (BOOT), command executions (INFOwithcmd/exit/runtime), service events (INFO/WARN), snapshot events (INFO) and errors (ERROR).logreads it (loglast 20,log -n <count>,log bootBOOT-only,log clear); the file auto-truncates to a ~200 KB tail when oversized. Interactivelog -f(tail -f) is intentionally not implemented (POC). - Package management —
pkgunifies the two real package channels behind one apt-style interface: lifo (lifo list/lifo install/lifo remove/lifo search— Lifo extension packages such aslifo-pkg-git,lifo-pkg-ffmpeg) and npm (real Node npm for the full ecosystem). Source is auto-detected: a package whoselifo-pkg-<name>exists on npm installs via lifo, otherwise via npm; on a name conflict lifo wins (tool packages).pkg listmerges both channels with aSOURCEcolumn,pkg searchmerges both searches,pkg install/removeecho the real command output and never swallow failures. The npm installed list is read from thenode_modulestop-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 view —
netstatrenders the port registry as a virtual listening-port table (Proto Local Address State,tcp 127.0.0.1:<port> LISTEN;netstat -padds the associated process, matched by port number in the process command,-when unmatched) andip addrshows the browser's virtual network identity (lo: virtual loopback,eth0: <preview-domain> (virtual)). Everything is honestly labeledvirtual— no fabricated interfaces, IPs, or connections. - System information & login banner —
unamereports the honest browser-native system identity (Succinix 0.6.0 js-runtime+webcontainer <api-version> <arch>; kernel identified asjs-runtime+webcontainer, never impersonating a Linux kernel;-aadds hostname/OS,-ris the@webcontainer/apiruntime version,-mis the UA-derived architecture) andmotdshows/edits the login banner at/etc/succinix.motd(persisted with snapshots; the default welcome line is printed on every boot and restored bymotd reset). - Self-test mode —
?test=1runs 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-<id>.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.
- Lint —
npm run lint(ESLint flat config ineslint.config.js).typescript-eslintrecommended + project rules:no-explicit-any(error), no leftoverconsole.log(warn;console.warn/errorallowed for the degradation-log convention, host-side files exempt), no unused vars/imports. Gate: 0 errors. - Typecheck —
npm run typecheck(tsc -p tsconfig.json --noEmit). Gate: 0 errors. - Unit tests —
npm run test(Vitest, node environment) covers the pure-logic modulessrc/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.tsagainst in-memory mocks (seetests/);src/commands/index.tspure functions (workspace/uname/netstat/port-matching/label) are also unit-tested.npm run test:coverageadds 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=1and?scenario=1are test-only: they expose internal handles onwindow(__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. - e2e —
npm run test:e2ebuilds once, then runs the CDP scripts sequentially againstvite previewin headless Chrome:scripts/verify-deploy.mjs— deploy-readiness gate +?test=1self-test (gate ≥71 passed, 0 failed);scripts/bench.mjs— performance benchmark (JSON output);scripts/scenarios.mjs— the 14 real-workflow scenario suite (S1–S14; definitions split acrossscripts/scenarios/);scripts/lang-verify.mjs— the language-ecosystem verification suite (TASK25);scripts/instance-demo.mjs— multi-instance + multi-user demo (dual-tab, R3);scripts/instance-routing.mjs— same-page instance routing (R5);scripts/cordis-app-e2e.mjs— an external@succinix/engineconsumer 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.ymlruns 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 heavyscenarios+lang-verify+instance-demosuite. See the CI badge at the top of this file. - pre-commit (optional, zero-dependency) —
npm run setup:hookswrites a.git/hooks/pre-committhat runstsc --noEmitand ESLint on the changed files only (scripts/pre-commit.sh). It is not forced: skippingsetup:hooksleaves 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 audit→ 0 vulnerabilities (all direct + transitive dependencies clean).npm outdated→ only@lifo-sh/core0.10.8 → 0.10.9 has a newer release; everything else is current. Not upgraded (policy), pending separate evaluation.public/host.jsis esbuild-minified (minify: trueinscripts/build-host.mjs); the host daemon stays small (~16.5 KB) while@lifo-sh/coreis bundled separately intopublic/lifo-core.js(~1 MB) and lazy-imported on the first Lifo command. Plainminifyis used because the full?test=1suite passes against the minified bundle (Lifo has noFunction.namedependency 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):
- Push this repository to GitHub / GitLab / Bitbucket.
- In the Vercel dashboard, Import Project → pick the repo. Vercel auto-detects Vite (
framework: vite,buildCommand: npm run build,outputDirectory: distfromvercel.json). - Deploy. Optionally add a custom domain, e.g.
succinix.alibicore.comor acjack.mesubdomain.
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)
| Command | Description |
|---|---|
help | Show command help |
clear | Clear the screen (Ctrl+L also works) |
sysinfo | Show browser-detected system information |
ports | List ready service ports with preview URLs |
db start | Start the tinbase database (auto-installs if missing) |
db status | Show database status (port registry + process table) |
db stop | Stop the database |
version | Show version |
whoami | Show current user (guest; the user id in ?user= mode) |
snapshot | Persistence status; snapshot now saves, snapshot clear --yes resets |
free | Show memory overview (device + JS heap; sandbox estimates marked ~) |
top | Live process table — 3 snapshots 2s apart, then exits |
reboot | Restart Succinix (browser reload; persisted data survives) |
shutdown | Power off (you can close this tab) |
cache | Show cache usage; cache clear cleans rebuildable caches |
workspace | List workspaces; create / switch / rm manage isolated workspaces |
env | List / set (env KEY=value) / unset (env -u KEY) environment variables, persisted in /etc/succinix.env |
settings | View / set / reset (settings reset KEY) system settings, persisted in /etc/succinix.settings |
service | List 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) |
log | Show 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 |
pkg | Package 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> |
netstat | List 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 addr | Show virtual network identity — lo: virtual loopback, eth0: <preview-domain> (virtual); no fabricated interfaces or IPs |
uname | Show 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) |
motd | View the login banner (/etc/succinix.motd); motd <text> sets it (persisted), motd reset restores the default |
lang | List built-in language runtimes: lang (table), lang python → Python 3.14.2 (Pyodide 314.0.4), lang node, lang typescript |
pwd | Show the session working directory (host-maintained, cd-synced, applied to node/python children) |
Host commands (TerminalExecutor, unified routing)
| Command | Route | Description |
|---|---|---|
node ... / npm ... / npx ... | Node | Real 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 ... | Python | Built-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, ... | Lifo | Unix tools, pipes, redirects |
ps | — | List the unified process table |
kill <pid> | — | Terminate a process (SIGTERM) |
cwd / setCwd <dir> / ping / exit | — | Protocol 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 innode -eare preserved (node -e "console.log(\"hi\")"->hi); an unterminated quote reportsunterminated quote in commandinstead 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=bartruly reaches child processes — anode -e "console.log(process.env.FOO)"child reportsbar(the env file lives underprocess.cwd()/etc/succinix.env, matching the browser write path). - Session cwd persistence (TASK24 复审):
cd's session cwd is persisted toprocess.cwd()/etc/succinix.cwd(previously the read-only virtual root — lost on refresh) and restored on host start, sopwd/ node / python cwd survive a refresh. - Process lifecycle:
spawna background service,psshows it,killtransitions it toexited. - Port registry:
server-readyevents surface preview URLs. - Database: tinbase (PGlite/WASM) boots and serves.
- Memory: device memory / JS heap stats reported by the browser (
freecan render). - Config:
envset/get/delete lifecycle andsettingswrite/reset persist to/etc/succinix.*. - Services:
servicelists the built-in tinbase definition; a temporary echo server can be started, observedrunning(process table + port registry), stopped, and removed with zero residue;service enable/disablewrite and remove the/etc/succinix.autostartfile (deduped). - Logs: command executions are recorded with
exit/runtime, boot events are recorded asBOOTentries, andlog clearempties the log file (asserted by the self-test suite). - Packages:
pkg listrenders the two-channel table (NAME / SOURCE / VERSION);pkg search githitslifo-pkg-git(network-dependent — skipped on failure, per the known-boundary convention). - Network view:
netstatrenders the port registry as a virtual listening-port table andnetstat -passociates a spawned echo server (port 3456) with its process; afterkillthe port disappears from the table.ip addrprints the virtual loopback and preview domain, honestly labeled(virtual). - System info:
unamerenders the honest system line (Succinix <version> js-runtime+webcontainer <api-version> <arch>) and the-a/-r/-mforms; the-r/-mflag parsing is additionally asserted through the command-dispatch path (not just the builders).motdset → read-back → reset leaves/etc/succinix.motdat 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;
rebootanddb startare excluded from the automated smoke (destructive/heavy side effects). - Languages (TASK27):
python -c "print(6*7)"returns42via 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 --versionreports Python 3.14.2;langlists node/python/typescript andlang pythonreports 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=1suite asserts all of it. See the Language Ecosystem — Verified Support Matrix for the authoritative, measurement-backed matrix. - Session cwd (TASK23):
cd /workspacesyncs the host session cwd and anode -e "console.log(process.cwd())"child follows it;cdinto a missing directory keeps the session cwd unchanged. (TASK24:/workspaceis the Lifo VFS view — real node/python subprocesses spawn in the mapped host directory, soprocess.cwd()inside a child reports the real path such as/home/<wc-id>/proj;pwd/cwdstill report the Lifo view/workspace/....) - EACCES hint (TASK24):
npm i -ghitting the read-only/usr/localappends 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 --versionchain,node -enested-quote file writes (preserved through tsc),npm i -gEACCES + 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.jsonchannel (no more parallel-channel race), retries read-only commands (ping/ps/cwd) once on transport failure, and the browser watchdog re-injects + respawnshost.jsafter 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).
| Language | Command | Status | Notable facts (measured) |
|---|---|---|---|
| Python | python / python3 / pip / pip3 | ✅ built-in | 3.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.js | node | ✅ built-in | 22.22.3; real binaries; node -e quote preservation; full TS toolchain (typescript/tsx/vitest) |
| npm | npm | ✅ built-in | 10.8.2; local installs into session cwd; global → EACCES + hint |
| TypeScript | npx tsc / tsx | ✅ via npm | tsc → node → vitest full loop (S13/S14) |
| Ruby | — | ⚠️ probe only | @ruby/wasm-wasi v2 runs in-container (6*7 → 42); not integrated |
| C / Rust / Go | — | ❌ absent | no compilers (which gcc/rustc/go → not found) |
| WASI | node:wasi | ✅ | precompiled 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:
curlto 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 (
lnreports 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).
/workspaceis a Lifo VFS view; real node/python children see real paths: the browser filesystem root (wc.fs/) and Lifo's/workspaceboth map to the host process cwd (/home/<wc-id>), and the container root/is a read-only system view.pwd/cwdreport the Lifo view (/workspace/...), whileprocess.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
pingprobe to the single-slot/cmd.jsonchannel; 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/stderrkeeps at most the last ~1 MB of output (large dumps are truncated to their tail). Normal use (seq 1 5000,catmid-size files,npm installlogs) 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-lengthfoo/bar, orvioverwriting 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 nopagehide/beforeunload(OOM kill, OS reclaim). - Declarative autostart (not a daemon):
service enableonly 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). Uselog/log -n <count>instead.log clearwipes/var/log/succinix.logand is therefore not itself recorded in the log.- Log append is a read-modify-write (backlog): WebContainer's
FileSystemAPIhas noappendFile, so eachlogwrite 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 — usepython -cinstead.pipis available via Pyodide's micropip (pure-Python wheels persist across refresh via/.pyodide/site-packages; compiled wheels such asnumpyneed onepip install <pkg>after refresh because the text snapshot does not carry binary.sofiles).python -m <module>runs viarunpy.run_module(only-m pipis special-cased).subprocessimports but cannot spawn — Pyodide raisesOSError: [Errno 138] emscripten does not support processes(see docs/LANGUAGES.md). - First
pythoncommand 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-timeloadPyodide, so the firstpythoncommand can take a few seconds; subsequent commands reuse the instance and are fast. It never depends on a usernpm 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(anddb 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 startlikewise reports "already running" if a process with that command is found. - Built-in tinbase service needs one install step: the preset
servicedefinition (tinbase) runsnpx tinbase start --port ${PORT} --engine wasm, which requires tinbase to be installed in the container. Rundb startonce first to complete the in-container install before usingservice start tinbase. - lifo packages are session-scoped; npm packages persist:
lifo installplaces 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_moduleson the shared filesystem and persist with the workspace snapshot.pkg listmerges both; the source rule is "lifo iflifo-pkg-<name>exists on npm, otherwise npm; lifo wins on a name conflict". pkginstalls need registry access:pkg install/search/infohit the npm registry (vialifo 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 (
guestis 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 (chmodsemantics) 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 viavercel.json. - No Content-Security-Policy header (evaluated, deferred): a CSP is not currently sent. WebContainer's internals need
worker-src blob:(worker bootstrap),script-srcwithwasm-unsafe-eval(Lifo/Pyodide), andconnect-srcto 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-deploypass 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
| Service | What it does |
|---|---|
ctx.fs | dsh file system: 12 primitives, 13 FS_* codes, sandboxMode |
ctx.sandbox | dsh sandbox provider: synchronous confine, node fail-closed |
ctx.terminals | dsh owner-scoped PTY registry with exact Agent owners |
ctx.sessionPersistence | dsh 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
- Supported features & capabilities — English · 简体中文
- Language support matrix — English · 简体中文
- File RPC protocol — English · 简体中文
- SDK / plugin integration — English · 简体中文
- Cordis plugin integration — English
- Migration guide — English
- Contract snapshot — English
- Agent & design guidelines — English · 简体中文
- Changelog — English · 简体中文
- Contributing — English · 简体中文
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:
netstatvirtual listening-port table +ip addrhonest virtual identity - dsh single-track engine:
@succinix/engine@0.6.0withctx.fs/ctx.sandbox/ctx.terminals/ctx.sessionPersistence - SunamAI integration: replace
shell_runengine 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).