Skip to content

Latest commit

 

History

History
674 lines (499 loc) · 24.4 KB

File metadata and controls

674 lines (499 loc) · 24.4 KB

My read: you are pointing at a “portal fabric,” not a distributed filesystem

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.


What I would keep from current Lanix

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)


The product metaphor: rooms and portals

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)


The architecture I would build

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)


The core object: Room

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.


The key design move: commands, not arbitrary writes

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.


The runtime: Worker-shaped, namespace-backed

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)


State: three levels, not one global answer

I would define three state tiers.

1. Materialized view state

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.

2. Commanded durable state

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.

3. True collaborative replica state

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

What “alive” means technically

“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)


Sharing and publishing: keep the split sacred

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 builder loop

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)


The filesystem answer in one design rule

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)


What I would build first

Slice 1: Web shell and room registry

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.

Slice 2: Portal room contract

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/...

Slice 3: CAS UI artifacts

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)

Slice 4: First generated app: family grocery/chores

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)

Slice 5: Agent edits portal UI

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.

Slice 6: Share room, then publish static

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.


Opinionated decisions I would make now

  1. 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.

  2. The visual unit is Portal. Portals can be embedded, nested, arranged, shared, and published. A page is a composition of portals.

  3. 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.

  4. 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.

  5. The filesystem is a projection layer. Agents, WASI, mesh peers, shells, and debuggers get files. Browser users get pages and fetch/SSE.

  6. 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.

  7. Every powerful agent action becomes an approval file/event. Agents propose; humans or policy agents approve. The approval boundary is visible and testable.

  8. Public apps never inherit local operator powers. No #task, #agent, #cpu, raw filesystem doors, or route-table writes for visitors.

  9. 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.

  10. “Alive” means eventful, inspectable, and forkable. Presence, events, agent suggestions, build status, approvals, and history should be first-class UI elements.


The version of Lanix I would iterate toward

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.