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
59 changes: 56 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
version: 10.11.0
- uses: actions/setup-node@v4
with:
node-version: 20
Expand All @@ -46,7 +46,7 @@ jobs:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
version: 10.11.0
- uses: actions/setup-node@v4
with:
node-version: 20
Expand Down Expand Up @@ -129,11 +129,64 @@ jobs:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
version: 10.11.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install
- run: pnpm --filter @nnmodelling/mcp-server run build
- run: pnpm --filter @nnmodelling/mcp-server run test

flatpak-ci:
name: Flatpak build and smoke test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.11.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Test Electron host
run: pnpm --dir desktop test
- name: Install Flatpak tooling
run: |
sudo apt-get update
sudo apt-get install --yes dbus-daemon dbus-x11 flatpak flatpak-builder xauth xvfb
flatpak remote-add --if-not-exists --user flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Build and install Flatpak
run: |
flatpak-builder --force-clean --user \
--install-deps-from=flathub \
--repo=repo --install builddir \
io.github.LucaSforza.NNModelling.yml
- name: Validate installed Flatpak
run: |
flatpak info --user io.github.LucaSforza.NNModelling
test -x "$(flatpak info --user --show-location \
io.github.LucaSforza.NNModelling)/files/bin/run.sh"
- name: Smoke test Flatpak launch
run: |
set +e
timeout --signal=TERM --kill-after=5s 20s \
dbus-run-session -- xvfb-run -a -s "-screen 0 1280x1024x24" \
flatpak run --user io.github.LucaSforza.NNModelling --disable-gpu
status=$?
if [ "$status" -ne 124 ]; then
echo "Flatpak exited before the smoke-test timeout (status $status)"
exit "$status"
fi
- name: Build Flatpak bundle
run: |
flatpak build-bundle repo NNModelling.flatpak \
io.github.LucaSforza.NNModelling \
--runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo
- uses: actions/upload-artifact@v4
with:
name: nnmodelling-flatpak
path: NNModelling.flatpak
2 changes: 1 addition & 1 deletion .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 10
version: 10.11.0

- name: Set up Node.js
uses: actions/setup-node@v4
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ __pycache__/
dist/
front-end/dist/
mcp-server/dist/
desktop/release/
desktop/renderer/
.flatpak-builder/
builddir/
repo/
NNModelling.flatpak


docs2/build/
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ A visual editor and DSL for designing neural networks. Create diagrams in the br

The GitHub Pages demo contains the visual editor and runs entirely in the browser. Remote training, conversion, and MCP/browser integration require a local or separately deployed backend.

Linux users can also run the same editor as an Electron Flatpak. The Flatpak
contains the desktop shell and shared frontend; FastAPI, Valkey, Podman/Docker
and worker images remain in the separately managed backend. See the
[Linux desktop documentation](docs2/source/desktop.rst) for source builds,
installation and the exact `app://nnmodelling` backend origin.

```
Stereotypes/ (JSON) → Svelte Flow Editor → NNTree (JSON) → convert.py → Hydra YAML → main.py → Training
→ infer.py → Inference
Expand Down
14 changes: 14 additions & 0 deletions converted/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ NNM_ALLOWED_ORIGINS=http://192.168.1.30:5174 \
just --justfile converted/backend/justfile backend
```

The Linux Flatpak uses the same remote backend as the web editor. Its renderer
origin is exactly `app://nnmodelling`; include that origin when the backend
is used by the packaged application, while retaining the origins for any web
frontends that should remain connected:

```bash
NNM_ALLOWED_ORIGINS=http://127.0.0.1:5174,http://localhost:5174,app://nnmodelling \
just --justfile converted/backend/justfile backend
```

The Flatpak does not bundle FastAPI, Valkey, Podman/Docker, worker images or
training jobs. Pairing and bearer-token ownership are unchanged across the web
and desktop renderers. Do not replace the explicit origin list with `*`.

`backend` creates an untracked `backend-secrets/admin.token` with mode `0600` when
needed. Approve the code displayed by the frontend:

Expand Down
2 changes: 1 addition & 1 deletion converted/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ services:
NNM_BACKEND_PACKAGE_ROOT: ${NNM_HOST_JOBS:?set NNM_HOST_JOBS to the absolute host jobs directory}/packages
NNM_BACKEND_DATASET_ROOT: ${NNM_HOST_JOBS:?set NNM_HOST_JOBS to the absolute host jobs directory}/datasets
NNM_ADMIN_TOKEN_FILE: /run/secrets/nnm-admin-token
NNM_ALLOWED_ORIGINS: ${NNM_ALLOWED_ORIGINS:-http://127.0.0.1:5174,http://localhost:5174,http://127.0.0.1:5173,http://localhost:5173}
NNM_ALLOWED_ORIGINS: ${NNM_ALLOWED_ORIGINS:-http://127.0.0.1:5174,http://localhost:5174,http://127.0.0.1:5173,http://localhost:5173,app://nnmodelling}
NNM_SESSION_TTL: ${NNM_SESSION_TTL:-24h}
NNM_CONTAINER_ENGINE: "${NNM_CONTAINER_ENGINE:-podman}"
NNM_CONTAINER_IMAGE: ${NNM_CONTAINER_IMAGE:?set NNM_CONTAINER_IMAGE to a digest-pinned worker image}
Expand Down
1 change: 1 addition & 0 deletions converted/src/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"http://127.0.0.1:5174",
"http://localhost:5173",
"http://localhost:5174",
"app://nnmodelling",
]


Expand Down
53 changes: 53 additions & 0 deletions converted/src/tests/test_backend_origins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Frontend-origin configuration contracts for the backend API."""

from __future__ import annotations

import asyncio
from pathlib import Path
from types import SimpleNamespace

import httpx

from backend.app import DEFAULT_ALLOWED_ORIGINS, _allowed_origins_from_environment, create_app


def test_default_allowed_origins_include_the_flatpak_renderer() -> None:
"""The packaged renderer has a stable, exact CORS origin."""

assert "app://nnmodelling" in DEFAULT_ALLOWED_ORIGINS


def test_configured_allowed_origins_preserve_the_desktop_origin(monkeypatch) -> None:
"""Explicit deployment configuration remains authoritative and normalized."""

monkeypatch.setenv(
"NNM_ALLOWED_ORIGINS",
"https://editor.example, app://nnmodelling/",
)

assert _allowed_origins_from_environment() == [
"https://editor.example",
"app://nnmodelling",
]


def test_default_cors_allows_the_flatpak_renderer_origin() -> None:
"""A desktop preflight receives the exact origin without wildcard access."""

app = create_app(manager=SimpleNamespace(artifact_root=Path("datasets")))

async def exercise() -> httpx.Response:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://backend") as client:
return await client.options(
"/health",
headers={
"Origin": "app://nnmodelling",
"Access-Control-Request-Method": "GET",
},
)

response = asyncio.run(exercise())
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "app://nnmodelling"
assert response.headers.get("access-control-allow-credentials") is None
50 changes: 50 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@nnmodelling/desktop",
"private": true,
"version": "0.1.0",
"description": "NNModelling Electron desktop host",
"author": "Luca Sforza",
"license": "GPL-3.0-or-later",
"desktopName": "io.github.LucaSforza.NNModelling.desktop",
"main": "dist/main.js",
"type": "commonjs",
"scripts": {
"build:renderer": "VITE_DESKTOP=1 pnpm --dir ../front-end build && rm -rf renderer && cp -a ../front-end/dist renderer",
"build:host": "rm -rf dist && tsc -p tsconfig.json",
"build": "pnpm run build:renderer && pnpm run build:host && electron-builder --linux dir",
"start": "electron .",
"package:dir": "electron-builder --linux dir",
"test": "vitest run src"
},
"devDependencies": {
"@types/node": "^22.0.0",
"electron": "38.8.6",
"electron-builder": "26.15.3",
"typescript": "^5.8.3",
"vitest": "^4.1.7"
},
"build": {
"appId": "io.github.LucaSforza.NNModelling",
"productName": "NNModelling",
"files": [
"dist/**/*",
"renderer/**/*",
"package.json"
],
"directories": {
"output": "release"
},
"electronDownload": {
"unsafelyDisableChecksums": false,
"checksums": {
"electron-v38.8.6-linux-x64.zip": "41a8f2bc7c0be836b4dc8de19550a65947cffa58433753032af36ee3b849e8be",
"electron-v38.8.6-linux-arm64.zip": "add5aaa35ed57e60f84b780f6461064b912de55effd27ec11e338462243867a5"
}
},
"linux": {
"target": ["dir"],
"executableName": "nnmodelling",
"category": "Education"
}
}
}
20 changes: 20 additions & 0 deletions desktop/src/__tests__/security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from "vitest"
import { isAllowedNavigation, MAIN_WINDOW_WEB_PREFERENCES } from "../security"

describe("desktop renderer boundary", () => {
test("keeps Node outside the sandboxed, context-isolated renderer", () => {
expect(MAIN_WINDOW_WEB_PREFERENCES).toMatchObject({
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
})
})

test("allows only application content and the configured development origin", () => {
expect(isAllowedNavigation("app://nnmodelling/index.html")).toBe(true)
expect(isAllowedNavigation("https://example.com/")).toBe(false)
expect(isAllowedNavigation("http://127.0.0.1:5173/editor", "http://127.0.0.1:5173")).toBe(true)
expect(isAllowedNavigation("http://127.0.0.1:5174/", "http://127.0.0.1:5173")).toBe(false)
})
})
Loading
Loading