Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ NDO over a live DHT.

### Added

- **Moss tool scaffold.** Nondominium runs as a Moss tool in `weave` dev mode: a dev config
with two agents and one group (`bun run tool-dev`), `@theweave/api` in the UI, and a
connection mode that takes the conductor client from Moss. The standalone build is
unchanged. See `documentation/specifications/moss-integration.md`.
- **Lobby DNA** as the entry point. Groups are announced here for discovery, and NDOs are
browsed and filtered across every group the agent belongs to.
- **Group DNA, one cloned cell per group.** Each group is its own DHT with its own
Expand Down
133 changes: 132 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions documentation/DOCUMENTATION_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ nondominium implements a **Governance-as-Operator** architecture that separates
| **[Private Participation Receipts](specifications/governance/private-participation-receipt.md)** | PPR system full specification | ✅ Complete |
| **[Governance Model (Legacy)](specifications/governance/governance.md)** | Legacy governance model and decision-making processes | 📦 Reference |
| **[UI Architecture](specifications/ui_architecture.md)** | Frontend design patterns & component structure | ✅ Complete |
| **[Moss Integration](specifications/moss-integration.md)** | Running Nondominium as a Moss tool (v0.1.0 pilot): dev config, host seam, what Moss does to our roles | 🔄 In progress (#163 to #165) |
| **[ValueFlows Action Usage](specifications/VfAction_Usage.md)** | ValueFlows implementation with governance examples | ✅ Complete |
| **[Protocol Bridge Specifications](specifications/protocol-bridge-specifications.md)** | Bun Protocol Bridge architecture for platform integration (Tiki, Odoo) | ✅ Complete |
| **[API Reference](API_REFERENCE.md)** | Complete function reference across all zomes | ✅ Complete |
Expand Down
12 changes: 12 additions & 0 deletions documentation/IMPLEMENTATION_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,18 @@ There is no `get_all_groups` extern in a Group cell; each cell represents one is

---

## Moss Tool 🔄 In progress (v0.1.0 pilot, #163 to #165)

Nondominium runs as a Moss tool in `weave` dev mode. Standalone (Lobby, Group DNA, invite codes) is unchanged.

- ✅ **Scaffold (#163).** `weave/weave.dev.config.ts` (two agents, one group, tool served from `localhost:8888`), `bun run tool-dev`, `@theweave/api` in `ui/`, host detection and the Moss connection mode in `ui/src/lib/moss/`, an empty `AppletServices`. The UI connects through the applet client Moss hands it.
- ❌ **Moss group context (#164).** Profiles from Moss, roles from `myAccountabilitiesPerGroup()`, the instance's `group` cell as the Nondominium group, NDO clones through the `WeaveClient`. Not yet implemented: inside Moss the Lobby and invite-code flows are not adapted.
- ❌ **NDOs as assets (#165).** WAL, `getAssetInfo`, `search`, asset view.

See [moss-integration.md](specifications/moss-integration.md).

---

## NDO Federation Extensions ✅ Implemented (PR #103)

The governance zome includes three additional public entry families:
Expand Down
1 change: 1 addition & 0 deletions documentation/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- [NDO v1 Architecture Design](specifications/ndo-v1-architecture-design.md)
- [Architecture Components](ARCHITECTURE_COMPONENTS.md)
- [UI Architecture](specifications/ui_architecture.md)
- [Moss Integration](specifications/moss-integration.md)
- [VfAction Usage](specifications/VfAction_Usage.md)
- [Protocol Bridge Specifications](specifications/protocol-bridge-specifications.md)
- [API Reference](API_REFERENCE.md)
Expand Down
2 changes: 2 additions & 0 deletions documentation/specifications/lobby-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@ NdoView.svelte

> **Superseded in part (2026-11-03).** The feature map below assumes Moss replaces the Group DNA and the Lobby. The Group DNA now stays in both modes, and in Moss there is one Nondominium group per Moss group, the group cell of the tool instance, joined automatically. Read the group rows of the table through ADR-014 (`adr/ADR-014-work-contexts-in-group-dht.md`) and REQ-GROUP-19 to REQ-GROUP-23 in `../requirements/lobby-dna.md`.

> **Partly superseded (v0.1.0 Moss pilot).** [moss-integration.md](moss-integration.md) is authoritative. Differences from the table and the sketch below: there is no Nondominium Lobby Tool DHT in Moss (every tool instance in every Moss group is its own network, so the Lobby DNA's global seed does not survive as an inter-group lobby), the Moss group is the federation layer, the instance's provisioned `group` cell is the one Nondominium group of that Moss group, and `AppletServices` in `@theweave/api` `0.7.0-rc.3` has `search`, `getAssetInfo` and `creatables` only (opening an asset is a render view, not a method). The entry-point sketch below calls `get_all_ndo_descriptors` on `lobby`, which does not exist; NDOs come from the group cell's `NdoAnchor` entries.

When deployed inside Moss, Nondominium appears as **one Tool applet**. Moss handles: group
invites, agent identity at the surface, and the app sidebar. Nondominium owns its internal
complexity (Group DHTs, NDO DHTs, hard links, smart agreements).
Expand Down
61 changes: 61 additions & 0 deletions documentation/specifications/moss-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Moss Integration

**Status:** v0.1.0 pilot, in progress. The tool scaffold and dev config are implemented (#163). The Moss group context (#164) and NDOs as assets (#165) follow. Supersedes the "Moss Integration Contract" in [lobby-architecture.md](lobby-architecture.md) §9 where they differ.

Moss is the Weave host (lightningrodlabs/moss). In the v0.1.0 pilot Nondominium runs as a **tool** inside Moss, started with the `weave` CLI in dev mode. The standalone build (Lobby DNA, Group DNA, invite codes) keeps compiling and running unchanged; the pilot ships Moss only.

## Decisions

- The pilot runs `@theweave/cli` in dev mode with a shared dev config and our own bootstrap and relay. Not the Moss app, not a curation list.
- Inside Moss the Moss group is the federation layer. There is one Nondominium group per Moss group: the tool instance's `group` cell.
- Each NDO is a clone cell of the `ndo` role. Profiles come from Moss, roles from `myAccountabilitiesPerGroup()`, NDOs are exposed as Moss assets.
- v0.1.0 ships no work context.

## Running the tool in dev mode

Moss is built on Holochain 0.7.1 only in its `0.16` release candidates. The pilot uses `@theweave/cli` `0.16.0-rc.5`, which downloads a patched Holochain `0.7.1-mdns.8` that cannot see stock Holochain peers.

```bash
bun run build:happ # inside nix develop, produces workdir/nondominium.happ
bun run moss:install # installs weave/ (Electron and the weave CLI), once
bun run tool-dev # Vite on :8888 plus two Moss windows, Alice and Bob
bun run tool-dev-1 # the same with Alice only
```

`weave/` is a separate package outside the root workspaces so that CI and the standalone build never install Electron. `weave/weave.dev.config.ts` defines one group, `Nondominium Pilot`, created by agent 1 and joined by agent 2, with the tool served from `localhost:8888` and the `.happ` from `workdir/`.

Click the group in the Moss sidebar, then the Nondominium tool.

**Two machines.** Every `weave` call must pass the same `--bootstrap-url` and `--relay-url` of a server both machines reach (`kitsune2-bootstrap-srv` serves the relay on `/relay`), and the same CLI version. The group `networkSeed` in the shared config is what puts both agents in the same group.

### Gotchas

- `weave` loads the config through `ts-node` inside Electron. It needs TypeScript 5.x (TypeScript 7 has no JS API and the CLI crashes), `tslib` (not declared by the CLI) and a tsconfig with `"module": "commonjs"`. `weave/package.json` and `weave/tsconfig.json` carry all three.
- A tool served from localhost must call `initializeHotReload()` before `WeaveClient.connect()`, or `connect()` waits forever. `ui/src/lib/moss/connect.ts` does it when `detectHost` sees the dev-mode iframe.
- The group icon is limited to about 200 KB. `ui/static/icon.png` is a 128 px version of the logo.
- `weave` opens an Electron window per agent. On a shared desktop, a nested X server keeps them contained: `Xephyr :99 -screen 1500x950 &` then `DISPLAY=:99 bun run tool-dev`.

## What the Moss manifest does to our roles

Verified in the #162 spike (see [issue #162](https://github.com/Sensorica/nondominium/issues/162)). Moss installs `workdir/happ.yaml` unchanged, deferred roles included, but it installs `group` and `ndo` as **provisioned** cells next to `lobby`, `nondominium` and `hrea`. So every tool instance already has its own `group` cell, which is the instance's group DHT and the one Nondominium group of that Moss group, and an unused `ndo` cell. NDO clones come on top. Clone limits of 64 and 512 were not refused.

## The UI seam

```
hc-connect.ts connectHolochainClient()
detectCurrentHost() moss/host.ts standalone, or Moss (with or without hot reload)
connectMoss() moss/connect.ts WeaveClient.connect + applet client
standalone modes launcher, manifest, env (unchanged)
```

- `moss/host.ts` decides from the page itself: the `applet://` protocol or an injected `__WEAVE_API__` is a packaged tool; a dev-build iframe carrying Moss's iframe-kind hash is a tool in dev mode; anything else is standalone. The standalone path is not touched when the host is not Moss.
- `moss/connect.ts` runs hot reload when needed, connects the `WeaveClient` with our `AppletServices`, and returns the applet client Moss hands the tool, the group hash and the applet hash. Inside Moss the client signs through Moss, so none of the standalone signing-credential code runs.
- `moss/applet-services.ts` is the `AppletServices` object. In #163 it declares no creatables and no assets; #165 fills `search` and `getAssetInfo`.
- The connection result carries `moss` so later slices reach the `WeaveClient` through the client service (`holochainClient.moss`).

Moss mode is a connection mode (`HolochainConnectionMode` gains `'moss'`). The cross-group view is not implemented: a cross-group render throws.

## Open items

- `weaveClient.createCloneCell(req, true)` fails whenever `req.modifiers.properties` is set, which NDO clones need. See the #162 result and #164.
- `AppletServices` in `@theweave/api` `0.7.0-rc.3` has `getAssetInfo`, `search` and `creatables`. Opening an asset is not a method on it: Moss renders the tool with an `asset` view (`renderInfo.view.type === 'asset'`), and the tool calls `weaveClient.openAsset` to open other assets.
3 changes: 3 additions & 0 deletions documentation/specifications/ui_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ On top of this hierarchy, the **next** UI iteration adds a cross-cutting **Persp

Dependencies only point down: routes render components, components read stores, stores call services, services call the Holochain client. The domain layer sits beside the services and is imported by stores and components. The three rules that keep it that way are enforced by a script (see [§17](#17-architecture-invariants)).

**Moss host (`lib/moss/`).** When the UI runs inside Moss it is a tool, not a page that owns its conductor connection. `lib/moss/` holds that seam as plain TypeScript beside the service layer: `host.ts` (pure host detection), `connect.ts` (the `WeaveClient` connection, injectable for tests) and `applet-services.ts` (what the tool tells Moss about itself). Only `utils/hc-connect.ts` imports it, as one more connection mode (§15.4), so components, stores and services see the same `AppClient` whether the host is Moss or a standalone conductor. Details in [moss-integration.md](moss-integration.md).

---


Expand Down Expand Up @@ -740,6 +742,7 @@ Each agent gets a **dedicated port = dedicated origin**, which yields fully isol
| `launcher` | `window.__HC_LAUNCHER_ENV__` present | hc-spin Electron path (legacy/optional) |
| `manifest` | `/hc-connection.json` has agents | **Primary web path**; picks the entry matching `getDevAgentIndex()`; polls up to 5 min while conductors install |
| `env` | `VITE_HC_ADMIN_WS_URL` + `VITE_HC_APP_WS_URL` | manual override |
| `moss` | `detectCurrentHost()` reports Moss | Checked first. The UI runs as a Moss tool: `connectMoss()` returns the applet client Moss hands the tool; Moss owns the conductor and signing, so there is no admin URL and no credential authorization. See [moss-integration.md](moss-integration.md) |


In `manifest`/`env` modes the result carries the `adminWsUrl` so the UI can authorize signing credentials for **runtime-created group clone cells**.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"sweettest:verbose": "bun run build:happ && CARGO_TARGET_DIR=target/native-tests cargo test -p nondominium_sweettest -- --nocapture",
"sweettest:only": "CARGO_TARGET_DIR=target/native-tests cargo test -p nondominium_sweettest",
"launch:happ": "node scripts/launch-happ.mjs",
"moss:install": "bun install --cwd weave",
"tool-dev": "concurrently -k \"UI_PORT=8888 bun run --filter ui start\" \"bun run --cwd weave agent1\" \"bun run --cwd weave agent2\"",
"tool-dev-1": "concurrently -k \"UI_PORT=8888 bun run --filter ui start\" \"bun run --cwd weave agent1\"",
"e2e": "bun run --filter ui test:e2e",
"e2e:ui": "bun run --filter ui test:e2e:ui",
"package": "npm run build:happ && npm run --filter ui package && hc web-app pack workdir --recursive",
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@holochain/client": "^0.21.1",
"@msgpack/msgpack": "^3.0.0",
"@nondominium/shared-types": "workspace:*",
"@theweave/api": "0.7.0-rc.3",
"@valueflows/vf-graphql-holochain": "file:../vendor/valueflows-vf-graphql-holochain-0.700.0-rc.0.tgz",
"effect": "4.0.0",
"melt": "^0.44.0"
Expand Down
25 changes: 25 additions & 0 deletions ui/src/lib/moss/applet-services.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { AppletServices } from '@theweave/api';
import { createAppletServices } from './applet-services';

describe('createAppletServices', () => {
it('builds a Weave AppletServices object', () => {
expect(createAppletServices()).toBeInstanceOf(AppletServices);
});

it('declares no creatables in v0.1.0', () => {
expect(createAppletServices().creatables).toEqual({});
});

it('answers search with no assets until NDOs are exposed as assets', async () => {
const services = createAppletServices();
const result = await services.search({} as never, new Uint8Array(), {} as never, 'anything');
expect(result).toEqual([]);
});

it('answers getAssetInfo with nothing until NDOs are exposed as assets', async () => {
const services = createAppletServices();
const result = await services.getAssetInfo({} as never, { hrl: [new Uint8Array(), new Uint8Array()] });
expect(result).toBeUndefined();
});
});
9 changes: 9 additions & 0 deletions ui/src/lib/moss/applet-services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AppletServices } from '@theweave/api';

/**
* What Nondominium tells Moss about itself. v0.1.0 scaffold: no creatables and
* no assets. NDOs become assets (`search`, `getAssetInfo`) in the asset slice.
*/
export function createAppletServices(): AppletServices {
return new AppletServices();
}
69 changes: 69 additions & 0 deletions ui/src/lib/moss/connect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect, vi } from 'vitest';
import type { AppClient } from '@holochain/client';
import { connectMoss, type MossDeps } from './connect';

function fakeDeps(renderInfo: unknown): { deps: MossDeps; calls: string[] } {
const calls: string[] = [];
const deps: MossDeps = {
initializeHotReload: vi.fn(async () => {
calls.push('hot-reload');
}),
connect: vi.fn(async () => {
calls.push('connect');
return { renderInfo } as never;
}),
createAppletServices: vi.fn(() => ({ marker: 'services' }) as never)
};
return { deps, calls };
}

const appletClient = { appInfo: vi.fn() } as unknown as AppClient;
const appletView = {
type: 'applet-view',
view: { type: 'main' },
appletClient,
groupHash: new Uint8Array([1, 2, 3]),
appletHash: new Uint8Array([4, 5, 6])
};

describe('connectMoss', () => {
it('runs hot reload before connecting when the host asks for it', async () => {
const { deps, calls } = fakeDeps(appletView);
await connectMoss({ hotReload: true }, deps);
expect(calls).toEqual(['hot-reload', 'connect']);
});

it('skips hot reload in a packaged tool', async () => {
const { deps, calls } = fakeDeps(appletView);
await connectMoss({ hotReload: false }, deps);
expect(calls).toEqual(['connect']);
});

it('hands the applet services to the Weave client', async () => {
const { deps } = fakeDeps(appletView);
await connectMoss({ hotReload: false }, deps);
expect(deps.connect).toHaveBeenCalledWith({ marker: 'services' });
});

it('returns the applet client and the group the tool instance lives in', async () => {
const { deps } = fakeDeps(appletView);
const connection = await connectMoss({ hotReload: false }, deps);
expect(connection.appClient).toBe(appletClient);
expect(connection.groupHash).toEqual(new Uint8Array([1, 2, 3]));
expect(connection.appletHash).toEqual(new Uint8Array([4, 5, 6]));
});

it('refuses a cross-group render, which this tool does not implement', async () => {
const { deps } = fakeDeps({ type: 'cross-group-view', view: { type: 'main' }, applets: new Map() });
await expect(connectMoss({ hotReload: false }, deps)).rejects.toThrow(/cross-group/i);
});

it('surfaces a hot reload failure instead of hanging on connect', async () => {
const { deps } = fakeDeps(appletView);
deps.initializeHotReload = vi.fn(async () => {
throw new Error('no parent frame');
});
await expect(connectMoss({ hotReload: true }, deps)).rejects.toThrow(/no parent frame/);
expect(deps.connect).not.toHaveBeenCalled();
});
});
54 changes: 54 additions & 0 deletions ui/src/lib/moss/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { AppClient, DnaHash, EntryHash } from '@holochain/client';
import {
initializeHotReload as weaveInitializeHotReload,
WeaveClient,
type AppletServices
} from '@theweave/api';
import { createAppletServices } from './applet-services';

export interface MossConnection {
weaveClient: WeaveClient;
/** The conductor client Moss hands the tool. Signing is handled by Moss. */
appClient: AppClient;
/** Hash of the Moss group this tool instance is installed in. */
groupHash: DnaHash | null;
appletHash: EntryHash;
}

/** The Weave calls this module makes, injectable so the seam is testable. */
export interface MossDeps {
initializeHotReload: () => Promise<void>;
connect: (services: AppletServices) => Promise<WeaveClient>;
createAppletServices: () => AppletServices;
}

const defaultDeps: MossDeps = {
initializeHotReload: weaveInitializeHotReload,
connect: (services) => WeaveClient.connect(services),
createAppletServices
};

/**
* Connects to Moss. `hotReload` is true in `weave` dev mode, where the tool is
* served from localhost: Moss only injects its API after the tool asks for the
* iframe script, and `WeaveClient.connect` waits for that forever otherwise.
*/
export async function connectMoss(
options: { hotReload: boolean },
deps: MossDeps = defaultDeps
): Promise<MossConnection> {
if (options.hotReload) {
await deps.initializeHotReload();
}
const weaveClient = await deps.connect(deps.createAppletServices());
const info = weaveClient.renderInfo;
if (info.type !== 'applet-view') {
throw new Error('Nondominium does not implement the Moss cross-group view.');
}
return {
weaveClient,
appClient: info.appletClient,
groupHash: info.groupHash,
appletHash: info.appletHash
};
}
Loading
Loading