clusterflux-public/MVP.md
Disasmer release dry run f22d0a5791 Public dry run dryrun-309831e1e021
Source commit: 309831e1e021f962c118452336776fd9a94025f9

Public tree identity: sha256:6fa95c1745579bd6256dbeb3d476db0b07c2f24aa9213b0f7783ce1adfc8aca5
2026-07-03 16:07:13 +02:00

3559 lines
92 KiB
Markdown

# Disasmer MVP Engineering Plan
**Status:** draft MVP execution plan
**Audience:** engineering / founder-engineer
**Goal:** build a public-launchable MVP that proves Disasmer can replace CI/build systems with a general distributed Wasm runtime whose flagship use case is distributed, local-first, source-debuggable builds.
---
## 0. Executive summary
Disasmer should launch as a **general distributed Wasm runtime** with a **build-system experience so strong that it is the obvious wedge**. The MVP is not a toy local scheduler and not merely a CI runner with nicer YAML. It must demonstrate the core thesis:
> A build can be a normal Rust program, running as one virtual process across multiple machines, with build steps represented as virtual threads/tasks, artifacts represented by a low-latency virtual filesystem, and debugging represented as one normal VS Code debug session.
The launchable MVP should include:
- Open-source local runtime, SDK, CLI, node runtime, protocol definitions, and VS Code extension.
- Hosted coordinator from day one, with persistent project/identity state in Postgres and private hosted Authentik/OIDC/community-policy modules; the community tier coordinates but does not provide arbitrary hosted compute.
- User-attached Linux and Windows nodes.
- Containerfile-based environment discovery for MVP user projects.
- Rootless Podman backend for Linux command execution.
- Windows command backend sufficient for executing commands unsandboxed, with the Windows sandboxing backend explicitly stubbed (to be implemented later) and isolated behind an interface.
- Wasmtime-based execution for Disasmer guest/orchestration code.
- Native command execution only through node runtimes, never through the hosted coordinator control plane.
- Small serializable task arguments and results.
- Large inputs/outputs passed through handles: `SourceSnapshot`, `Artifact`, `Blob`, and VFS manifests.
- Local-first VFS where `flush()` publishes metadata and `sync()` triggers explicit user-chosen export/transfer/durability behavior; artifacts are otherwise ephemeral node-local data.
- Git support as an optional source-provider module that is included by default, with non-Git providers pluggable and dependencies avoidable when unused.
- All-stop debugging with Debug Epochs as a mandatory MVP capability.
- A Disasmer DAP adapter that targets VS Code first while staying usable by other DAP clients.
- Task restart from filesystem/artifact checkpoint boundaries, not live stack migration or hot-swapping.
- Abuse controls designed into the object model and control plane, not bolted on later.
- Operator panel foundations in the protocol and runtime-state model, with the full immediate-mode panel as the second product milestone.
The flagship demo should make people immediately understand why this is not “yet another CI system”:
1. The repo has `envs/linux/Containerfile` and `envs/windows/Containerfile`.
2. The build is one Rust source file using `#[disasmer::main]`, `#[disasmer::task]`, `spawn::task`, `env!("linux")`, `env!("windows")`, `cmd!(...)`, `fs::flush()`, and `fs::sync() / artifact::export_to_node()`.
3. A Linux node and a Windows node can be attached to the hosted coordinator.
4. Pressing F5 in VS Code starts one virtual process.
5. VS Code shows `main`, `compile linux`, `compile windows`, and `package` as threads of one debug target.
6. A breakpoint in the Windows build task stops the whole virtual process using a Debug Epoch.
7. The user edits the build logic, restarts only the failed task from a clean VFS checkpoint, and continues.
8. Artifacts appear through the VFS without coordinator-routed bulk data.
---
## 1. Product stance and launch bar
### 1.1 Positioning
Disasmer should be positioned as:
> A distributed Wasm runtime that makes distributed execution feel like ordinary source-level-debuggable multithreaded programming, with build systems as the first killer use case.
It is not primarily:
- a hosted CI SaaS;
- a YAML syntax replacement;
- a remote shell wrapper;
- a Kubernetes abstraction;
- a general-purpose compute subsidy;
- a task queue with logs;
- or a build cache alone.
It should feel closer to:
> “What if my build system were an actual program, and the debugger saw the whole distributed build as one process?”
### 1.2 Public-launch promise
The launch should be built around a concrete promise:
```text
Add a Containerfile.
Reference it from Rust with env!("name").
Press F5.
Debug the distributed build like one normal program.
```
The public launch should be able to show a short video or GIF with these beats:
```text
1. VS Code open on a Rust build program.
2. env!("linux") and env!("windows") autocomplete from envs/**/Containerfile.
3. Press F5.
4. Hosted coordinator shows two attached nodes.
5. Linux and Windows build tasks start.
6. VS Code thread list shows virtual threads.
7. Breakpoint hits in the Windows task.
8. Both tasks stop.
9. User inspects args/locals/artifact handles.
10. User restarts only the failed task.
11. Final artifacts are explicitly exported.
```
### 1.3 What must be true before public launch
The MVP is launchable only if these are true:
- A user can use the hosted coordinator without you granting them compute.
- A user can attach their own Linux node and run a build task on it.
- A user can attach a Windows node and run a Windows command task on it.
- Source code references environments by logical name, not by runner label or machine name.
- Local-first Linux builds do not upload the repo, tar the checkout, route file reads through the coordinator, or automatically upload build outputs.
- `flush()` is cheap and metadata-first.
- `sync()` is explicit and may move bytes.
- The debugger shows one process and many virtual threads.
- A Debug Epoch stops all Wasm tasks and controlled native command tasks for the virtual process.
- Task restart works from task entrypoint or last published VFS checkpoint.
- Community hosted users cannot run arbitrary containers or native commands on your infrastructure.
- Every public object has tenant ownership, quota accounting, and authorization checks.
### 1.4 What can be rough at launch
These can be rough, provided the rough edges are honest and isolated:
- Windows sandboxing can be a stubbed backend with a clear `windows-command-dev` capability, as long as hosted managed Windows compute is not offered to untrusted users.
- Nix can be included as a default plugin but not required for the flagship Containerfile demo.
- The operator panel can be scaffolded and visible as a preview, with the full immediate-mode UI in the second milestone.
- Debugger variable inspection can prioritize task args, Disasmer handles, top-level locals around Disasmer API calls, stack frames, and command status before supporting every possible Rust compiler local perfectly.
- NAT traversal can be coordinator-aided without relay. If direct node-to-node connectivity fails, scheduling and artifact flows should avoid requiring peer transfer rather than silently relaying bulk data through the coordinator.
---
## 2. Hard MVP decisions from the project constraints
### 2.1 Product scope
The MVP includes both:
- **open-source local-first runtime**, because credibility depends on not hiding the core runtime; and
- **hosted coordinator**, because the GitHub Actions replacement story needs an easy entry point, public rendezvous, auth, node registry, debug sessions, and a web surface.
The hosted coordinator is a control plane, not a general-purpose compute platform.
### 2.2 First target user
The first target user is:
> A developer or small team that currently uses GitHub Actions, Forgejo Actions, or similar CI, and wants something more programmable, local-first, debuggable, and self-hostable.
The first user does not need managed compute from Disasmer. They need:
- local and self-hosted nodes;
- a hosted coordinator for convenience;
- VS Code debugging;
- artifact metadata, ephemeral artifact handles, and explicit output export;
- a better mental model than YAML pipelines;
- and a credible path to replacing CI.
### 2.3 Host platforms
MVP node platforms:
```text
Linux required
Windows required
macOS not required for launch, but kept in the model
BSD-like not required for launch, but kept in the model
```
Linux should be the most complete backend. Windows should be complete enough to run a real attached worker and prove cross-platform task placement. macOS remains a design-supported later node type.
### 2.4 Environment support
MVP user-facing environment support:
```text
Containerfile / Dockerfile discovery: required
Nix plugin: included by default, but not launch-critical for all flows
Windows sandbox backend: interface + stub, not trusted managed compute
```
The MVP should discover:
```text
envs/<name>/Containerfile
envs/<name>/Dockerfile
```
It may also discover:
```text
envs/<name>/flake.nix
envs/<name>/flake.lock
```
But the flagship launch demo should not depend on Nix unless it is already stable in the implementation.
### 2.5 Execution support
The MVP must run both:
- Wasm task entrypoints, using Wasmtime on node runtimes; and
- native commands, through `cmd!(...)`, executed only by node runtimes inside declared environments.
The coordinator must not run native commands or containers. The hosted control loop may run zero-capability Wasm, but only with strict fuel, memory, log, state, and wall-clock limits.
### 2.6 Wasm model
Use the simplest Wasmtime model that is secure and functional:
```text
Core Wasm module + explicit host imports first.
Component Model support later or behind an internal abstraction.
```
Reasoning:
- Core Wasm embedding is enough for task entrypoints, host imports, memory access, and debug probes.
- The MVP does not need to expose a stable cross-language component ABI yet.
- The runtime should keep an internal `GuestAbi` trait so Component Model support can replace or augment this later.
### 2.7 Data model
MVP task boundaries allow only:
```text
small serializable values
immutable content-addressed objects
Disasmer runtime handles
```
Do not allow:
```text
raw pointers
borrowed references
local file handles
sockets
process handles
MutexGuards
unbounded byte vectors
native closures
live stack migration
```
Large data moves through:
```text
SourceSnapshot
Blob
Artifact
VirtualFile
VfsManifest
```
### 2.8 Checkpoint model
MVP checkpointing means:
```text
filesystem/artifact checkpoints for task restart
```
It does not mean:
```text
live Wasm stack checkpointing
native process checkpointing
socket checkpointing
transparent migration of running processes
hot-swapping code into existing stack frames
```
A restartable task must be recreated from:
```text
task name
new module hash
original serialized args
same environment handle
same source snapshot or selected dirty overlay
latest allowed VFS checkpoint
```
### 2.9 Debugging model
All-stop debugging is mandatory.
The debugger must present:
```text
one virtual process
many virtual threads/tasks
normal breakpoints in Disasmer Rust task code
normal thread list
stacks
selected locals / task args / handle inspection
stdout/stderr per task
artifacts and VFS state in optional views
restart selected task
```
The MVP should provide a Disasmer-owned DAP adapter. VS Code is the first supported client, but the adapter should speak ordinary DAP so other IDEs or tools can integrate later without Disasmer providing those integrations. LLDB/lldb-dap can be used internally later for richer DWARF support, but the virtual-process illusion should remain owned by Disasmer.
### 2.10 Transport
Use Quinn for MVP QUIC transport, behind a transport abstraction:
```text
node-to-node data/control links: QUIC over Quinn
node-to-coordinator persistent session: QUIC preferred, HTTPS/WebSocket allowed for auth/bootstrap
bulk transfer: node-to-node only, no coordinator bulk relay
```
Quinn is the MVP default because it is Rust-native and async-friendly, which keeps packaging and dogfooding simpler than an external C runtime dependency. Keep the `Transport` trait strict enough that quiche, TQUIC, or another implementation can replace it later if benchmarks, platform packaging, or protocol features justify a change.
The coordinator can help with rendezvous and endpoint candidate exchange. It should not relay bulk traffic. MVP can avoid cross-node bulk transfer if direct connectivity fails by scheduling tasks where the data already is or requiring explicit export to a receiver node or user-configured storage integration.
### 2.11 Storage
Use an abstract coordinator store with a strict split between persistent product state and ephemeral virtual-process state.
Persistent state should use Postgres in the hosted deployment and should be limited to things that must survive coordinator restart:
```text
users / tenants / memberships where applicable
projects
node identities and enrollment records
node public keys / credentials / revocation state
bundle registry and environment metadata worth reusing
source-provider configuration metadata
hosted/community policy configuration
billing/plan flags later
longer-lived audit events where needed
```
Ephemeral runtime state should live in coordinator memory for the MVP:
```text
active virtual processes
virtual threads and task placement
Debug Epochs
pending spawn queues
scheduler decisions
live node sessions and heartbeats
recent logs/events
VFS manifest cache for active processes
artifact metadata for active processes
quota counters for active processes, except hosted/community abuse ledgers that the private service wants to persist
```
If the coordinator restarts during the MVP, active virtual processes may be lost and should restart cleanly. That is acceptable. Do not overbuild durable process recovery before the runtime/debugging thesis is proven.
The store abstraction still matters because later the coordinator may need durable process recovery, Raft, sharding, or a different store. The MVP should not spend engineering effort on distributed consensus before the product is proven.
### 2.12 Auth
Support two auth modes from the start:
```text
hosted/community service auth: Authentik through OAuth2/OIDC, implemented in a private hosted module
CLI human auth: browser callback against the hosted service
CLI/agent auth: public-key identity with explicit enrollment/authorization
node attach: short-lived enrollment token -> long-lived node key/certificate
service-to-service: scoped service tokens where needed
open-source/private coordinator auth: simple local/team trust model with pluggable auth hooks, not the hosted OIDC stack by default
```
User OAuth tokens must not be passed to nodes. Nodes authenticate with node identity, and tasks receive only explicit, scoped, short-lived task tokens or secrets.
The CLI should be comfortable for both humans and agents. Browser login is fine for a developer at a workstation; agents should be able to use a generated keypair, an enrollment challenge, and a scoped project/node credential without opening a browser.
### 2.13 Community hosted abuse stance
The community hosted tier should prove the platform, not subsidize builds. The word “community” is intentional: it should be useful for serious users, while enterprise/team use cases should naturally need more than one active process, longer retention, stronger identity, or managed capacity.
Community hosted mode supports:
```text
sign in
create project (may be implicitly created, max 1)
create virtual process (may be implicitly created, max 1)
open coordinator UI
attach own node (max 4 nodes)
spawn tasks onto own node (max 8 tasks in flight)
debug from VS Code
see logs/artifact metadata for active processes (logs can't be bigger than 16KiB)
```
Community hosted mode does not support:
```text
hosted containers
hosted native commands
default hosted artifact storage
public artifact hosting
network access from hosted Wasm
long-running compute
relay bandwidth
```
### 2.14 Operator panel
The operator panel is second milestone, but the MVP should design toward it:
- runtime-state structures for panel state and events;
- protocol namespace for typed UI updates;
- control-plane UI slot for a panel;
- built-in widgets only;
- no custom HTML/JS;
- read-only behavior when the virtual process is stopped in a debugger.
- The panel has very tight size/state constraints for community mode
For public launch, a fixed coordinator dashboard is enough if it clearly hints at the immediate-mode panel API.
---
## 3. MVP architecture
### 3.1 High-level architecture
```text
VS Code
├─ Disasmer VS Code extension
└─ Disasmer DAP adapter
Hosted or local coordinator
├─ Auth / tenants / projects
├─ node registry and rendezvous
├─ virtual process registry
├─ scheduler
├─ debug epoch coordinator
├─ VFS/artifact metadata for active processes
├─ hosted/community policy hooks
├─ operator panel state/events scaffold
├─ in-memory active runtime state
└─ Postgres persistent project/identity store
├────────────── QUIC / control session ───────────────┐
▼ ▼
Linux node runtime Windows node runtime
├─ Wasmtime task runner ├─ Wasmtime task runner
├─ command runner ├─ command runner
├─ Podman environment backend ├─ Windows command backend
├─ Nix plugin backend ├─ Windows sandbox stub backend
├─ VFS overlay store ├─ VFS overlay store
├─ ephemeral artifact/blob cache ├─ ephemeral artifact/blob cache
├─ debug agent ├─ debug agent
├─ node cache ├─ node cache
└─ Quinn peer transport └─ Quinn peer transport
```
### 3.2 Control plane vs data plane
The coordinator owns:
```text
identity
membership
node capabilities
bundle metadata
virtual process metadata
task placement decisions
debug epochs
VFS manifest metadata for active processes
artifact metadata for active processes
quota accounting
operator panel metadata
rendezvous
```
All of these need constraints so that the community service can't be abused.
The nodes own:
```text
Wasm execution
native command execution
environment materialization
source checkout access
build caches
VFS bytes
artifact bytes
large blob transfer
debug stop/resume implementation
```
User-attached nodes are the users' responsibility and can run broad local capabilities. The hosted coordinator must still validate what capabilities are accepted for a hosted/community process, but the open-source/private coordinator should not block normal local-node capabilities by default.
The coordinator must not become:
```text
a network filesystem
a blob proxy
a remote compiler filesystem
a hidden build runner
a relay by default
```
### 3.3 Recommended repository layout
```text
disasmer/
crates/
disasmer-sdk/ # guest-facing Rust API
disasmer-macros/ # #[disasmer::main], #[disasmer::task], env!()
disasmer-abi/ # guest/host ABI structs and serialization
disasmer-cli/ # user CLI
disasmer-coordinator/ # hosted/local coordinator service
disasmer-node/ # node runtime daemon
disasmer-dap/ # custom Debug Adapter Protocol server
disasmer-vscode-protocol/ # extension <-> CLI/DAP shared types
disasmer-transport/ # Quinn-backed QUIC wrapper and protocol framing
disasmer-store/ # store traits + Postgres persistent-store impl
disasmer-runtime-state/ # in-memory active process/thread/debug state
disasmer-vfs/ # VFS manifests, overlays, blob/chunk references
disasmer-env/ # env discovery, normalization, caching
disasmer-source/ # source-provider traits and manifests
disasmer-git/ # default optional Git source provider module
disasmer-debug-model/ # breakpoints, epochs, stack/locals schema
disasmer-ui-model/ # immediate-mode UI schema scaffold
disasmer-auth/ # node enrollment, pubkey identities, pluggable auth hooks
disasmer-protocol/ # coordinator/node/DAP wire messages
private/
hosted-oidc/ # Authentik/OIDC integration for the public service
hosted-policy/ # community quotas, abuse controls, zero-cap hosted-loop rules
hosted-admin/ # service admin tools, kill switches, internal dashboards
vscode/
extension/ # TypeScript extension
examples/
launch-build/ # flagship Linux + Windows build demo
local-one-node/ # minimal spawn/join example
operator-panel-preview/ # second-milestone preview
docs/
MVP.md
architecture.md
protocol.md
security.md
debugging.md
```
The private folders are normal repository folders during development, but must be easy to exclude from the public source release. The public crates should expose policy/auth interfaces; the private modules implement hosted-service-specific OIDC, community quota enforcement, hosted zero-capability Wasm limits, abuse tooling, and admin affordances.
### 3.4 Process model
MVP process types:
```text
disasmer coordinator
Hosted or local control-plane service.
disasmer node
Long-running node runtime attached to a coordinator.
disasmer dap
Debug Adapter Protocol process launched by VS Code.
disasmer cli
User/agent CLI for login, init, build, run, node attach, artifact export.
```
### 3.5 Primary user commands
```bash
# Authenticate a human against the hosted coordinator.
disasmer login https://disasmer.example.com
# Create or enroll a non-browser CLI identity for an agent.
disasmer auth keygen --name forgejo-worker
disasmer auth enroll-key --coordinator https://disasmer.example.com --project my-project --public-key ~/.config/disasmer/keys/forgejo-worker.pub
# Initialize project metadata; optional because defaults should work.
disasmer init
# Show discovered environments, source providers, and snapshot policy.
disasmer inspect
# Build the virtual binary/bundle.
disasmer build
# Attach a node. OS, architecture, container backend, Git availability, and most
# capabilities are auto-detected; --cap remains available for explicit overrides.
disasmer node attach --coordinator https://disasmer.example.com --token dis_node_enroll_...
# Override or add an auto-detected capability only when needed.
disasmer node attach --coordinator https://disasmer.example.com --token dis_node_enroll_... --cap env.windows-command-dev=true
# Run without VS Code. Hosted coordinator is the default when logged in;
# project defaults to the current directory. Entry point is optional.
disasmer run
disasmer run build-linux
disasmer run --project ../other-repo package-release
disasmer run --local smoke-test
# Export final artifacts by asking an attached/local receiver node to pull them.
# There is no default coordinator artifact store.
disasmer artifact export --process <id> --to ./dist
```
`disasmer run` should accept an optional entry point. This is a good idea because one Disasmer project may contain several useful virtual-process entrypoints: `build`, `test`, `package`, `release`, `watch`, `operator`, or project-specific workflows. The default remains the `#[disasmer::main]` entrypoint when no entry is supplied.
VS Code should hide most of this behind:
```text
Disasmer: Login
Disasmer: Attach Local Node
Disasmer: Build Bundle
Disasmer: Run
Disasmer: Debug
Disasmer: Restart Failed Task
Disasmer: Export Artifacts
```
---
## 4. The flagship launch example
### 4.1 Project layout
```text
launch-build-demo/
Cargo.toml
Cargo.lock
src/
main.rs
envs/
linux/
Containerfile
windows/
Containerfile
inputs/
build-config.json
disasmer.toml # optional; only used for launch polish/overrides
```
### 4.2 Example `disasmer.toml`
The file should be optional, but the demo can include it to make platform constraints obvious.
```toml
[project]
name = "launch-build-demo"
[envs.linux]
path = "envs/linux/Containerfile"
kind = "container"
requires = { os = "linux", arch = "x86_64" }
[envs.windows]
path = "envs/windows/Containerfile"
kind = "container"
requires = { os = "windows", arch = "x86_64" }
backend_hint = "windows-command-dev"
[vfs]
include = [
"src/**",
"Cargo.toml",
"Cargo.lock",
"inputs/**"
]
exclude = [
"target/**",
".git/**"
]
[git]
enabled = true
dirty_overlay = true
submodules = true
```
### 4.3 Example Rust build program
```rust
use disasmer::prelude::*;
#[derive(Serialize, Deserialize, Clone, Debug, DisasmerArg)]
enum Target {
LinuxX64,
WindowsX64,
}
impl Target {
fn env(&self) -> Env {
match self {
Target::LinuxX64 => env!("linux"),
Target::WindowsX64 => env!("windows"),
}
}
fn name(&self) -> &'static str {
match self {
Target::LinuxX64 => "linux-x64",
Target::WindowsX64 => "windows-x64",
}
}
fn command(&self) -> CommandSpec {
match self {
Target::LinuxX64 => cmd!("cargo", "build", "--release"),
Target::WindowsX64 => cmd!("cargo", "build", "--release"),
}
}
}
#[disasmer::main]
async fn main() -> Result<()> {
// The hosted coordinator control loop is intentionally capless. Source access
// happens on a user-attached node through the default Git source-provider
// module, not inside the coordinator process.
let src = spawn::task(prepare_source)
.placement(placement::any_node().with_cap("source.git"))
.queue_until_node_available()
.start()
.await?
.join()
.await?;
let completed = DAtomicU32::new(0).await?;
let linux = spawn::task(compile)
.arg(BuildArgs::new(Target::LinuxX64, src.clone(), completed.clone()))
.env(env!("linux"))
.start()
.await?;
let windows = spawn::task(compile)
.arg(BuildArgs::new(Target::WindowsX64, src.clone(), completed.clone()))
.env(env!("windows"))
.start()
.await?;
let linux_artifact = linux.join().await?;
let windows_artifact = windows.join().await?;
let dist = package(vec![linux_artifact, windows_artifact]).await?;
assert_eq!(completed.load(Ordering::SeqCst).await?, 2);
// Export is explicit code. The coordinator does not provide a default
// artifact store; this asks an attached receiver/local node to pull bytes.
artifact::export_to_node(dist, receiver::local("dist")).await?;
Ok(())
}
#[disasmer::task]
async fn prepare_source() -> Result<SourceSnapshot> {
source::git::current_repo().snapshot().await
}
#[derive(Serialize, Deserialize, Clone, Debug, DisasmerArg)]
struct BuildArgs {
target: Target,
src: SourceSnapshot,
completed: DAtomicU32,
}
impl BuildArgs {
fn new(target: Target, src: SourceSnapshot, completed: DAtomicU32) -> Self {
Self { target, src, completed }
}
}
#[disasmer::task]
async fn compile(args: BuildArgs) -> Result<Artifact> {
fs::mount("/src", args.src).await?;
args.target.command()
.cwd("/src")
.env("DISASMER_TARGET", args.target.name())
.run()
.await?;
let output_path = match args.target {
Target::LinuxX64 => "/src/target/release/app",
Target::WindowsX64 => "/src/target/release/app.exe",
};
let artifact_path = format!("/vfs/artifacts/{}/{}", args.target.name(), basename(output_path));
fs::copy(output_path, &artifact_path).await?;
fs::flush().await?;
args.completed.fetch_add(1, Ordering::SeqCst).await?;
Artifact::from_path(artifact_path).await
}
#[disasmer::task]
async fn package(outputs: Vec<Artifact>) -> Result<Artifact> {
fs::create_dir_all("/vfs/artifacts/dist").await?;
for output in outputs {
fs::copy(output.path(), "/vfs/artifacts/dist/").await?;
}
fs::flush().await?;
Artifact::from_path("/vfs/artifacts/dist").await
}
```
### 4.4 What the demo proves
The demo proves:
- The build graph is source code, not YAML.
- Environment resources live in the repo.
- Source code references `env!("linux")` and `env!("windows")`, not runner labels.
- The coordinator schedules by logical environment and node capability.
- Linux and Windows tasks can run in the same virtual process.
- `cmd!(...)` runs on nodes, not on the hosted coordinator.
- Outputs become artifact handles through the VFS.
- `flush()` publishes artifacts for downstream tasks.
- Final artifact export is explicit code and does not imply coordinator artifact storage.
- VS Code sees one virtual process and multiple virtual threads.
- A Debug Epoch stops the whole virtual process.
- Restarting a failed task after editing source is part of the normal loop.
---
## 5. Rust SDK and task API
### 5.1 MVP API surface
The MVP Rust SDK should include:
```rust
#[disasmer::main]
#[disasmer::task]
env!("name")
spawn::task(task_fn)
TaskHandle<T>
Artifact
Blob
SourceSnapshot
VirtualFile
DAtomicU32
DAtomicU64
DChannel<T>
fs::mount()
fs::copy()
fs::flush()
fs::sync() / artifact::export_to_node()
cmd!(...)
source::git::current_repo().snapshot()
ui::* scaffold only
```
Do not include every distributed data structure in the first API. The launch demo needs small data, atomics/counters, task handles, source snapshots, artifacts, and VFS operations more than it needs a complete distributed memory library.
### 5.2 Task registration
`#[disasmer::task]` should generate:
- a stable task symbol;
- a task ABI descriptor;
- argument/result serialization metadata;
- debug metadata for task entry, source spans, and selected local/argument values;
- capability declarations inferred from APIs used where possible;
- task restart compatibility hash;
- a registration entry in the bundle manifest.
Example generated manifest entry:
```json
{
"task_id": "compile@sha256:...",
"symbol": "compile",
"source": "src/main.rs",
"arg_type": "BuildArgs@sha256:...",
"result_type": "Artifact@sha256:...",
"requires": ["vfs", "cmd", "artifact.publish"],
"debug": {
"spans": "debug/compile.spans.json",
"locals_schema": "debug/compile.locals.json"
}
}
```
### 5.3 Argument rules
Task arguments and results must implement `DisasmerArg`.
Allowed:
```text
u32, u64, bool, String
small structs/enums
SourceSnapshot
Artifact
Blob
Env
DAtomicU32 / DAtomicU64
DChannel<T>
VirtualFile
small Vec<T> with configured size limit
```
Rejected:
```text
&T / &mut T
raw pointers
Rc<T>
RefCell<T>
std::fs::File
TcpStream
MutexGuard
native thread/process handles
unbounded Vec<u8>
closures with implicit captures
```
The compiler error should explain the distributed boundary. For example:
```text
BuildArgs cannot be passed to a Disasmer virtual thread because it contains `&Path`.
Use String, SourceSnapshot, VirtualFile, Blob, or Artifact instead.
```
### 5.4 Size limits
Set a default task argument size limit such as:
```text
soft warning: 256 KiB
hard default: 4 MiB
project override: allowed but noisy
```
The hosted/community coordinator control loop should use much tighter limits, for example a 4 KiB warning and 16 KiB hard limit. User-attached nodes can keep the normal node defaults unless project policy overrides them.
A user accidentally passing an 82 MB `Vec<u8>` should see:
```text
Task argument is 82 MB and would be copied to the spawned virtual thread.
Use Blob::from_file, SourceSnapshot, Artifact, or VirtualFile instead.
```
### 5.5 Host imports
The MVP guest ABI should be explicit and small.
Core imports:
```text
disasmer.spawn.start
disasmer.spawn.join
disasmer.task.yield_safepoint
disasmer.task.current
disasmer.fs.mount
disasmer.fs.copy
disasmer.fs.flush
disasmer.fs.sync
disasmer.artifact.from_path
disasmer.artifact.open
disasmer.blob.open
disasmer.cmd.start
disasmer.cmd.poll
disasmer.cmd.kill
disasmer.atomic.new_u32
disasmer.atomic.fetch_add_u32
disasmer.atomic.load_u32
disasmer.debug.probe
disasmer.log.write
disasmer.ui.emit_snapshot # scaffold only
```
### 5.6 Native command API
`cmd!(...)` should build a `CommandSpec` rather than immediately running a process.
```rust
cmd!("cargo", "build", "--release")
.cwd("/src")
.env("RUSTFLAGS", "...")
.timeout(Duration::from_secs(600))
.run()
.await?;
```
The host checks:
- current virtual thread has `cmd.run` capability;
- selected environment supports native commands;
- the task is running on a node, not the hosted coordinator;
- process limits are configured;
- filesystem mounts are inside the VFS/environment policy;
- network policy is explicit;
- logs are capped and streamed with backpressure.
The hosted coordinator control plane never has this capability. An open-source/private coordinator may run with an embedded local node for trusted deployments; that node can have normal local command capabilities by default. Keep this as a compact policy/runtime module boundary rather than a separate node implementation, so dogfooding still uses the same node runtime code.
### 5.7 Compile-time environment references
`env!("linux")` should validate against generated environment metadata.
Implementation:
1. `disasmer build` scans `envs/**` and optional `disasmer.toml`.
2. It writes generated metadata to `target/disasmer/generated/envs.rs` and a JSON manifest.
3. The macro consults generated metadata during build.
4. VS Code reads the same metadata for autocomplete and diagnostics.
Missing env error:
```text
env!("windows") does not exist.
Discovered environments: linux.
Create envs/windows/Containerfile or add [envs.windows] to disasmer.toml.
```
---
## 6. Bundle and build pipeline
### 6.1 Bundle concept
A Disasmer bundle is a content-addressed virtual binary.
It includes:
```text
Wasm module(s)
Disasmer manifest
registered tasks
debug metadata
environment definitions
selected environment context files
source snapshot metadata
selected VFS seed files
capability declarations
content hashes
```
It should not embed full container images.
### 6.2 Bundle layout
```text
app.dis/
manifest/disasmer.json
modules/main.wasm
debug/main.dwarf
debug/disasmer-debug.json
envs/linux/Containerfile
envs/windows/Containerfile
envs/manifest.json
source/source-snapshot.json
source/dirty-overlay-manifest.json
inputs/build-config.json
vfs/seed-manifest.json
capabilities.json
```
The on-disk `.dis` can initially be a directory during development and later become a zip/tar-like archive with content-addressed chunks.
### 6.3 Build command pipeline
```text
disasmer build
1. discover project root
2. load disasmer.toml if present
3. discover envs/**/Containerfile and envs/**/Dockerfile
4. discover Nix flakes if plugin enabled
5. resolve source snapshot through enabled source-provider module, Git by default
6. compute dirty overlay manifest
7. generate env metadata for env!()
8. compile Rust guest to Wasm
9. run macro/task registration collection
10. collect debug metadata
11. build VFS seed manifest
12. compute bundle digest
13. write app.dis
```
### 6.4 Digest strategy
Bundle digest should include:
```text
Wasm bytes
task ABI descriptors
environment recipe files
environment context file hashes
source snapshot provider identity and revision, such as a Git commit id
dirty overlay file hashes
selected input file hashes
capability declarations
debug metadata hash
SDK ABI version
```
It should not include:
```text
node-local cache paths
absolute host checkout path
local build timestamps
unselected .git internals
unselected target directory contents
```
### 6.5 Compatibility hashes
Task restart requires compatibility checks. Generate hashes for:
```text
task public signature
argument schema
result schema
capability declaration
environment reference set
VFS mount contract
Disasmer ABI version
```
If only implementation changed and public schema is compatible, allow selected task restart. If schema changed, restart the whole virtual process.
---
## 7. Node runtime
### 7.1 Node responsibilities
The node runtime is where real work happens.
It must support:
```text
registration with coordinator
capability reporting
heartbeat and health reporting
bundle fetch or local bundle lookup
Wasmtime task instantiation
native command execution
environment materialization/caching
VFS overlay storage
ephemeral artifact/blob cache
source snapshot providers
debug epoch handling
logs/events streaming
quota enforcement
Quinn peer links
```
### 7.2 Node state directories
Linux default:
```text
~/.local/share/disasmer/node/
config.toml
identity/
bundles/
env-cache/
vfs/
blobs/
artifacts/
logs/
tmp/
```
Windows default:
```text
%LOCALAPPDATA%\Disasmer\node\
config.toml
identity\
bundles\
env-cache\
vfs\
blobs\
artifacts\
logs\
tmp\
```
### 7.3 Node capability model
Node capabilities should be explicit and signed by node identity plus coordinator policy.
Example:
```json
{
"node_id": "node_123",
"tenant_id": "tenant_abc",
"os": "linux",
"arch": "x86_64",
"capabilities": {
"wasm": true,
"cmd": true,
"container.podman.rootless": true,
"nix": true,
"git.source_provider": true,
"debug.suspend_process_tree": true
},
"limits": {
"max_tasks": 4,
"max_log_bytes_per_task": 10485760,
"max_arg_bytes": 4194304
}
}
```
Windows dev backend example:
```json
{
"node_id": "node_win_123",
"tenant_id": "tenant_abc",
"os": "windows",
"arch": "x86_64",
"capabilities": {
"wasm": true,
"cmd": true,
"windows.command_dev": true,
"windows.sandbox_stub": true,
"debug.job_object_suspend": true
},
"security_note": "not a managed-untrusted sandbox backend"
}
```
### 7.4 Wasmtime task runner
Each running virtual thread/task maps to:
```text
Wasmtime Engine
compiled module cache
Store<ThreadHostState>
Linker with Disasmer imports
task entrypoint dispatcher
fuel/epoch/resource limits
debug probe table
VFS overlay handle
handle table
```
`ThreadHostState` should contain:
```rust
struct ThreadHostState {
tenant_id: TenantId,
project_id: ProjectId,
virtual_process_id: VirtualProcessId,
virtual_thread_id: VirtualThreadId,
task_id: TaskId,
bundle_digest: Digest,
debug_epoch: Option<DebugEpochId>,
vfs_overlay: VfsOverlayHandle,
handles: HandleTable,
limits: ResourceLimits,
coordinator: CoordinatorClient,
node_store: NodeStore,
command_runner: Arc<dyn CommandRunner>,
}
```
### 7.5 Command runner
The command runner must abstract platform differences.
```rust
trait CommandRunner {
async fn start(&self, spec: CommandSpec, ctx: CommandContext) -> Result<CommandHandle>;
async fn poll(&self, handle: CommandHandle) -> Result<CommandStatus>;
async fn suspend_for_debug(&self, handle: CommandHandle, epoch: DebugEpochId) -> Result<()>;
async fn resume_from_debug(&self, handle: CommandHandle, epoch: DebugEpochId) -> Result<()>;
async fn terminate(&self, handle: CommandHandle) -> Result<()>;
}
```
Linux implementations:
```text
PodmanRootlessCommandRunner
LocalProcessGroupCommandRunner for development
```
Windows implementations:
```text
WindowsCommandDevRunner
WindowsSandboxRunnerStub
```
Do not offer `WindowsCommandDevRunner` as secure managed compute for untrusted public workloads.
### 7.6 Linux Podman backend
Linux MVP should use rootless Podman.
Responsibilities:
```text
build image from Containerfile/Dockerfile
cache image by environment digest
mount /src, /work, /cache, /out or VFS paths
run command with resource limits where practical
capture stdout/stderr
pause/unpause or suspend process tree for debug epoch
publish outputs into VFS overlay
```
Default mounts:
```text
/src source snapshot or bind mount
/work task-local writable overlay
/cache node-local dependency cache keyed by env/project
/out artifact staging path
```
### 7.7 Windows backend
For the public-launch MVP, Windows should support an attached node that can run build commands in a controlled workspace.
Minimum Windows backend:
```text
create per-task temp workspace
materialize source snapshot into workspace or bind local checkout when safe
set environment variables from normalized env
run command as non-admin service user where possible
capture stdout/stderr
track process tree with Job Object-like lifecycle management
stage artifacts into VFS overlay
support best-effort debug suspend/resume
```
Windows sandboxing should be behind an interface but not claimed as production isolation until implemented and tested.
Potential future backend options:
```text
Windows Sandbox CLI backend
AppContainer / Less-Privileged AppContainer backend
Hyper-V isolated runner backend
Windows container backend, if project needs it
```
### 7.8 Logs
Logs are both a UX feature and an abuse vector.
Node should:
- stream recent log chunks to coordinator;
- keep larger logs node-local unless `sync()` or retention policy requires upload;
- redact configured secrets before sending;
- enforce per-task and per-process log byte caps;
- include task id, virtual thread id, sequence number, timestamp, and stream name.
Log event:
```json
{
"tenant_id": "tenant_abc",
"process_id": "vp_123",
"thread_id": "vt_456",
"task_id": "task_789",
"stream": "stderr",
"seq": 42,
"bytes": "...",
"truncated": false
}
```
---
## 8. Coordinator
### 8.1 Coordinator responsibilities
The coordinator should provide:
```text
auth/session handling; hosted OIDC through private module
project registry
node enrollment and identity
node heartbeats
node capability registry
virtual process lifecycle
task placement
bundle registry
debug session registry
Debug Epoch coordination
VFS manifest metadata for active processes
artifact metadata for active processes
hosted/community policy hooks and optional private quota ledger
operator panel state/events scaffold
rendezvous for node-to-node QUIC
web dashboard
CLI API
VS Code/DAP API
```
It should not run arbitrary user commands.
### 8.2 Coordinator state split
The coordinator needs two state layers in the MVP.
Persistent Postgres state:
```text
users / tenants / memberships if hosted module is enabled
projects
node enrollment records
node public keys / credentials / revocations
project-scoped source-provider configuration
bundle registry entries worth reusing
persistent environment metadata/cache hints
hosted/community plan or policy records in the private module
longer-lived audit records where needed
```
In-memory runtime state:
```text
active virtual processes
virtual threads
pending task queues
live node sessions and heartbeats
scheduler placements
Debug Epochs
recent logs/events
VFS manifest metadata for active processes
artifact metadata and locations for active processes
operator panel snapshots/events for active processes
```
For the MVP, if the coordinator restarts, active virtual processes restart or fail cleanly. Do not persist every process/thread/log/VFS row merely to survive coordinator restarts. Persistence is for product identity and project continuity, not durable runtime recovery.
### 8.3 Store interfaces and indicative persistent tables
Define a persistent store trait for things that must survive restart:
```rust
trait CoordinatorPersistentStore {
async fn create_tenant(...);
async fn create_project(...);
async fn register_node_identity(...);
async fn revoke_node_credential(...);
async fn create_bundle(...);
async fn save_environment_metadata(...);
async fn save_source_provider_config(...);
async fn authorize_persistent_object(...);
}
```
Define a separate runtime-state trait that is in-memory for the MVP:
```rust
trait CoordinatorRuntimeState {
async fn create_virtual_process(...);
async fn create_virtual_thread(...);
async fn update_thread_state(...);
async fn create_debug_epoch(...);
async fn publish_vfs_manifest(...);
async fn publish_artifact_metadata(...);
async fn append_recent_log_event(...);
async fn reserve_runtime_quota(...);
}
```
Indicative persistent Postgres tables:
```sql
users(
id,
authentik_subject nullable,
email nullable,
created_at,
suspended_at
)
tenants(
id,
slug,
plan,
created_at
)
tenant_members(
tenant_id,
user_id,
role
)
projects(
id,
tenant_id,
name,
visibility,
created_at
)
nodes(
id,
tenant_id,
project_id nullable,
display_name,
os,
arch,
trust_label,
created_at,
revoked_at
)
node_capabilities_last_seen(
node_id,
capability,
value,
reported_at
)
node_credentials(
node_id,
public_key,
cert_fingerprint nullable,
expires_at nullable,
revoked_at
)
bundles(
id,
tenant_id,
project_id,
digest,
manifest_json,
created_at
)
environments(
id,
tenant_id,
project_id,
bundle_id nullable,
name,
kind,
digest,
requirements_json,
recipe_manifest_json,
created_at
)
source_provider_configs(
id,
tenant_id,
project_id,
provider,
config_json,
created_at
)
audit_events(
id,
tenant_id,
project_id nullable,
actor_id nullable,
event_kind,
object_id nullable,
event_json,
created_at
)
```
Runtime-only structures may look like the old tables conceptually, but they should be ordinary in-memory maps/queues first: `virtual_processes`, `virtual_threads`, `debug_epochs`, active `vfs_manifests`, artifact locations, recent logs, and operator panel snapshots.
### 8.4 Tenant safety invariant
Every row that belongs to user content should include `tenant_id`. Every API should authorize by tenant and object relationship.
Required tests:
```text
user A cannot list user B nodes
user A cannot attach to user B process
user A cannot read user B logs
user A cannot read user B artifact metadata for active processes
user A cannot request debug memory from user B process
node A cannot claim tenant B process
node A cannot publish artifact for tenant B
```
### 8.5 Node enrollment
Flow:
```text
1. User opens coordinator UI or CLI.
2. User creates a node enrollment token for a project or tenant.
3. Token has short TTL and may include allowed capability constraints; node OS, architecture, and most capabilities are auto-detected at attach time.
4. Node CLI starts with token.
5. Node generates keypair.
6. Node calls enrollment endpoint.
7. Coordinator validates token and policy.
8. Coordinator creates node identity/certificate.
9. Node stores credential locally.
10. Node opens persistent session to coordinator.
```
Enrollment token example:
```json
{
"tenant_id": "tenant_abc",
"project_id": "proj_123",
"expires_at": "...",
"allowed_capabilities": [
"wasm",
"cmd",
"container.podman.rootless"
],
"auto_detect": ["os", "arch", "env", "source_providers"],
"overrides_allowed": true
}
```
### 8.6 Scheduler
The MVP scheduler should be simple, deterministic, and locality-aware.
It should also be an easily replacible module in case the user wants different scheduling, or perhaps they have limitless resources (e.g. cloud backed)
Inputs:
```text
task environment requirements
node capabilities
node health
source snapshot locations
environment cache locations
artifact locations
node current load
tenant/project policy
quota availability
debug affinity / restart affinity
```
Scoring:
```text
score(node) =
+ environment compatible
+ source snapshot already local
+ environment digest cached
+ dependency cache likely warm
+ needed artifacts local
+ same node as previous failed attempt when restart is desired
- current load
- expected bytes to transfer
- recent failures
- policy warnings
```
Placement should prefer the local node if it can run the task and already has the repo.
### 8.7 State machine
Virtual process states:
```text
CREATED
STARTING
RUNNING
FREEZE_REQUESTED
FROZEN
RESUMING
COMPLETED
FAILED
CANCELLED
```
Virtual thread states:
```text
PENDING
PLACED
STARTING
RUNNING
WAITING
COMMAND_RUNNING
FREEZE_REQUESTED
FROZEN
COMPLETED
FAILED
CANCELLED
RESTARTING
```
Debug epoch states:
```text
REQUESTED
FREEZING
FROZEN
RESUMING
RESUMED
FAILED
```
---
## 9. Transport and networking
### 9.1 Transport choices
Use Quinn for QUIC in the MVP, behind a Rust abstraction.
```rust
trait Transport {
async fn connect_node(&self, endpoint: Endpoint, identity: NodeIdentity) -> Result<Connection>;
async fn accept_node(&self) -> Result<Connection>;
async fn open_stream(&self, kind: StreamKind) -> Result<Stream>;
async fn datagram(&self, msg: Datagram) -> Result<()>;
}
```
The abstraction is important because Rust bindings and platform packaging may be rough. Keep QUIC-specific details in `disasmer-transport`.
### 9.2 Coordinator sessions
The coordinator session carries:
```text
node heartbeat
capability updates
task start/stop messages
debug epoch messages
VFS manifest publication
artifact metadata for active processes publication
log event stream
rendezvous endpoint exchange
quota updates
```
For development, it is acceptable to bootstrap auth over HTTPS and then use QUIC for persistent sessions.
### 9.3 Node-to-node sessions
Node-to-node sessions carry:
```text
blob chunk requests
artifact reads
source snapshot delta transfer
VFS chunk transfer
debug lazy memory/page reads where appropriate
```
They should not carry unauthenticated traffic. Every stream must include:
```text
tenant id
project id
process id when applicable
authorized object id
fencing token / epoch where applicable
request id
quota accounting key
```
### 9.4 Rendezvous without relay
Coordinator-aided connection:
```text
1. Node A needs artifact from Node B.
2. A asks coordinator for artifact locations.
3. Coordinator authorizes A and returns candidate endpoints for B.
4. Coordinator notifies B to expect A.
5. A and B attempt direct QUIC connection.
6. If connection succeeds, data moves node-to-node.
7. If connection fails, scheduler avoids this transfer or asks user to sync/export through configured durable storage.
```
No silent bulk relay through the coordinator.
### 9.5 Failure handling
If direct node-to-node connection fails:
- do not fall back to coordinator bulk transfer by default;
- mark the object location as unreachable from the requesting node;
- choose a different placement if possible;
- if impossible, fail with a clear action:
```text
Artifact is on node windows-1, but linux-1 cannot establish direct QUIC.
Run an artifact receiver node, configure an explicit external storage/export task, or run the dependent task on windows-1.
```
---
## 10. Environment system
### 10.1 Discovery
Scan default paths:
```text
envs/<name>/Containerfile
envs/<name>/Dockerfile
envs/<name>/flake.nix
envs/<name>/flake.lock
```
Normalize to:
```json
{
"name": "linux",
"kind": "container",
"digest": "sha256:...",
"recipe": "envs/linux/Containerfile",
"context": "envs/linux",
"requires": {
"os": "linux",
"arch": "x86_64"
},
"capabilities": {
"cmd": true,
"network": false
}
}
```
### 10.2 Containerfile MVP
Containerfile is the MVP path.
Linux:
```text
Containerfile -> Podman build -> local image id -> run task command
```
Windows:
```text
Containerfile discovered and hashed
Windows backend may treat it as a declared environment contract/stub
actual command execution uses WindowsCommandDevRunner for attached user nodes
```
This is intentionally not marketed as secure Windows container isolation.
### 10.3 Nix plugin
Nix should be present as a plugin because the architecture already expects pluggable environment resources, and Nix is useful for development reproducibility.
MVP Nix support can be:
```text
discovery of flake.nix and flake.lock
hashing into environment digest
optional local materialization on nodes with Nix installed
cache location reporting
not required for the flagship launch demo
```
### 10.4 Environment materialization cache
Cache key:
```text
env digest + backend kind + platform + Disasmer env ABI version
```
Cache record:
```json
{
"env_digest": "sha256:...",
"node_id": "node_123",
"backend": "podman-rootless",
"materialized_ref": "localhost/disasmer/env@sha256:...",
"status": "ready",
"last_used_at": "..."
}
```
### 10.5 Environment security policy
Default task environment policy:
```text
network disabled unless explicitly granted
host filesystem inaccessible except declared VFS mounts
secrets absent unless explicitly granted
no privileged containers
no Docker/Podman socket mount
read-only source mount when possible
write access only to /work and artifact staging
logs capped
process lifetime capped by policy
```
For user-attached nodes, Disasmer should still enforce its policy, but documentation must be honest that the node owner controls the machine and can bypass local restrictions.
---
## 11. VFS, artifacts, and latency
### 11.1 Core rule
```text
Move metadata eagerly.
Move bytes lazily.
Run work where the bytes already are.
```
This should be treated as an engineering invariant, not a performance optimization later.
### 11.2 Mount model
Default virtual filesystem:
```text
/src or /vfs/project
read-only source/input snapshot
/work or /vfs/work
task-local writable overlay
/vfs/artifacts
published artifact namespace
/vfs/shared
optional explicit shared namespace, probably not launch-critical
```
### 11.3 Source snapshots and source-provider modules
Source support should be pluggable. Git is the default included source-provider module, not a hard dependency of the VFS core.
A source-provider module must be able to produce a `SourceSnapshot` manifest such as:
```text
provider name and version
source identity
base revision or content root
sub-resource state if applicable
changed-file / dirty overlay manifest
included untracked or generated inputs
ignored-file policy
```
For Git specifically, the default module should model a source snapshot as:
```text
repo identity
commit hash
submodule state
dirty overlay manifest
included untracked files
ignored-file policy
```
Local node behavior:
```text
if task runs on node with checkout:
bind mount checkout read-only where safe
materialize dirty overlay if needed
else:
fetch missing content through the enabled source provider or by content hash
```
Do not tar and upload the entire repo by default. Do not require users who use another source system to carry Git dependencies in their runtime.
### 11.4 Dirty overlay
Dirty overlay manifest:
```json
{
"base_commit": "abc123",
"files": [
{
"path": "src/main.rs",
"mode": "100644",
"digest": "sha256:...",
"size": 12345
}
],
"deleted": ["old/file.rs"]
}
```
Dirty bytes move only if the chosen node does not already have them.
### 11.5 VFS overlay commits
Each task has a writable overlay. `flush()` creates a manifest delta:
```json
{
"process_id": "vp_123",
"thread_id": "vt_456",
"parent_epoch": 4,
"epoch": 5,
"created": [
{
"path": "/vfs/artifacts/linux-x64/app",
"digest": "sha256:...",
"size": 9912321,
"locations": ["node_linux_1"]
}
],
"modified": [],
"deleted": []
}
```
The coordinator stores the manifest and locations. It does not receive the bytes unless a configured durability/export policy requires it.
### 11.6 `flush()` semantics
`flush()` means:
```text
close current task write epoch
hash changed files/chunks
publish VFS manifest delta
publish artifact metadata for /vfs/artifacts changes
make changes visible to downstream tasks according to VFS consistency rules
return after metadata is accepted by coordinator
```
`flush()` should not:
```text
upload large artifacts by default
replicate to other nodes by default
export to local dist by default
route bytes through coordinator
```
### 11.7 `sync()` and explicit artifact movement semantics
`sync()` means:
```text
apply an explicit export/transfer/durability policy chosen by the program or CLI
verify referenced bytes still exist on at least one node or explicit store
move bytes directly between nodes or to a user-configured external destination
update artifact metadata for active processes with the result
```
`sync()` should not imply a default coordinator artifact store. By default, artifacts and other large data are nearly ephemeral: they live on nodes for as long as a task, downstream consumer, export, restart, or configured cache/GC policy needs them. They may linger long enough for practical task restart, but unsynced node-local artifacts are not a durability guarantee.
Example policies:
```rust
// Ask an attached/local receiver node to pull bytes into ./dist.
artifact::export_to_node(dist, receiver::local("dist")).await?;
// Send bytes to a user-provided storage integration implemented by project code.
artifact::copy_to(outputs, storage::s3("release-bucket")).await?;
// Keep artifacts available on the producing node long enough for likely restarts.
artifact::retain(outputs, RetainPolicy::BestEffort { min_ttl: Duration::from_hours(24) }).await?;
```
MVP should implement at least:
```text
metadata-only flush
node-local ephemeral artifact/blob cache
explicit export to an attached/local receiver node
best-effort retention/GC knobs
clear errors when bytes have been garbage-collected or are unreachable
```
Users who want artifacts on their own machine should either run a receiver node there or write/use an explicit storage/export integration. The coordinator should keep artifact metadata for active processes, not act as the default artifact data store.
### 11.8 Artifact lifecycle
Artifact states:
```text
PUBLISHED_METADATA
AVAILABLE_ON_NODE
RETAINED_BEST_EFFORT
EXPORTED
DURABLE_EXTERNAL
EXPIRED
DELETED
```
To allow producing artifacts without much effort from the user, the control pane could provide a download button through the immediate-mode UI, which will stream the (best-effort retained) artefact from the node that retains it. "Best effort" and usage tier limits apply as normal, but it should be an error to create a download link to an artifact that can't be downloaded within the limits in the first place. Download links should be secure and not just allow anyone that guesses the correct link to download any artifact.
Artifact metadata:
```json
{
"artifact_id": "art_123",
"path": "/vfs/artifacts/linux-x64/app",
"digest": "sha256:...",
"size": 9912321,
"producer_thread_id": "vt_linux",
"locations": [
{ "kind": "node", "node_id": "node_linux_1" }
],
"durability_state": "PUBLISHED_METADATA",
"retention": "best_effort_node_gc"
}
```
### 11.9 Local-first acceptance test
For a local Linux build from an existing Git checkout:
- no full-repo tarball is created;
- no source bytes are uploaded to coordinator;
- compiler file reads are local;
- build cache reads are local;
- artifact bytes remain local after `flush()`;
- coordinator receives only metadata, logs within caps, and debug/session events;
- explicit export/transfer code is the first operation allowed to move final artifact bytes for durability or user-visible export.
---
## 12. Task execution lifecycle
### 12.1 Spawn flow
```text
1. Guest calls spawn::task(compile).arg(...).env(env!("linux")).start().await.
2. Wasm guest invokes disasmer.spawn.start host import.
3. Node host validates current task capability.
4. Host serializes TaskSpec with task id, env id, args, handles, VFS base epoch.
5. Coordinator creates virtual_thread row in PENDING state.
6. Scheduler chooses node.
7. Coordinator sends StartTask to chosen node.
8. Node fetches or locates bundle.
9. Node materializes environment if needed.
10. Node creates VFS overlay.
11. Node instantiates Wasmtime module or command wrapper.
12. Node calls named task entrypoint.
13. Thread becomes visible in debugger and coordinator UI.
```
### 12.2 Join flow
```text
1. Parent awaits TaskHandle.join().
2. Parent task parks or yields.
3. Child completes with serialized result or error.
4. Result may contain handles such as Artifact.
5. Coordinator records completion.
6. Parent is woken.
7. Parent receives decoded result.
```
### 12.3 Command flow inside task
```text
1. Guest task calls cmd!(...).run().await.
2. Host import validates command capability.
3. Node command runner starts process in task environment.
4. Task enters COMMAND_RUNNING state.
5. Logs stream to node and coordinator under caps.
6. Debug Epoch requests suspend controlled process tree.
7. Command exit status is returned to guest.
8. Guest decides whether to publish artifacts.
```
### 12.4 Failure behavior
MVP rules:
```text
Task trap -> task FAILED, process may enter debug break depending policy.
Command nonzero exit -> task receives error, can handle or fail.
Node lost with non-replicated running state -> virtual process FAILED.
Node lost after flushed but unsynced artifact -> artifact may be unavailable.
Node lost after synced artifact -> artifact survives according to sync policy.
```
Do not pretend non-replicated state is recoverable.
### 12.5 Restart flow
```text
1. User edits source.
2. VS Code triggers disasmer build.
3. New bundle is produced.
4. User selects Restart Failed Task or Restart Selected Virtual Thread.
5. DAP/coordinator checks task compatibility hash.
6. Runtime stops old task if needed.
7. Unflushed overlay changes are discarded unless explicitly preserved.
8. New task starts with original args, same env, chosen VFS checkpoint.
9. Breakpoints are rebound.
10. Other tasks remain alive only if compatibility checks pass.
```
If compatibility fails:
```text
Task signature changed; restarting whole virtual process is required.
```
This capability is important to advertise in any demo(s).
---
## 13. Debugging
### 13.1 Design goal
The user should experience Disasmer as one ordinary debug target:
```text
Process: launch-build-demo
Threads:
1 main
2 compile linux-x64
3 compile windows-x64
4 package
```
The debugger should not force the user to understand node IDs, QUIC links, VFS manifests, or environment cache internals unless they open the Disasmer inspector.
### 13.2 Use a custom DAP adapter
Use `disasmer-dap` as the VS Code debug adapter.
Why:
- DAP is the right integration point for VS Code.
- The target is not one native process; it is a virtual process spanning nodes.
- Disasmer must control the thread list, stop reasons, restart commands, artifact views, and all-stop semantics.
- LLDB can be a future internal helper, but it should not own the UX.
### 13.3 Debug components
```text
VS Code extension
launches disasmer-dap and contributes commands/views
disasmer-dap
speaks DAP to VS Code
speaks Disasmer debug protocol to coordinator
maps DAP process/thread/stack/variables to virtual model
coordinator debug service
owns debug sessions and Debug Epochs
broadcasts freeze/resume
aggregates thread state
node debug agent
sets breakpoints/probes
freezes/resumes Wasmtime tasks
suspends/resumes controlled command processes
captures stacks/locals/handle state
```
### 13.4 Debug Epoch protocol
When a breakpoint is hit:
```text
1. Node detects breakpoint/trap/probe in virtual thread T.
2. Node reports BreakHit(process, thread, location) to coordinator.
3. Coordinator creates DebugEpoch E.
4. Coordinator marks process FREEZE_REQUESTED.
5. Coordinator sends Freeze(E) to all nodes hosting threads for the process.
6. Wasm tasks stop at current probe/safepoint or next safepoint.
7. Native command tasks are suspended using backend-specific process-tree control.
8. VFS overlay manifests are pinned.
9. Memory/handle snapshots are pinned copy-on-write where applicable.
10. Nodes report Frozen(E, thread states).
11. Coordinator marks epoch FROZEN.
12. DAP emits stopped event for all threads.
```
Resume:
```text
1. User presses Continue.
2. DAP sends Continue(E) to coordinator.
3. Coordinator sends Resume(E) to nodes.
4. Nodes unpin debug snapshots.
5. Wasm tasks continue.
6. Native command tasks resume.
7. Coordinator marks epoch RESUMED.
```
### 13.5 Safepoints and breakpoints
MVP should use layered breakpoint support:
#### Layer 1: explicit debug probes
`#[disasmer::task]` and `#[disasmer::main]` generate probes at:
```text
task entry
before/after spawn
before/after join
before/after cmd run
before/after flush/sync
await boundaries
explicit disasmer::debug::probe!() calls
```
This provides reliable all-stop behavior early.
#### Layer 2: source-line breakpoints
The build pipeline maps source line breakpoints to probe points and, where possible, Wasm code offsets using debug metadata.
Launch acceptance should include:
```text
breakpoint in main()
breakpoint in compile()
breakpoint before/after cmd!().run()
breakpoint before artifact publish/flush
breakpoint in package()
```
#### Layer 3: richer DWARF/LLDB integration
After the DAP skeleton works, improve locals/stacks by reading Rust/Wasm debug metadata. This is important, but it should not block the all-stop architecture.
### 13.6 Variables and locals
Launchable debugger variable support should include:
```text
task arguments
return values
Target enums and simple structs
Artifact handles
SourceSnapshot handles
Blob handles
DAtomic values
DChannel status
current VFS mounts
current command spec/status
recent stdout/stderr tail
```
For arbitrary Rust locals, use best-effort DWARF support where available. If a local cannot be inspected, the debugger should say so clearly and still show task args/handles.
### 13.7 Native command tasks in debugger
A native command is debugger-visible as part of the virtual thread, but MVP does not need to debug inside `cargo` or the compiler itself.
The command frame should show:
```text
command argv
working directory
environment name
node id in inspector only
start time
status
exit code when done
recent stdout/stderr
resource limits
```
When the virtual process is frozen, controlled native command processes should be suspended. If the backend cannot safely suspend a command, the node must report failure and the debugger must show a clear error rather than claiming all-stop succeeded.
### 13.8 DAP features required for launch
Required:
```text
initialize
launch / attach
setBreakpoints
configurationDone
threads
stackTrace
scopes
variables
continue
pause
next / step-over at probe granularity
disconnect
stopped events
continued events
output events
custom restart-task request
```
Not required for launch:
```text
watchpoints
reverse debugging
non-stop debugging
hot code replace
debugging arbitrary child process internals
```
### 13.9 Disasmer inspector
Default debugger view remains normal. Add an optional inspector tree:
```text
Virtual Process
Threads
compile linux-x64 -> node linux-1 -> env linux -> running/frozen
compile windows-x64 -> node windows-1 -> env windows -> running/frozen
VFS
epoch 4
artifacts
Debug Epoch
id E
frozen nodes
Environments
linux digest cached on linux-1
windows digest cached/stub on windows-1
Transport
node links status
```
This helps explain the system without polluting the normal debug UX.
---
## 14. VS Code extension
### 14.1 Extension responsibilities
The extension should:
```text
watch envs/**/Containerfile
watch envs/**/Dockerfile
watch envs/**/flake.nix
watch envs/**/flake.lock
watch inputs/**
watch src/**
run disasmer build before debugging
launch/attach DAP session
show virtual threads in normal debug UI
show per-task logs
show artifact tree
show node connection status
provide env!("...") autocomplete
provide disasmer.toml diagnostics
provide restart selected/failed task commands
```
### 14.2 Launch configuration
Example `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Disasmer Build",
"type": "disasmer",
"request": "launch",
"coordinator": "https://disasmer.example.com",
"project": "${workspaceFolder}",
"preLaunchTask": "disasmer: build",
"localFirst": true
}
]
}
```
### 14.3 Editor diagnostics
Diagnostics:
```text
env!("name") missing
env discovered but incompatible with available nodes
Containerfile changed; bundle rebuild needed
large task argument warning
cmd! used in task without cmd capability
fs::sync missing for final artifact export warning
Windows backend is dev/stub isolation warning
```
### 14.4 VS Code views
MVP views:
```text
Disasmer: Nodes
Disasmer: Virtual Processes
Disasmer: Artifacts
Disasmer: Logs
Disasmer: Inspector
```
Operator panel preview can appear as:
```text
Disasmer: Operator Panel Preview
```
But the full immediate-mode panel is second milestone.
---
## 15. Hosted coordinator and product UX
### 15.1 Hosted coordinator MVP
Hosted coordinator pages:
```text
Sign in
Projects
Project detail
Node attach instructions
Node list
Virtual process list
Virtual process detail
Logs
Artifacts
Debug session status
Operator panel placeholder/preview
Settings / quotas
```
### 15.2 Hosted Authentik/OIDC integration
Use Authentik as the identity provider through OIDC in the private hosted module. The open-source/private coordinator should expose auth hooks and simpler local/team auth, but should not require the hosted OIDC stack.
Implementation requirements:
```text
authorization code + PKCE for web login
strict redirect URI allowlist
minimal scopes
short session lifetime
refresh handling if needed
logout/back-channel support later
CLI token issuance after web login
node enrollment tokens separate from user sessions
```
### 15.3 Community tier
Community tier defaults:
```text
1 active virtual process
tiny hosted Wasm control loop limit
no hosted native commands
no hosted containers
no hosted network from tasks
small logs
small metadata
small artifact retention
4 user-attached nodes allowed
operator panel access
```
The hosted control loop is useful for trying Disasmer and coordinating attached nodes, not for mining, scraping, compiling huge repos, or hosting files.
### 15.4 Abuse controls as architecture
Every resource-consuming action must pass through a hosted/community quota policy module before work starts.
Quota dimensions:
```text
active virtual processes
hosted Wasm fuel
hosted Wasm memory
hosted process wall-clock
API calls
node heartbeats
spawn attempts
log bytes
metadata bytes
artifact bytes
artifact downloads
debug memory reads
operator panel updates
rendezvous attempts
```
Pattern:
```rust
quota.reserve(tenant, Resource::SpawnAttempt, 1).await?;
// perform operation
quota.commit(reservation).await?;
```
For streams, use incremental accounting and backpressure.
### 15.5 Public input threat model
Treat as hostile:
```text
users
OAuth identities
nodes
programs
build files
Containerfiles
Git repos
logs
artifacts
operator panel text/events
debug requests
node capabilities
cache claims
```
### 15.6 Public launch security baseline
Required before public launch:
- Tenant ID on every object.
- Authorization check on every API endpoint.
- Negative cross-tenant tests.
- No hosted containers on community tier.
- No hosted native commands on community tier.
- No arbitrary network access from hosted Wasm.
- Rate limits on auth, APIs, rendezvous, logs, spawn, and UI events.
- Log and artifact size caps.
- Private artifacts by default.
- No public file hosting by default.
- Node identity signing/revocation.
- Task capabilities explicit.
- Debug attach requires project permission.
- Debug memory reads quota-limited and audited.
- Operator panel uses built-in widgets only.
- No custom HTML or JS in panels.
- No password fields in user-defined panels.
- No OAuth flows inside operator panels.
---
## 16. Operator panel scaffold and second milestone
### 16.1 MVP scaffold
Even if the full panel is second milestone, the MVP should include:
```text
ui model crate
panel snapshot table
panel event table
coordinator API namespace
web placeholder area
DAP/debug interaction policy
security restrictions
```
The first visible version can be a fixed coordinator dashboard showing:
```text
process state
tasks
progress
logs
artifacts
Debug / Restart / Cancel buttons as control-plane actions
```
### 16.2 Immediate-mode second milestone
The second milestone adds:
```rust
let panel = ui::panel("Repo builder").await?;
loop {
let action = panel.draw(|ui| {
ui.heading("Repo builder");
ui.text("Build status");
ui.task_table("Tasks", tasks.snapshot());
ui.log_tail("Recent output", logs.tail());
if ui.button("Restart failed").clicked() {
ui.emit(Action::RestartFailed);
}
}).await?;
if let Some(action) = action {
handle(action).await?;
}
}
```
### 16.3 Panel security model
MVP/second milestone rules:
```text
built-in widgets only
no custom HTML
no custom JavaScript
all text escaped
external links restricted
no OAuth/password widgets
panel labeled as user-program-provided
rate-limited updates
state size capped
events typed and schema-checked
read-only while debugger has process frozen
control-plane actions may remain available while frozen
```
---
## 17. Security and capability model
### 17.1 Capability-first execution
Every task has a capability set.
Example:
```json
{
"task": "compile",
"capabilities": {
"vfs.read": ["/src"],
"vfs.write": ["/work", "/vfs/artifacts/linux-x64"],
"cmd.run": true,
"network.outbound": false,
"secrets": []
}
}
```
### 17.2 Coordinator authz checks
Every coordinator API checks:
```text
authenticated principal
principal tenant membership
object tenant id
project membership/role
operation permission
quota availability
object state compatibility
```
### 17.3 Node trust model
Default rule:
```text
A user's work runs only on that user's nodes, managed paid nodes, or explicitly shared team nodes.
```
For MVP:
- do not mix tenants on user-provided nodes;
- do not trust user-provided nodes to produce truthful artifacts for other tenants;
- label artifacts with node provenance;
- expose trust label in inspector;
- eventually support reproducible repeated builds for higher trust.
### 17.4 Bundle identity
Bundles should be content-addressed and integrity-checked.
Later signing can be added, but MVP should at least ensure:
```text
coordinator records bundle digest
node verifies bundle digest before running
manifest records environment/resource digests
task specs reference bundle digest
artifact provenance references bundle/task/node
```
### 17.5 Secrets
MVP secrets support should be minimal.
Recommended launch stance:
```text
no secrets in hosted/community tasks
secrets not shown in logs/UI/debug by default
explicit grant required per task/environment
short-lived scoped secret materialization
secret access audited
secret redaction in logs
```
For the first launch demo, avoid secrets entirely.
### 17.6 Container/runtime hardening
Linux Podman backend should default to:
```text
rootless
no privileged containers
no host Docker/Podman socket mount
read-only source mount
writable task workdir only
limited network by default
seccomp/AppArmor/SELinux where available
resource limits where practical
```
Windows dev backend should be clearly scoped as user-attached-node execution, not public managed secure isolation.
---
## 18. Implementation milestones
This sequence avoids building a beautiful UI on top of a runtime that cannot yet prove the thesis.
### Milestone 0: Repository and protocol spine
Deliverables:
```text
monorepo layout
shared IDs and protocol types
CoordinatorStore trait
Postgres persistent-store migrations skeleton
CLI skeleton
node skeleton
SDK skeleton
VS Code extension skeleton
DAP skeleton
example project skeleton
```
Acceptance criteria:
```text
cargo workspace builds
CLI can print discovered project info
coordinator starts locally with Postgres for persistent project/node state and in-memory runtime state
node starts and registers fake heartbeat locally
VS Code extension can run a dummy DAP session
```
Key considerations:
- Do not overdesign protocol versioning, but add a protocol version field immediately.
- Use stable IDs everywhere.
- Add tenant_id to data models from the first migration.
### Milestone 1: Local single-node Wasm task execution
Deliverables:
```text
#[disasmer::main]
#[disasmer::task]
DisasmerArg derive
named task registry
Wasmtime runner
spawn/join on same node
small serialized args/results
basic logs
basic DAtomicU32 or equivalent demo handle
```
Acceptance criteria:
```text
example local-one-node runs main task
main spawns child Wasm task
child returns serializable result
parent joins result
logs show per virtual thread
```
Considerations:
- Keep the first task ABI tiny.
- Ban closures for MVP.
- Fail early on invalid args.
### Milestone 2: Environment discovery and bundle generation
Deliverables:
```text
envs/** discovery
env!("name") compile/editor validation
disasmer build creates app.dis directory
bundle manifest
task manifest
environment manifest
source snapshot manifest placeholder
VFS seed manifest
```
Acceptance criteria:
```text
adding envs/linux/Containerfile makes env!("linux") valid
missing env gives useful error
bundle digest changes when Containerfile changes
bundle digest changes when task ABI changes
```
Considerations:
- Keep `disasmer.toml` optional.
- Make generated metadata visible and inspectable.
### Milestone 3: VFS and artifacts
Deliverables:
```text
per-task VFS overlay
/vfs/project or /src mount
/vfs/work
/vfs/artifacts
fs::copy
fs::flush metadata publication
Artifact::from_path
fs::sync export-to-local-directory
content-addressed blob/chunk store
```
Acceptance criteria:
```text
task writes artifact
flush publishes artifact metadata for active processes
parent joins Artifact handle
package task reads artifact handle
sync exports final artifact to dist/
large artifact bytes do not go through coordinator in local mode
```
Considerations:
- Build the metadata/byte split now.
- Ensure artifact path namespace is stable.
- Add tests proving `flush()` is metadata-only.
### Milestone 4: Linux Podman command backend
Deliverables:
```text
Podman rootless env materialization
Containerfile build/cache by digest
cmd!(...).run()
stdout/stderr streaming
process limits/log caps
artifact staging from command output
basic process suspend/resume for debug epoch
```
Acceptance criteria:
```text
Linux task runs cargo build in envs/linux/Containerfile
logs stream to CLI/coordinator
artifact is published through VFS
local source checkout is bind-mounted when task runs locally
```
Considerations:
- Keep the first Containerfile expectations simple.
- Do not support arbitrary privileged containers.
- Do not mount the container engine socket.
### Milestone 5: Coordinator/node split with hosted-ready persistent store
Deliverables:
```text
coordinator service
Postgres-backed persistent store
node enrollment token
node identity
node heartbeat
bundle registration
task placement
StartTask command over persistent session
logs/events over stream
local coordinator mode still works
```
Acceptance criteria:
```text
coordinator runs separately from node
node attaches with token
disasmer run schedules task onto attached node
node executes task and reports completion
Postgres contains persistent project/node/bundle metadata; active process/thread/artifact metadata lives in memory
```
Considerations:
- Use same code path for local and hosted as much as possible.
- Do not create a special “demo scheduler” that will be thrown away.
### Milestone 6: Quinn transport and rendezvous
Deliverables:
```text
Quinn transport wrapper
node-to-coordinator persistent session if practical
node-to-node direct connection
rendezvous endpoint exchange
authenticated stream framing
blob/artifact chunk transfer protocol
no coordinator bulk relay
```
Acceptance criteria:
```text
two nodes establish direct QUIC link
node A can request authorized artifact chunk from node B
failed direct connection produces clear error
coordinator never receives bulk artifact bytes by default
```
Considerations:
- Keep HTTPS bootstrap if it makes auth simpler.
- Put transport behind traits for testability.
- Add trace logs for failed connectivity.
### Milestone 7: Windows attached node backend
Deliverables:
```text
Windows node build/test
WindowsCommandDevRunner
per-task workspace
process/log capture
artifact staging
best-effort process tree lifecycle
Windows sandbox backend trait + stub
Forgejo Windows worker setup docs
```
Acceptance criteria:
```text
Windows node attaches to hosted/local coordinator
scheduler places env!("windows") task on Windows node
Windows task runs cargo build or a simple command
artifact is published and joined by parent
VS Code/coordinator shows Windows virtual thread
```
Considerations:
- Be explicit that this is not production-grade untrusted Windows sandboxing.
- Do not offer managed public Windows compute until isolation is real.
### Milestone 8: All-stop debugging and DAP
Deliverables:
```text
custom DAP adapter
launch/attach
thread list
breakpoint setting
source/probe mapping
Debug Epoch coordinator
node freeze/resume
Wasm safepoints/probes
command process suspend/resume integration
stack/scopes/variables for task args and handles
stdout/stderr output events
restart selected/failed task command
```
Acceptance criteria:
```text
VS Code F5 starts a virtual process
breakpoint in main stops all virtual threads
breakpoint in Linux compile task stops Windows task too
breakpoint in Windows task stops Linux task too
thread list is coherent
user can inspect Target arg and Artifact handle
continue resumes all tasks
task restart works after source edit
```
Considerations:
- This is the riskiest milestone. Start early.
- Use probe-granularity stepping first.
- Make all-stop correctness more important than perfect DWARF locals.
### Milestone 9: Local-first Git/source snapshots
Deliverables:
```text
git source provider module
commit/submodule/dirty overlay manifest
local checkout detection
bind-mount local checkout for local tasks
remote missing-content transfer by hash
scheduler locality scoring
latency regression tests
```
Acceptance criteria:
```text
local Linux build from local checkout does not upload repo
remote Windows build receives only required source content
changing one file transfers only that file/overlay where possible
scheduler prefers node with source checkout and env cache
```
Considerations:
- Keep source provider modular.
- Do not hardwire Git into the VFS core.
### Milestone 10: Hosted coordinator launch hardening
Deliverables:
```text
private hosted Authentik/OIDC login
tenant/project UI
node attach UI
hosted/community quota policy module
authz middleware and private hosted OIDC integration
cross-tenant negative tests
community tier limits
rate limiting
log/artifact caps
admin kill switch
basic plan/policy flags, even if only community exists
```
Acceptance criteria:
```text
public user can sign in
public user can create project
public user can attach own node
public user cannot run hosted native command/container
public user can run task on own node
cross-tenant tests pass
quota tests pass
```
Considerations:
- Do not launch without abuse controls.
- Design quotas as first-class resource accounting.
### Milestone 11: Launch demo polish
Deliverables:
```text
launch-build example
README quickstart
short screencast script
hosted landing page
install script/packages
VS Code extension packaging
error message pass
first-run diagnostics
artifact export UX
fixed coordinator dashboard
operator panel preview scaffold
```
Acceptance criteria:
```text
fresh developer can follow quickstart
Linux node attach works
Windows node attach works
F5 demo works
breakpoint demo works
restart failed task demo works
artifact export works
README explains community tier honestly
```
Considerations:
- Public launch users will try weird setups immediately.
- Error messages matter as much as features.
- Make the first demo resilient.
---
## 19. Testing strategy
### 19.1 Unit tests
Required areas:
```text
DisasmerArg derive allowed/rejected types
environment discovery
bundle digest stability
manifest generation
VFS path normalization
flush manifest generation
sync policy selection
hosted/community quota policy module
scheduler scoring
authz checks
protocol encoding/decoding
```
### 19.2 Integration tests
Local integration tests:
```text
single-node spawn/join
two Wasm tasks with DAtomic
task writes artifact and parent consumes it
cmd runner fake backend
Debug Epoch with two Wasm tasks
restart failed task with compatible bundle
restart fails with incompatible schema
```
Linux integration tests:
```text
Podman build env
Podman run command
local checkout bind mount
artifact publish/export
process suspend/resume if supported
```
Windows integration tests:
```text
node attach
command execution
log capture
artifact staging
process lifecycle cleanup
```
Network integration tests:
```text
node-to-node QUIC connect
artifact chunk transfer
connection failure clear error
no coordinator bulk relay
```
### 19.3 End-to-end tests
E2E test scenarios:
```text
F5 local single node
hosted coordinator + attached Linux node
hosted coordinator + Linux + Windows nodes
breakpoint in Linux task all-stops process
breakpoint in Windows task all-stops process
restart failed Windows task
local-first no-upload test
community tier quota enforcement
cross-tenant forbidden access
```
### 19.4 Latency regression tests
Add tests that fail if:
```text
local source bytes are uploaded during local build
artifact bytes go to coordinator during flush
full repo tarball is created by default
large artifact is transferred before consumer/export/sync needs it
scheduler chooses remote node despite local compatible warm node without reason
```
### 19.5 Security tests
Required negative tests:
```text
user cannot read another tenant's process
user cannot attach debugger to another tenant's process
node cannot claim another tenant's task
node cannot publish artifact for unauthorized process
community hosted task cannot request cmd.run
community hosted task cannot request network
operator panel cannot submit custom HTML/JS
log cap truncates noisy task
quota rejects spawn storm
```
---
## 20. Documentation required for MVP
### 20.1 User docs
```text
README quickstart
What is Disasmer?
Install CLI
Install VS Code extension
Login to hosted coordinator
Attach a Linux node
Attach a Windows node
Create envs/<name>/Containerfile
Write first build program
Debug in VS Code
Publish artifacts with flush()
Export artifacts through an attached receiver node or explicit storage integration
Community tier limits
Security model for user-attached nodes
```
### 20.2 Engineering docs
```text
architecture.md
protocol.md
task-abi.md
vfs.md
debugging.md
transport.md
security.md
operator-panel.md
windows-backend.md
```
### 20.3 Error messages as docs
Invest heavily in errors:
```text
missing env
node unavailable
environment incompatible
large task arg
host-only type in DisasmerArg
cmd used on coordinator
cmd capability missing
artifact bytes unavailable
QUIC direct connection failed
sync required for export
Debug Epoch freeze failed
restart incompatible after source change
community tier quota exceeded
```
---
## 21. Key risks and mitigations
### 21.1 Risk: debugger is the hardest part
Mitigation:
- Build DAP skeleton early.
- Implement Debug Epochs before fancy UI.
- Use generated debug probes for reliable breakpoints first.
- Add DWARF/local richness incrementally.
- Define launch acceptance around real breakpoints in the demo code, thread list, task args, handles, stacks, all-stop, continue, and restart.
Experiment result: `experiments/debugger-gate` has derisked the core VS Code debugging UX. It proves a custom DAP adapter can present a virtual process with `main` and task threads, bind ordinary VS Code gutter breakpoints to Wasmtime-backed Rust/Wasm source locations, hit breakpoints in multiple virtual tasks, support basic stepping/watch/variables, and keep the backend boundary shaped like a replaceable virtual-process runtime. Treat that experiment as a proof and reference, not as implementation foundation: it is intentionally hacky prototype code with hardcoded example-local variable mapping and a simplified local Wasmtime scheduler.
### 21.2 Risk: Windows isolation is not real yet
Mitigation:
- Keep Windows as user-attached node execution only.
- Label backend as `windows-command-dev`.
- Do not offer public managed Windows compute in MVP.
- Keep sandbox backend interface ready.
- Investigate Windows Sandbox/AppContainer after public launch or before managed nodes.
### 21.3 Risk: hosted community tier becomes compute abuse target
Mitigation:
- No community hosted containers.
- No community hosted native commands.
- Zero-cap hosted Wasm control loop only.
- Fuel, wall-clock, memory, logs, and metadata limits.
- Rate-limit every API and event stream.
- Require attached user node for real work.
### 21.4 Risk: QUIC/NAT becomes launch blocker
Mitigation:
- Use coordinator control session for scheduling and metadata.
- Require direct node-to-node only when bytes must move.
- Prefer scheduling where bytes already are.
- Fail clearly when direct transfer is impossible.
- For launch demo, place Linux and Windows nodes in a connectivity setup you control.
### 21.5 Risk: local-first goal gets lost
Mitigation:
- Add tests that fail on unwanted uploads.
- Instrument bytes moved per build.
- Show “bytes moved” in logs/dashboard.
- Make `flush()` metadata-only by default from day one.
### 21.6 Risk: product sounds too ambitious
Mitigation:
- Demo one narrow workflow extremely well.
- Keep the public README focused on build systems.
- Keep architecture docs honest about non-goals.
- Do not claim managed secure compute until it exists.
- Emphasize open-source local-first runtime.
---
## 22. Definition of done for the public-launch MVP
The public-launch MVP is done when a clean machine can:
```text
1. Install Disasmer CLI.
2. Install the VS Code extension.
3. Sign in to hosted coordinator through Authentik/OIDC.
4. Create or select a project.
5. Attach a Linux node.
6. Attach a Windows node.
7. Clone the launch-build demo repo.
8. Open it in VS Code.
9. See env!("linux") and env!("windows") recognized.
10. Press F5.
11. See one virtual process start.
12. See Linux and Windows virtual threads in VS Code.
13. Hit a breakpoint in a spawned task.
14. Observe all-stop Debug Epoch behavior.
15. Inspect task args, command status, logs, and artifact handles.
16. Edit source and restart only the failed/selected task.
17. Finish the build.
18. Export artifacts through an attached/local receiver node or explicit storage integration.
19. Confirm local-first behavior in logs/metrics.
20. Confirm hosted coordinator did not run arbitrary native commands.
```
---
## 23. Suggested implementation order inside each milestone
When in doubt, build vertical slices rather than broad infrastructure.
Recommended pattern:
```text
1. Fake backend, real API.
2. Real backend, local only.
3. Real backend, coordinator split.
4. Real backend, hosted auth/quotas.
5. Polish only after the vertical slice proves the thesis.
```
Example for debugging:
```text
1. DAP shows fake virtual process with fake threads.
2. DAP attaches to local one-node Wasm process.
3. Breakpoint probe freezes one Wasm task.
4. Debug Epoch freezes two Wasm tasks.
5. Debug Epoch freezes Wasm + command task.
6. Same flow across two nodes.
7. VS Code restart selected task.
8. Richer locals/stacks.
```
Example for VFS:
```text
1. In-memory manifest.
2. Local disk overlay.
3. Artifact handle.
4. Metadata-only flush.
5. Export sync.
6. Node-to-node chunk fetch.
7. Explicit receiver-node export.
```
---
## 24. External technical references to keep nearby
These are useful implementation references, not product dependencies:
- Wasmtime Rust embedding API: https://docs.wasmtime.dev/api/wasmtime/
- Wasmtime Component Model API: https://docs.wasmtime.dev/api/wasmtime/component/index.html
- Quinn repository and docs: https://github.com/quinn-rs/quinn
- Debug Adapter Protocol: https://microsoft.github.io/debug-adapter-protocol/
- VS Code debugger extension guide: https://code.visualstudio.com/api/extension-guides/debugger-extension
- Authentik OAuth2/OIDC provider docs: https://docs.goauthentik.io/add-secure-apps/providers/oauth2/
- Podman: https://podman.io/
- Nix flakes: https://nix.dev/concepts/flakes.html
- Windows Sandbox CLI: https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/windows-sandbox-cli
- Windows AppContainer isolation: https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation
- OWASP API Security Top 10 2023, unrestricted resource consumption: https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/
- OAuth 2.0 Security Best Current Practice, RFC 9700: https://datatracker.ietf.org/doc/rfc9700/
- NIST SP 800-190, Application Container Security Guide: https://csrc.nist.gov/pubs/sp/800/190/final
---
## 25. Final guidance
Do not shrink the ambition of the demo, but aggressively constrain the implementation semantics.
The MVP should be ambitious in what the user sees:
```text
one Rust build program
hosted coordinator
Linux + Windows nodes
source-level VS Code debugging
all-stop Debug Epochs
task restart
local-first artifacts
```
It should be conservative in what the runtime promises internally:
```text
no transparent raw remote pointers
no live stack migration
no coordinator bulk data plane
no community hosted native compute
no fake Windows sandbox claims
no arbitrary child-process source debugging
no automatic survival of non-replicated node failure
```
That combination is what can make the launch credible: the visible experience is striking, while the implementation model remains honest enough to actually build.