neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A Linux sandbox that boots inside your browser tab — a BusyBox shell, CPython
3.14 with NumPy, and Jupyter (Notebook and JupyterLab) on a real ipykernel —
running on the Yurt kernel compiled to WebAssembly.
Nothing runs on a server: the kernel, the filesystem and Python all execute in
the tab, and the page keeps working with the network off.
The same playground is also a desktop app for macOS and Linux, where the
sandbox runs natively on your machine instead of in the tab: it boots in
seconds, and the guest has real network access. Download links are on the home
page.README 전문 보기 ↗Try it: https://yurt-playground.pages.dev
A Linux sandbox that boots inside your browser tab — a BusyBox shell, CPython
3.14 with NumPy, and Jupyter (Notebook and JupyterLab) on a real ipykernel —
running on the Yurt kernel compiled to WebAssembly.
Nothing runs on a server: the kernel, the filesystem and Python all execute in
the tab, and the page keeps working with the network off.
Where it is going: the roadmap. What changed in each release: the release notes.
The same playground is also a desktop app for macOS and Linux, where the sandbox runs natively on your machine instead of in the tab: it boots in seconds, and the guest has real network access. Download links are on the home page.
| Browser | Desktop app | |
|---|---|---|
Terminal (ash), Python 3.14, NumPy, Jupyter Notebook / Lab | ✓ | ✓ |
| Runs in | the tab (WebAssembly, no server) | the native Yurt runtime on your machine |
| Boot | ~30–60 s (compiles in the tab) | ~12 s |
| Network from inside the sandbox | none, by design | yes (Python verifies TLS; BusyBox wget does not) |
| Works offline | yes | yes |
| Needs | a desktop browser with cross-origin isolation (Chrome, Edge, Firefox, Safari) | any browser to display; nothing to install besides the app |
The browser page, the desktop app and yurt run playground boot the same image
(pinned in artifacts/pins.json). It contains:
| Package | Version | For |
|---|---|---|
| BusyBox | 1.37.0 | 151 applets, ash among them: core utilities, grep, sed, awk, vi, less, tar, gzip, bzip2, unzip, wget, nc, ps, top |
| CPython, pip | 3.14.7, 26.1 | python3; pip |
| NumPy | 2.5.0 | arrays |
| ipykernel, IPython, pyzmq | 7.2.0, 9.13.0, 26.4.0 | the Jupyter kernel behind Notebook and Lab |
All four are in the image.
The image is becoming modular: a small base image (BusyBox, git, curl, jq,
sqlite3, zsh, make, zstd, xz, CA certificates, and a passwordless
sudo for the default user) with the pkg client and a trusted-repository
configuration for
YurtOS/yurt-packages, and the heavy
tools as signed packages there. That base image is in progress; the image pinned
here carries neither pkg nor sudo yet, so nothing can be installed into it
today.
yurt run: the sandbox has a network, so
sudo pkg update, then sudo pkg install NAME, in the terminal. Every pkg
command needs root, sudo pkg list and sudo pkg search too: it locks and
writes its cache and state under /var/cache/yurt-pkg and
/var/lib/yurt-pkg, and installs into system directories. That is what the
base image's passwordless sudo is for; the browser's Packages control uses
the same sudo. pip install of pure-Python packages and matching wheels
works there already.installed NAME (or why pkg
refused), and the package's command runs in the terminal. The page downloads
the repository's signed index, the package metadata, and the archives with
their Sigstore bundles, and lays them out in the sandbox as a file-backed copy
of the repository. The sandbox's own pkg then runs update and install
against that copy, trusting the signing identity and Sigstore trust root the
image configures for the real repository, so every check it makes on the
desktop (index and archive signatures, every sha256 and size the index names,
the archive's own metadata) runs unchanged. Its rollback protection starts
from an empty cache in each session, as on a fresh desktop sandbox.
sudo pkg install typed in the browser terminal still fails: only the
Packages control can download.
Which packages are published, and which are coming, is tracked in
YurtOS/yurt-packages (its signed
index.json is the list), not here. The builds published in yurt-packages today
predate the current package format and carry placeholder signatures, so pkg
refuses them, in the browser and on the desktop alike, until they are
republished.
macOS (Apple Silicon). Open Yurt-Playground-aarch64-apple-darwin.dmg and
drag Yurt Playground to Applications. The app is not notarized yet, so macOS
blocks the first open: allow it under System Settings → Privacy & Security →
Open Anyway (or run
xattr -dr com.apple.quarantine "/Applications/Yurt Playground.app"), then open
it again. A terminal window shows the server; close it to stop.
Debian / Ubuntu.
sudo apt install ./Yurt-Playground-<arch>-unknown-linux-gnu.deb, then run
yurt-playground (or the Yurt Playground desktop entry). Ctrl-C stops it.
Either way the launcher prints Yurt playground: http://127.0.0.1:<port>/ and
opens it in your default browser. yurt-playground --port N fixes the port and
--no-open skips the browser (a headless box, a script); --help lists them.
Inside the app: the launcher (this repo, compiled with Deno), the site as
deployed, and the native sandbox — yurt-desktop-host, the Yurt runtime, the
kernel and the playground image — as pinned in artifacts/pins.json. The
launcher starts the host, which boots the image with a public network interface,
your resolvers and a few mapped ports, and relays the terminal and the Jupyter
kernel to the page over WebSockets on the launcher's own origin.
The page exposes window.yurt for a driver -- an agent, a test -- on the hosted
site and in the desktop app alike: await yurt.ready, then
yurt.exec(cmd, { stdin, timeoutMs, maxOutputBytes, cwd, env }) for a result
with stdout, stderr, code or signal, timedOut and the truncation
flags; yurt.spawn for a handle with wait() and kill(signal);
yurt.fs.read/write/list/download; yurt.status and <html data-yurt-status>
for "idle", "booting", "running" or "failed". Every command is a process of the
page's own, not a keystroke in the terminal.
code and signal are the process's wait status, as waitpid(2) gives it,
whoever sent the signal: a normal exit is code (the exit status) with
signal: null; a signal death is signal ("SIGTERM", "SIGKILL", ...) with
code: null, and coreDumped says whether it dumped core. A signal death is
never reported as code 128+n -- that is only a shell's convention -- so
kill -TERM $$ gives signal: "SIGTERM", while a process that catches the
page's SIGINT and exits 130 gives code: 130. In the desktop app the host
reports a single number, so there a signal is inferred from the kill the page
sent.
The desktop app also serves the same thing over HTTP for a program on the
machine, on the launcher's loopback port under /api/. The token is printed
once (API token: …) and left in ~/.yurt/playground.json (mode 0600) while
the launcher runs (with no url in it until the sandbox is up). One launcher
runs per user: a second launch says so, with the URL, and exits. Every request
carries the token as Authorization: Bearer <token>:
T=$(jq -r .apiToken ~/.yurt/playground.json); U=$(jq -r .url ~/.yurt/playground.json)
curl -H "Authorization: Bearer $T" ${U}api/status
ID=$(curl -s -H "Authorization: Bearer $T" -H 'content-type: application/json' \
-d '{"cmd":"python3 -c \"print(6*7)\"","timeoutMs":60000}' ${U}api/executions | jq -r .id)
curl -H "Authorization: Bearer $T" "${U}api/executions/$ID?wait=1"
curl -H "Authorization: Bearer $T" -X PUT --data-binary @data.csv "${U}api/fs/content?path=/home/user/data.csv"
| Route | Does |
|---|---|
GET /api/status | {native, status, bootMs, executions: {active, limit}} |
POST /api/executions {cmd, stdin?, stdinBase64?, timeoutMs?, maxOutputBytes?, cwd?, env?} | 201 {id}; 429 past 16 active |
GET /api/executions | every execution still held |
GET /api/executions/{id} | its state; with ?wait=1 the result, read once |
DELETE /api/executions/{id} {signal?} | signal it (SIGKILL by default) |
GET / PUT /api/fs/content?path= | a file's bytes, application/octet-stream; PUT takes X-Yurt-Mode: 644 and X-Yurt-Atomic: 0 |
GET /api/fs/entries?path= | [{name, type, size, mode}] |
Results are JSON, so a command's output is text there; binary output goes
through a file. A process running as you on this machine is you: that is the
whole of the access model. Errors are {error, code}; an Origin other than
the launcher's own is refused, so no web page can reach it. The page's
window.yurt and a curl share one registry: at most 16 running or stuck
executions per sandbox, results kept ten minutes or until read.
A merge to main deploys the site (.github/workflows/deploy-pages.yml). To try
a branch on a real device before it merges, publish it as a preview:
gh workflow run deploy-pages.yml --ref main -f ref=<branch>
It builds and verifies that branch exactly as the production deploy does (the
snapshot included) and publishes it at its Cloudflare branch alias,
https://<alias>.yurt-playground.pages.dev: the branch name lowercased, every
other character a hyphen, cut to 28 characters, with no leading or trailing
hyphen (agent/foo_Bar → agent-foo-bar). The run's summary links the exact
URL. Production is never the target: the preview path refuses main and the
Pages project's production branch. Run it from main (--ref main), not from the
branch: a branch older than this preview mode carries the old workflow, which
deploys to production. Previews carry the production headers (dist/_headers:
the CSP and cross-origin isolation).
A static page loads kernel.wasm and the playground image, boots the kernel in
web workers, spawns ash on a host-side PTY that xterm.js talks to, and
launches ipykernel inside the sandbox. The Jupyter Notebook and JupyterLab
interfaces are JupyterLite's frontend with one kernel plugin that relays every
message to that real ipykernel — none of JupyterLite's own kernels ship, and
no Python runs in the browser itself. The page needs cross-origin isolation
(COOP/COEP headers) for the shared memory the workers use, which is why it is
hosted on Cloudflare Pages; phones are told the sandbox needs a desktop-class
tab.
The "Is this really running in your browser?" section on the home page has checks you can do yourself, including re-hashing every file the page downloaded against the published pins.
A second kernelspec, Python 3 (Yurt, suspend/resume), runs one CPython process
built so the sandbox can be sealed (scripts/build-python-seal.sh): its
notebook has Suspend and Resume buttons, and a cell that is printing when you
suspend carries on at the same line when you resume, from the image in the
browser's IndexedDB. src/notebook_kernel_worker.ts is the whole kernel.
A new release of all of that is one train,
playground-<YYYY.MM.DD>-<kernel rev>, cut by scripts/release-playground.sh:
it dispatches yurt-sandbox's release workflows from exact commits, generates
artifacts/pins.json from what they published, verifies it, and asks before
merging the pin PR that deploys the page. --validate builds everything and
publishes nothing; --pins-only regenerates the pins from releases cut by hand.
The page restores a sealed sandbox made for its device by default, instead of
booting the slim image and installing into it: a desktop browser the one with
the Python set and Jupyter (snapshot in the pins), a tablet one with python
alone (tabletSnapshot), a phone the slim image as it boots, without Python
(phoneSnapshot). Whenever its snapshot cannot be had or is refused, the page
boots the image as before, with a console note; ?snapshot=0 always boots the
image. Each snapshot records what is in that Linux, which the page shows above
the terminal and the shell prints when it starts (it is also /etc/motd). A
repeat visit takes the checked parts from the browser's cache (Cache API, keyed
by each part's sha256) instead of downloading them again. See
Snapshots in use for each one's size and the memory it
needs.
A snapshot is the kernel's memory after an install in the browser, so it is
captured locally rather than by the train:
deno run -A scripts/release-snapshot.ts --kind desktop|tablet|phone, from a
clean checkout at its pushed head whose artifacts/pins.json names the train
(with the pinned blobs installed, the sibling kernel checkout clean at the
pinned rev, and Playwright's Chromium). It serves the page, runs
?snapshot=capture in headless Chromium, checks that the file decodes in full
and was built from the pinned kernel and image and the Python the live index
offers, publishes it as the prerelease snapshot-<train>
(snapshot-tablet-<train>, snapshot-phone-<train>) and writes its pin, with
its kernel memory and what it holds. --dry-run stops before the release and
the pin; --tag names a second snapshot of one train (a published one is never
replaced). A train keeps the pin only while release-snapshot.ts --check-pin
accepts it, and otherwise drops it and says to run the script again; so does a
change to the page's boot setup (BOOT_SETUP in src/sandbox_snapshot.ts).
Only the deploy fetches the snapshot (YURT_INSTALL_SNAPSHOT=1), and a problem
with it never fails the deploy.
To try another snapshot without a pin or a deploy, capture and upload it locally:
deno run -A scripts/snapshot-local.ts [--packages pandas,scipy] [--name NAME]
It captures the way release-snapshot.ts does (same prerequisites, same checks;
the page checkout need not be a pushed head, which it only notes), and
--packages installs those packages after the Python set, before the seal. It
then pushes the parts and a manifest.json (the pin's fields) as one parentless
commit to refs/snapshots/<name> in YurtOS/yurt-playground (not a branch, so a
clone does not fetch it), reads the manifest back from raw.githubusercontent.com
and prints https://yurt-playground.pages.dev/?snapshot=<commit sha>. It first
checks that the live site runs this checkout's build (its /pins.json), since
the site restores only its own. The page takes only a 40-digit commit sha, and
restores it only when a refs/snapshots/* ref of the repository points at it:
raw.githubusercontent.com also serves fork and pull-request commits under the
repository's path, so the page asks the GitHub API's list of those refs first
(60 unauthenticated requests an hour per address; without an answer it boots the
image). The manifest takes every check the pin does, each part is checked
against it, and anything else boots the image as without the flag. --file PATH
uploads an existing capture, --dry-run stops before the push. --kind picks
the snapshot as above (?snapshot=capture, capture-tablet, capture-phone);
each restores only on a page of its kind, since a page refuses a snapshot whose
Python is not the one it asks the live index about (none on a phone, python
alone on a tablet), and the printed URL names its platform. --before-deploy
turns the site check into a note, for a snapshot of pins that a pending deploy
brings. List the uploads with git ls-remote origin 'refs/snapshots/*'; delete
one with git push origin :refs/snapshots/<name>, after which the page refuses
its URL. Two things the script does not check. First, a capture from page code
that changed SEAL_VERSION, BOOT_SETUP or the header format against the
deployed page: only the pins are compared, and the site then boots the image
(keep the page checkout at the pushed main). Second, --packages extras whose
install moves a member of the Python set: the snapshot keeps the set's label,
which no longer describes its memory.
Train playground-2026.10.29-3d32068. Generated from artifacts/pins.json; deno run -A scripts/snapshot-table.ts rewrites it.
| Kind | Restored by | Release | Download | Kernel memory | RAM to restore (peak) | This Linux |
|---|---|---|---|---|---|---|
| desktop | desktop browser (Mac, PC): ?platform=desktop | snapshot-playground-2026.10.29-3d32068-2 | 131.0 MB in 7 parts | 520.6 MiB | 2449 MiB | BusyBox shell, git 2.55.0, Python 3.14.7, numpy 2.5.0, pyzmq 26.4.0, Jupyter kernel 0.0.1, ca-certificates |
| tablet | tablet (iPad): ?platform=tablet | snapshot-tablet-playground-2026.10.29-3d32068 | 94.8 MB in 5 parts | 381.1 MiB | 1254 MiB | BusyBox shell, git 2.55.0, Python 3.14.7, ca-certificates; no Jupyter |
| phone | phone (iPhone): ?platform=phone | snapshot-phone-playground-2026.10.29-3d32068 | 31.2 MB in 2 parts | 136.1 MiB | 612 MiB | BusyBox shell, git 2.55.0, ca-certificates; no Python, no Jupyter |
The page picks its platform from the device: the bare URL gains
?platform=phone, tablet or desktop (aliases iphone, ipad, mac)
before anything loads, and that platform's snapshot is the only one it fetches.
A hand-written ?platform= overrides the device as far as the device can run
it: a desktop may run the phone's page; a phone asked for the desktop's runs its
own and says so. ?snapshot=0 boots the image instead.
Measured by hand with scripts/measure-boot.ts (headless, served by the dev
server, so the download is local and fast) on 2026-10-29. "RAM to restore" is
the peak resident memory of Chromium's renderer processes while the page
restores and starts its shell, in the device's emulation, the page itself
included (about 110 MiB before Start). WebKit's is not measured.
| Platform | Engine | Time to a prompt, cold / warm | Jupyter ready |
|---|---|---|---|
| phone (iPhone 13) | Chromium | 0.96 s / 0.63 s | none |
| phone (iPhone 13) | WebKit | 0.74 s / 0.53 s | none |
| tablet (iPad Pro 11, iPadOS landscape and mini) | Chromium | 1.48-1.52 s / 1.06-1.07 s | none |
| tablet (same three) | WebKit | 1.11-1.15 s / 0.83-0.89 s | none |
| desktop | Chromium | 1.88 s / 1.48 s | 5.2 s / 4.4 s |
| desktop | WebKit | 1.78 s / 1.43 s | 8.2 s / 7.9 s |
For comparison, booting the slim image instead (?snapshot=0, Chromium): a
prompt at about 1.4 s on every platform, with a renderer peak of 620-630 MiB on
a phone or tablet since the boot keeps one copy of each file for its symlinks
(#267; 1,830-1,910 MiB before it); a desktop then installs the Python set and
starts Jupyter, ready at about 88 s, with a peak of about 2,700 MiB.
Apache-2.0.