feat: ship download-ready Nova Desktop Node#3
Conversation
|
CI note from Codex: the initial GitHub returned this check-run annotation:
Local and standalone-export verification passed before publishing; rerun the workflow after the GitHub billing/account lock is cleared. |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
| let config = { | ||
| nodeUrl: "http://localhost:8000", | ||
| model: "qwen2.5-coder:3b", | ||
| }; |
There was a problem hiding this comment.
🔴 Desktop app points at the wrong port and shows the node as offline out of the box
The desktop program defaults its backend address to port 8000 (nodeUrl: "http://localhost:8000" at desktop/core/node-client.js:2, also the fallback at desktop/core/node-client.js:91), but the governed Node backend actually serves all of its endpoints on port 8080, so a freshly downloaded desktop cannot reach the node until the user manually edits the settings.
Impact: After following the quickstart, the desktop shows "Node offline" and no tools, receipts, replay, or status work until the URL is changed by hand, breaking the advertised ten-minute setup.
Port mismatch between desktop client and node server
The backend is launched via python -m nova.api, whose main() binds NOVA_PORT defaulting to 8080 (nova/api.py:651), and all node routes (/node/status, /node/tool, etc.) are registered on that same app. Every user-facing doc states the Node API is http://127.0.0.1:8080 (README services table, WINDOWS-DESKTOP.md:43, GITHUB-10-MINUTE-START.md:74). Port 8000 is the optional AAIS service, not the node. desktop/renderer/status.js calls window.nodeAPI.status() which hits ${config.nodeUrl}/node/status = http://localhost:8000/node/status on startup and every 5s; the request fails, so the heartbeat turns red and the panel shows "Node offline". Nothing in settings.js, model-select.js, or index.js overrides the default to 8080, so the mismatch persists until the user types a new URL in the settings strip.
| let config = { | |
| nodeUrl: "http://localhost:8000", | |
| model: "qwen2.5-coder:3b", | |
| }; | |
| let config = { | |
| nodeUrl: "http://localhost:8080", | |
| model: "qwen2.5-coder:3b", | |
| }; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| app.include_router(node_status_routes.router) | ||
| app.include_router(node_event_routes.router) | ||
| app.include_router(node_agent_routes.router) | ||
| app.include_router(node_evidence_routes.router) | ||
| app.include_router(node_federation_routes.router) | ||
| app.include_router(node_submit_routes.router) | ||
| app.include_router(node_tool_routes.router) | ||
| app.include_router(node_replay_routes.router) | ||
| app.include_router(node_mesh_routes.router) | ||
| app.include_router(node_alert_routes.router) | ||
| app.include_router(node_policy_routes.router) | ||
| app.include_router(node_conformance_routes.router) |
There was a problem hiding this comment.
🟨 Governed tool and evidence endpoints bypass the API-key gate
In nova/api.py, only the directly-decorated routes (/node/status, /node/submit, /node/result) use Depends(_require_api_key). All the router-based endpoints included afterwards — /node/tool, /node/replay/{trace_id}, /node/verify/{trace_id}, /node/compare-receipts, /node/events, /node/evidence-bundle, /node/gossip, etc. — are registered with no authentication dependency. When an operator sets NOVA_API_KEY expecting the node to be protected, the model-invoking coding tool endpoint (nova/node/tools/routes.py:38) and the federation/gossip endpoints remain fully open.
Was this helpful? React with 👍 or 👎 to provide feedback.
| signing_secret="local-api-secret", | ||
| provider=_build_provider(), | ||
| ) | ||
| turn = llm.ask( |
There was a problem hiding this comment.
🟨 Governance receipts signed with a hardcoded HMAC secret
The in-process Lawful LLM receipts are HMAC-signed using a constant secret baked into the source: signing_secret="local-api-secret" in nova/api.py:431-432, signing_secret="local-dev-secret" in nova/cli.py, and "gate-secret"/"chain-gate-secret" in the productization gate. Because the signing key is public in the repository, the receipt_verified / verify_receipt result provides no real tamper-evidence: anyone can forge a valid signature for an arbitrary receipt payload.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def sign_payload(payload: dict[str, Any], private_key: dict[str, Any] | None = None) -> str | None: | ||
| key = private_key or load_operator_private_key() | ||
| if not key: | ||
| return None | ||
| n = int(str(key["n"]), 0) | ||
| d = int(str(key["d"]), 0) | ||
| digest = int.from_bytes(_canonical_digest(payload), "big") % n | ||
| signature = pow(digest, d, n) | ||
| byte_len = max(1, (n.bit_length() + 7) // 8) | ||
| return base64.b64encode(signature.to_bytes(byte_len, "big")).decode("ascii") | ||
|
|
||
|
|
||
| def verify_payload_signature( | ||
| payload: dict[str, Any], | ||
| signature: str | None, | ||
| public_key: dict[str, Any] | None = None, | ||
| ) -> bool: | ||
| key = public_key or load_operator_public_key() | ||
| if not key or not signature: | ||
| return False | ||
| try: | ||
| n = int(str(key["n"]), 0) | ||
| e = int(str(key["e"]), 0) | ||
| sig_int = int.from_bytes(base64.b64decode(signature), "big") | ||
| digest = int.from_bytes(_canonical_digest(payload), "big") % n | ||
| return pow(sig_int, e, n) == digest | ||
| except Exception: | ||
| return False |
There was a problem hiding this comment.
🟨 Home-rolled textbook RSA (no padding) used for federation trust signatures
nova/node/identity.py implements RSA sign/verify manually: sign_payload computes pow(digest, d, n) and verify_payload_signature checks pow(sig_int, e, n) == digest, where digest = int.from_bytes(sha256(...)) % n. This is textbook RSA with no padding scheme (no PKCS#1 v1.5 / PSS), and the digest is reduced modulo n. These signatures gate peer trust_level decisions and policy-hash consensus in federation.py/mesh.py.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def load_receipt(trace_id: str) -> dict[str, Any]: | ||
| path = runtime_dir() / "tool-receipts" / f"{trace_id}.json" | ||
| if not path.exists(): | ||
| raise FileNotFoundError(f"Receipt not found: {path}") | ||
| return json.loads(path.read_text(encoding="utf-8")) |
There was a problem hiding this comment.
🟨 trace_id path parameter interpolated directly into filesystem paths
nova/node/tools/replay_coding.py:12-16 builds runtime_dir() / "tool-receipts" / f"{trace_id}.json" directly from the caller-supplied trace_id, and nova/node/submit.py similarly builds results/{trace_id}.json. These are reached from unauthenticated path routes like /node/verify/{trace_id} and /node/replay/{trace_id}. If a trace_id containing path traversal sequences reaches the loader, it could read/write files outside the intended runtime directory.
Was this helpful? React with 👍 or 👎 to provide feedback.
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data:;"> |
There was a problem hiding this comment.
🟨 Electron renderer CSP allows inline scripts alongside broad filesystem bridge
desktop/renderer/index.html:5 sets a CSP of script-src 'self' 'unsafe-inline', while desktop/preload.js exposes fileAPI.readFile, fileAPI.writeFile, and fileAPI.createProject (arbitrary paths) to the renderer. The renderer also builds DOM via innerHTML in several panels. 'unsafe-inline' weakens the primary XSS defense; if any injected/model-returned content reaches an unescaped sink, it could execute and drive the exposed file read/write bridge.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if [[ -f "$HOME/.novarc" && ! -f "$HOME/.novarc.bak" ]]; then | ||
| cp "$HOME/.novarc" "$HOME/.novarc.bak" | ||
| warn "Backed up $HOME/.novarc" | ||
| fi | ||
| cp "$REPO_ROOT/config/.novarc" "$HOME/.novarc" | ||
| success "Deployed: $HOME/.novarc" |
There was a problem hiding this comment.
🟨 Bootstrap overwrites the operator's ~/.novarc which repo policy marks as sacred
setup/bootstrap.sh:106-111 now unconditionally copies the repo template over $HOME/.novarc (cp "$REPO_ROOT/config/.novarc" "$HOME/.novarc"). AGENTS.md explicitly lists ~/.novarc (and ~/.novarc.ps1, .env, .env.local) under Never: "Overwrite ... these are sacred." Although a one-time .bak backup is made, the live user config (which can contain operator paths/secrets) is still clobbered on every run.
Was this helpful? React with 👍 or 👎 to provide feedback.
What
Publishes the updated Lawful Nova shell as a source-only, download-ready standalone branch for
agentic-coding-agent.This branch includes:
nova/nodedesktopGITHUB-10-MINUTE-START.mdfor clone-to-running setupWhy
The goal is that someone can download or clone this repository from GitHub and have the Node plus desktop shell working in about ten minutes, without cloud orchestration, committed binaries, node_modules, or generated zip artifacts.
How to run
Windows:
macOS/Linux:
chmod +x quickstart.sh bin/nova setup/*.sh ./quickstart.shThen open:
http://127.0.0.1:8080npm startfromdesktop/Verification
Source workspace:
..\.venv\Scripts\python.exe -m pytest tests -q-> 61 passed, 1 warningnpm testindesktop/-> 8 passedStandalone export:
E:\project-infi\.venv\Scripts\python.exe -m pytest tests -q-> 61 passed, 1 warningnpm ciindesktop/-> installed cleanly, 0 vulnerabilitiesnpm testindesktop/-> 8 passedgit diff --cached --checkbefore commit -> OKNotes
This is intentionally a source-only export of
lawful-nova-shellinto the repository root. Generated zips, Electron build outputs, runtime state,node_modules, and packaged executables are excluded. The included GitHub workflow can produce downloadable source artifacts from the branch.