Task board: the pilot app with an untrusted UI
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.task-board is a small Lattice App that ships its own UI. It is the Explorer
sample's pilot for the whole app path: a manifest with a presentation and a
ui section, a bundle that runs in a sandboxed frame, and discovery from the
in-image app source. Its cards are JSON values stored under tasks/{id} in the
app's one tree, tasks.
What is in the folder
| Path | Role |
|---|---|
src/manifest.json |
The manifest: one tree, declared for replication, the viewer and editor roles, the presentation, and the UI bundle with its digests and bridge operations. |
src/ui/index.html |
The entry fragment, inserted into the frame's body. |
src/ui/app.css |
The app's stylesheet, layered on the AppKit kit stylesheet (lattice-app.css). |
src/ui/app.mjs |
The app's one ES module. It is self-contained and talks to the Explorer only through globalThis.lattice. |
src/ui/icon.svg |
The catalogue icon, shown through an <img> element only. |
src/TaskBoardApp.cs |
The slug and embedded-resource names the silo registers the app with. |
test/ |
TaskBoard.Tests: the manifest validates, every digest matches, the in-image source enumerates and serves the app, and the bundle stays self-contained. |
There is no build toolchain. The bundle is plain HTML, CSS and one ES module,
embedded into the class library as resources. The digests in manifest.json
are checked, not produced: after editing a bundle file, run
dotnet test samples/Explorer/Apps/TaskBoard/test/TaskBoard.Tests.csproj
and paste the values the failing The_manifest_digests_match_the_bundle_files
test prints into manifest.json. The bundle files are pinned to LF line
endings, because the digests are over their exact bytes.
How the sample registers it
The Explorer sample adds the app to the silo's in-image source with one line:
silo.AddLatticeApp(TaskBoardApp.Slug, TaskBoardApp.Assembly, TaskBoardApp.ManifestResourceName);
Registering an app only makes it available. Nothing is installed, consented or enabled until an administrator does so in the Explorer.
Walkthrough
The walkthrough switches between identities in one run, and the sample keeps everything in memory, so start it signed out:
dotnet run --project samples/Explorer/Explorer.csproj -- --sign-in-as none
Sign in as explorer-admin (any password) from the console's Sign in
dialog, then:
Find it in the catalogue. Open Apps. The source selector lists
in-imagebeside All sources; choosein-imageand the Available filter. Task board appears with its icon, display name and summary. All of that text comes from the manifest and is shown as plain text.Review consent. Open the entry and choose Install.... The review shows what the app asks for, drawn against its own
a/task-board/namespace:- one tree,
tasks; - two roles,
viewer(Read,RangeRead) andeditor(Read,RangeRead,Write,Delete), both scoped totasks; - replication of
tasks, last-writer-wins (see Tenants); - six bridge operations for its UI:
context.read,data.read,data.write,data.delete,nav.syncandui.notify, the data ones limited totasks.
Nothing reaches outside the app's namespace, so there are no exceptions to approve. The app never asks for
context.user, so it never learns your name. It learns only which of its own roles you hold, throughcontext.read.- one tree,
Bind roles to groups. Bind
editorto thetask-editorsgroup andviewerto thetask-viewersgroup. Leave thevisitorsgroup unbound.Install, then enable. Install records the consent and the bindings; enabling the install is what lets role holders open it.
Open it. Sign out, then sign in as a member of
task-editors(alice), open Apps, then Task board, then its Open tab. The board loads in a sandboxed frame. Add a task, select it, move it between To do, Doing and Done, and delete it. Selecting a card updates the address line, so the link to a task can be copied and reopened. The board is drawn in Paper whatever the console's theme: the Explorer currently gives every app frame the default appearance and does not tell it when the console's theme changes.
The same app, three groups
| Signed in as | Group | Role | What they see |
|---|---|---|---|
alice |
task-editors |
editor |
The full board: add, move and delete controls. |
bob |
task-viewers |
viewer |
The same board, read-only. context.read reports only viewer, so the add, move and delete controls are not shown, and a note says the role cannot change the board. |
carol |
visitors |
none | Nothing. Task board is not in her apps at all, and its address resolves as not found. |
Tenants
An app is installed per tenant: each install has its own consent, role
bindings and lifecycle, and its trees live in that tenant's namespace. The
manifest's one tree, tasks, is a/task-board/tasks in the default tenant and
t/{tenant}/a/task-board/tasks in any other.
The manifest also declares tasks for replication, last-writer-wins. Declaring
it enrols nothing by itself: installing and enabling the app enrols that
install's tree, through the cluster's runtime replication control. On the
Explorer sample's two-region estate that control is on, so every install's
tasks tree replicates between east and west; with --minimal there is no
peer and the declaration is inert.
The sample installs the task board in tenant acme at startup, the way a
tenant's own install would be recorded: editor bound to acme-editors
(acme-admin), and three cards on the board. Its tree,
t/acme/a/task-board/tasks, is listed in Replication > Enrolled trees as a
runtime enrolment, and its link to west is live. As the operator, /t/acme/data
lists it as an app tree.
The walkthrough above installs a second, independent copy. An install from the
console lands in the tenant its address names: the walkthrough starts at
/t/default/apps, where the operator's console opens, so its copy is in the
default tenant, which is why alice, bob and carol, who belong to no tenant,
see it. The same steps at /t/globex/apps would install a copy in globex
instead. The default tenant's copy and acme's copy share nothing: separate
consent, separate bindings, separate boards.
How the UI stays untrusted
- The frame is sandboxed with an opaque origin and a policy that forbids every
network connection. The module never calls
fetch, opens a socket, reads storage or cookies, or touches the parent window; a test enforces that. - Every read and write goes through
lattice.request(...)to the Explorer's broker and then to the cluster, which allows it only when the app's consented grants, an app role the signed-in user holds by binding, and the user's own rights all allow it. An operator who can write every tree still gets a read-only board when bound only as aviewer. - The frame learns which of this app's roles the signed-in user holds from the
rolesmember ofcontext.read: role names only, never their groups. The board starts read-only and shows its write controls only when that list holdseditor, the one role in its manifest that may write. A viewer's list holdsviewer, so a viewer never sees a control the bridge would refuse. If a write is still refused (a role unbound mid-session, say), the board drops back to read-only. A host that sends norolesleaves the board read-only. The role list only hides or shows controls; the cluster enforces every write. - Stored values are treated as untrusted input: a card that does not parse is skipped, and titles are always rendered as text.
- The layout reflows from three columns to one on a 360px screen, every control is a 44px touch target, and nothing depends on hover.