The north star I would choose is:
Lanix is a web-first, agent-native fabric for creating small living rooms of software. Each room has a cozy human portal, a durable resource, a scoped sandbox, an agent-readable contract, and a share/publish boundary.
The big inversion: start with the webpage, not the filesystem. The page is an empty canvas/shelf/garden. You create portals into little apps, sites, notes, boards, family tools, dashboards, agents, and experiments. Behind every portal is a Room: a scoped authority boundary with state, commands, events, source, artifact history, collaborators, and agents.
But I would not throw away the filesystem idea. I would demote it. The filesystem should stop being the product metaphor and become the kernel/agent ABI: the thing tasks, agents, WASI guests, mesh peers, and debugging tools use to inspect and operate a room. This matches the strongest part of Lanix: one FileSystem contract for storage, tasks, agents, terminals, KV, CAS, and remote resources; per-process namespaces; and the browser as a client of a native runtime, not the runtime itself. (Loopwork) (Loopwork)
So the answer to “is a native filesystem needed?” is:
Yes, as a capability projection. No, as the primary product or as a general-purpose distributed POSIX filesystem.
Build resources, rooms, commands, events, CAS artifacts, and sync logs. Then project those as files when an agent, WASI program, shell, or mesh peer needs that view.
You already have the right substrate pieces. I would keep these, but rearrange their order of importance.
Lanix’s docs already say the browser is one excellent client of a native OS core, and that the single FileSystem trait is the common contract that reaches local services and remote machines. That is exactly right for the lower layers. (Loopwork)
Your uploaded micro-app design note lands on the right UI pattern: resource-first web facade: static/CAS UI shell, materialized JSON/HTML views, command endpoints, and event streams, with heavier client-side JS or CRDTs only when the interaction truly needs them.
Your local-vs-published split is also load-bearing: local app = scoped room I operate in; published app = CAS-backed artifact or bounded service I publish. That distinction prevents public visitors from accidentally inheriting terminal/task/raw-filesystem powers.
Your Automerge resource plan is valuable, but I would treat it as one resource type, not the universal app substrate. It is perfect for multi-writer documents, offline-ish collaboration, and small shared state, but most microapps should see a materialized view and commands rather than every phone browser loading Automerge internals. Your grocery proof already does this: CAS UI plus /api/view.json, command endpoints, and SSE, while real Automerge commits happen on the Lanix side. (Loopwork) (Loopwork)
I would make these the user-facing objects:
Home
a cozy blank page / shelf / canvas
Portal
a visible tile, pane, iframe, card, app, site, note, or tool surface
Room
the durable thing behind a portal:
identity, state, source, policy, commands, events, agents, history
Artifact
a CAS-pinned UI/build output
Resource
stateful object: Automerge doc, simple store, volume, app resource, tool, feed
Agent
a scoped collaborator inside a room
The Portal is what a human sees. The Room is what the system operates. The Resource is where durable truth lives. The Artifact is the current UI skin. The Agent can inspect and change the room through explicit capabilities.
This gives you the “Claude Artifacts” feel—describe something, see a standalone interactive thing, edit/iterate/version it—but with durable resources, real collaboration, scoped authority, and agent-readable internals. Claude’s own artifact docs emphasize shareable apps/tools/content, single-page websites, interactive components, versioned iteration, AI-powered apps, and persistent storage for published artifacts; Lanix can take that affordance but make the resource boundary yours. (Claude Help Center)
Browser Shell
app.localhost / app.<domain>
empty canvas, portal shelf, inspector, agent chat, share/publish UI
Gateway / WebDoor
maps room names to origins/routes
serves CAS UI artifacts
exposes same-origin resource APIs
stamps web principals
Room Supervisor
owns room identity, manifest, policy, collaborators
binds state resources, command handlers, event streams
starts sandboxed app/agent turns
records provenance and approvals
Resource Layer
Automerge docs
simple room stores
volumes / filesets
CAS artifacts
jobs/tools
event logs and materialized views
Runtime Layer
qjs / wasm fetch handlers
WASI tasks
agent sessions
iroh mesh endpoints
file projection adapters
This is close to “Cloudflare Workers + Durable Objects + Claude Artifacts + Automerge,” but the capability model is Lanix-native. Cloudflare Durable Objects are useful as a mental model because each object has a unique name, can coordinate multiple clients, and has durable storage colocated with compute; recent Dynamic Worker/Fast DO material also points at AI-generated persistent apps needing a supervisor, per-app state, and sandboxed generated code. (Cloudflare Docs) (The Cloudflare Blog)
The Lanix version should be:
one Room = one named actor/supervisor + one scoped namespace + one policy file
Not:
one app = ambient filesystem + browser IndexedDB + hidden agent powers
OpenClaw is a useful warning: it supports sandbox backends to reduce blast radius, but its docs explicitly say sandboxing is not a perfect boundary and that unsandboxed tools run on the host. For Lanix, the safer opinionated default is: every generated portal app and every agent tool turn runs in a room sandbox unless explicitly elevated by an approval. (OpenClaw)
A room should have a manifest like this:
{
"kind": "lanix.room.v0",
"id": "room_01J...",
"name": "family-groceries",
"title": "Family Groceries",
"artifact": "cas:b3:...",
"source": "cas:b3:...",
"state": {
"primary": {
"kind": "automerge",
"resource": "am:groceries"
}
},
"views": {
"default": "/api/view.json",
"html": "/api/view.html"
},
"commands": {
"add": {
"schema": "/api/commands/add.schema.json",
"method": "POST",
"path": "/api/commands/add"
},
"set-done": {
"schema": "/api/commands/set-done.schema.json",
"method": "POST",
"path": "/api/commands/set-done"
}
},
"events": "/api/events",
"policy": {
"visibility": "private",
"members": {
"jesse": "owner",
"family": "edit"
},
"agent": {
"defaultRole": "builder",
"requiresApprovalFor": ["publish", "share", "mount", "network", "delete"]
}
},
"runtime": {
"kind": "wasm-worker",
"limits": {
"memoryBytes": 67108864,
"maxRequestBodyBytes": 65536,
"timeoutMs": 100
}
}
}A browser sees:
GET / -> CAS UI artifact
GET /api/view.json -> current materialized state
GET /api/view.html -> optional fragment/hypermedia view
GET /api/events -> SSE updates
POST /api/commands/<name> -> principal-stamped mutation
An agent or WASI task sees the same room projected as files:
/room/
manifest.json
policy.json
artifact.cas
source/
api/
spec.json
view.json
view.html
events
commands/
add.schema.json
set-done.schema.json
state/
primary/
snapshot.am
heads.json
commit/
events
jobs/
agents/
approvals/
history/
diagnostics.json
That tree does not need to be a fast distributed filesystem. It can be a projection over room/resource APIs. The uploaded docs already converge on this: meaningful app state and mutations should stay resource-visible through spec.json, view.json, command schemas, events, jobs, and status; the browser can be sophisticated, but the system should remain inspectable.
For cozy microapps, I would avoid exposing “write any file under /state” as the default browser/app interface. Make every meaningful mutation a command:
Command = schema + handler + policy + event + provenance
Example:
{
"name": "add",
"title": "Add grocery item",
"input": {
"type": "object",
"required": ["item"],
"properties": {
"item": { "type": "string", "maxLength": 120 }
}
},
"effects": ["state.primary.items"],
"idempotency": "clientMutationId optional",
"requires": "room:write"
}The command path:
browser/agent submits command
-> gateway stamps principal
-> room supervisor validates schema + policy
-> command handler runs in sandbox
-> resource changes are committed
-> view is refreshed/materialized
-> event emitted
-> provenance recorded
This keeps the user-facing web API simple, keeps agents legible, and avoids turning a raw filesystem into your product API. Your Automerge design already makes the same authority distinction: Automerge actor IDs are document mechanics; the Lanix principal is the authority, and commit results/events should be principal-stamped.
I would standardize generated dynamic apps around a small Worker-like contract:
export default {
async fetch(req: Request, env: Env, ctx: Ctx): Promise<Response> {
// handle web requests
},
async command(name: string, input: unknown, env: Env, ctx: Ctx) {
// optional command-specific entry
},
async view(name: string, env: Env, ctx: Ctx) {
// optional materialized view renderer
}
}But the env is not Cloudflare bindings. It is a Lanix namespace/capability set:
type Env = {
room: RoomApi
store?: KvLikeStore
doc?: AutomergeResource
cas?: CasStore
events: EventSink
fetch?: ScopedFetch
fs?: ScopedFileSystem // only if manifest grants it
}This aligns with your AppResource docs: AppResource should have one resource identity, one manifest, one granted namespace, one lifecycle policy, and HTTP/filesystem surfaces; the host owns identity, grants, limits, lifecycle, streams, and transport, while the guest owns app decisions and semantics. (Loopwork)
The runtime rule should be:
App code gets capabilities, not ambient access.
Your AppFS docs already phrase this well: “AppResource returns capabilities, not bytes. Host moves bytes; guest names meaning.” They also make the important distinction between ToolFS, AppResource, and #cpu, which should not collapse into one overloaded execution model. (Loopwork) (Loopwork)
I would define three state tiers.
This is what browsers and agents read first:
/api/view.json
/api/view.html
/api/events
It is fast, compact, phone-friendly, and easy for agents to understand. This is the default for groceries, chores, family pages, dashboards, simple boards, trip plans, and tool panels. Your uploaded notes explicitly argue that a phone should not need full CRDT/editor weight just to open a checklist.
This is the default mutation model:
command -> validate -> append op/change -> update resource -> emit event
The backing store can be Automerge, SQLite/redb, a tiny KV, a volume, or an app-specific resource. The app author should not need to care at first. They should think in terms of commands and views.
Use browser-loaded Automerge/Yjs/CRDT only for routes that earn it: rich text, whiteboards, local-first offline editing, cursors, multi-user drag interactions, or editor-grade undo. Your own micro-app comparison says Automerge should be a Lanix resource contract first, not a mandatory browser runtime; browser CRDTs are for true editing peers.
For the first year of iteration, I would make this the default:
Most portals:
CAS UI + view.json + commands + SSE
Serious collaborative editors:
CAS UI + CRDT bootstrap/sync route + resource commit bridge
Agents and native tasks:
file projection + command/resource APIs
“Alive” should not mean magic background processes everywhere. It should mean visible streams:
events
presence
agent suggestions
watch results
build status
approval requests
view invalidations
history/provenance
Lanix already has the right instincts here: #watch makes change notification a file you read, with a never-EOF event stream and version-based change records; the uploaded micro-app docs also standardize events/SSE as part of the resource facade. (Loopwork)
A portal should visibly breathe:
“Sam added milk”
“Agent drafted a better checklist UI”
“Build passed”
“New version available”
“Approval needed: publish to family”
“3 people viewing”
“Resource changed; view refreshed”
But every living thing should have an addressable cause. Your traceable namespace docs point in the right direction: agent work becomes trustworthy when mutations point back to the command, task, or agent turn that caused them, and when history can be mounted, diffed, forked, or restored. (Loopwork)
There are two different buttons:
Share room
invite people/collaborators into this live room
Publish
freeze or expose an artifact/service to an audience
Do not blur them.
For sharing, users are entering a room. They may get view/comment/edit/build roles. They can use commands and maybe agents, depending on policy.
For publishing, you produce a CAS artifact or bounded dynamic service. Static publishing should be boring: host points to CAS hash, rollback is repointing to an older hash, and agents can review diffs between artifacts. Your local/published note says exactly this: published apps are deploys, not operator rooms, and public visitors should not get #task, #agent, #cpu, qjs-shell, raw/native filesystem doors, route-table writes, or local portal APIs.
This maps well to current WebDoor/CAS work. WebDoor already composes static files and a mesh/resource API under one named localhost origin, avoiding CORS ceremony; CAS preview already freezes a static app into CAS and opens it in a sandboxed iframe through a narrow portal adapter. (Loopwork) (Loopwork)
The agent-native creation loop should feel like this:
User:
“make a shared grocery portal for the family”
System:
creates room family-groceries
chooses state kind: simple Automerge-backed checklist
creates command schemas: add, set-done, remove, rename
creates view.json and view.html conventions
generates a static UI artifact
pins it to CAS
opens portal in the canvas
starts event stream
Agent:
can inspect manifest/view/source
can propose code changes
can run build/test in a sandbox
can create a new CAS artifact
can ask approval to replace the active artifact
can ask approval to share/publish
Agent actions should use the same file-shaped approval pattern you already have: powerful actions park in pending, and nothing proceeds until a human writes an approval decision to ctl. (Loopwork)
The agent should not be an all-powerful invisible daemon. It should be a room member with tools, operating through room capabilities:
/room/manifest.json
/room/source/
/room/api/view.json
/room/api/commands/
/room/jobs/build
/room/approvals/pending
/room/history/
That is the product-level expression of “agents are the operators namespaces always needed”: agents read by cat, mutate by write, list by ls, spawn work through control files, and operate best in a confined namespace. (Loopwork)
I would implement this interface first:
trait RoomResource {
fn spec(&self) -> Json;
fn view(&self, name: &str, principal: Principal) -> View;
fn command(&self, name: &str, input: Json, principal: Principal) -> CommandResult;
fn events(&self, cursor: Option<EventCursor>, principal: Principal) -> EventStream;
fn project_fs(&self, principal: Principal) -> Box<dyn FileSystem>;
}Then build adapters:
HTTP adapter
GET /api/view.json
POST /api/commands/add
GET /api/events
Agent/WASI adapter
/room/api/view.json
/room/api/commands/add.schema.json
/room/api/events
Mesh adapter
one room/resource ticket
iroh transport
sync missing events/changes/resources
Browser adapter
CAS UI + fetch + EventSource
That gives you filesystem semantics where they are powerful, without committing to a general distributed filesystem cache as the center of the product.
In other words:
Fake the filesystem at the edges, but make the resource protocol real.
The current served-resource contract already says a served resource is one FileSystem packaged behind one network endpoint/ticket, and AppResource already has HTTP/filesystem surfaces. Keep that. Just make the greenfield product talk about rooms/portals/resources first, and file projection second. (Loopwork) (Loopwork)
Build app.localhost as the product root:
empty canvas
+ create portal button
+ prompt box
+ recent rooms
+ portal inspector
+ agent panel
Use an embedded local database or simple durable store for the room registry. Do not make the registry a distributed filesystem. It needs indexing, search, recent history, permissions, and artifact pointers.
Implement one room type with:
manifest.json
policy.json
view.json
commands/
events
artifact cas hash
source snapshot
history
Expose it both ways:
web:
/api/view.json
/api/commands/*
/api/events
file projection:
/room/...
A portal UI is always a CAS-pinned artifact:
source -> build -> dist -> cas hash -> active artifact pointer
The page can show current, previous, and draft artifacts. This makes agent edits safe: every proposal is a new immutable artifact, not mutation of a live blob. Your CAS device already has the right shape: write bytes, get a content hash, read by hash. (Loopwork)
Use exactly the existing grocery shape:
GET /
GET /api/view.json
GET /api/view.html
POST /api/commands/add
POST /api/commands/set-done
POST /api/commands/remove
GET /api/events
This proves the whole loop: cozy UI, real state, events, commands, CAS artifact, agent-readable contract, no Automerge JS in the browser. (Loopwork)
Let an agent:
read manifest/view/source
generate a new UI
run build
pin CAS artifact
open preview
ask approval to activate
No public sharing yet. No dynamic public execution yet. Just local cozy malleability.
After local rooms feel good, add:
share room with named people
role policy
principal-stamped commands
presence/events
Then add static publish:
host -> cas <hash>
Dynamic public apps should come later and require manifest review, explicit mounts, limits, and policy. Your uploaded local/published note is right that dynamic publishing should be manifest-first and should review authority rather than merely copying code.
-
The product unit is
Room, not filesystem, app, document, or worker. A room can contain all of those, but users and agents need one durable object to point at. -
The visual unit is
Portal. Portals can be embedded, nested, arranged, shared, and published. A page is a composition of portals. -
The default app shape is static/CAS UI + command/view/event API. This preserves normal web ergonomics while keeping state and authority visible to agents.
-
The default state interface is commands and materialized views, not raw FS writes and not browser CRDT. Use CRDTs only when the use case earns it.
-
The filesystem is a projection layer. Agents, WASI, mesh peers, shells, and debuggers get files. Browser users get pages and fetch/SSE.
-
Every generated thing is scoped by a manifest. Code can be vibe-generated because authority is not in the code; authority is in reviewed mounts, commands, and policy.
-
Every powerful agent action becomes an approval file/event. Agents propose; humans or policy agents approve. The approval boundary is visible and testable.
-
Public apps never inherit local operator powers. No
#task,#agent,#cpu, raw filesystem doors, or route-table writes for visitors. -
Iroh is for resource reach and sync, not for making the browser join a distributed filesystem. Browser talks HTTP/SSE. Native Lanix peers and agents can use iroh tickets and file/resource projections.
-
“Alive” means eventful, inspectable, and forkable. Presence, events, agent suggestions, build status, approvals, and history should be first-class UI elements.
Lanix Portal Fabric
A local-first web workspace where humans and agents create tiny living apps.
Each portal is a CAS-pinned UI bound to a durable room resource.
Each room exposes views, commands, events, source, policy, history, and approvals.
Agents operate the same room through file-shaped capabilities.
Wasm/qjs handlers run in scoped micro-sandboxes.
Automerge is used for real collaborative documents, not every render path.
Iroh shares resources between Lanix peers.
Publishing freezes artifacts or exposes bounded dynamic services with reviewed mounts.
That gives you the cozy personal software thing and the multi-user/agent system without forcing the hardest possible substrate problem to be solved first.
The most important reframing:
Do not build “a distributed filesystem with apps on top.” Build “rooms with portals,” then give every room a filesystem-shaped shadow so agents and runtimes can operate it.