From eecf332fbbbd0cbadd37aa3c7370fd4483142a86 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 30 Oct 2026 11:50:44 -0700 Subject: [PATCH] Scope nibling to web-shaped workloads, run runners as JS isolates, and add firehose triggers D6 now says runners run the same JS in Deno or V8 isolates, not Linux microVMs, and report back by push. D13 records the StackBlitz-style scope. D14 makes Jetstream-triggered automations headless app cores, developed against replayed events in the browser. --- docs/VISION.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/VISION.md b/docs/VISION.md index a67012d..40b1d9c 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -27,7 +27,7 @@ These extend the principles in the root README. | kernel | Processes, a filesystem view, and module loading for one sandbox. The current `@nibling/kernel`. | | process runtime | How a process sees the world: `web` (ESM and web globals, the default), `node` (Node globals and builtins), or `bash`. | | sandbox | One origin with its own Service Worker and kernel, plus a manifest that declares its permissions and a lockfile that pins its dependencies. | -| placement | Where a sandbox runs: the browser, or a runner service identified by a DID. | +| placement | Where a sandbox runs: the browser, or a runner service identified by a DID that runs the same JS in isolates. | | mount | A tree attached to a sandbox's namespace: content-addressed (read-only), an atproto repo, a Space, a git repo, or a writable overlay. | | capability | A `nib:*` module whose implementation the host provides. One definition generates the ESM API, the `.d.ts`, and a `nib` CLI command. | | provider | A third-party atproto service the kernel brokers, such as mail, domains, or payments. | @@ -138,13 +138,24 @@ This replaces step 4 of the runtime build order as the next storage work. The jo Spaces: we have access to the atproto Spaces alpha (proposal 0016, alpha released 2026-08-18) and build assuming it ships. Not yet confirmed: the encryption model, and whether the spec defines how a record moves between a public repo and a Space. -### D6. Runners are the server-side placement +### D6. Runners are the server-side placement, and they run JS, not Linux -Some work cannot run in a tab: the realtime relay, agents that run after the tab closes, cron and webhooks, pushing to knots over SSH, URLs that stay up, native binaries and heavy builds, and model key custody if a user wants it. +Some work cannot run in a tab: the realtime relay, agents that run after the tab closes, firehose triggers (D14), cron and webhooks, pushing to knots over SSH, URLs that stay up, and model key custody if a user wants it. A Service Worker does not change this. Chrome stops an idle one after 30 seconds and caps each event at 5 minutes, and a background tab can be throttled or discarded. Push is the one background mechanism every engine supports, so a runner reports back by push, and the page's Service Worker wakes to sync the result. atproto already has the pattern. Tangled's repo manifest names its CI runner by DID (`spindle`), knots host git data, and a Space is announced by an `#atproto_space_host` service entry. nibling adds a runner: a self-hostable service identified by DID that a workspace record names. nibling.run operates a default runner. -The syscall table is already the boundary between a process and its kernel, so a remote kernel can speak the same interface over a WebSocket. Because `nib:*` implementations come from the host, a runner can be plain Deno, with `--allow-*` flags generated from the sandbox manifest. +Because nibling's workloads are web-shaped (D13), a runner runs the same JS against the same `nib:*` interfaces, with server-side implementations. It does not need a Linux microVM per sandbox, as CodeSandbox's Firecracker fleet provides. A runner is either Deno, with `--allow-*` flags generated from the sandbox manifest, or V8 isolates in the Cloudflare Workers model. Isolates are dense and cheap, and they are enough isolation for JS. The syscall table is already the boundary between a process and its kernel, so a remote kernel can speak the same interface over a WebSocket. + +One manifest drives both placements: + +| Manifest concept | Browser | Runner | +|---|---|---| +| mounts | the sandbox's file tree | the isolate's mounted tree | +| `net` | the sandbox origin's Service Worker and CSP | Deno `--allow-net`, or the isolate's fetch policy | +| isolation boundary | the sandbox origin | the isolate or Deno process | +| credentials | the page's credential broker | the runner's credential broker | + +A runner holds write tokens for the users whose work it runs. That is why runners are self-hostable and identified by DID: a user chooses whom to trust with their tokens. ### D7. Kernel tools copy agent tools that already work @@ -212,6 +223,40 @@ Consequences: - Tests drive the core directly. GUI tests cover only rendering and input. - Runners can run apps headless, for example a Calendar reminder that fires with no tab open (D6). +### D13. nibling's workloads are web-shaped + +nibling follows StackBlitz, not CodeSandbox. CodeSandbox runs arbitrary Linux in Firecracker microVMs. StackBlitz runs web development in the browser, and its agent product (Bolt) builds web apps. Everything nibling plans is web-shaped already: JS and TypeScript, Node and npm, web frameworks, atproto records, and the apps in D11. + +Consequences: + +- Python, data science, and native toolchains are non-goals. A runner does not absorb them. Pyodide may cover some Python use, and nibling makes no promise beyond that. +- Runners stay JS isolates (D6), which keeps server costs far below a microVM fleet. +- Tool fidelity for agents is measured against web-shaped tasks: an agent-weighted task corpus, scored by how many tasks finish in the browser and how many need a runner. + +### D14. Firehose triggers run headless app cores + +An automation is an app's headless core (D12) whose trigger is atproto events instead of a person. Its manifest declares the trigger, and its code exports a handler: + +```js +// manifest: { trigger: { jetstream: { collections: ["app.bsky.feed.post"], dids: ["did:plc:..."] } }, +// nib: ["at"], publish: { budget: "20/day" } } +export default { + async onRecord(event, { at }) { /* match, then write a record through nib:at */ }, +}; +``` + +The same handler runs in two places. In the browser, it runs against a recorded or live slice of Jetstream, for writing and debugging. On a runner, it runs continuously. Code mode fits: an agent writes the handler, and a low-code builder in the style of airglow.run generates one. + +The runner side needs hosted infrastructure: + +- A Jetstream consumer that filters by collection and DID before dispatch, because the firehose volume is high. +- A dispatcher that hands each event to an isolate. A Durable Object per automation fits: one writer, its own state, and its own cursor. +- Delivery at least once, resumed from Jetstream's cursor. Handlers must be idempotent; deterministic record keys make record writes idempotent. +- The runner's credential broker, holding OAuth tokens whose scopes come from the manifest (D9). +- Per-automation rate limits and the publish budget (D10), because automated posting is spam by default. + +Automations stay event-driven and short-lived. They are not a path back to general-purpose server compute. + ## Where the code stands | Area | State | Gap | @@ -232,6 +277,8 @@ Consequences: - Which CRDT, and is it per file or per tree? - Relay transport: a WebSocket per space, or WebRTC with signaling? - Runner protocol and discovery: a DID service entry, a workspace record field, or both? +- Runner isolation: Deno processes, V8 isolates (workerd), or both? How are a runner's isolates limited for CPU and memory? +- How much do tab throttling and discarding affect a long-running sandbox in a background tab? (Not yet measured.) - Which content lexicon for Markdown documents? - jj in wasm (probed 2026-10-30 on jj-lib 0.46.0, commit 633b5fcd1e56): unmodified `jj-lib` fails on `wasm32-wasip1` because `file_util.rs` has no WASI platform module. With open upstream PR jj-vcs/jj#9849 applied (a WASI platform module and a lockfile-based `FileLock`), `cargo check -p jj-lib --no-default-features --target wasm32-wasip2` passes. `wasm32-unknown-unknown` then fails only on `getrandom` needing its `wasm_js` feature; the fork PR manno23/jj-api#2 reports a clean build and passing tests with that fix, which we did not reproduce. The open question is the git backend. `--no-default-features` drops gitoxide, and without it jj cannot read or write the git repos that knots serve. Nobody has tried gitoxide on WASI. jj-lib's storage also calls `std::fs` directly, so it needs a WASI layer over our kernel's VFS rather than a pluggable store. - Knot CORS (probed 2026-10-30, answered for reads): `knot1.tangled.sh` sends `Access-Control-Allow-Origin: *` on `info/refs` and `git-upload-pack`, and its preflight returns 204. A browser can clone directly from a knot. It cannot clone through `tangled.org`, which answers with a 307 to the knot that carries no CORS headers, and answers `OPTIONS` with 405. So the client must resolve the knot URL itself; which record names it is not yet confirmed. The allowed request headers are `Authorization, Content-Type, atproto-proxy`, so a browser cannot send `Git-Protocol` and is limited to protocol v1. A `git-receive-pack` preflight also passes, but whether a knot accepts HTTPS push, and with what credential, is unknown. -- knotmirror 0.0.1