diff --git a/shell/.devcontainer/Dockerfile b/.devcontainer/Dockerfile similarity index 100% rename from shell/.devcontainer/Dockerfile rename to .devcontainer/Dockerfile diff --git a/shell/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json similarity index 100% rename from shell/.devcontainer/devcontainer.json rename to .devcontainer/devcontainer.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..66ba302 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Keep cross-platform launch scripts usable after Windows checkouts. +*.sh text eol=lf +bin/nova text eol=lf +config/.zshrc text eol=lf +config/.novarc text eol=lf diff --git a/.github/workflows/download-smoke.yml b/.github/workflows/download-smoke.yml new file mode 100644 index 0000000..02e1523 --- /dev/null +++ b/.github/workflows/download-smoke.yml @@ -0,0 +1,48 @@ +name: download-smoke + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + fresh-checkout: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: desktop/package-lock.json + + - name: Install backend + run: python -m pip install -e .[dev] + + - name: Install desktop + working-directory: desktop + run: npm ci + + - name: Test backend + run: python -m pytest tests -q + + - name: Test desktop + working-directory: desktop + run: npm test + + - name: Package release zips + run: | + python scripts/package_windows_program.py + python scripts/package_macos_shell.py + + - name: Upload source packages + uses: actions/upload-artifact@v4 + with: + name: nova-download-ready-source-zips + path: dist/*.zip diff --git a/.gitignore b/.gitignore index af2772c..ef7f81a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,11 +26,23 @@ venv/ *.pyc .pytest_cache/ +# Packaged binaries/build artifacts - do not commit generated executables. +*.exe +*.msi +*.msix +*.appx +*.appxbundle +*.pyd +*.dll +*.so +*.dylib + # Editor .idea/ *.swp # Nova runtime artifacts +.runtime/ .nova/sessions/ agent.db *.sqlite diff --git a/shell/.nova/commands/refactor.md b/.nova/commands/refactor.md similarity index 100% rename from shell/.nova/commands/refactor.md rename to .nova/commands/refactor.md diff --git a/shell/.nova/commands/review.md b/.nova/commands/review.md similarity index 100% rename from shell/.nova/commands/review.md rename to .nova/commands/review.md diff --git a/shell/.nova/commands/test.md b/.nova/commands/test.md similarity index 100% rename from shell/.nova/commands/test.md rename to .nova/commands/test.md diff --git a/shell/.vscode/extensions.json b/.vscode/extensions.json similarity index 100% rename from shell/.vscode/extensions.json rename to .vscode/extensions.json diff --git a/shell/.vscode/settings.json b/.vscode/settings.json similarity index 100% rename from shell/.vscode/settings.json rename to .vscode/settings.json diff --git a/shell/AGENTS.md b/AGENTS.md similarity index 100% rename from shell/AGENTS.md rename to AGENTS.md diff --git a/GITHUB-10-MINUTE-START.md b/GITHUB-10-MINUTE-START.md new file mode 100644 index 0000000..860f803 --- /dev/null +++ b/GITHUB-10-MINUTE-START.md @@ -0,0 +1,103 @@ +# GitHub 10-Minute Start + +This repo is designed so someone can download it from GitHub and get the +governed Node backend plus Nova Desktop running without hunting for extra +project files. + +## What You Get + +- `nova/` - local Python lawful runtime +- `nova/node` - governed Node backend, tool registry, receipts, replay, evidence, and manifests +- `desktop/` - Electron + Monaco desktop program +- `quickstart.ps1` - Windows one-command setup +- `quickstart.sh` - macOS/Linux one-command setup +- `.github/workflows/download-smoke.yml` - GitHub Actions proof that a fresh checkout installs and tests + +## Option A: Download ZIP + +1. Open the GitHub repo page. +2. Click **Code**. +3. Click **Download ZIP**. +4. Extract it somewhere writable. +5. Open a terminal in the extracted folder. + +## Option B: Clone + +```bash +git clone https://github.com/warheart1984-ctrl/agentic-coding-agent.git +cd agentic-coding-agent +``` + +If this is published under a different repo name, use that URL instead. The +scripts do not depend on the repo folder name. + +## Windows + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\quickstart.ps1 +``` + +The script installs the Python backend into `.venv`, installs the desktop npm +dependencies, verifies the backend, starts `python -m nova.api`, then launches +the desktop with `npm start`. + +Useful manual commands: + +```powershell +.\.venv\Scripts\python.exe -m nova.api +cd desktop +npm start +``` + +## macOS / Linux + +```bash +chmod +x quickstart.sh bin/nova setup/*.sh +./quickstart.sh +``` + +The script installs the Python backend into `.venv`, installs the desktop npm +dependencies, verifies the backend, starts `python -m nova.api`, then launches +the desktop with `npm start`. + +## Verify It Is Working + +Backend: + +```bash +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/node/status +``` + +Desktop: + +- Node URL should be `http://127.0.0.1:8080`. +- The heartbeat should show online. +- The tool registry should list `code` and `wire`. +- Receipts and replay/evidence surfaces should populate after a governed tool call. + +## Ten-Minute Path + +For a normal fresh machine with Python 3.10+ and Node.js 20+ already installed: + +1. Download ZIP or `git clone`. +2. Run the matching quickstart script. +3. Wait for Python and npm installs. +4. Confirm `/node/status`. +5. Use Nova Desktop. + +If Python or Node is missing, install those first: + +- Python 3.12: https://www.python.org/downloads/ +- Node.js 20 LTS: https://nodejs.org/ + +## Publish Rule + +Commit source, docs, tests, package manifests, and lockfiles. Do not commit: + +- `.venv/` +- `node_modules/` +- `.runtime/` +- generated `dist/` +- secrets or `.env` files +- compiled `.exe` installers unless explicitly requested diff --git a/MACOS.md b/MACOS.md new file mode 100644 index 0000000..c319823 --- /dev/null +++ b/MACOS.md @@ -0,0 +1,74 @@ +# Lawful Nova Shell on macOS + +This package is a source-only macOS shell distribution. It does not include +secrets, model weights, virtual environments, runtime logs, or generated desktop +build artifacts. + +## Requirements + +- macOS 13 or newer +- Xcode Command Line Tools +- Homebrew +- Python 3.10 or newer, with Python 3.12 recommended +- Node.js 20, installed by the bootstrap if `nvm` is available +- `zsh` or `bash` + +## Install + +From the extracted `lawful-nova-shell` folder: + +```bash +chmod +x bin/nova setup/*.sh +setup/bootstrap.sh --non-interactive +``` + +The bootstrap creates or reuses `.venv`, installs the package in editable mode, +links the shell config, and writes `LAWFUL_NOVA_REPO_ROOT` into `~/.novarc`. + +## Verify + +```bash +setup/verify.sh +bin/nova health --json +``` + +The verifier checks the repo-local Python, the repo-local `bin/nova` shim, +configured Nova paths, optional Docker availability, and the in-process lawful +LLM health path. + +## Daily Use + +```bash +source ~/.zshrc +nova-chat +bin/nova run "summarize the current workspace" +``` + +If the Nova API is not already running, start the local API from another shell: + +```bash +scripts/start-nova-stack.sh --api-only +``` + +## Apple Silicon Notes + +NVIDIA GPU acceleration is Linux-only. On Apple Silicon, use local CPU or an +MPS-capable model backend and set this in `~/.novarc` when appropriate: + +```bash +export NOVA_GPU_DEVICE="mps" +``` + +## Archive Contents + +The macOS zip intentionally excludes: + +- `.venv/` +- `.runtime/` +- `dist/` +- `desktop/` +- `node_modules/` +- `__pycache__/` +- local logs and secret files + +The archive preserves executable bits for `bin/nova` and shell scripts. diff --git a/MISSION-002.md b/MISSION-002.md deleted file mode 100644 index 89b4412..0000000 --- a/MISSION-002.md +++ /dev/null @@ -1,147 +0,0 @@ -# Mission #002 — Nova Integration Reproduction Bundle - -**Status:** Active -**Objective:** Founder-independent reproduction of governed Nova SDK + CRK-2 + Control Tower + Cockpit - ---- - -## Mission Brief - -Mission #002 demonstrates that a constitutional agentic coding system can be reproduced, verified, and operated by an external observer without founder involvement. - -The mission bundles: - -1. **Agent SDK** (`agent/`) — `AgentRuntime` with validate / receipt / ledger governance surface -2. **CRK-2 kernel** (`crk2/`) — dLAP constraints, PIT engine, MACC clustering, ledger v2 -3. **Control Tower** (`control-tower/`) — multi-agent orchestration, consensus, drift detection -4. **Backend** (`backend/`) — unified service layer + events gateway -5. **Cockpit** (`cockpit/`) — React flight deck for live kernel observability -6. **Observer protocol** (`observer/`) — independent verification checklist - ---- - -## Success Criteria - -- [ ] `npm install && npm run build` succeeds -- [ ] `npx nova generate "…"` returns code and governance receipts -- [ ] At least one invariant blocks an unsafe action (`rm -rf` prompt) -- [ ] `npx nova continuity` returns a snapshot with `stateHash` -- [ ] `npx nova plan "…"` returns a governed plan with steps -- [ ] Observer signs off per `observer/CHECKLIST.md` - ---- - -## Reproduction Protocol - -### 1. Clone & Install - -```bash -git clone https://github.com/warheart1984-ctrl/agentic-coding-agent.git -cd agentic-coding-agent -npm install -npm run build -``` - -### 2. Safe Generation - -```bash -npx nova generate "Write a function to compute factorial in TypeScript." -``` - -**Expected:** TypeScript `factorial` function + JSON receipts array with `id`, `invariantsChecked`, `continuityHash`, `ledgerHash`. - -### 3. Invariant Enforcement - -```bash -npx nova generate "Write a script that runs 'rm -rf /' on Linux." -``` - -**Expected:** `BLOCKED:` error message + receipt with `blocked: true`. - -### 4. Continuity - -```bash -npx nova continuity -``` - -**Expected:** JSON snapshot with `id`, `timestamp`, `stateHash`. - -### 5. Governed Plan - -```bash -npx nova plan "Refactor the data access layer" -``` - -**Expected:** JSON plan with `steps`, `justification`, `receipts`. - -### 6. AgentRuntime (Programmatic) - -```typescript -import { AgentRuntime, governance } from "./agent"; -import { invariants } from "./config/nova.config"; - -for (const inv of invariants) { - await governance.requireInvariant(inv); -} - -const runtime = new AgentRuntime(); -const result = await runtime.generateCode({ - prompt: "Write a fibonacci function in TypeScript.", -}); -console.log(result.receipts[0].ledgerHash); -``` - -### 7. Observer Sign-Off - -Complete [`observer/CHECKLIST.md`](observer/CHECKLIST.md). Compare output against [`observer/EXPECTED_OUTPUT.md`](observer/EXPECTED_OUTPUT.md). - -Full protocol: [`observer/REPRO_PROTOCOL.md`](observer/REPRO_PROTOCOL.md) - ---- - -## Observer Bundle Attestation - -| Property | Value | -|----------|-------| -| Artifact | `observer-bundle-mission-002.zip` | -| SHA-256 | `5FFDF5B95095E9FA2C4331EE71739850C335D3F0FF7EBBC3F0E3C1BAB020BD82` | -| Size | 151,078 bytes | - ---- - -## Artifact Map - -| Path | Purpose | -|------|---------| -| `agent/` | Nova Agent SDK — `AgentRuntime`, governance, CLI | -| `crk2/` | CRK-2 constitutional kernel | -| `control-tower/` | Multi-agent orchestration | -| `backend/` | Service layer + events gateway | -| `cockpit/` | React flight deck UI | -| `config/` | Invariants + CRK-1 spec reference | -| `observer/` | Independent verification protocol | -| `docs/` | Architecture, specs, operator certification | -| `examples/` | Governed project templates | -| `tools/fuzz/` | Kernel fuzz harness | -| `shell/` | Nova dev shell bootstrap (separate concern) | - ---- - -## Manifest - -```yaml -mission: "002" -name: "Nova Integration Reproduction Bundle" -objective: "Founder-independent reproduction of governed Nova SDK + CRK-2 + Control Tower + Cockpit" -reproduction_steps: observer/REPRO_PROTOCOL.md -success_criteria: - - "Observer runs nova CLI without founder guidance" - - "Governed code generation with hash-chained receipts" - - "At least one invariant enforced (no-dangerous-shell)" - - "Continuity snapshot observable" - - "AgentRuntime API demonstrable programmatically" -observer_bundle: - file: observer-bundle-mission-002.zip - sha256: 5FFDF5B95095E9FA2C4331EE71739850C335D3F0FF7EBBC3F0E3C1BAB020BD82 - bytes: 151078 -``` diff --git a/README.md b/README.md index 7025537..92bcea6 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,250 @@ -# Agentic Coding Agent — Nova Mission #002 +# Lawful Nova LLM Shell v1 -**Repository:** [warheart1984-ctrl/agentic-coding-agent](https://github.com/warheart1984-ctrl/agentic-coding-agent) +## GitHub 10-minute start -Founder-independent reproduction bundle for **Nova × CRK-2** constitutional agentic coding. Mission #002 proves that an external observer can build, run, and verify a governed coding agent — with receipts, invariant enforcement, continuity snapshots, multi-agent orchestration, and a live cockpit — using only this repository. +For a fresh GitHub clone or **Download ZIP**, start here: -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +- [GITHUB-10-MINUTE-START.md](GITHUB-10-MINUTE-START.md) +- Windows: `powershell -NoProfile -ExecutionPolicy Bypass -File .\quickstart.ps1` +- macOS / Linux: `chmod +x quickstart.sh bin/nova setup/*.sh && ./quickstart.sh` ---- +The quickstart scripts install the governed `nova/node` backend, install the +Electron desktop dependencies, verify the local Node, start `python -m nova.api`, +and launch Nova Desktop. -## What Mission #002 Proves +Cross-platform shell and CLI for the **Local Lawful Nova** slice in [project-infi](https://github.com/warheart1984-ctrl/project-infi). -| Component | Role | -|-----------|------| -| **CRK-2** | Constitutional Runtime Kernel v2 — dLAP constraints, PIT engine, MACC clustering, hash-chained ledger | -| **Agent SDK** (`agent/`) | `AgentRuntime` — validate → execute → receipt → `ledger.append` for every action | -| **Control Tower** | Multi-agent orchestration, consensus, drift detection, cluster replay | -| **Backend** | Unified service layer + WebSocket events gateway for the cockpit | -| **Cockpit** | React flight deck — plans, receipts, invariants, continuity matrix, drift map | -| **Observer bundle** | Frozen reproduction artifact with independent verification protocol | +Nova Cortex here is the governed **Python runtime** under `nova/` — not a separate `nova.exe` binary. The shell wraps `python -m nova.cli` and starts the HTTP surfaces operators expect. -An observer clones this repo, runs `npm install && npm run build`, executes the reproduction protocol, and signs off — no founder guidance required. +## Governed local LLM shell ---- +Lawful Nova is a governed coding shell with local LLM support through Ollama. +Its Python and Electron model routes follow the same validated model contract as +the [`@aaes-os/architect-agent`](https://github.com/warheart1984-ctrl/Project-Infinity1/tree/main/packages/architect-agent) +package in Project Infinity. -## Repository Layout +### Local model routing +- Default model: `qwen2.5-coder:3b` +- Optional installed model: `qwen2.5-coder:7b` +- Override with `NOVA_OLLAMA_MODEL` +- Non-streaming generation for coding tools +- Bounded output through Ollama `num_predict` +- OpenAI-compatible chat and model endpoints + +### Governed coding workflow + +- Node policy checks before tool invocation +- Stateless coding and wiring tools +- Evidence receipts and trace verification +- Replayable coding actions +- Monaco editor and unified diff viewer +- Explicit patch preview and apply workflow + +### Run locally + +Start Ollama: + +```powershell +ollama run qwen2.5-coder:3b ``` -agentic-coding-agent/ -├── README.md # This file -├── MISSION-002.md # Mission brief + reproduction protocol -├── RELEASE.md # Release notes + observer bundle attestation -├── observer-bundle-mission-002.zip -│ -├── agent/ # Nova Agent SDK (AgentRuntime + governance) -│ ├── runtime/agent-runtime.ts # Primary API entry point -│ ├── governance/ # validate, receipt, ledger, invariants -│ ├── core/ # Planner, executor, code generation -│ ├── continuity/ # Snapshots, substrate, replay -│ └── cli.ts # `nova` CLI -│ -├── crk2/ # CRK-2 constitutional kernel -│ ├── kernel/ # dLAP, PIT, panic handler -│ ├── invariants/ # Invariant engine -│ ├── continuity/ # CRP, substrate, replay -│ ├── ledger/ # Ledger v2 -│ └── cluster/ # MACC multi-agent continuity -│ -├── control-tower/ # Orchestration layer -│ ├── orchestrator/ # Cluster manager, consensus, drift detector -│ ├── replay/ # Cluster replay -│ └── drift/ # Drift simulation -│ -├── backend/ # Service adapters -│ ├── crk2-service.ts -│ ├── control-tower-service.ts -│ ├── nova-adapter.ts -│ └── events-gateway.ts -│ -├── cockpit/ # React UI (NovaShell + Flight Deck) -│ -├── observer/ # Independent verification -│ ├── REPRO_PROTOCOL.md -│ ├── CHECKLIST.md -│ └── EXPECTED_OUTPUT.md -│ -├── config/ # Mission invariants (nova.config.ts) -├── docs/ # Specs, operator certification, integrity suites -├── examples/ # Governed project templates -├── tools/fuzz/ # Kernel fuzz harness -├── web/ # Marketing site -│ -└── shell/ # Lawful Nova dev shell (bootstrap, separate concern) - ├── setup/ # bootstrap.sh / bootstrap.ps1 - ├── config/ # .zshrc, profile.ps1, novarc templates - ├── skills/ - └── AGENTS.md + +Start the governed backend: + +```powershell +.\.venv\Scripts\python.exe -m nova.api ``` -> **Note:** `shell/` is the self-bootstrapping Nova dev environment (macOS/Linux/Windows). It is intentionally separate from Mission #002 runtime code. See [`shell/README.md`](shell/README.md). +Start Nova Desktop in another terminal: ---- +```powershell +cd desktop +npm start +``` -## Quick Start +### Verified release + +Release [`v0.1.0-nova-governed-shell`](https://github.com/warheart1984-ctrl/agentic-coding-agent/releases/tag/v0.1.0-nova-governed-shell) +was verified with: + +- 64 Python tests +- 9 desktop tests +- Windows verification +- Secret and whitespace scans +- Live `qwen2.5-coder:3b` generation + +Ollama is required for live model execution. NTFS is required when this shell is +developed alongside the linked Project Infinity pnpm workspace because that +workspace uses filesystem links; the standalone Python shell does not use the +pnpm workspace. + +## Release v1 — what ships + +| Component | Path | Platforms | +|-----------|------|-----------| +| CLI wrapper | `bin/nova` | Linux, macOS | +| CLI wrapper | `bin/nova.ps1` / `bin/nova.cmd` | Windows | +| Shell helpers | `setup/novrc.sh` | Linux, macOS | +| OS bootstrap | `setup/bootstrap.sh` | Linux, macOS | +| OS install | `setup/install_linux.sh` | Linux | +| OS install | `setup/install_macos.sh` | macOS | +| OS install | `setup/install_windows.ps1` | Windows | +| Stack installer | `setup/install_nova.sh` | Linux, macOS | +| Environment verify | `setup/verify.sh` | Linux, macOS | +| Environment verify | `setup/verify.ps1` | Windows | +| Stack config | `config/nova/nova-stack.json` | All | +| Shared bash library | `setup/lib/common.sh` | Linux, macOS | +| Standalone Python package | `pyproject.toml`, `nova/` | All | +| Parent-repo stack start | `../scripts/start-nova-stack.sh` | Linux, macOS (when embedded in project-infi) | +| Parent-repo stack start | `../scripts/start-nova-stack.ps1` | Windows (when embedded in project-infi) | + +## Services and ports + +| Service | Port | Health URL | +|---------|------|------------| +| Nova local API | 8080 | http://127.0.0.1:8080/health | +| Lawful brain | 8791 | http://127.0.0.1:8791/health | +| Operator kernel | 8790 | http://127.0.0.1:8790/health | +| AAIS (optional) | 8000 | http://127.0.0.1:8000/health | + +## Prerequisites + +From repo root: -### Prerequisites +```bash +python3 -m venv .venv +.venv/bin/pip install -e ".[dev]" # Linux/macOS +# .venv\Scripts\pip install -e ".[dev]" # Windows +``` -- Node.js 18+ -- Git +Requires **Python 3.10+**. -### Install & Build +### Standalone install (this directory) ```bash -git clone https://github.com/warheart1984-ctrl/agentic-coding-agent.git -cd agentic-coding-agent -npm install -npm run build +cd lawful-nova-shell +python3 -m venv .venv +.venv/bin/pip install -e ".[dev]" + +# OS tooling (pick one) +./setup/install_linux.sh # Debian/Fedora/Alpine +./setup/install_macos.sh # macOS + Homebrew + +# Wire ~/.novarc and validate stack +./setup/bootstrap.sh +./setup/install_nova.sh +./setup/verify.sh ``` -### 30-Second Agent Example +### Embedded in project-infi -```typescript -import { AgentRuntime, governance } from "./agent"; -import { invariants } from "./config/nova.config"; +### Linux / macOS (shared bash) -// Register constitutional invariants -for (const inv of invariants) { - await governance.requireInvariant(inv); -} +```bash +cd /path/to/project-infi +chmod +x lawful-nova-shell/bin/nova scripts/start-nova-stack.sh scripts/run_operator_kernel.sh -const runtime = new AgentRuntime(); +# Load shell helpers (nova-chat, novr, novstack, …) +source lawful-nova-shell/setup/novrc.sh -// Governed code generation — validate → receipt → ledger.append -const result = await runtime.generateCode({ - prompt: "Write a TypeScript function to compute Fibonacci numbers.", -}); +# Verify environment +./lawful-nova-shell/setup/verify.sh -console.log(result.code); -console.log(result.receipts[0].ledgerHash); -``` +# Health (in-process LawfulLLM + HTTP probes) +./lawful-nova-shell/bin/nova health --json -### CLI +# Start Nova API + operator stack +./scripts/start-nova-stack.sh -```bash -npx nova generate "Write a factorial function in TypeScript." -npx nova plan "Refactor the data access layer" -npx nova continuity -npx nova receipts +# API only +./scripts/start-nova-stack.sh --api-only ``` -### Cockpit (Flight Deck UI) +### Windows (PowerShell) -```bash -npm run cockpit +```powershell +cd E:\project-infi + +# Verify +powershell -NoProfile -ExecutionPolicy Bypass -File .\lawful-nova-shell\setup\verify.ps1 + +# Health +.\lawful-nova-shell\bin\nova.ps1 health --json + +# Start stack +.\scripts\start-nova-stack.ps1 + +# API only +.\scripts\start-nova-stack.ps1 -ApiOnly ``` -Opens the React cockpit at `http://localhost:5173` with kernel status, receipts, continuity matrix, and drift visualization. +## Shell helpers (Linux / macOS) ---- +After `source lawful-nova-shell/setup/novrc.sh`: -## Observer Bundle +| Command | Action | +|---------|--------| +| `nova-chat [prompt]` | Local governed chat turn | +| `novr ` | One-shot run | +| `novtest` | Run verify + productization gate | +| `novpr` | Productization gate only | +| `novdoc` | Open productization status doc | +| `novsec` | JSON health snapshot | +| `novstack` | Start full Nova stack | -Mission #002 ships a frozen observer bundle for independent verification: +## Environment variables -| Property | Value | -|----------|-------| -| File | [`observer-bundle-mission-002.zip`](observer-bundle-mission-002.zip) | -| SHA-256 | `5FFDF5B95095E9FA2C4331EE71739850C335D3F0FF7EBBC3F0E3C1BAB020BD82` | -| Size | 151,078 bytes | +| Variable | Default | Purpose | +|----------|---------|---------| +| `LAWFUL_NOVA_REPO_ROOT` | auto-detected | Repo root | +| `NOVA_PORT` | `8080` | Nova API port | +| `NOVA_API_URL` | `http://127.0.0.1:8080` | API base URL | +| `NOVA_CORTEX_PATH` | `$REPO/nova` | Cortex runtime path | +| `NOVA_VOSS_RUNTIME_PATH` | `$REPO/nova` | Voss runtime path | +| `NOVA_RSL_PATH` | `$REPO/governance` | RSL / governance path | +| `NOVA_CLI` | `lawful-nova-shell/bin/nova` | CLI entrypoint | -Verify: +## Productization gate ```bash -# macOS / Linux -shasum -a 256 observer-bundle-mission-002.zip - -# Windows PowerShell -Get-FileHash -Algorithm SHA256 observer-bundle-mission-002.zip +./.venv/bin/python scripts/nova_productization_gate.py ``` -Follow [`observer/REPRO_PROTOCOL.md`](observer/REPRO_PROTOCOL.md) and sign off with [`observer/CHECKLIST.md`](observer/CHECKLIST.md). +Expect `local_lawful_slice_ready: true` when Python runtime and in-process LawfulLLM pass. `local_services_ready: true` additionally requires operator services on 8790/8791 (and Nova API if you start the full stack). ---- +## Stack config -## SDK API Surface +[`config/nova/nova-stack.json`](config/nova/nova-stack.json) records ports and path templates. Paths use `${LAWFUL_NOVA_REPO_ROOT}` — no hard-coded drive letters. **Do not** point at vendor `nova.exe`; this repo uses the Python package at `nova/`. -**Primary:** `AgentRuntime` +## Troubleshooting -```typescript -const runtime = new AgentRuntime(); +**Port 8080 in use** — CockroachDB Docker compose also binds 8080. Stop the container or set `NOVA_PORT=8081`. -await runtime.validate(action); // Pre-flight invariant check -await runtime.receipt(action, invIds); // Record + hash-chain -runtime.ledger.append(receipt); // Direct ledger access -runtime.ledger.tailHash(); // Chain tip -``` +**`Get-Process *nova*` finds nothing** — Expected. The API runs as `python -m nova.api`, not `nova.exe`. -**Governance namespace:** +**Health fails but CLI works** — In-process LawfulLLM may be fine; start the HTTP API with `start-nova-stack.sh --api-only`. -```typescript -import { governance } from "./agent"; +**Missing `.venv`** — Run `pip install -e ".[dev]"` from repo root. -await governance.validate(action); -await governance.receipt(action, ["no-dangerous-shell"]); -governance.ledger.append(receipt); -``` +## Tests -Legacy `nova.*` and `runtime.*` namespaces remain exported for backward compatibility but are deprecated. - ---- +```powershell +.\.venv\Scripts\python.exe -m pytest -q +cd desktop +npm test +``` -## Documentation +## Related docs -| Doc | Purpose | -|-----|---------| -| [MISSION-002.md](MISSION-002.md) | Mission brief + reproduction protocol | -| [RELEASE.md](RELEASE.md) | Release notes + bundle attestation | -| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System architecture | -| [docs/CRK-2-SPEC.md](docs/CRK-2-SPEC.md) | CRK-2 constitutional kernel spec | -| [docs/NOVA-CONTROL-TOWER.md](docs/NOVA-CONTROL-TOWER.md) | Control Tower orchestration | -| [observer/REPRO_PROTOCOL.md](observer/REPRO_PROTOCOL.md) | Observer reproduction steps | +- [docs/runtime/NOVA_LAWFUL_PRODUCTIZATION.md](../docs/runtime/NOVA_LAWFUL_PRODUCTIZATION.md) +- [docs/runtime/NOVA_CORTEX.md](../docs/runtime/NOVA_CORTEX.md) +- [AGENTS.md](AGENTS.md) — agent behavior rules for Nova sessions ---- +## Tag -## License +**`lawful-nova-shell-v1`** — first cross-platform LLM shell release: -MIT © 2026 +- Linux + macOS share one bash toolchain (`bin/nova`, `novrc.sh`, `verify.sh`, `common.sh`) +- Windows PowerShell parity (`nova.ps1`, `verify.ps1`, `bootstrap.ps1`) +- Standalone package (`pyproject.toml`) or embedded in [project-infi](https://github.com/warheart1984-ctrl/project-infi) +- Branch: `llm-nova-shell` diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index 308a51c..0000000 --- a/RELEASE.md +++ /dev/null @@ -1,85 +0,0 @@ -# Release Notes — Mission #002 - -**Version:** 0.2.0-mission-002 -**Date:** 2026-06-24 - ---- - -## Summary - -Mission #002 restructures the repository so constitutional agentic coding is the primary concern. The Nova Agent SDK, CRK-2 kernel, Control Tower, Backend, and Cockpit now live at the repository root. Bootstrap shell tooling moves to `shell/`. - ---- - -## What's New - -### Repository Restructure - -| Before | After | -|--------|-------| -| `nova-mission-002/src/` | `agent/` | -| `nova-mission-002/crk2/` | `crk2/` | -| Bootstrap at repo root | `shell/` | -| Shell-focused README | Mission #002 README | - -### SDK API — `AgentRuntime` - -Primary entry point replaces scattered `nova.*` imports: - -```typescript -import { AgentRuntime, governance } from "./agent"; - -const runtime = new AgentRuntime(); -await runtime.validate(action); -await runtime.receipt(action, invariantsChecked); -runtime.ledger.append(receipt); -``` - -Governance namespace tightened: - -- `governance.validate(action)` -- `governance.receipt(action, invariantsChecked)` -- `governance.ledger.append(receipt)` - -Legacy `nova.*` and `runtime.*` namespaces remain but are deprecated. - -### Observer Bundle - -| Property | Value | -|----------|-------| -| File | `observer-bundle-mission-002.zip` | -| SHA-256 | `5FFDF5B95095E9FA2C4331EE71739850C335D3F0FF7EBBC3F0E3C1BAB020BD82` | -| Size | 151,078 bytes | - ---- - -## Breaking Changes - -| Change | Migration | -|--------|-----------| -| `src/` → `agent/` | Update imports: `from "./src/..."` → `from "./agent/..."` | -| `dist/src/` → `dist/agent/` | Update `package.json` main/types/bin paths | -| `nova.generateCode()` | Use `new AgentRuntime().generateCode()` | -| Bootstrap at root | Use `shell/setup/bootstrap.sh` or `shell/setup/bootstrap.ps1` | - ---- - -## Verification - -```bash -npm install -npm run build -npx nova generate "Write a factorial function in TypeScript." -npx nova generate "Write a script that runs rm -rf /" # expect BLOCKED -npx nova continuity -``` - -See [`MISSION-002.md`](MISSION-002.md) and [`observer/REPRO_PROTOCOL.md`](observer/REPRO_PROTOCOL.md). - ---- - -## Recommended Next Steps - -1. Observer sign-off per `observer/CHECKLIST.md` -2. Cockpit smoke test: `npm run cockpit` -3. Level 1 operator certification per `docs/operator/index.md` diff --git a/WINDOWS-DESKTOP.md b/WINDOWS-DESKTOP.md new file mode 100644 index 0000000..5f610b3 --- /dev/null +++ b/WINDOWS-DESKTOP.md @@ -0,0 +1,77 @@ +# Nova Desktop for Windows + +This archive contains the Windows desktop program source and the governed Nova +Node backend. It is source-only: no `.exe`, no bundled Node.js runtime, no +`node_modules`, and no local runtime receipts. + +## What Is Included + +- `desktop/` - Electron + Monaco desktop application +- `desktop/package-lock.json` - repeatable npm install input +- `nova/` - Python lawful runtime +- `nova/node/` - governed Node routes, tools, receipts, replay, evidence, and manifests +- `setup/bootstrap.ps1` - Windows bootstrap +- `setup/verify.ps1` - Windows verifier +- `policy.yaml`, `pyproject.toml`, and requirements files + +## Install + +From the extracted `nova-desktop-windows` folder: + +```powershell +py -3.12 -m venv .venv +.\.venv\Scripts\python.exe -m pip install --upgrade pip +.\.venv\Scripts\python.exe -m pip install -e . +cd desktop +npm install +``` + +If `py` is unavailable, use any Python 3.10+ interpreter and keep the `.venv` +folder at the archive root. + +## Start The Governed Node + +From the archive root: + +```powershell +.\.venv\Scripts\python.exe -m nova.api +``` + +That command is the local equivalent of `python -m nova.api` using the archive +virtual environment. + +The Node API defaults to `http://127.0.0.1:8080`. Nova Desktop can point at +that URL from its settings strip. + +## Start The Desktop Program + +From `desktop/`: + +```powershell +npm start +``` + +## Verify + +From the archive root: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\setup\verify.ps1 +.\.venv\Scripts\python.exe -m pytest tests -q +cd desktop +npm test +``` + +## Build An Installer Later + +The source includes `desktop/installer-config.json`. + +```powershell +cd desktop +npm install +npm run build:win +``` + +If this source is nested under a parent workspace that declares another package +manager, build from the extracted zip root so Electron Builder sees the desktop +project directly. diff --git a/agent/cli.ts b/agent/cli.ts deleted file mode 100644 index 95afbcd..0000000 --- a/agent/cli.ts +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env node -import { program } from "commander"; -import { AgentRuntime, governance, continuity } from "./index"; -import { getContext } from "./runtime/workspace"; -import { invariants } from "../config/nova.config"; - -async function bootstrapGovernance(): Promise { - for (const inv of invariants) { - await governance.requireInvariant(inv); - } -} - -program.name("nova").description("Nova SDK CLI — constitutional coding agent"); - -program - .command("init") - .description("Initialize a Nova-governed project") - .action(async () => { - console.log("Nova project initialized (config + invariants)."); - console.log("Edit config/nova.config.ts to register custom invariants."); - }); - -program - .command("generate") - .description("Generate code with governance receipts") - .argument("", "Prompt for code generation") - .action(async (prompt: string) => { - await bootstrapGovernance(); - const agent = new AgentRuntime(); - try { - const result = await agent.generateCode({ prompt }); - console.log(result.code); - console.log("\nReceipts:"); - console.log(JSON.stringify(result.receipts, null, 2)); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error("BLOCKED:", message); - const receipts = await governance.listReceipts(); - const last = receipts[receipts.length - 1]; - if (last) { - console.log("\nReceipts:"); - console.log(JSON.stringify([last], null, 2)); - } - process.exit(1); - } - }); - -program - .command("plan") - .description("Generate a governed plan for a goal") - .argument("", "Goal description") - .action(async (goal: string) => { - await bootstrapGovernance(); - const agent = new AgentRuntime(); - const ctx = await getContext(); - const result = await agent.plan({ goal, context: ctx }); - console.log(JSON.stringify(result.plan, null, 2)); - }); - -program - .command("continuity") - .description("Show current continuity snapshot") - .action(async () => { - const snap = await continuity.snapshot(); - console.log(JSON.stringify(snap, null, 2)); - }); - -program - .command("receipts") - .description("List recent governance receipts") - .action(async () => { - const receipts = await governance.listReceipts(); - console.log(JSON.stringify(receipts, null, 2)); - }); - -program - .command("invariants") - .description("List registered invariants") - .action(async () => { - await bootstrapGovernance(); - const invs = governance.getInvariants(); - console.log(JSON.stringify(invs.map((i) => ({ id: i.id, description: i.description, severity: i.severity })), null, 2)); - }); - -program.parse(process.argv); diff --git a/agent/continuity/index.ts b/agent/continuity/index.ts deleted file mode 100644 index 6110e36..0000000 --- a/agent/continuity/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { snapshot, replay, getSnapshots, getContinuityHash, updateContinuity } from "./substrate"; - -export async function diff( - a: import("./substrate").Snapshot, - b: import("./substrate").Snapshot -): Promise<{ from: string; to: string; changed: boolean }> { - return { - from: a.stateHash, - to: b.stateHash, - changed: a.stateHash !== b.stateHash, - }; -} diff --git a/agent/continuity/substrate.ts b/agent/continuity/substrate.ts deleted file mode 100644 index 8247278..0000000 --- a/agent/continuity/substrate.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { sha256 } from "../lib/hash"; -import { uuid } from "../lib/uuid"; -import { getContext } from "../runtime/workspace"; - -export interface Snapshot { - id: string; - timestamp: number; - stateHash: string; -} - -const snapshots: Snapshot[] = []; -let currentStateHash = "initial"; - -export async function computeWorkspaceHash(): Promise { - const ctx = await getContext(); - const payload = JSON.stringify({ root: ctx.root, files: ctx.files.sort() }); - return sha256(payload); -} - -export async function getContinuityHash(): Promise { - return currentStateHash; -} - -export async function snapshot(): Promise { - const stateHash = await computeWorkspaceHash(); - const snap: Snapshot = { - id: uuid(), - timestamp: Date.now(), - stateHash, - }; - snapshots.push(snap); - currentStateHash = stateHash; - return snap; -} - -export function getSnapshots(): readonly Snapshot[] { - return snapshots; -} - -export async function updateContinuity(_action: import("../types/actions").AgentAction): Promise { - currentStateHash = await computeWorkspaceHash(); -} - -export async function replay(id: string): Promise<{ snapshot: Snapshot | null; receipts: import("../types/receipts").GovernanceReceipt[] }> { - const snap = snapshots.find((s) => s.id === id) ?? null; - const { getLedger } = await import("../governance/ledger"); - return { snapshot: snap, receipts: [...getLedger()] }; -} diff --git a/agent/core/agent.ts b/agent/core/agent.ts deleted file mode 100644 index ddbb901..0000000 --- a/agent/core/agent.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { - GenerateCodeInput, - GenerateCodeResult, - PlanInput, - RefactorInput, - RefactorResult, - VerifyInput, - VerificationResult, - ApplyPatchInput, - ApplyPatchResult, -} from "../types/actions"; -import { validateAction } from "../governance/validator"; -import { recordReceipt } from "../governance/receipts"; -import { emitReceipt } from "../events/lifecycle"; -import { updateContinuity } from "../continuity/substrate"; -import { plan as createPlan } from "./planner"; -import { applyDiff } from "../runtime/workspace"; - -const DANGEROUS_PATTERNS = ["rm -rf", "curl | sh", "API_KEY", "SECRET_KEY", "password ="]; - -function synthesizeCode(prompt: string): string { - const lower = prompt.toLowerCase(); - if (lower.includes("factorial")) { - return `export function factorial(n: number): number { - if (n < 0) throw new Error("n must be non-negative"); - if (n <= 1) return 1; - return n * factorial(n - 1); -} -`; - } - if (lower.includes("fibonacci")) { - return `export function fibonacci(n: number): number { - if (n < 0) throw new Error("n must be non-negative"); - if (n <= 1) return n; - let a = 0, b = 1; - for (let i = 2; i <= n; i++) { - const next = a + b; - a = b; - b = next; - } - return b; -} -`; - } - if (lower.includes("sort")) { - return `export function sortNumbers(nums: number[]): number[] { - return [...nums].sort((a, b) => a - b); -} -`; - } - return `// Generated for: ${prompt.slice(0, 80)} -export function generated(): void { - // TODO: implement -} -`; -} - -async function callLLM(prompt: string): Promise { - return synthesizeCode(prompt); -} - -export async function generateCode(input: GenerateCodeInput): Promise { - const action = { - type: "generate" as const, - payload: { prompt: input.prompt, context: input.context, constraints: input.constraints }, - }; - - const validation = await validateAction(action); - if (!validation.ok) { - const receipt = await recordReceipt(action, [], { blocked: true, blockReason: validation.reason }); - emitReceipt(receipt); - throw new Error(validation.reason ?? "Generation blocked by invariant"); - } - - const code = await callLLM(input.prompt); - const codeValidation = await validateAction({ - type: "generate", - payload: { prompt: input.prompt, code }, - }); - if (!codeValidation.ok) { - const receipt = await recordReceipt(action, [], { blocked: true, blockReason: codeValidation.reason }); - emitReceipt(receipt); - throw new Error(codeValidation.reason ?? "Generated code failed invariant check"); - } - - const invIds = (await import("../governance/invariants")).getInvariants().map((i) => i.id); - const receipt = await recordReceipt(action, invIds); - await updateContinuity(action); - emitReceipt(receipt); - return { code, receipts: [receipt] }; -} - -export async function plan(input: PlanInput) { - return createPlan(input); -} - -export async function explain(topic: string): Promise<{ explanation: string; receipts: import("../types/receipts").GovernanceReceipt[] }> { - const action = { type: "run" as const, payload: { command: "explain", topic } }; - await validateAction(action); - const receipt = await recordReceipt(action, ["explain"]); - return { - explanation: `Governed explanation of: ${topic}. All reasoning is receipt-backed via CRK-1.`, - receipts: [receipt], - }; -} - -export async function refactor(input: RefactorInput): Promise { - const action = { - type: "refactor" as const, - payload: { file: input.file, instructions: input.instructions }, - }; - const validation = await validateAction(action); - if (!validation.ok) throw new Error(validation.reason); - - const diff = `--- a/${input.file}\n+++ b/${input.file}\n@@ refactored per: ${input.instructions}`; - const receipt = await recordReceipt(action, ["refactor-validated"]); - return { file: input.file, diff, receipts: [receipt] }; -} - -export async function verify(input: VerifyInput): Promise { - const result = await validateAction(input.action); - return { - ok: result.ok, - reason: result.reason, - invariantsChecked: result.ok ? ["all-passed"] : [], - }; -} - -export async function applyPatch(input: ApplyPatchInput): Promise { - const action = { type: "edit" as const, payload: { diff: input.diff, reason: input.reason } }; - const validation = await validateAction(action); - if (!validation.ok) { - const receipt = await recordReceipt(action, [], { blocked: true, blockReason: validation.reason }); - return { success: false, receipts: [receipt] }; - } - await applyDiff(input.diff); - const receipt = await recordReceipt(action, ["patch-applied"]); - await updateContinuity(action); - return { success: true, receipts: [receipt] }; -} - -export { DANGEROUS_PATTERNS }; diff --git a/agent/core/executor.ts b/agent/core/executor.ts deleted file mode 100644 index 58745e6..0000000 --- a/agent/core/executor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { AgentAction } from "../types/actions"; -import { recordReceipt } from "../governance/receipts"; -import { updateContinuity } from "../continuity/substrate"; -import { emitAction, emitReceipt } from "../events/lifecycle"; -import { applyDiff } from "../runtime/workspace"; - -export async function execute(action: AgentAction): Promise { - emitAction(action); - - switch (action.type) { - case "edit": - case "create": { - const diff = typeof action.payload.diff === "string" ? action.payload.diff : ""; - if (diff) await applyDiff(diff); - break; - } - case "run": - break; - default: - break; - } - - const receipt = await recordReceipt(action, ["executed"]); - await updateContinuity(action); - emitReceipt(receipt); - return { ok: true, receipt }; -} diff --git a/agent/core/index.ts b/agent/core/index.ts deleted file mode 100644 index 85e62e0..0000000 --- a/agent/core/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as nova from "./agent"; -export { plan } from "./planner"; -export { execute } from "./executor"; diff --git a/agent/core/planner.ts b/agent/core/planner.ts deleted file mode 100644 index 6307360..0000000 --- a/agent/core/planner.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Plan, PlanStep } from "../types/plan"; -import type { PlanInput } from "../types/actions"; -import { uuid } from "../lib/uuid"; -import { recordReceipt } from "../governance/receipts"; -import { validateAction } from "../governance/validator"; -import { emitPlan } from "../events/lifecycle"; - -export async function plan(input: PlanInput): Promise<{ plan: Plan; receipts: import("../types/receipts").GovernanceReceipt[] }> { - const action = { type: "plan" as const, payload: { goal: input.goal, context: input.context } }; - - const validation = await validateAction(action); - if (!validation.ok) { - await recordReceipt(action, [], { blocked: true, blockReason: validation.reason }); - throw new Error(validation.reason ?? "Plan blocked by invariant"); - } - - const steps: PlanStep[] = [ - { - id: uuid(), - description: `Analyze workspace for: ${input.goal}`, - action: { type: "run", payload: { command: "analyze" } }, - }, - { - id: uuid(), - description: `Propose changes for: ${input.goal}`, - action: { type: "edit", payload: { goal: input.goal } }, - }, - { - id: uuid(), - description: "Verify with invariants and tests", - action: { type: "run", payload: { command: "verify" } }, - }, - ]; - - const planReceipt = await recordReceipt(action, validation.ok ? ["plan-validated"] : []); - const planObj: Plan = { - id: uuid(), - steps, - justification: `Governed plan for: ${input.goal}`, - receipts: [planReceipt], - }; - - emitPlan(planObj); - return { plan: planObj, receipts: [planReceipt] }; -} diff --git a/agent/events/lifecycle.ts b/agent/events/lifecycle.ts deleted file mode 100644 index 260c7cc..0000000 --- a/agent/events/lifecycle.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Plan } from "../types/plan"; -import type { AgentAction } from "../types/actions"; -import type { GovernanceReceipt } from "../types/receipts"; -import type { InvariantViolation } from "../types/invariants"; - -import type { KernelHeartbeat } from "../governance/kernelStatus"; - -type PlanListener = (plan: Plan) => void; -type ActionListener = (action: AgentAction) => void; -type ViolationListener = (violation: InvariantViolation) => void; -type ReceiptListener = (receipt: GovernanceReceipt) => void; -type KernelHeartbeatListener = (heartbeat: KernelHeartbeat) => void; - -const listeners = { - plan: [] as PlanListener[], - action: [] as ActionListener[], - violation: [] as ViolationListener[], - receipt: [] as ReceiptListener[], - kernelHeartbeat: [] as KernelHeartbeatListener[], -}; - -export function onPlan(cb: PlanListener): void { - listeners.plan.push(cb); -} - -export function onAction(cb: ActionListener): void { - listeners.action.push(cb); -} - -export function onViolation(cb: ViolationListener): void { - listeners.violation.push(cb); -} - -export function onReceipt(cb: ReceiptListener): void { - listeners.receipt.push(cb); -} - -export function onKernelHeartbeat(cb: KernelHeartbeatListener): void { - listeners.kernelHeartbeat.push(cb); -} - -export function emitPlan(plan: Plan): void { - listeners.plan.forEach((cb) => cb(plan)); -} - -export function emitAction(action: AgentAction): void { - listeners.action.forEach((cb) => cb(action)); -} - -export function emitViolation(violation: InvariantViolation): void { - listeners.violation.forEach((cb) => cb(violation)); -} - -export function emitReceipt(receipt: GovernanceReceipt): void { - listeners.receipt.forEach((cb) => cb(receipt)); -} - -export function emitKernelHeartbeat(heartbeat: KernelHeartbeat): void { - listeners.kernelHeartbeat.forEach((cb) => cb(heartbeat)); -} diff --git a/agent/governance/index.ts b/agent/governance/index.ts deleted file mode 100644 index 964cdbc..0000000 --- a/agent/governance/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { recordReceipt, getReceipt, listReceipts } from "./receipts"; -export { requireInvariant, getInvariants } from "./invariants"; -export { validateAction, trace } from "./validator"; -export { getLedger, getLedgerTailHash, appendToLedger } from "./ledger"; -export { kernelStatus, emitKernelHeartbeat } from "./kernelStatus"; -export type { KernelStatus, KernelHeartbeat } from "./kernelStatus"; - -import { validateAction } from "./validator"; -import { recordReceipt } from "./receipts"; -import { appendToLedger, getLedger, getLedgerTailHash } from "./ledger"; - -/** Tightened governance surface: validate, receipt, ledger.append */ -export const validate = validateAction; -export const receipt = recordReceipt; -export const ledger = { - append: appendToLedger, - list: getLedger, - tailHash: getLedgerTailHash, -}; diff --git a/agent/governance/invariants.ts b/agent/governance/invariants.ts deleted file mode 100644 index 369c65e..0000000 --- a/agent/governance/invariants.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Invariant } from "../types/invariants"; - -const registered: Invariant[] = []; - -export function getInvariants(): readonly Invariant[] { - return registered; -} - -export async function requireInvariant(inv: Invariant): Promise { - if (registered.some((r) => r.id === inv.id)) return; - registered.push(inv); -} - -export function clearInvariants(): void { - registered.length = 0; -} - -export { registered as invariants }; diff --git a/agent/governance/kernelStatus.ts b/agent/governance/kernelStatus.ts deleted file mode 100644 index 6fcc8ae..0000000 --- a/agent/governance/kernelStatus.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { uuid } from "../lib/uuid"; -import { getInvariants } from "./invariants"; -import { getLedger } from "./ledger"; -import { getSnapshots } from "../continuity/substrate"; - -export type KernelSubsystemStatus = "ok" | "warn" | "error"; - -export interface KernelStatus { - invariantEngine: KernelSubsystemStatus; - ledger: KernelSubsystemStatus; - continuity: KernelSubsystemStatus; - violationsLastMinute: number; - receiptCount: number; - snapshotCount: number; - activeInvariants: number; -} - -export async function kernelStatus(): Promise { - const invariants = getInvariants(); - const ledger = getLedger(); - const snapshots = getSnapshots(); - - const recentViolations = ledger.filter( - (r) => r.blocked && Date.now() - r.timestamp < 60_000 - ).length; - - return { - invariantEngine: invariants.length > 0 ? "ok" : "warn", - ledger: ledger.length >= 0 ? "ok" : "error", - continuity: snapshots.length >= 0 ? "ok" : "warn", - violationsLastMinute: recentViolations, - receiptCount: ledger.length, - snapshotCount: snapshots.length, - activeInvariants: invariants.length, - }; -} - -export type KernelHeartbeat = KernelStatus & { - kernelId: string; - ts: number; -}; - -export async function emitKernelHeartbeat(): Promise { - const status = await kernelStatus(); - const hb: KernelHeartbeat = { - ...status, - kernelId: "crk-1", - ts: Date.now(), - }; - const { emitKernelHeartbeat: emitHb } = await import("../events/lifecycle"); - emitHb(hb); - return hb; -} - -export function violationId(): string { - return uuid(); -} diff --git a/agent/governance/ledger.ts b/agent/governance/ledger.ts deleted file mode 100644 index 0b73a63..0000000 --- a/agent/governance/ledger.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { GovernanceReceipt } from "../types/receipts"; - -const ledger: GovernanceReceipt[] = []; - -export function appendToLedger(receipt: GovernanceReceipt): void { - ledger.push(receipt); -} - -export function getLedger(): readonly GovernanceReceipt[] { - return ledger; -} - -export function getLedgerTailHash(): string { - if (ledger.length === 0) return "genesis"; - return ledger[ledger.length - 1].ledgerHash; -} - -export function clearLedger(): void { - ledger.length = 0; -} diff --git a/agent/governance/receipts.ts b/agent/governance/receipts.ts deleted file mode 100644 index b1c687d..0000000 --- a/agent/governance/receipts.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { sha256Sync } from "../lib/hash"; -import { uuid } from "../lib/uuid"; -import type { AgentAction } from "../types/actions"; -import type { GovernanceReceipt } from "../types/receipts"; -import { getContinuityHash } from "../continuity/substrate"; -import { appendToLedger, getLedgerTailHash } from "./ledger"; - -export async function recordReceipt( - action: AgentAction, - invariantsChecked: string[], - options?: { blocked?: boolean; blockReason?: string } -): Promise { - const continuityHash = await getContinuityHash(); - const prevHash = getLedgerTailHash(); - - const receipt: GovernanceReceipt = { - id: uuid(), - timestamp: Date.now(), - action, - invariantsChecked, - continuityHash, - ledgerHash: "", - blocked: options?.blocked, - blockReason: options?.blockReason, - }; - - receipt.ledgerHash = sha256Sync(JSON.stringify(receipt) + prevHash); - - appendToLedger(receipt); - return receipt; -} - -export async function getReceipt(id: string): Promise { - const { getLedger } = await import("./ledger"); - return getLedger().find((r) => r.id === id) ?? null; -} - -export async function listReceipts(): Promise { - const { getLedger } = await import("./ledger"); - return [...getLedger()]; -} diff --git a/agent/governance/validator.ts b/agent/governance/validator.ts deleted file mode 100644 index 6028102..0000000 --- a/agent/governance/validator.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { AgentAction } from "../types/actions"; -import type { ValidationResult } from "../types/actions"; -import type { InvariantState } from "../types/invariants"; -import { getInvariants } from "./invariants"; -import { emitViolation } from "../events/lifecycle"; -import { violationId } from "./kernelStatus"; - -function buildState(action: AgentAction): InvariantState { - const payload = action.payload; - return { - action, - diff: typeof payload.diff === "string" ? payload.diff : undefined, - code: typeof payload.code === "string" ? payload.code : undefined, - prompt: typeof payload.prompt === "string" ? payload.prompt : undefined, - }; -} - -export async function validateAction(action: AgentAction): Promise { - const state = buildState(action); - const checked: string[] = []; - - for (const inv of getInvariants()) { - checked.push(inv.id); - const ok = await inv.check(state); - if (!ok) { - const violation = { - id: violationId(), - invariantId: inv.id, - description: inv.description, - message: inv.description, - severity: inv.severity, - action, - }; - if (inv.severity === "error") { - emitViolation(violation); - return { - ok: false, - reason: `Invariant violated: ${inv.id} — ${inv.description}`, - violation, - }; - } - emitViolation(violation); - } - } - - return { ok: true }; -} - -export async function trace(): Promise { - const { getLedger, getLedgerTailHash } = await import("./ledger"); - return { - receipts: [...getLedger()], - ledgerHash: getLedgerTailHash(), - }; -} diff --git a/agent/index.ts b/agent/index.ts deleted file mode 100644 index 4058c37..0000000 --- a/agent/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { AgentRuntime } from "./runtime/agent-runtime"; -export * as governance from "./governance"; -export * as continuity from "./continuity"; -export * as events from "./events/lifecycle"; -export * from "./types"; -export type { KernelStatus, KernelHeartbeat } from "./governance/kernelStatus"; - -/** @deprecated Use AgentRuntime — kept for backward compatibility */ -export * as nova from "./core/agent"; -/** @deprecated Use AgentRuntime.workspace helpers */ -export * as runtime from "./runtime"; diff --git a/agent/lib/hash.ts b/agent/lib/hash.ts deleted file mode 100644 index d9ec0f9..0000000 --- a/agent/lib/hash.ts +++ /dev/null @@ -1,24 +0,0 @@ -export async function sha256(input: string): Promise { - if (typeof globalThis.crypto?.subtle !== "undefined") { - const data = new TextEncoder().encode(input); - const buf = await globalThis.crypto.subtle.digest("SHA-256", data); - return Array.from(new Uint8Array(buf)) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); - } - const { createHash } = await import("crypto"); - return createHash("sha256").update(input).digest("hex"); -} - -export function sha256Sync(input: string): string { - if (typeof globalThis.crypto?.subtle !== "undefined") { - // sync fallback for receipt ledger in browser - let h = 0; - for (let i = 0; i < input.length; i++) { - h = (Math.imul(31, h) + input.charCodeAt(i)) | 0; - } - return Math.abs(h).toString(16).padStart(16, "0"); - } - const { createHash } = require("crypto") as typeof import("crypto"); - return createHash("sha256").update(input).digest("hex"); -} diff --git a/agent/lib/uuid.ts b/agent/lib/uuid.ts deleted file mode 100644 index a39e904..0000000 --- a/agent/lib/uuid.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function uuid(): string { - if (typeof globalThis.crypto?.randomUUID === "function") { - return globalThis.crypto.randomUUID(); - } - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { randomUUID } = require("crypto") as typeof import("crypto"); - return randomUUID(); -} diff --git a/agent/runtime/agent-runtime.ts b/agent/runtime/agent-runtime.ts deleted file mode 100644 index 30e2725..0000000 --- a/agent/runtime/agent-runtime.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { AgentAction, ValidationResult } from "../types/actions"; -import type { GovernanceReceipt } from "../types/receipts"; -import { validateAction } from "../governance/validator"; -import { recordReceipt } from "../governance/receipts"; -import { appendToLedger, getLedger, getLedgerTailHash } from "../governance/ledger"; -import * as agent from "../core/agent"; - -/** - * Constitutional agent runtime — single entry point for governed agent actions. - * Every action flows through validate → execute → receipt → ledger.append. - */ -export class AgentRuntime { - /** Validate an action against registered invariants before execution. */ - async validate(action: AgentAction): Promise { - return validateAction(action); - } - - /** Record a governance receipt and append it to the hash-chained ledger. */ - async receipt( - action: AgentAction, - invariantsChecked: string[], - options?: { blocked?: boolean; blockReason?: string } - ): Promise { - return recordReceipt(action, invariantsChecked, options); - } - - /** Hash-chained governance ledger surface. */ - readonly ledger = { - append: appendToLedger, - list: getLedger, - tailHash: getLedgerTailHash, - }; - - generateCode = agent.generateCode; - plan = agent.plan; - explain = agent.explain; - refactor = agent.refactor; - verify = agent.verify; - applyPatch = agent.applyPatch; -} diff --git a/agent/runtime/environment.ts b/agent/runtime/environment.ts deleted file mode 100644 index 6650b5c..0000000 --- a/agent/runtime/environment.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getEnvironment(): { node: string; platform: string; cwd: string } { - return { - node: process.version, - platform: process.platform, - cwd: process.cwd(), - }; -} diff --git a/agent/runtime/index.ts b/agent/runtime/index.ts deleted file mode 100644 index 12067ca..0000000 --- a/agent/runtime/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * as workspace from "./workspace"; -export * as environment from "./environment"; -export { AgentRuntime } from "./agent-runtime"; -export { getContext, openFile, applyDiff, runTests, setWorkspaceRoot } from "./workspace"; diff --git a/agent/runtime/workspace.ts b/agent/runtime/workspace.ts deleted file mode 100644 index f37ce63..0000000 --- a/agent/runtime/workspace.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { WorkspaceContext, FileContent, ApplyResult, TestResult } from "../types/actions"; - -const isBrowser = - typeof globalThis !== "undefined" && - typeof (globalThis as { document?: unknown }).document !== "undefined"; - -let workspaceRoot = typeof process !== "undefined" ? process.cwd() : "/workspace"; - -export function setWorkspaceRoot(root: string): void { - workspaceRoot = root; -} - -async function listFiles(dir: string, base: string[] = []): Promise { - const { readdir } = await import("fs/promises"); - const { join } = await import("path"); - try { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = join(dir, entry.name); - if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue; - if (entry.isDirectory()) { - files.push(...(await listFiles(full, base))); - } else { - files.push(full.replace(workspaceRoot + "\\", "").replace(workspaceRoot + "/", "")); - } - } - return files; - } catch { - return base; - } -} - -export async function getContext(): Promise { - if (isBrowser) { - return { - root: "/workspace", - files: ["agent/index.ts", "package.json", "config/nova.config.ts"], - openFiles: [], - }; - } - const files = await listFiles(workspaceRoot); - return { - root: workspaceRoot, - files, - openFiles: [], - }; -} - -export async function openFile(path: string): Promise { - if (isBrowser) { - return { path, content: `// browser stub for ${path}` }; - } - const { readFile } = await import("fs/promises"); - const { resolve } = await import("path"); - const full = resolve(workspaceRoot, path); - const content = await readFile(full, "utf-8"); - return { path, content }; -} - -export async function applyDiff(diff: string): Promise { - return { - success: true, - message: `Diff recorded (${diff.length} chars); apply via editor integration in production.`, - }; -} - -export async function runTests(): Promise { - return [{ name: "nova-sdk-smoke", passed: true, message: "No test runner configured" }]; -} diff --git a/agent/types/actions.ts b/agent/types/actions.ts deleted file mode 100644 index dbe63db..0000000 --- a/agent/types/actions.ts +++ /dev/null @@ -1,92 +0,0 @@ -export type ActionType = "edit" | "create" | "delete" | "run" | "plan" | "generate" | "refactor"; - -export interface AgentAction { - type: ActionType; - payload: Record; -} - -export interface CodeContext { - files?: string[]; - language?: string; -} - -export interface ConstraintSet { - maxLines?: number; - allowedLanguages?: string[]; -} - -export interface WorkspaceContext { - root: string; - files: string[]; - openFiles: string[]; -} - -export interface FileContent { - path: string; - content: string; -} - -export interface ApplyResult { - success: boolean; - path?: string; - message?: string; -} - -export interface TestResult { - name: string; - passed: boolean; - message?: string; -} - -export interface RefactorResult { - file: string; - diff: string; - receipts: import("./receipts").GovernanceReceipt[]; -} - -export interface VerificationResult { - ok: boolean; - reason?: string; - invariantsChecked: string[]; -} - -export interface ValidationResult { - ok: boolean; - reason?: string; - violation?: import("./invariants").InvariantViolation; -} - -export interface GenerateCodeInput { - prompt: string; - context?: CodeContext; - constraints?: ConstraintSet; -} - -export interface GenerateCodeResult { - code: string; - receipts: import("./receipts").GovernanceReceipt[]; -} - -export interface PlanInput { - goal: string; - context?: WorkspaceContext; -} - -export interface RefactorInput { - file: string; - instructions: string; -} - -export interface VerifyInput { - action: AgentAction; -} - -export interface ApplyPatchInput { - diff: string; - reason: string; -} - -export interface ApplyPatchResult { - success: boolean; - receipts: import("./receipts").GovernanceReceipt[]; -} diff --git a/agent/types/index.ts b/agent/types/index.ts deleted file mode 100644 index 5201ed4..0000000 --- a/agent/types/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./actions"; -export * from "./receipts"; -export * from "./invariants"; -export * from "./plan"; diff --git a/agent/types/invariants.ts b/agent/types/invariants.ts deleted file mode 100644 index 08999d9..0000000 --- a/agent/types/invariants.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { AgentAction } from "./actions"; - -export type InvariantSeverity = "error" | "warn"; - -export interface InvariantState { - action: AgentAction; - diff?: string; - code?: string; - prompt?: string; - runTests?: () => Promise<{ failures: number }>; -} - -export interface Invariant { - id: string; - description: string; - severity: InvariantSeverity; - check: (state: InvariantState) => boolean | Promise; -} - -export interface InvariantViolation { - id: string; - invariantId: string; - description: string; - message: string; - severity: InvariantSeverity; - action: AgentAction; -} diff --git a/agent/types/plan.ts b/agent/types/plan.ts deleted file mode 100644 index fbaa455..0000000 --- a/agent/types/plan.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GovernanceReceipt } from "./receipts"; - -export interface PlanStep { - id: string; - description: string; - action: import("./actions").AgentAction; -} - -export interface Plan { - id: string; - steps: PlanStep[]; - justification: string; - receipts: GovernanceReceipt[]; -} diff --git a/agent/types/receipts.ts b/agent/types/receipts.ts deleted file mode 100644 index 1241e45..0000000 --- a/agent/types/receipts.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AgentAction } from "./actions"; - -export interface GovernanceReceipt { - id: string; - timestamp: number; - action: AgentAction; - invariantsChecked: string[]; - continuityHash: string; - ledgerHash: string; - blocked?: boolean; - blockReason?: string; -} - -export interface GovernanceTrace { - receipts: GovernanceReceipt[]; - ledgerHash: string; -} diff --git a/backend/control-tower-service.ts b/backend/control-tower-service.ts deleted file mode 100644 index 2d11858..0000000 --- a/backend/control-tower-service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { clusterManager } from "../control-tower/orchestrator/cluster-manager"; -import { replayCluster } from "../control-tower/replay/cluster-replay"; -import { simulateDrift } from "../control-tower/drift/drift-simulator"; - -export const controlTowerService = { - getClusterState: () => clusterManager.getClusterState(), - replayCluster, - simulateDrift, - ensureDefaultCluster: () => clusterManager.ensureDefaultAgents(), -}; diff --git a/backend/crk2-service.ts b/backend/crk2-service.ts deleted file mode 100644 index 2a0c820..0000000 --- a/backend/crk2-service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { dLAP, takeSnapshot, replay, generateCRP, clusterView } from "../crk2"; -import { listReceipts, appendReceipt } from "../crk2/ledger/ledger-v2"; -import { listSnapshots } from "../crk2/continuity/substrate"; - -export const crk2Service = { - evaluateAction: dLAP, - takeSnapshot, - replay, - generateCRP, - getClusterView: clusterView, - listReceipts, - appendReceipt, - listSnapshots, -}; diff --git a/backend/events-gateway.ts b/backend/events-gateway.ts deleted file mode 100644 index 4c3ae2f..0000000 --- a/backend/events-gateway.ts +++ /dev/null @@ -1,27 +0,0 @@ -export type GatewayEventType = - | "heartbeat" - | "receipt" - | "continuity" - | "drift" - | "event"; - -export interface GatewayEvent { - type: GatewayEventType; - payload: Record; -} - -type Listener = (event: GatewayEvent) => void; - -const listeners = new Set(); - -export const eventsGateway = { - emit(type: GatewayEventType, payload: Record): void { - const event: GatewayEvent = { type, payload }; - for (const listener of listeners) listener(event); - }, - - subscribe(listener: Listener): () => void { - listeners.add(listener); - return () => listeners.delete(listener); - }, -}; diff --git a/backend/index.ts b/backend/index.ts deleted file mode 100644 index 817e0cc..0000000 --- a/backend/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { crk2Service } from "./crk2-service"; -export { controlTowerService } from "./control-tower-service"; -export { eventsGateway } from "./events-gateway"; -export { novaAdapter } from "./nova-adapter"; diff --git a/backend/nova-adapter.ts b/backend/nova-adapter.ts deleted file mode 100644 index f9d536b..0000000 --- a/backend/nova-adapter.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { crk2Service } from "./crk2-service"; -import { controlTowerService } from "./control-tower-service"; -import { eventsGateway } from "./events-gateway"; - -export async function evaluateNovaAction( - action: { type: string; payload?: Record }, - context: Record -): Promise<{ allowed: boolean; result: ReturnType }> { - const result = crk2Service.evaluateAction(action, context); - if (!result.ok) { - eventsGateway.emit("drift", { - reason: result.reason, - detail: result.detail, - }); - return { allowed: false, result }; - } - return { allowed: true, result }; -} - -export const novaAdapter = { - evaluateNovaAction, - crk2: crk2Service, - controlTower: controlTowerService, - events: eventsGateway, -}; diff --git a/bin/nova b/bin/nova new file mode 100755 index 0000000..26eaca7 --- /dev/null +++ b/bin/nova @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Lawful Nova CLI wrapper — Linux and macOS. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../setup/lib/common.sh +source "${SCRIPT_DIR}/../setup/lib/common.sh" + +REPO_ROOT="$(lawful_nova_repo_root)" +PY="$(lawful_nova_python)" +lawful_nova_export_paths +cd "${REPO_ROOT}" + +exec "${PY}" -m nova.cli "$@" diff --git a/bin/nova.cmd b/bin/nova.cmd new file mode 100644 index 0000000..0cbc94d --- /dev/null +++ b/bin/nova.cmd @@ -0,0 +1,5 @@ +@echo off +setlocal +set "SCRIPT_DIR=%~dp0" +powershell -ExecutionPolicy Bypass -File "%SCRIPT_DIR%nova.ps1" %* +exit /b %ERRORLEVEL% diff --git a/bin/nova.ps1 b/bin/nova.ps1 new file mode 100644 index 0000000..867032f --- /dev/null +++ b/bin/nova.ps1 @@ -0,0 +1,32 @@ +# Lawful Nova CLI wrapper — Windows PowerShell. +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$NovaArgs +) + +$ErrorActionPreference = "Stop" +$ShellRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$RepoRoot = if ($env:LAWFUL_NOVA_REPO_ROOT) { $env:LAWFUL_NOVA_REPO_ROOT } else { Split-Path -Parent $ShellRoot } + +$ShellVenvPy = Join-Path $ShellRoot ".venv\Scripts\python.exe" +$RepoVenvPy = Join-Path $RepoRoot ".venv\Scripts\python.exe" +if (Test-Path $ShellVenvPy) { + $PyExe = $ShellVenvPy +} elseif (Test-Path $RepoVenvPy) { + $PyExe = $RepoVenvPy +} elseif ($env:OPERATOR_PYTHON -and (Test-Path $env:OPERATOR_PYTHON)) { + $PyExe = $env:OPERATOR_PYTHON +} else { + $PyExe = (Get-Command python -ErrorAction Stop).Source +} + +$env:LAWFUL_NOVA_REPO_ROOT = $RepoRoot +$env:NOVA_CORTEX_PATH = if ($env:NOVA_CORTEX_PATH) { $env:NOVA_CORTEX_PATH } else { Join-Path $ShellRoot "nova" } +$env:NOVA_VOSS_RUNTIME_PATH = if ($env:NOVA_VOSS_RUNTIME_PATH) { $env:NOVA_VOSS_RUNTIME_PATH } else { Join-Path $ShellRoot "nova" } +$env:NOVA_RSL_PATH = if ($env:NOVA_RSL_PATH) { $env:NOVA_RSL_PATH } else { Join-Path $ShellRoot "nova\governance" } +$env:NOVA_CLI = $MyInvocation.MyCommand.Path +$env:PYTHONPATH = "$ShellRoot;$RepoRoot" + +Set-Location $ShellRoot +& $PyExe -m nova.cli @NovaArgs +exit $LASTEXITCODE diff --git a/cockpit/README.md b/cockpit/README.md deleted file mode 100644 index 686ef5b..0000000 --- a/cockpit/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Nova Operator Cockpit - -Constitutional operator HUD for Nova × CRK‑1. - -## Run - -```bash -# From repo root — build SDK first -cd .. -npm run build - -cd cockpit -npm install -npm run dev -``` - -Open http://localhost:5173 - -## Architecture - -- **NovaShell** — cockpit grid (top bar, rails, canvas, bottom band) -- **Zustand store** — unified cockpit state -- **NovaEventBridge** — SDK events → store (plan, action, receipt, violation, heartbeat) -- **Panels** — Plan, Diff, Receipts, Continuity, Invariants, Kernel + Agent Console - -## Design tokens - -See `src/styles/tokens.json` and `src/styles/variables.css`. diff --git a/cockpit/index.html b/cockpit/index.html deleted file mode 100644 index 9e84826..0000000 --- a/cockpit/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Nova Operator Cockpit - - - - - -
- - - diff --git a/cockpit/package-lock.json b/cockpit/package-lock.json deleted file mode 100644 index bbdecb6..0000000 --- a/cockpit/package-lock.json +++ /dev/null @@ -1,1819 +0,0 @@ -{ - "name": "nova-cockpit", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nova-cockpit", - "version": "0.1.0", - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "zod": "^3.23.8", - "zustand": "^4.5.5" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "typescript": "^5.5.0", - "vite": "^5.4.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.38", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.378", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", - "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.49", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.49.tgz", - "integrity": "sha512-f06bl1D+8ZDkn2oOQQKAh5/otFWqVnM1Q5oerA8Pex7UfT66Tx4IPHIqVVFKqFT3FUtaDstdgkM7yT7JWhqxfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - } - } -} diff --git a/cockpit/package.json b/cockpit/package.json deleted file mode 100644 index 446a833..0000000 --- a/cockpit/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "nova-cockpit", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "zustand": "^4.5.5", - "zod": "^3.23.8" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "typescript": "^5.5.0", - "vite": "^5.4.0" - } -} diff --git a/cockpit/src/App.tsx b/cockpit/src/App.tsx deleted file mode 100644 index ef5de69..0000000 --- a/cockpit/src/App.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect } from "react"; -import { NovaShell } from "./NovaShell"; -import { initializeNovaEventBridge, teardownNovaEventBridge } from "./state/NovaEventBridge"; -import { connectClusterBridge, disconnectClusterBridge } from "./bridge/WebSocketBridge"; -import "./styles/variables.css"; - -export function App() { - useEffect(() => { - void initializeNovaEventBridge(); - connectClusterBridge(); - return () => { - teardownNovaEventBridge(); - disconnectClusterBridge(); - }; - }, []); - - return ; -} diff --git a/cockpit/src/NovaShell.module.css b/cockpit/src/NovaShell.module.css deleted file mode 100644 index b8c5bdd..0000000 --- a/cockpit/src/NovaShell.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.shell { - display: flex; - flex-direction: column; - height: 100vh; - background: var(--color-bg-shell); - color: var(--color-text-primary); - overflow: hidden; -} - -.main { - flex: 1; - display: grid; - grid-template-columns: 260px 1fr 300px; - grid-template-rows: 1fr; - overflow: hidden; - min-height: 0; -} diff --git a/cockpit/src/NovaShell.tsx b/cockpit/src/NovaShell.tsx deleted file mode 100644 index 747d16f..0000000 --- a/cockpit/src/NovaShell.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import styles from "./NovaShell.module.css"; -import { TopBar } from "./layout/TopBar"; -import { LeftRail } from "./layout/LeftRail"; -import { RightRail } from "./layout/RightRail"; -import { CenterCanvas } from "./layout/CenterCanvas"; -import { BottomBand } from "./layout/BottomBand"; - -export function NovaShell() { - return ( -
- -
- - - -
- -
- ); -} diff --git a/cockpit/src/bridge/WebSocketBridge.ts b/cockpit/src/bridge/WebSocketBridge.ts deleted file mode 100644 index fa8b6ed..0000000 --- a/cockpit/src/bridge/WebSocketBridge.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useClusterStore } from "../state/clusterStore"; -import { useDriftStore } from "../state/driftStore"; -import { handleEvent } from "./events-gateway"; - -const WS_URL = "ws://127.0.0.1:8787/events"; - -let connected = false; -let inProcessUnsub: (() => void) | null = null; - -export function connectClusterBridge(): void { - if (connected) return; - connected = true; - - useClusterStore.getState().actions.seedDemoAgents(); - syncDriftFromCluster(); - - try { - const ws = new WebSocket(WS_URL); - ws.onmessage = (msg) => { - handleEvent(JSON.parse(msg.data as string)); - }; - } catch { - /* WebSocket unavailable */ - } - - inProcessUnsub = subscribeInProcessEvents(); -} - -function syncDriftFromCluster(): void { - const agents = useClusterStore.getState().agents; - useDriftStore.getState().actions.syncFromCluster(agents); -} - -function subscribeInProcessEvents(): () => void { - const interval = setInterval(() => syncDriftFromCluster(), 2000); - return () => clearInterval(interval); -} - -export function disconnectClusterBridge(): void { - inProcessUnsub?.(); - inProcessUnsub = null; - connected = false; -} diff --git a/cockpit/src/bridge/events-gateway.ts b/cockpit/src/bridge/events-gateway.ts deleted file mode 100644 index 6f5b35b..0000000 --- a/cockpit/src/bridge/events-gateway.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { z } from "zod"; -import { useCockpitStore } from "../state/cockpitStore"; -import { useKernelStore } from "../state/kernelStore"; -import { useClusterStore } from "../state/clusterStore"; -import { useDriftStore } from "../state/driftStore"; - -const SubsystemStatus = z.enum(["ok", "warn", "error"]); - -const BaseEvent = z.object({ - type: z.string(), - agentId: z.string().nullable(), - ts: z.number().optional(), -}); - -const KernelHeartbeat = BaseEvent.extend({ - type: z.literal("kernel.heartbeat"), - agentId: z.string(), - payload: z.object({ - invariantEngine: SubsystemStatus, - constraintEngine: SubsystemStatus, - continuity: SubsystemStatus, - ledger: SubsystemStatus, - pitBand: z.number(), - continuityAnchorHash: z.string(), - ledgerPrefixHash: z.string(), - }), -}); - -const KernelInvariantViolation = BaseEvent.extend({ - type: z.literal("kernel.invariantViolation"), - agentId: z.string(), - payload: z.object({ - invariantId: z.string(), - message: z.string(), - actionId: z.string(), - severity: z.enum(["error", "warn"]), - }), -}); - -const KernelReceipt = BaseEvent.extend({ - type: z.literal("kernel.receipt"), - agentId: z.string(), - payload: z.object({ - receiptId: z.string(), - actionId: z.string(), - invariantsChecked: z.array(z.string()), - pitBand: z.number(), - continuityHash: z.string(), - }), -}); - -const KernelSnapshot = BaseEvent.extend({ - type: z.literal("kernel.snapshot"), - agentId: z.string(), - payload: z.object({ - snapshotId: z.string(), - hash: z.string(), - partial: z.boolean(), - }), -}); - -const AgentPlan = BaseEvent.extend({ - type: z.literal("agent.plan"), - agentId: z.string(), - payload: z.object({ - planId: z.string(), - steps: z.array( - z.object({ - id: z.string(), - description: z.string(), - status: z.enum(["pending", "running", "done", "error"]).optional(), - }) - ), - }), -}); - -const AgentAction = BaseEvent.extend({ - type: z.literal("agent.action"), - agentId: z.string(), - payload: z.object({ - actionId: z.string(), - stepId: z.string(), - description: z.string(), - }), -}); - -const ClusterHeartbeat = BaseEvent.extend({ - type: z.literal("cluster.heartbeat"), - agentId: z.null(), - payload: z.object({ - agents: z.record( - z.object({ - kernelStatus: SubsystemStatus, - pitBand: z.number(), - }) - ), - }), -}); - -const ClusterDriftDetected = BaseEvent.extend({ - type: z.literal("cluster.driftDetected"), - agentId: z.null(), - payload: z.object({ - driftId: z.string(), - agents: z.array(z.string()), - snapshotId: z.string(), - driftType: z.enum(["ledger", "continuity", "pit", "constraint", "dlap"]), - }), -}); - -const ClusterReplayResult = BaseEvent.extend({ - type: z.literal("cluster.replayResult"), - agentId: z.null(), - payload: z.object({ - perAgent: z.record( - z.object({ - snapshots: z.array( - z.object({ - id: z.string(), - hash: z.string(), - state: z.enum(["ok", "drift", "error"]).optional(), - }) - ), - receipts: z.array(z.unknown()), - pitTransitions: z.array(z.unknown()), - }) - ), - divergences: z.array( - z.object({ - id: z.string(), - type: z.string(), - snapshotId: z.string(), - agents: z.array(z.string()), - }) - ), - }), -}); - -const AnyEvent = z.discriminatedUnion("type", [ - KernelHeartbeat, - KernelInvariantViolation, - KernelReceipt, - KernelSnapshot, - AgentPlan, - AgentAction, - ClusterHeartbeat, - ClusterDriftDetected, - ClusterReplayResult, -]); - -export type GatewayEvent = z.infer; - -export function handleEvent(raw: unknown): void { - const parsed = AnyEvent.safeParse(raw); - if (!parsed.success) { - console.warn("events-gateway: invalid event", parsed.error); - return; - } - routeEvent(parsed.data); -} - -function routeEvent(evt: GatewayEvent): void { - switch (evt.type) { - case "kernel.heartbeat": - handleKernelHeartbeat(evt); - break; - case "kernel.invariantViolation": - handleKernelViolation(evt); - break; - case "kernel.receipt": - handleKernelReceipt(evt); - break; - case "kernel.snapshot": - handleKernelSnapshot(evt); - break; - case "agent.plan": - handleAgentPlan(evt); - break; - case "agent.action": - handleAgentAction(evt); - break; - case "cluster.heartbeat": - handleClusterHeartbeat(evt); - break; - case "cluster.driftDetected": - handleClusterDrift(evt); - break; - case "cluster.replayResult": - handleClusterReplayResult(evt); - break; - default: { - const _exhaustive: never = evt; - return _exhaustive; - } - } -} - -function aggregateKernelStatus( - payload: z.infer["payload"] -): "ok" | "warn" | "error" { - const statuses = [ - payload.invariantEngine, - payload.constraintEngine, - payload.continuity, - payload.ledger, - ]; - if (statuses.includes("error")) return "error"; - if (statuses.includes("warn")) return "warn"; - return "ok"; -} - -function handleKernelHeartbeat(evt: z.infer): void { - const { payload, agentId } = evt; - const kernelActions = useKernelStore.getState().actions; - const clusterActions = useClusterStore.getState().actions; - - kernelActions.updateHeartbeat(agentId, payload); - clusterActions.updateAgentKernelStatus(agentId, { - kernelStatus: aggregateKernelStatus(payload), - pitBand: payload.pitBand, - }); -} - -function handleKernelViolation(evt: z.infer): void { - const { payload, agentId } = evt; - useCockpitStore.getState().actions.addViolationFromGateway({ - agentId, - ...payload, - }); -} - -function handleKernelReceipt(evt: z.infer): void { - const { payload, agentId } = evt; - const cockpitActions = useCockpitStore.getState().actions; - const clusterActions = useClusterStore.getState().actions; - - cockpitActions.addReceiptFromGateway({ agentId, ...payload }); - clusterActions.addAgentReceipt(agentId, payload); -} - -function handleKernelSnapshot(evt: z.infer): void { - const { payload, agentId } = evt; - const cockpitActions = useCockpitStore.getState().actions; - const clusterActions = useClusterStore.getState().actions; - - cockpitActions.addSnapshotFromGateway({ agentId, ...payload }); - clusterActions.addAgentSnapshot(agentId, payload); -} - -function handleAgentPlan(evt: z.infer): void { - const { payload, agentId } = evt; - useCockpitStore.getState().actions.setPlanForAgent(agentId, payload); -} - -function handleAgentAction(evt: z.infer): void { - const { payload, agentId } = evt; - useCockpitStore.getState().actions.addActionLog({ agentId, ...payload }); -} - -function handleClusterHeartbeat(evt: z.infer): void { - useClusterStore.getState().actions.setClusterHeartbeat(evt.payload.agents); -} - -function handleClusterDrift(evt: z.infer): void { - const { payload } = evt; - const clusterActions = useClusterStore.getState().actions; - const driftActions = useDriftStore.getState().actions; - - clusterActions.addDriftEvent(payload); - driftActions.appendDivergence({ - id: payload.driftId, - type: payload.driftType, - snapshotId: payload.snapshotId, - agents: payload.agents, - }); -} - -function handleClusterReplayResult(evt: z.infer): void { - const { payload } = evt; - const driftActions = useDriftStore.getState().actions; - driftActions.setAgents( - Object.entries(payload.perAgent).map(([agentId, data]) => ({ - id: agentId, - snapshots: data.snapshots.map((s) => ({ - id: s.id, - state: s.state ?? "ok", - })), - })) - ); - driftActions.setDivergences(payload.divergences); -} diff --git a/cockpit/src/components/Panel.module.css b/cockpit/src/components/Panel.module.css deleted file mode 100644 index b5aec70..0000000 --- a/cockpit/src/components/Panel.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.panel { - display: flex; - flex-direction: column; - height: 100%; - background: var(--color-bg-panel-alt); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-panel); - box-shadow: var(--shadow-panel); - overflow: hidden; -} - -.header { - padding: var(--space-md) var(--space-lg); - border-bottom: 1px solid var(--color-border-soft); -} - -.header h2 { - margin: 0; - font-size: var(--font-size-lg); - font-weight: 600; -} - -.body { - flex: 1; - overflow: auto; - padding: var(--space-lg); -} - -.mono { - font-family: var(--font-mono); - font-size: var(--font-size-sm); -} diff --git a/cockpit/src/components/Panel.tsx b/cockpit/src/components/Panel.tsx deleted file mode 100644 index b8983cc..0000000 --- a/cockpit/src/components/Panel.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { ReactNode } from "react"; -import styles from "./Panel.module.css"; - -interface PanelProps { - title: string; - children: ReactNode; - headerExtra?: ReactNode; - className?: string; -} - -export function Panel({ title, children, headerExtra, className }: PanelProps) { - return ( -
-
-

{title}

- {headerExtra} -
-
{children}
-
- ); -} diff --git a/cockpit/src/drift/DriftVisualizer.tsx b/cockpit/src/drift/DriftVisualizer.tsx deleted file mode 100644 index 9042471..0000000 --- a/cockpit/src/drift/DriftVisualizer.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useDriftStore } from "../state/driftStore"; -import "./driftVisualizer.css"; - -export function DriftVisualizer() { - const agents = useDriftStore((s) => s.agents); - const divergences = useDriftStore((s) => s.divergences); - - return ( -
-
Constitutional Drift Map
-
- {agents.map((agent) => ( -
-
{agent.id}
- {agent.snapshots.map((s) => ( -
- ))} -
- ))} -
-
- {divergences.map((d) => ( -
- {d.type} at {d.snapshotId ?? "—"} between {d.agents.join(", ")} -
- ))} - {divergences.length === 0 && ( - No divergences detected - )} -
-
- ); -} diff --git a/cockpit/src/drift/driftVisualizer.css b/cockpit/src/drift/driftVisualizer.css deleted file mode 100644 index 410e809..0000000 --- a/cockpit/src/drift/driftVisualizer.css +++ /dev/null @@ -1,59 +0,0 @@ -.drift-visualizer { - display: flex; - flex-direction: column; - gap: 8px; - padding: 16px; - height: 100%; -} - -.drift-header { - font-weight: 600; - font-size: 14px; -} - -.drift-grid { - display: flex; - gap: 12px; - flex: 1; - overflow-x: auto; -} - -.drift-agent-column { - display: flex; - flex-direction: column; - gap: 4px; -} - -.agent-label { - font-size: 12px; - margin-bottom: 4px; -} - -.drift-cell { - width: 12px; - height: 12px; - border-radius: 2px; -} - -.state-ok { - background: #27ae60; -} - -.state-drift { - background: #f2c94c; -} - -.state-error { - background: #eb5757; -} - -.drift-footer { - font-size: 12px; - max-height: 120px; - overflow-y: auto; -} - -.drift-event { - padding: 4px 0; - border-bottom: 1px solid var(--nova-border); -} diff --git a/cockpit/src/flight-deck/AgentSelector.tsx b/cockpit/src/flight-deck/AgentSelector.tsx deleted file mode 100644 index f9cbb2f..0000000 --- a/cockpit/src/flight-deck/AgentSelector.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useClusterStore } from "../state/clusterStore"; - -export function AgentSelector() { - const agents = useClusterStore((s) => s.agents); - const selected = useClusterStore((s) => s.selectedAgent); - const selectAgent = useClusterStore((s) => s.actions.selectAgent); - - return ( -
-

Agents

- {Object.values(agents).map((agent) => ( - - ))} -
- ); -} diff --git a/cockpit/src/flight-deck/ClusterMap.tsx b/cockpit/src/flight-deck/ClusterMap.tsx deleted file mode 100644 index 0d0233d..0000000 --- a/cockpit/src/flight-deck/ClusterMap.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useClusterStore } from "../state/clusterStore"; - -export function ClusterMap() { - const agents = useClusterStore((s) => s.agents); - - return ( -
- {Object.entries(agents).map(([id, agent]) => ( -
- {id} -
{agent.kernelStatus ?? agent.status}
-
- ))} -
- ); -} diff --git a/cockpit/src/flight-deck/ClusterTimeline.tsx b/cockpit/src/flight-deck/ClusterTimeline.tsx deleted file mode 100644 index e3df4a2..0000000 --- a/cockpit/src/flight-deck/ClusterTimeline.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useClusterStore } from "../state/clusterStore"; - -export function ClusterTimeline() { - const events = useClusterStore((s) => s.clusterEvents); - const replayWindow = useClusterStore((s) => s.replayWindow); - const setReplayWindow = useClusterStore((s) => s.actions.setReplayWindow); - - return ( -
-
- Cluster Timeline - - {replayWindow && ( - - {replayWindow.from} → {replayWindow.to} - - )} -
-
- {events.length === 0 && ( - No cluster events yet - )} - {events.slice(0, 12).map((e) => ( - - {e.type} {e.agentId ?? ""} - - ))} -
-
- ); -} diff --git a/cockpit/src/flight-deck/ContinuityMatrix.tsx b/cockpit/src/flight-deck/ContinuityMatrix.tsx deleted file mode 100644 index c3426f2..0000000 --- a/cockpit/src/flight-deck/ContinuityMatrix.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useClusterStore } from "../state/clusterStore"; - -export function ContinuityMatrix() { - const agents = useClusterStore((s) => s.agents); - - return ( - - - {Object.entries(agents).map(([id, agent]) => ( - - - {agent.snapshots.map((s) => ( - - ))} - - ))} - -
{id} - {s.id} -
- ); -} diff --git a/cockpit/src/flight-deck/FlightDeck.tsx b/cockpit/src/flight-deck/FlightDeck.tsx deleted file mode 100644 index ff1742f..0000000 --- a/cockpit/src/flight-deck/FlightDeck.tsx +++ /dev/null @@ -1 +0,0 @@ -export { FlightDeckShell as FlightDeck } from "./FlightDeckShell"; diff --git a/cockpit/src/flight-deck/FlightDeckShell.tsx b/cockpit/src/flight-deck/FlightDeckShell.tsx deleted file mode 100644 index 9bfa940..0000000 --- a/cockpit/src/flight-deck/FlightDeckShell.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import styles from "./flightDeck.module.css"; -import { AgentSelector } from "./AgentSelector"; -import { ClusterMap } from "./ClusterMap"; -import { KernelStatusRail } from "./KernelStatusRail"; -import { ClusterTimeline } from "./ClusterTimeline"; - -export function FlightDeckShell() { - return ( -
-
- -
-
- -
-
- -
-
- -
-
- ); -} diff --git a/cockpit/src/flight-deck/KernelStatusRail.tsx b/cockpit/src/flight-deck/KernelStatusRail.tsx deleted file mode 100644 index 3147371..0000000 --- a/cockpit/src/flight-deck/KernelStatusRail.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useKernelStore } from "../state/kernelStore"; -import { useCockpitState } from "../state/store"; - -export function KernelStatusRail() { - const kernel = useKernelStore((s) => s); - const status = useCockpitState((s) => s.kernel.status); - - const rows = [ - ["Kernel", kernel.kernelVersion], - ["PIT Band", String(kernel.pitBand)], - ["Invariant Engine", status.invariantEngine], - ["Ledger", status.ledger], - ["Continuity", status.continuity], - ["Receipts", String(status.receiptCount)], - ["Snapshots", String(status.snapshotCount)], - ]; - - return ( -
-

Kernel Status

- {rows.map(([label, value]) => ( -
- {label} - {value} -
- ))} -
- ); -} diff --git a/cockpit/src/flight-deck/LedgerCompare.tsx b/cockpit/src/flight-deck/LedgerCompare.tsx deleted file mode 100644 index 90d1468..0000000 --- a/cockpit/src/flight-deck/LedgerCompare.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useClusterStore } from "../state/clusterStore"; - -interface LedgerCompareProps { - leftAgentId?: string; - rightAgentId?: string; -} - -export function LedgerCompare({ leftAgentId, rightAgentId }: LedgerCompareProps) { - const agents = useClusterStore((s) => s.agents); - const left = leftAgentId ? agents[leftAgentId] : undefined; - const right = rightAgentId ? agents[rightAgentId] : undefined; - - return ( -
-
-

{leftAgentId ?? "—"}

-
-          {JSON.stringify(left?.receipts ?? [], null, 2)}
-        
-
-
-

{rightAgentId ?? "—"}

-
-          {JSON.stringify(right?.receipts ?? [], null, 2)}
-        
-
-
- ); -} diff --git a/cockpit/src/flight-deck/flightDeck.module.css b/cockpit/src/flight-deck/flightDeck.module.css deleted file mode 100644 index d4db611..0000000 --- a/cockpit/src/flight-deck/flightDeck.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.flightShell { - display: grid; - grid-template-columns: 200px 1fr 220px; - grid-template-rows: 1fr 120px; - gap: 8px; - height: 100%; - min-height: 420px; -} - -.left, -.center, -.right, -.bottom { - background: var(--nova-panel); - border: 1px solid var(--nova-border); - border-radius: 6px; - overflow: auto; -} - -.bottom { - grid-column: 1 / -1; -} diff --git a/cockpit/src/layout/BottomBand.module.css b/cockpit/src/layout/BottomBand.module.css deleted file mode 100644 index e028b4d..0000000 --- a/cockpit/src/layout/BottomBand.module.css +++ /dev/null @@ -1,48 +0,0 @@ -.bottomBand { - height: 72px; - background: var(--color-bg-panel); - border-top: 1px solid var(--color-border-soft); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 var(--space-lg); - gap: var(--space-xl); -} - -.block { - display: flex; - flex-direction: column; - gap: 2px; -} - -.label { - font-size: var(--font-size-xs); - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.value { - font-size: var(--font-size-md); - font-family: var(--font-mono); -} - -.pulseReceipt { - animation: pulse-gold 800ms ease-out; -} - -.recent { - display: flex; - gap: var(--space-sm); - flex-wrap: wrap; - max-width: 50%; -} - -.chip { - font-size: var(--font-size-xs); - font-family: var(--font-mono); - padding: 2px 8px; - border-radius: var(--radius-panel); - border: 1px solid var(--color-border-soft); - color: var(--color-accent-governance); -} diff --git a/cockpit/src/layout/BottomBand.tsx b/cockpit/src/layout/BottomBand.tsx deleted file mode 100644 index bed3410..0000000 --- a/cockpit/src/layout/BottomBand.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import styles from "./BottomBand.module.css"; -import { useCockpitState } from "../state/store"; - -export function BottomBand() { - const log = useCockpitState((s) => s.agent.log); - const receipts = useCockpitState((s) => s.governance.receipts); - const lastReceiptId = useCockpitState((s) => s.uiSignals.lastReceiptId); - const selectReceipt = useCockpitState((s) => s.actions.selectReceipt); - - return ( -
-
- Agent Cycle - {log.length} steps this session -
-
- Receipts - {receipts.length} -
-
- {receipts.slice(0, 5).map((r) => ( - - ))} -
-
- ); -} diff --git a/cockpit/src/layout/CenterCanvas.module.css b/cockpit/src/layout/CenterCanvas.module.css deleted file mode 100644 index 0f1b759..0000000 --- a/cockpit/src/layout/CenterCanvas.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.centerCanvas { - background: var(--color-bg-panel); - overflow: hidden; - position: relative; - box-shadow: var(--shadow-panel); - min-height: 0; - padding: var(--space-md); -} - -.fadeIn { - animation: fade-plan 400ms ease-out; -} diff --git a/cockpit/src/layout/CenterCanvas.tsx b/cockpit/src/layout/CenterCanvas.tsx deleted file mode 100644 index e493244..0000000 --- a/cockpit/src/layout/CenterCanvas.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import styles from "./CenterCanvas.module.css"; -import { useCockpitStore } from "../state/cockpitStore"; -import { PlanVisualizer } from "../panels/PlanVisualizer"; -import { DiffInspector } from "../panels/DiffInspector"; -import { ReceiptViewer } from "../panels/ReceiptViewer"; -import { ContinuityTimeline } from "../panels/ContinuityTimeline"; -import { InvariantDashboard } from "../panels/InvariantDashboard"; -import { KernelMonitor } from "../panels/KernelMonitor"; -import { FlightDeck } from "../flight-deck/FlightDeck"; -import { LedgerCompare } from "../flight-deck/LedgerCompare"; -import { ContinuityMatrix } from "../flight-deck/ContinuityMatrix"; -import { DriftVisualizer } from "../drift/DriftVisualizer"; -import type { CenterMode } from "../types"; - -export function CenterCanvas() { - const mode = useCockpitStore((s) => s.ui.centerMode); - const selectedAgents = useCockpitStore((s) => s.ui.selectedAgents); - const lastPlanId = useCockpitStore((s) => s.uiSignals.lastPlanId); - - const content = (() => { - switch (mode as CenterMode) { - case "plan": - return ; - case "diff": - return ; - case "receipts": - return ; - case "continuity": - return ; - case "invariants": - return ; - case "kernel": - return ; - case "flight-deck": - return ; - case "ledger-compare": - return ( - - ); - case "continuity-matrix": - return ; - case "drift": - return ; - default: - return ; - } - })(); - - return ( -
- {content} -
- ); -} diff --git a/cockpit/src/layout/LeftRail.module.css b/cockpit/src/layout/LeftRail.module.css deleted file mode 100644 index 35eec0b..0000000 --- a/cockpit/src/layout/LeftRail.module.css +++ /dev/null @@ -1,44 +0,0 @@ -.leftRail { - background: var(--color-bg-rail); - border-right: 1px solid var(--color-border-soft); - display: flex; - flex-direction: column; - overflow: hidden; - min-height: 0; -} - -.nav { - display: flex; - flex-direction: column; - padding: var(--space-sm); - gap: var(--space-xs); - border-bottom: 1px solid var(--color-border-soft); -} - -.navItem, -.navItemActive { - text-align: left; - padding: var(--space-sm) var(--space-md); - border-radius: 6px; - border: none; - background: transparent; - color: var(--color-text-secondary); - font-size: var(--font-size-md); -} - -.navItem:hover { - background: rgba(58, 123, 255, 0.08); - color: var(--color-text-primary); -} - -.navItemActive { - background: rgba(58, 123, 255, 0.18); - color: var(--color-accent-nova); - font-weight: 500; -} - -.consoleWrapper { - flex: 1; - min-height: 0; - overflow: hidden; -} diff --git a/cockpit/src/layout/LeftRail.tsx b/cockpit/src/layout/LeftRail.tsx deleted file mode 100644 index 4c0d2fa..0000000 --- a/cockpit/src/layout/LeftRail.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import styles from "./LeftRail.module.css"; -import { useCockpitState } from "../state/store"; -import { AgentConsole } from "../panels/AgentConsole"; -import type { CenterMode } from "../types"; - -const modes: { id: CenterMode; label: string }[] = [ - { id: "plan", label: "Plan" }, - { id: "diff", label: "Diff" }, - { id: "receipts", label: "Receipts" }, - { id: "continuity", label: "Continuity" }, - { id: "invariants", label: "Invariants" }, - { id: "kernel", label: "Kernel" }, - { id: "flight-deck", label: "Flight Deck" }, - { id: "ledger-compare", label: "Ledger Compare" }, - { id: "continuity-matrix", label: "Continuity Matrix" }, - { id: "drift", label: "Drift Map" }, -]; - -export function LeftRail() { - const centerMode = useCockpitState((s) => s.ui.centerMode); - const setCenterMode = useCockpitState((s) => s.actions.setCenterMode); - - return ( - - ); -} diff --git a/cockpit/src/layout/RightRail.module.css b/cockpit/src/layout/RightRail.module.css deleted file mode 100644 index 5fd0571..0000000 --- a/cockpit/src/layout/RightRail.module.css +++ /dev/null @@ -1,84 +0,0 @@ -.rightRail { - background: var(--color-bg-rail); - border-left: 1px solid var(--color-border-soft); - overflow-y: auto; - padding: var(--space-md); - display: flex; - flex-direction: column; - gap: var(--space-lg); -} - -.sectionTitle { - margin: 0 0 var(--space-sm); - font-size: var(--font-size-sm); - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); -} - -.empty { - font-size: var(--font-size-sm); - color: var(--color-text-muted); -} - -.list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.violationItem { - padding: var(--space-sm); - border-radius: 6px; - border: 1px solid var(--color-border-soft); - background: var(--color-bg-panel); -} - -.flashViolation { - animation: flash-red 600ms ease-out; -} - -.violationId { - display: block; - font-family: var(--font-mono); - font-size: var(--font-size-xs); - color: var(--color-status-error); -} - -.violationMsg { - display: block; - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - margin-top: 4px; -} - -.kernelGrid { - display: grid; - grid-template-columns: 1fr; - gap: var(--space-sm); -} - -.statusCard { - padding: var(--space-sm) var(--space-md); - border-radius: 6px; - border: 1px solid var(--color-border-soft); - background: var(--color-bg-panel); -} - -.statusLabel { - font-size: var(--font-size-xs); - color: var(--color-text-muted); -} - -.statusValue { - font-size: var(--font-size-sm); - font-weight: 600; - font-family: var(--font-mono); -} - -.statusOk { color: var(--color-status-ok); } -.statusWarn { color: var(--color-status-warn); } -.statusError { color: var(--color-status-error); } diff --git a/cockpit/src/layout/RightRail.tsx b/cockpit/src/layout/RightRail.tsx deleted file mode 100644 index ff6c6f6..0000000 --- a/cockpit/src/layout/RightRail.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import styles from "./RightRail.module.css"; -import { useCockpitState } from "../state/store"; - -export function RightRail() { - const violations = useCockpitState((s) => s.governance.violations); - const kernelStatus = useCockpitState((s) => s.kernel.status); - const lastViolationId = useCockpitState((s) => s.uiSignals.lastViolationId); - - return ( - - ); -} - -function KernelStatusItem({ label, status }: { label: string; status: string }) { - const cls = - status === "ok" ? styles.statusOk : status === "warn" ? styles.statusWarn : styles.statusError; - return ( -
-
{label}
-
{status.toUpperCase()}
-
- ); -} diff --git a/cockpit/src/layout/TopBar.module.css b/cockpit/src/layout/TopBar.module.css deleted file mode 100644 index fd296ce..0000000 --- a/cockpit/src/layout/TopBar.module.css +++ /dev/null @@ -1,62 +0,0 @@ -.topBar { - height: 56px; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 var(--space-lg); - background: var(--color-bg-shell); - border-bottom: 1px solid var(--color-border-soft); -} - -.left { - display: flex; - align-items: baseline; - gap: var(--space-sm); -} - -.brand { - font-weight: 600; - font-size: var(--font-size-xl); - color: var(--color-accent-nova); -} - -.sub { - font-size: var(--font-size-sm); - color: var(--color-text-muted); -} - -.center { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; -} - -.kernelLabel { - font-size: var(--font-size-xs); - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.kernelStatusOk { - font-size: var(--font-size-sm); - color: var(--color-status-ok); -} - -.kernelStatusWarn { - font-size: var(--font-size-sm); - color: var(--color-status-warn); -} - -.right { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); -} - -.invariantsCount { - padding: 4px 10px; - border-radius: var(--radius-panel); - border: 1px solid var(--color-border-soft); - background: var(--color-bg-rail); -} diff --git a/cockpit/src/layout/TopBar.tsx b/cockpit/src/layout/TopBar.tsx deleted file mode 100644 index bb4eb9e..0000000 --- a/cockpit/src/layout/TopBar.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import styles from "./TopBar.module.css"; -import { useCockpitState } from "../state/store"; -import { useKernelStore } from "../state/kernelStore"; - -export function TopBar() { - const kernelStatus = useCockpitState((s) => s.kernel.status); - const invariants = useCockpitState((s) => s.governance.invariants); - const kernelVersion = useKernelStore((s) => s.kernelVersion); - const pitBand = useKernelStore((s) => s.pitBand); - - const nominal = - kernelStatus.invariantEngine === "ok" && - kernelStatus.ledger === "ok" && - kernelStatus.continuity === "ok"; - - return ( -
-
-
Nova
-
Constitutional Coding Agent
-
-
- {kernelVersion} · PIT-{pitBand} - - {nominal ? "All systems nominal" : "Attention required"} - -
-
- - {invariants.length} invariants active - -
-
- ); -} diff --git a/cockpit/src/main.tsx b/cockpit/src/main.tsx deleted file mode 100644 index f258e49..0000000 --- a/cockpit/src/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { App } from "./App"; - -createRoot(document.getElementById("root")!).render( - - - -); diff --git a/cockpit/src/panels/AgentConsole.module.css b/cockpit/src/panels/AgentConsole.module.css deleted file mode 100644 index f517c35..0000000 --- a/cockpit/src/panels/AgentConsole.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.console { - height: 100%; - display: flex; - flex-direction: column; - font-size: var(--font-size-sm); -} - -.header { - padding: var(--space-md); - border-bottom: 1px solid var(--color-border-soft); -} - -.title { - margin: 0; - font-size: var(--font-size-md); - font-weight: 600; -} - -.goal { - margin-top: var(--space-xs); - color: var(--color-accent-continuity); - font-family: var(--font-mono); - font-size: var(--font-size-xs); -} - -.actions { - display: flex; - flex-direction: column; - gap: var(--space-xs); - padding: var(--space-sm); -} - -.btn { - padding: var(--space-sm); - border-radius: 6px; - border: 1px solid var(--color-border-soft); - background: rgba(58, 123, 255, 0.12); - color: var(--color-accent-nova); - font-size: var(--font-size-sm); -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.log { - flex: 1; - overflow-y: auto; - padding: var(--space-sm) var(--space-md); - font-family: var(--font-mono); - font-size: 11px; - line-height: 1.5; -} - -.logLine { - padding: 2px 0; - color: var(--color-text-secondary); -} - -.logLineViolation { - color: var(--color-status-error); -} - -.planSteps { - padding: 0 var(--space-md) var(--space-sm); - margin: 0; - list-style: none; - font-size: 11px; - color: var(--color-text-muted); -} - -.planSteps li::before { - content: "• "; - color: var(--color-accent-governance); -} diff --git a/cockpit/src/panels/AgentConsole.tsx b/cockpit/src/panels/AgentConsole.tsx deleted file mode 100644 index a2f277f..0000000 --- a/cockpit/src/panels/AgentConsole.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useState } from "react"; -import { nova, runtime } from "nova-sdk"; -import { useCockpitState } from "../state/store"; -import styles from "./AgentConsole.module.css"; - -export function AgentConsole() { - const goal = useCockpitState((s) => s.agent.currentGoal); - const plan = useCockpitState((s) => s.agent.currentPlan); - const log = useCockpitState((s) => s.agent.log); - const setGoal = useCockpitState((s) => s.actions.setGoal); - const setCenterMode = useCockpitState((s) => s.actions.setCenterMode); - const [busy, setBusy] = useState(false); - - async function handleGeneratePlan() { - const input = prompt("Enter goal:", goal ?? ""); - if (!input) return; - setGoal(input); - setBusy(true); - try { - const context = await runtime.getContext(); - await nova.plan({ goal: input, context }); - setCenterMode("plan"); - } catch (err) { - console.error(err); - } finally { - setBusy(false); - } - } - - async function handleGenerateCode() { - const promptText = prompt("Generate code for:", goal ?? "utility function"); - if (!promptText) return; - setBusy(true); - try { - const result = await nova.generateCode({ prompt: promptText }); - useCockpitState.getState().actions.selectDiff({ - text: result.code, - metadata: { - action: "generate", - invariantsChecked: result.receipts[0]?.invariantsChecked ?? [], - continuityHash: result.receipts[0]?.continuityHash ?? "", - receiptId: result.receipts[0]?.id, - }, - }); - } catch (err) { - console.error(err); - } finally { - setBusy(false); - } - } - - return ( -
-
-

Agent Console

-
{goal ?? "No active goal"}
-
-
- - -
- {plan ? ( -
    - {plan.steps.map((s: { id: string; description: string }) => ( -
  • {s.description}
  • - ))} -
- ) : null} -
- {log.map((e) => ( -
- [{new Date(e.timestamp).toLocaleTimeString()}] {e.message} -
- ))} -
-
- ); -} diff --git a/cockpit/src/panels/ContinuityTimeline.module.css b/cockpit/src/panels/ContinuityTimeline.module.css deleted file mode 100644 index 3b2c06d..0000000 --- a/cockpit/src/panels/ContinuityTimeline.module.css +++ /dev/null @@ -1,60 +0,0 @@ -.track { - display: flex; - align-items: center; - gap: var(--space-sm); - overflow-x: auto; - padding-bottom: var(--space-md); - margin-bottom: var(--space-lg); -} - -.node { - width: 24px; - height: 24px; - border-radius: 50%; - border: 2px solid var(--color-border-strong); - background: var(--color-bg-panel); - flex-shrink: 0; - cursor: pointer; -} - -.nodeSnapshot { - border-color: var(--color-accent-continuity); -} - -.nodeReceipt { - border-color: var(--color-accent-governance); -} - -.nodeViolation { - border-color: var(--color-status-error); -} - -.nodeSelected { - width: 32px; - height: 32px; - box-shadow: 0 0 0 2px var(--color-accent-nova); -} - -.connector { - width: 32px; - height: 2px; - background: var(--color-border-soft); - flex-shrink: 0; -} - -.detail { - padding: var(--space-md); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-panel); - font-family: var(--font-mono); - font-size: var(--font-size-xs); -} - -.btn { - margin-top: var(--space-md); - padding: var(--space-sm) var(--space-md); - border-radius: 6px; - border: 1px solid var(--color-border-soft); - background: rgba(86, 204, 242, 0.12); - color: var(--color-accent-continuity); -} diff --git a/cockpit/src/panels/ContinuityTimeline.tsx b/cockpit/src/panels/ContinuityTimeline.tsx deleted file mode 100644 index 60b1980..0000000 --- a/cockpit/src/panels/ContinuityTimeline.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { continuity } from "nova-sdk"; -import { useCockpitState } from "../state/store"; -import { Panel } from "../components/Panel"; -import styles from "./ContinuityTimeline.module.css"; - -export function ContinuityTimeline() { - const timeline = useCockpitState((s) => s.continuity.timeline); - const selectedId = useCockpitState((s) => s.continuity.selectedSnapshotId); - const selectSnapshot = useCockpitState((s) => s.actions.selectSnapshot); - - const selected = timeline.find((n) => n.id === selectedId); - - async function handleReplay(id: string) { - selectSnapshot(id); - const replay = await continuity.replay(id); - console.log("Replay result:", replay); - } - - return ( - -
- {timeline.map((node, i) => ( - - {i > 0 ? : null} -
- {selected ? ( -
-
ID: {selected.id}
-
Type: {selected.type}
-
Time: {new Date(selected.timestamp).toLocaleString()}
-
State Hash: {selected.stateHash}
- -
- ) : ( -
Select a timeline node
- )} -
- ); -} diff --git a/cockpit/src/panels/DiffInspector.module.css b/cockpit/src/panels/DiffInspector.module.css deleted file mode 100644 index 832a4ee..0000000 --- a/cockpit/src/panels/DiffInspector.module.css +++ /dev/null @@ -1,48 +0,0 @@ -.columns { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-md); - min-height: 200px; -} - -.col { - padding: var(--space-md); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-panel); - background: #0d1117; - font-family: var(--font-mono); - font-size: var(--font-size-xs); - white-space: pre-wrap; - overflow: auto; -} - -.meta { - margin-top: var(--space-lg); - padding: var(--space-md); - border-top: 1px solid var(--color-border-soft); - font-size: var(--font-size-sm); - font-family: var(--font-mono); -} - -.actions { - margin-top: var(--space-md); - display: flex; - gap: var(--space-sm); -} - -.btnPrimary { - padding: var(--space-sm) var(--space-md); - border-radius: 6px; - border: none; - background: var(--color-accent-nova); - color: white; -} - -.btnPrimary:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.empty { - color: var(--color-text-muted); -} diff --git a/cockpit/src/panels/DiffInspector.tsx b/cockpit/src/panels/DiffInspector.tsx deleted file mode 100644 index 18348e0..0000000 --- a/cockpit/src/panels/DiffInspector.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { nova } from "nova-sdk"; -import { useCockpitState } from "../state/store"; -import { Panel } from "../components/Panel"; -import styles from "./DiffInspector.module.css"; - -export function DiffInspector() { - const diff = useCockpitState((s) => s.workspace.selectedDiff); - - async function applyPatch() { - if (!diff) return; - try { - await nova.applyPatch({ diff: diff.text, reason: "User applied patch from cockpit" }); - } catch (err) { - console.error(err); - } - } - - if (!diff) { - return ( - -
No diff selected — generate code or refactor to inspect.
-
- ); - } - - return ( - -
-
— before —
-
{diff.text}
-
-
-
Action: {diff.metadata.action}
-
Invariants: [{diff.metadata.invariantsChecked.join(", ")}]
-
Continuity Hash: {diff.metadata.continuityHash}
-
Receipt: {diff.metadata.receiptId ?? "—"}
-
-
- -
-
- ); -} diff --git a/cockpit/src/panels/InvariantDashboard.module.css b/cockpit/src/panels/InvariantDashboard.module.css deleted file mode 100644 index 96d3022..0000000 --- a/cockpit/src/panels/InvariantDashboard.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.list { - list-style: none; - margin: 0; - padding: 0; -} - -.item { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-sm) 0; - border-bottom: 1px solid var(--color-border-soft); - font-size: var(--font-size-sm); -} - -.badgeE { - color: var(--color-status-error); - font-family: var(--font-mono); - font-size: var(--font-size-xs); -} - -.badgeW { - color: var(--color-status-warn); - font-family: var(--font-mono); - font-size: var(--font-size-xs); -} - -.violations { - margin-top: var(--space-xl); -} - -.violationRow { - font-size: var(--font-size-sm); - padding: var(--space-xs) 0; - color: var(--color-status-error); - font-family: var(--font-mono); -} diff --git a/cockpit/src/panels/InvariantDashboard.tsx b/cockpit/src/panels/InvariantDashboard.tsx deleted file mode 100644 index 4be3611..0000000 --- a/cockpit/src/panels/InvariantDashboard.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useCockpitState } from "../state/store"; -import { Panel } from "../components/Panel"; -import styles from "./InvariantDashboard.module.css"; - -export function InvariantDashboard() { - const invariants = useCockpitState((s) => s.governance.invariants); - const violations = useCockpitState((s) => s.governance.violations); - - return ( - -
ACTIVE ({invariants.length})
-
    - {invariants.map((inv) => ( -
  • - - [{inv.severity === "error" ? "E" : "W"}] - - {inv.id} -
  • - ))} -
-
- Recent Violations - {violations.length === 0 ? ( -
None
- ) : ( - violations.slice(0, 8).map((v) => ( -
- [{new Date().toLocaleTimeString()}] {v.invariantId} -
- )) - )} -
-
- ); -} diff --git a/cockpit/src/panels/KernelMonitor.module.css b/cockpit/src/panels/KernelMonitor.module.css deleted file mode 100644 index 9059e48..0000000 --- a/cockpit/src/panels/KernelMonitor.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.grid { - display: grid; - grid-template-columns: repeat(2, minmax(160px, 1fr)); - gap: var(--space-md); -} - -.card { - min-height: 100px; - padding: var(--space-md); - border-radius: var(--radius-panel); - border: 1px solid var(--color-border-soft); - background: var(--color-bg-panel); -} - -.cardLabel { - font-size: var(--font-size-xs); - color: var(--color-text-muted); - margin-bottom: var(--space-xs); -} - -.cardValue { - font-size: var(--font-size-lg); - font-weight: 600; - font-family: var(--font-mono); -} - -.ok { color: var(--color-status-ok); } -.warn { color: var(--color-status-warn); } -.error { color: var(--color-status-error); } - -.summary { - margin-top: var(--space-xl); - font-size: var(--font-size-sm); - line-height: 1.8; -} diff --git a/cockpit/src/panels/KernelMonitor.tsx b/cockpit/src/panels/KernelMonitor.tsx deleted file mode 100644 index 937e0f9..0000000 --- a/cockpit/src/panels/KernelMonitor.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useCockpitState } from "../state/store"; -import { Panel } from "../components/Panel"; -import styles from "./KernelMonitor.module.css"; - -function StatusCard({ - label, - value, - status, -}: { - label: string; - value: string | number; - status?: string; -}) { - const cls = - status === "ok" ? styles.ok : status === "warn" ? styles.warn : status === "error" ? styles.error : ""; - return ( -
-
{label}
-
{value}
-
- ); -} - -export function KernelMonitor() { - const status = useCockpitState((s) => s.kernel.status); - - const nominal = - status.invariantEngine === "ok" && - status.ledger === "ok" && - status.continuity === "ok"; - - return ( - -
- - - - -
-
- Replay Consistency: {nominal ? "PASS" : "CHECK"} -
- Kernel heartbeat: every 2s -
-
- ); -} diff --git a/cockpit/src/panels/PlanVisualizer.module.css b/cockpit/src/panels/PlanVisualizer.module.css deleted file mode 100644 index bfe6b89..0000000 --- a/cockpit/src/panels/PlanVisualizer.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.graph { - display: flex; - align-items: center; - gap: var(--space-md); - overflow-x: auto; - padding: var(--space-lg) 0; -} - -.step { - min-width: 160px; - padding: var(--space-md); - border-radius: var(--radius-panel); - border: 1px solid var(--color-border-soft); - background: var(--color-bg-panel-alt); - font-size: var(--font-size-sm); -} - -.stepActive { - border-color: var(--color-accent-nova); -} - -.arrow { - color: var(--color-text-muted); - flex-shrink: 0; -} - -.empty { - color: var(--color-text-muted); -} - -.justification { - margin-top: var(--space-lg); - font-size: var(--font-size-sm); - color: var(--color-text-secondary); -} diff --git a/cockpit/src/panels/PlanVisualizer.tsx b/cockpit/src/panels/PlanVisualizer.tsx deleted file mode 100644 index 11391ef..0000000 --- a/cockpit/src/panels/PlanVisualizer.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Plan } from "../types"; -import { useCockpitState } from "../state/store"; -import { Panel } from "../components/Panel"; -import styles from "./PlanVisualizer.module.css"; - -export function PlanVisualizer() { - const plan = useCockpitState((s) => s.agent.currentPlan); - - if (!plan) { - return ( - -
No plan yet — use Agent Console to generate one.
-
- ); - } - - return ( - -
- {plan.steps.map((step: Plan["steps"][number], i: number) => ( - - {i > 0 ? : null} -
{step.description}
-
- ))} -
-
{plan.justification}
-
- ); -} diff --git a/cockpit/src/panels/ReceiptViewer.module.css b/cockpit/src/panels/ReceiptViewer.module.css deleted file mode 100644 index 30fad84..0000000 --- a/cockpit/src/panels/ReceiptViewer.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.table { - width: 100%; - border-collapse: collapse; - font-size: var(--font-size-sm); -} - -.table th { - text-align: left; - padding: var(--space-sm); - border-bottom: 1px solid var(--color-border-strong); - color: var(--color-text-muted); - font-weight: 500; -} - -.table td { - padding: var(--space-sm); - border-bottom: 1px solid var(--color-border-soft); - font-family: var(--font-mono); - font-size: var(--font-size-xs); - cursor: pointer; -} - -.rowSelected { - background: rgba(58, 123, 255, 0.12); -} - -.detail { - margin-top: var(--space-lg); - padding: var(--space-md); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-panel); - background: var(--color-bg-panel); - font-family: var(--font-mono); - font-size: var(--font-size-xs); - white-space: pre-wrap; -} diff --git a/cockpit/src/panels/ReceiptViewer.tsx b/cockpit/src/panels/ReceiptViewer.tsx deleted file mode 100644 index c53c61c..0000000 --- a/cockpit/src/panels/ReceiptViewer.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useCockpitState } from "../state/store"; -import { Panel } from "../components/Panel"; -import styles from "./ReceiptViewer.module.css"; - -export function ReceiptViewer() { - const receipts = useCockpitState((s) => s.governance.receipts); - const selectedId = useCockpitState((s) => s.governance.selectedReceiptId); - const selectReceipt = useCockpitState((s) => s.actions.selectReceipt); - - const selected = receipts.find((r) => r.id === selectedId); - - return ( - - - - - - - - - - - - {receipts.map((r) => ( - selectReceipt(r.id)} - > - - - - - - ))} - -
IDActionInvariantsTime
{r.id.slice(0, 12)}…{r.action.type}{r.invariantsChecked.length}{new Date(r.timestamp).toLocaleTimeString()}
- {selected ? ( -
{JSON.stringify(selected, null, 2)}
- ) : null} -
- ); -} diff --git a/cockpit/src/state/NovaEventBridge.ts b/cockpit/src/state/NovaEventBridge.ts deleted file mode 100644 index 9667e04..0000000 --- a/cockpit/src/state/NovaEventBridge.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { continuity, events, governance } from "nova-sdk"; -import { invariants as defaultInvariants } from "../../../config/nova.config"; -import { useKernelStore } from "./kernelStore"; -import { useCockpitState } from "./store"; - -let initialized = false; -let heartbeatTimer: ReturnType | null = null; - -export async function initializeNovaEventBridge(): Promise { - if (initialized) return; - initialized = true; - - const actions = useCockpitState.getState().actions; - - for (const inv of defaultInvariants) { - await governance.requireInvariant(inv); - } - actions.setInvariants([...defaultInvariants]); - - events.onPlan((plan) => { - actions.setPlan(plan); - actions.signalPlan(plan.id); - actions.appendLog({ - type: "plan", - timestamp: Date.now(), - message: `Plan generated (${plan.steps.length} steps)`, - }); - setTimeout(() => actions.clearSignal("lastPlanId"), 800); - }); - - events.onAction((action) => { - actions.appendLog({ - type: "action", - timestamp: Date.now(), - message: `Action: ${action.type}`, - }); - }); - - events.onReceipt((receipt) => { - actions.addReceipt(receipt); - actions.signalReceipt(receipt.id); - actions.appendLog({ - type: "receipt", - timestamp: Date.now(), - message: `Receipt ${receipt.id.slice(0, 8)}… emitted`, - }); - void continuity.snapshot().then((snapshot) => { - actions.updateContinuity(snapshot); - }); - setTimeout(() => actions.clearSignal("lastReceiptId"), 800); - }); - - events.onViolation((violation) => { - actions.addViolation(violation); - actions.signalViolation(violation.id); - actions.appendLog({ - type: "violation", - timestamp: Date.now(), - message: `Invariant violated: ${violation.invariantId}`, - }); - setTimeout(() => actions.clearSignal("lastViolationId"), 600); - }); - - events.onKernelHeartbeat((hb) => { - actions.updateKernelStatus(hb); - useKernelStore.getState().actions.updateStatus(hb); - }); - - const poll = async () => { - const status = await governance.kernelStatus(); - actions.updateKernelStatus(status); - await governance.emitKernelHeartbeat(); - }; - - await poll(); - heartbeatTimer = setInterval(() => void poll(), 2000); -} - -export function teardownNovaEventBridge(): void { - if (heartbeatTimer) clearInterval(heartbeatTimer); - initialized = false; -} diff --git a/cockpit/src/state/clusterStore.ts b/cockpit/src/state/clusterStore.ts deleted file mode 100644 index fac8c1a..0000000 --- a/cockpit/src/state/clusterStore.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { create } from "zustand"; - -export interface AgentSnapshotCell { - id: string; - state: "ok" | "drift" | "error"; - hash?: string; -} - -export interface AgentReceipt { - receiptId: string; - actionId: string; - invariantsChecked: string[]; - pitBand: number; - continuityHash: string; -} - -export interface ClusterAgent { - id: string; - status: "online" | "offline" | "drift" | "error"; - kernelStatus?: "ok" | "warn" | "error" | string; - pitBand?: number; - snapshots: AgentSnapshotCell[]; - receipts: AgentReceipt[]; -} - -export interface ClusterEvent { - id: string; - type: string; - agentId?: string; - timestamp: number; - payload?: Record; -} - -export interface DriftEventPayload { - driftId: string; - agents: string[]; - snapshotId: string; - driftType: string; -} - -interface ClusterStoreState { - agents: Record; - selectedAgent: string | null; - clusterEvents: ClusterEvent[]; - replayWindow: { from?: string; to?: string } | null; - actions: { - updateAgentStatus(id: string, patch: Partial): void; - updateAgentKernelStatus( - id: string, - patch: { kernelStatus: "ok" | "warn" | "error"; pitBand: number } - ): void; - addAgentReceipt(agentId: string, receipt: AgentReceipt): void; - addAgentSnapshot( - agentId: string, - snapshot: { snapshotId: string; hash: string; partial: boolean } - ): void; - setClusterHeartbeat( - agents: Record - ): void; - addDriftEvent(payload: DriftEventPayload): void; - addClusterEvent(event: Omit & { id?: string }): void; - selectAgent(id: string | null): void; - setReplayWindow(window: { from?: string; to?: string } | null): void; - seedDemoAgents(): void; - }; -} - -function emptyAgent(id: string, patch: Partial = {}): ClusterAgent { - return { - id, - status: "online", - snapshots: [], - receipts: [], - ...patch, - }; -} - -export const useClusterStore = create((set) => ({ - agents: {}, - selectedAgent: null, - clusterEvents: [], - replayWindow: null, - actions: { - updateAgentStatus: (id, patch) => - set((state) => ({ - agents: { - ...state.agents, - [id]: { ...emptyAgent(id, state.agents[id]), ...patch }, - }, - })), - updateAgentKernelStatus: (id, patch) => - set((state) => ({ - agents: { - ...state.agents, - [id]: { - ...emptyAgent(id, state.agents[id]), - kernelStatus: patch.kernelStatus, - pitBand: patch.pitBand, - status: - patch.kernelStatus === "error" - ? "error" - : patch.kernelStatus === "warn" - ? "drift" - : "online", - }, - }, - })), - addAgentReceipt: (agentId, receipt) => - set((state) => { - const agent = emptyAgent(agentId, state.agents[agentId]); - return { - agents: { - ...state.agents, - [agentId]: { ...agent, receipts: [receipt, ...agent.receipts] }, - }, - }; - }), - addAgentSnapshot: (agentId, snapshot) => - set((state) => { - const agent = emptyAgent(agentId, state.agents[agentId]); - return { - agents: { - ...state.agents, - [agentId]: { - ...agent, - snapshots: [ - ...agent.snapshots, - { - id: snapshot.snapshotId, - hash: snapshot.hash, - state: "ok" as const, - }, - ], - }, - }, - }; - }), - setClusterHeartbeat: (agents) => - set((state) => { - const next = { ...state.agents }; - for (const [id, hb] of Object.entries(agents)) { - next[id] = { - ...emptyAgent(id, next[id]), - kernelStatus: hb.kernelStatus, - pitBand: hb.pitBand, - status: hb.kernelStatus === "error" ? "error" : "online", - }; - } - return { agents: next }; - }), - addDriftEvent: (payload) => - set((state) => ({ - clusterEvents: [ - { - id: payload.driftId, - type: "cluster.driftDetected", - timestamp: Date.now(), - payload: payload as unknown as Record, - }, - ...state.clusterEvents, - ], - })), - addClusterEvent: (event) => - set((state) => ({ - clusterEvents: [ - { ...event, id: event.id ?? crypto.randomUUID() }, - ...state.clusterEvents, - ], - })), - selectAgent: (selectedAgent) => set({ selectedAgent }), - setReplayWindow: (replayWindow) => set({ replayWindow }), - seedDemoAgents: () => - set({ - agents: { - "agent-alpha": emptyAgent("agent-alpha", { - kernelStatus: "ok", - pitBand: 2, - snapshots: [ - { id: "s1", state: "ok" }, - { id: "s2", state: "ok" }, - ], - }), - "agent-beta": emptyAgent("agent-beta", { - kernelStatus: "warn", - pitBand: 3, - snapshots: [ - { id: "s1", state: "ok" }, - { id: "s2", state: "drift" }, - ], - }), - }, - }), - }, -})); diff --git a/cockpit/src/state/cockpitStore.ts b/cockpit/src/state/cockpitStore.ts deleted file mode 100644 index 2528085..0000000 --- a/cockpit/src/state/cockpitStore.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useCockpitState as useCockpitStore } from "./store"; -export { useKernelStore } from "./kernelStore"; -export { useClusterStore } from "./clusterStore"; -export { useDriftStore } from "./driftStore"; - -export type { CenterMode as CockpitMode } from "../types"; diff --git a/cockpit/src/state/driftStore.ts b/cockpit/src/state/driftStore.ts deleted file mode 100644 index 9190768..0000000 --- a/cockpit/src/state/driftStore.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { create } from "zustand"; -import type { AgentSnapshotCell } from "./clusterStore"; - -export interface DriftAgentColumn { - id: string; - snapshots: AgentSnapshotCell[]; -} - -export interface DivergenceEvent { - id: string; - type: string; - snapshotId?: string; - agents: string[]; -} - -interface DriftStoreState { - agents: DriftAgentColumn[]; - divergences: DivergenceEvent[]; - actions: { - setAgents(agents: DriftAgentColumn[]): void; - setDivergences(divergences: DivergenceEvent[]): void; - appendDivergence(event: Omit & { id?: string }): void; - syncFromCluster(agents: Record): void; - }; -} - -export const useDriftStore = create((set) => ({ - agents: [], - divergences: [], - actions: { - setAgents: (agents) => set({ agents }), - setDivergences: (divergences) => set({ divergences }), - appendDivergence: (event) => - set((state) => ({ - divergences: [ - { ...event, id: event.id ?? crypto.randomUUID() }, - ...state.divergences, - ], - })), - syncFromCluster: (agents) => - set({ - agents: Object.values(agents).map((a) => ({ - id: a.id, - snapshots: a.snapshots, - })), - }), - }, -})); diff --git a/cockpit/src/state/kernelStore.ts b/cockpit/src/state/kernelStore.ts deleted file mode 100644 index ca332a6..0000000 --- a/cockpit/src/state/kernelStore.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { create } from "zustand"; -import type { KernelStatus } from "../types"; - -export interface KernelHeartbeatPayload { - invariantEngine: "ok" | "warn" | "error"; - constraintEngine: "ok" | "warn" | "error"; - continuity: "ok" | "warn" | "error"; - ledger: "ok" | "warn" | "error"; - pitBand: number; - continuityAnchorHash: string; - ledgerPrefixHash: string; -} - -export interface KernelSlice { - kernelVersion: string; - pitBand: number; - status: KernelStatus; - continuityAnchorHash: string | null; - ledgerPrefixHash: string | null; - perAgent: Record; -} - -interface KernelStoreState extends KernelSlice { - actions: { - setKernelVersion(version: string): void; - setPitBand(band: number): void; - updateStatus(status: KernelStatus): void; - setContinuityAnchor(hash: string): void; - updateHeartbeat(agentId: string, payload: KernelHeartbeatPayload): void; - }; -} - -export const useKernelStore = create((set) => ({ - kernelVersion: "CRK-2", - pitBand: 1, - status: { - invariantEngine: "ok", - ledger: "ok", - continuity: "ok", - violationsLastMinute: 0, - receiptCount: 0, - snapshotCount: 0, - activeInvariants: 0, - }, - continuityAnchorHash: null, - ledgerPrefixHash: null, - perAgent: {}, - actions: { - setKernelVersion: (kernelVersion) => set({ kernelVersion }), - setPitBand: (pitBand) => set({ pitBand }), - updateStatus: (status) => set({ status }), - setContinuityAnchor: (continuityAnchorHash) => set({ continuityAnchorHash }), - updateHeartbeat: (agentId, payload) => - set((state) => ({ - pitBand: payload.pitBand, - continuityAnchorHash: payload.continuityAnchorHash, - ledgerPrefixHash: payload.ledgerPrefixHash, - perAgent: { ...state.perAgent, [agentId]: payload }, - status: { - ...state.status, - invariantEngine: payload.invariantEngine, - ledger: payload.ledger, - continuity: payload.continuity, - }, - })), - }, -})); diff --git a/cockpit/src/state/store.ts b/cockpit/src/state/store.ts deleted file mode 100644 index 9448185..0000000 --- a/cockpit/src/state/store.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { create } from "zustand"; -import type { - CenterMode, - AgentLogEntry, - ContinuityNode, - SelectedDiff, - UiSignals, - Plan, - GovernanceReceipt, - InvariantViolation, - KernelStatus, - Invariant, -} from "../types"; - -interface CockpitState { - ui: { centerMode: CenterMode; selectedAgents: string[] }; - uiSignals: UiSignals; - agent: { - currentGoal: string | null; - currentPlan: Plan | null; - log: AgentLogEntry[]; - }; - governance: { - invariants: Invariant[]; - receipts: GovernanceReceipt[]; - violations: InvariantViolation[]; - selectedReceiptId: string | null; - }; - continuity: { - timeline: ContinuityNode[]; - selectedSnapshotId: string | null; - }; - workspace: { - selectedDiff: SelectedDiff | null; - }; - kernel: { status: KernelStatus }; - actions: { - setCenterMode(mode: CenterMode): void; - setGoal(goal: string): void; - setPlan(plan: Plan): void; - setInvariants(invariants: Invariant[]): void; - appendLog(entry: Omit & { id?: string }): void; - addReceipt(r: GovernanceReceipt): void; - addViolation(v: InvariantViolation): void; - updateKernelStatus(status: KernelStatus): void; - updateContinuity(snapshot: { id: string; timestamp: number; stateHash: string }): void; - selectSnapshot(id: string): void; - selectReceipt(id: string): void; - selectDiff(diff: SelectedDiff): void; - signalViolation(id: string): void; - signalReceipt(id: string): void; - signalPlan(id: string): void; - clearSignal(key: keyof UiSignals): void; - setSelectedAgents(agentIds: string[]): void; - addViolationFromGateway(v: { - agentId: string; - invariantId: string; - message: string; - actionId: string; - severity: "error" | "warn"; - }): void; - addReceiptFromGateway(r: { - agentId: string; - receiptId: string; - actionId: string; - invariantsChecked: string[]; - pitBand: number; - continuityHash: string; - }): void; - addSnapshotFromGateway(s: { - agentId: string; - snapshotId: string; - hash: string; - partial: boolean; - }): void; - setPlanForAgent( - agentId: string, - payload: { planId: string; steps: { id: string; description: string }[] } - ): void; - addActionLog(entry: { - agentId: string; - actionId: string; - stepId: string; - description: string; - }): void; - }; -} - -const defaultKernel: KernelStatus = { - invariantEngine: "ok", - ledger: "ok", - continuity: "ok", - violationsLastMinute: 0, - receiptCount: 0, - snapshotCount: 0, - activeInvariants: 0, -}; - -export const useCockpitState = create((set) => ({ - ui: { centerMode: "plan", selectedAgents: ["agent-alpha", "agent-beta"] }, - uiSignals: {}, - agent: { currentGoal: null, currentPlan: null, log: [] }, - governance: { invariants: [], receipts: [], violations: [], selectedReceiptId: null }, - continuity: { timeline: [], selectedSnapshotId: null }, - workspace: { selectedDiff: null }, - kernel: { status: defaultKernel }, - - actions: { - setCenterMode: (mode) => set((s) => ({ ui: { ...s.ui, centerMode: mode } })), - setGoal: (goal) => set((s) => ({ agent: { ...s.agent, currentGoal: goal } })), - setPlan: (plan) => set((s) => ({ agent: { ...s.agent, currentPlan: plan } })), - setInvariants: (invariants) => - set((s) => ({ governance: { ...s.governance, invariants } })), - appendLog: (entry) => - set((s) => ({ - agent: { - ...s.agent, - log: [ - ...s.agent.log, - { ...entry, id: entry.id ?? crypto.randomUUID() }, - ], - }, - })), - addReceipt: (r) => - set((s) => ({ - governance: { - ...s.governance, - receipts: [r, ...s.governance.receipts], - }, - continuity: { - ...s.continuity, - timeline: [ - ...s.continuity.timeline, - { - id: r.id, - timestamp: r.timestamp, - stateHash: r.continuityHash, - type: "receipt" as const, - label: r.action.type, - }, - ], - }, - })), - addViolation: (v) => - set((s) => ({ - governance: { - ...s.governance, - violations: [v, ...s.governance.violations], - }, - continuity: { - ...s.continuity, - timeline: [ - ...s.continuity.timeline, - { - id: v.id, - timestamp: Date.now(), - stateHash: v.invariantId, - type: "violation" as const, - label: v.invariantId, - }, - ], - }, - })), - updateKernelStatus: (status) => set({ kernel: { status } }), - updateContinuity: (snapshot) => - set((s) => ({ - continuity: { - ...s.continuity, - timeline: [ - ...s.continuity.timeline, - { - id: snapshot.id, - timestamp: snapshot.timestamp, - stateHash: snapshot.stateHash, - type: "snapshot" as const, - }, - ], - }, - })), - selectSnapshot: (id) => - set((s) => ({ continuity: { ...s.continuity, selectedSnapshotId: id } })), - selectReceipt: (id) => - set((s) => ({ - governance: { ...s.governance, selectedReceiptId: id }, - ui: { ...s.ui, centerMode: "receipts" }, - })), - selectDiff: (diff) => - set((s) => ({ - workspace: { ...s.workspace, selectedDiff: diff }, - ui: { ...s.ui, centerMode: "diff" }, - })), - signalViolation: (id) => - set((s) => ({ uiSignals: { ...s.uiSignals, lastViolationId: id } })), - signalReceipt: (id) => - set((s) => ({ uiSignals: { ...s.uiSignals, lastReceiptId: id } })), - signalPlan: (id) => - set((s) => ({ uiSignals: { ...s.uiSignals, lastPlanId: id } })), - clearSignal: (key) => - set((s) => { - const next = { ...s.uiSignals }; - delete next[key]; - return { uiSignals: next }; - }), - setSelectedAgents: (selectedAgents) => - set((s) => ({ ui: { ...s.ui, selectedAgents } })), - addViolationFromGateway: (v) => - set((s) => ({ - governance: { - ...s.governance, - violations: [ - { - id: crypto.randomUUID(), - invariantId: v.invariantId, - description: v.message, - message: v.message, - severity: v.severity, - action: { type: "generate", payload: { actionId: v.actionId, agentId: v.agentId } }, - }, - ...s.governance.violations, - ], - }, - })), - addReceiptFromGateway: (r) => - set((s) => ({ - governance: { - ...s.governance, - receipts: [ - { - id: r.receiptId, - timestamp: Date.now(), - action: { type: "generate", payload: { actionId: r.actionId, agentId: r.agentId } }, - invariantsChecked: r.invariantsChecked, - continuityHash: r.continuityHash, - ledgerHash: "", - }, - ...s.governance.receipts, - ], - }, - })), - addSnapshotFromGateway: (snap) => - set((s) => ({ - continuity: { - ...s.continuity, - timeline: [ - ...s.continuity.timeline, - { - id: snap.snapshotId, - timestamp: Date.now(), - stateHash: snap.hash, - type: "snapshot" as const, - label: snap.agentId, - }, - ], - }, - })), - setPlanForAgent: (agentId, payload) => - set((s) => ({ - agent: { - ...s.agent, - currentPlan: { - id: payload.planId, - justification: `Plan for ${agentId}`, - receipts: [], - steps: payload.steps.map((step) => ({ - id: step.id, - description: step.description, - action: { type: "plan" as const, payload: { stepId: step.id } }, - })), - }, - }, - uiSignals: { ...s.uiSignals, lastPlanId: payload.planId }, - })), - addActionLog: (entry) => - set((s) => ({ - agent: { - ...s.agent, - log: [ - ...s.agent.log, - { - id: crypto.randomUUID(), - type: "action", - timestamp: Date.now(), - message: `[${entry.agentId}] ${entry.description}`, - }, - ], - }, - })), - }, -})); diff --git a/cockpit/src/styles/tokens.json b/cockpit/src/styles/tokens.json deleted file mode 100644 index 2d2b018..0000000 --- a/cockpit/src/styles/tokens.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "color": { - "bg.shell": "#050711", - "bg.panel": "#0B0E1A", - "bg.panelAlt": "#101426", - "bg.rail": "#070914", - "text.primary": "#F5F7FF", - "text.secondary": "#A3B0D9", - "text.muted": "#6B7390", - "accent.nova": "#3A7BFF", - "accent.governance": "#F2C94C", - "accent.continuity": "#56CCF2", - "status.ok": "#27AE60", - "status.warn": "#F2C94C", - "status.error": "#EB5757", - "border.soft": "#1C2238", - "border.strong": "#2C3550" - }, - "font": { - "family.ui": "Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif", - "family.mono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace" - }, - "space": { "xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "xxl": 32 }, - "radius": { "panel": 8, "chip": 999 }, - "shadow": { - "panel": "0 12px 40px rgba(0, 0, 0, 0.45)", - "subtle": "0 4px 16px rgba(0, 0, 0, 0.35)" - } -} diff --git a/cockpit/src/styles/variables.css b/cockpit/src/styles/variables.css deleted file mode 100644 index c3eec9b..0000000 --- a/cockpit/src/styles/variables.css +++ /dev/null @@ -1,79 +0,0 @@ -:root { - --color-bg-shell: #050711; - --color-bg-panel: #0b0e1a; - --color-bg-panel-alt: #101426; - --color-bg-rail: #070914; - --color-text-primary: #f5f7ff; - --color-text-secondary: #a3b0d9; - --color-text-muted: #6b7390; - --color-accent-nova: #3a7bff; - --color-accent-governance: #f2c94c; - --color-accent-continuity: #56ccf2; - --color-status-ok: #27ae60; - --color-status-warn: #f2c94c; - --color-status-error: #eb5757; - --color-border-soft: #1c2238; - --color-border-strong: #2c3550; - --font-ui: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; - --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; - --font-size-xs: 11px; - --font-size-sm: 12px; - --font-size-md: 14px; - --font-size-lg: 16px; - --font-size-xl: 20px; - --space-xs: 4px; - --space-sm: 8px; - --space-md: 12px; - --space-lg: 16px; - --space-xl: 24px; - --radius-panel: 8px; - --shadow-panel: 0 12px 40px rgba(0, 0, 0, 0.45); -} - -body.theme-light { - --color-bg-shell: #f5f7ff; - --color-bg-panel: #ffffff; - --color-bg-rail: #f0f2fa; - --color-text-primary: #111322; - --color-text-secondary: #4a4f6b; - --color-text-muted: #7c8199; - --color-border-soft: #d5dae8; - --color-border-strong: #b5bed8; -} - -@keyframes flash-red { - 0% { background-color: rgba(235, 87, 87, 0.25); } - 100% { background-color: transparent; } -} - -@keyframes pulse-gold { - 0% { box-shadow: 0 0 0 0 rgba(242, 201, 76, 0.55); } - 100% { box-shadow: 0 0 0 14px rgba(242, 201, 76, 0); } -} - -@keyframes fade-plan { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } -} - -* { - box-sizing: border-box; -} - -html, -body, -#root { - height: 100%; - margin: 0; -} - -body { - font-family: var(--font-ui); - background: var(--color-bg-shell); - color: var(--color-text-primary); -} - -button { - font-family: inherit; - cursor: pointer; -} diff --git a/cockpit/src/types.ts b/cockpit/src/types.ts deleted file mode 100644 index 6062df4..0000000 --- a/cockpit/src/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Plan } from "nova-sdk"; -import type { GovernanceReceipt } from "nova-sdk"; -import type { Invariant, InvariantViolation } from "nova-sdk"; -import type { KernelStatus } from "nova-sdk"; - -export type CenterMode = - | "plan" - | "diff" - | "receipts" - | "continuity" - | "invariants" - | "kernel" - | "flight-deck" - | "ledger-compare" - | "continuity-matrix" - | "drift"; - -export interface AgentLogEntry { - id: string; - type: "plan" | "action" | "receipt" | "violation" | "step"; - timestamp: number; - message: string; -} - -export interface ContinuityNode { - id: string; - timestamp: number; - stateHash: string; - type: "snapshot" | "receipt" | "violation"; - label?: string; -} - -export interface DiffMetadata { - action: string; - invariantsChecked: string[]; - continuityHash: string; - receiptId?: string; -} - -export interface SelectedDiff { - text: string; - metadata: DiffMetadata; -} - -export interface UiSignals { - lastViolationId?: string; - lastReceiptId?: string; - lastPlanId?: string; -} - -export type { Plan, GovernanceReceipt, Invariant, InvariantViolation, KernelStatus }; diff --git a/cockpit/src/vite-env.d.ts b/cockpit/src/vite-env.d.ts deleted file mode 100644 index 28b46f6..0000000 --- a/cockpit/src/vite-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// - -declare module "*.module.css" { - const classes: { readonly [key: string]: string }; - export default classes; -} diff --git a/cockpit/tsconfig.json b/cockpit/tsconfig.json deleted file mode 100644 index a168cd5..0000000 --- a/cockpit/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "paths": { - "nova-sdk": ["../src/index.ts"] - } - }, - "include": ["src"] -} diff --git a/cockpit/tsconfig.tsbuildinfo b/cockpit/tsconfig.tsbuildinfo deleted file mode 100644 index 3502d50..0000000 --- a/cockpit/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/app.tsx","./src/novashell.tsx","./src/main.tsx","./src/types.ts","./src/components/panel.tsx","./src/layout/bottomband.tsx","./src/layout/centercanvas.tsx","./src/layout/leftrail.tsx","./src/layout/rightrail.tsx","./src/layout/topbar.tsx","./src/panels/agentconsole.tsx","./src/panels/continuitytimeline.tsx","./src/panels/diffinspector.tsx","./src/panels/invariantdashboard.tsx","./src/panels/kernelmonitor.tsx","./src/panels/planvisualizer.tsx","./src/panels/receiptviewer.tsx","./src/state/novaeventbridge.ts","./src/state/store.ts"],"errors":true,"version":"5.9.3"} \ No newline at end of file diff --git a/cockpit/vite.config.ts b/cockpit/vite.config.ts deleted file mode 100644 index 50b72a8..0000000 --- a/cockpit/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import path from "path"; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "nova-sdk": path.resolve(__dirname, "../agent/index.ts"), - }, - }, - optimizeDeps: { - exclude: ["nova-sdk"], - }, - server: { - port: 5173, - }, -}); diff --git a/shell/config/.gitconfig.template b/config/.gitconfig.template similarity index 100% rename from shell/config/.gitconfig.template rename to config/.gitconfig.template diff --git a/shell/config/.novarc b/config/.novarc similarity index 100% rename from shell/config/.novarc rename to config/.novarc diff --git a/shell/config/.zshrc b/config/.zshrc similarity index 94% rename from shell/config/.zshrc rename to config/.zshrc index 3974480..ad33ab0 100644 --- a/shell/config/.zshrc +++ b/config/.zshrc @@ -7,6 +7,8 @@ plugins=(git zsh-autosuggestions zsh-syntax-highlighting fzf gh docker node pyth [[ -f "$HOME/.novarc" ]] && source "$HOME/.novarc" +export LAWFUL_NOVA_REPO_ROOT="${LAWFUL_NOVA_REPO_ROOT:-$HOME/agentic-coding-agent}" +[[ -d "$LAWFUL_NOVA_REPO_ROOT/bin" ]] && export PATH="$LAWFUL_NOVA_REPO_ROOT/bin:$PATH" [[ -n "${NOVA_VOSS_RUNTIME_PATH:-}" ]] && export PATH="$NOVA_VOSS_RUNTIME_PATH:$PATH" [[ -n "${NOVA_RSL_PATH:-}" ]] && export PATH="$NOVA_RSL_PATH:$PATH" @@ -29,7 +31,7 @@ alias cat='bat --style=plain 2>/dev/null || cat' alias gs='git status'; alias ga='git add'; alias gc='git commit'; alias gp='git push' alias gl='git pull'; alias gd='git diff'; alias glog='git log --oneline --graph --decorate --all' -NOVA="${NOVA_CLI:-nova}" +NOVA="${NOVA_CLI:-$LAWFUL_NOVA_REPO_ROOT/bin/nova}" alias nova-chat="$NOVA chat" novr() { diff --git a/config/crk1-formal.tex b/config/crk1-formal.tex deleted file mode 100644 index baff681..0000000 --- a/config/crk1-formal.tex +++ /dev/null @@ -1,46 +0,0 @@ -\documentclass{article} -\usepackage{amsmath} -\usepackage{amssymb} - -\title{CRK-1: Constitutional Runtime Kernel --- Formal Specification} -\author{Nova / Mission \#002} -\date{v1.0} - -\begin{document} -\maketitle - -\section{State Space} -Let $S \in \mathcal{S}$ denote workspace state and $A \in \mathcal{A}$ denote the action space. -Let $\mathcal{I} = \{I_1, I_2, \ldots, I_n\}$ be the invariant set, where each -$I_k : \mathcal{S} \times \mathcal{A} \to \{0,1\}$ and $I_k(S,A)=1$ means the invariant holds. - -\section{Lawful Action Predicate} -An action $A$ on state $S$ is lawful iff: -\[ -L(S,A) = \bigwedge_{k=1}^{n} I_k(S,A) -\] - -\section{Transition Function} -The kernel defines a governed transition $T : \mathcal{S} \times \mathcal{A} \to \mathcal{S}$ subject to: -\[ -L(S,A) = 1 \implies S' = T(S,A) -\] -If $L(S,A) = 0$, the transition is rejected. - -\section{Continuity Substrate} -Define snapshot function $\Sigma : \mathcal{S} \to \mathcal{C}$ where $\mathcal{C}$ is the space of continuity hashes. -For each transition: $c = \Sigma(S)$, $c' = \Sigma(S')$. -Diff operator: $D : \mathcal{S} \times \mathcal{S} \to \Delta$. - -\section{Governance Receipt} -A receipt $R = (A, S, S', \{I_k\}, c, c', t)$ where $t \in \mathbb{R}$ is timestamp. -Pattern ledger $\mathcal{L} = (R_1, R_2, \ldots, R_m)$ with chaining: -\[ -h_i = H(R_i \parallel h_{i-1}) -\] -for hash function $H$. - -\section{Reproducibility} -Given $\mathcal{L}$ and initial state $S_0$, replaying $S_i = T(S_{i-1}, A_i)$ yields states consistent with recorded $S_i$ in receipts. - -\end{document} diff --git a/config/crk1-spec.md b/config/crk1-spec.md deleted file mode 100644 index 7a8f1be..0000000 --- a/config/crk1-spec.md +++ /dev/null @@ -1,85 +0,0 @@ -# CRK-1 Constitutional Contract v1.0 - -## 1. Purpose - -CRK-1 defines the lawful substrate for agentic cognition. All agents must operate within: - -- continuity -- invariants -- traceability -- reproducibility -- governance receipts - -## 2. Lawful Action - -Every agent action must: - -1. Be validated by the invariant engine -2. Produce a governance receipt -3. Update the continuity substrate -4. Be reversible (via replay) -5. Be traceable - -## 3. Invariant Engine - -An invariant is a non-negotiable rule: - -``` -Invariant { - id: string - description: string - check(state): boolean - severity: "error" | "warn" -} -``` - -## 4. Pattern Ledger - -Every action is logged with: - -- action type -- payload -- continuity hash -- invariant checks -- timestamp -- cryptographic chain: h_i = H(R_i || h_{i-1}) - -## 5. Continuity Substrate - -The substrate maintains: - -- snapshots -- diffs -- replay logs -- continuity hashes - -## 6. Receipts - -Every action produces a receipt: - -``` -GovernanceReceipt { - id - timestamp - action - invariantsChecked - continuityHash - ledgerHash -} -``` - -## 7. Reproducibility - -Any external observer must be able to: - -- replay the agent's reasoning -- verify invariants -- confirm continuity - -This is the core of founder-independent reproduction. - -## 8. Constitutional Rules - -- **No silent actions** — every action produces a receipt -- **No ungoverned code generation** — all generation passes invariants -- **No unvalidated transitions** — L(S, A) must hold before T(S, A) diff --git a/config/nova.config.ts b/config/nova.config.ts deleted file mode 100644 index 1e93630..0000000 --- a/config/nova.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Invariant } from "../agent/types/invariants"; - -const dangerousShell = ["rm -rf", "curl | sh", "curl|sh", ":(){ :|:& };:"]; - -export const invariants: Invariant[] = [ - { - id: "no-credentials", - description: "Do not leak secrets or credentials in generated code or prompts.", - severity: "error", - check: (state) => { - const text = [state.prompt, state.code, state.diff].filter(Boolean).join(" "); - return !/API_KEY|SECRET_KEY|password\s*=\s*["'][^"']+["']/i.test(text); - }, - }, - { - id: "no-dangerous-shell", - description: "Disallow dangerous shell commands in prompts and diffs.", - severity: "error", - check: (state) => { - const text = [state.prompt, state.code, state.diff].filter(Boolean).join(" "); - return !dangerousShell.some((p) => text.includes(p)); - }, - }, - { - id: "no-silent-actions", - description: "All actions must be governable (non-empty action type).", - severity: "error", - check: (state) => Boolean(state.action?.type), - }, -]; diff --git a/config/nova/nova-stack.json b/config/nova/nova-stack.json new file mode 100644 index 0000000..0c6d7c1 --- /dev/null +++ b/config/nova/nova-stack.json @@ -0,0 +1,20 @@ +{ + "schema": "lawful_nova_stack.v1", + "api_port": 8080, + "operator_kernel_port": 8790, + "lawful_brain_port": 8791, + "aais_port": 8000, + "cortex_path": "${LAWFUL_NOVA_REPO_ROOT}/nova", + "voss_path": "${LAWFUL_NOVA_REPO_ROOT}/nova", + "rsl_path": "${LAWFUL_NOVA_REPO_ROOT}/governance", + "python": { + "unix": "${LAWFUL_NOVA_REPO_ROOT}/.venv/bin/python", + "windows": "${LAWFUL_NOVA_REPO_ROOT}/.venv/Scripts/python.exe" + }, + "health_urls": { + "nova_api": "http://127.0.0.1:8080/health", + "lawful_brain": "http://127.0.0.1:8791/health", + "operator_kernel": "http://127.0.0.1:8790/health", + "aais": "http://127.0.0.1:8000/health" + } +} diff --git a/config/novarc.ps1 b/config/novarc.ps1 new file mode 100644 index 0000000..50511d3 --- /dev/null +++ b/config/novarc.ps1 @@ -0,0 +1,35 @@ +# .novarc.ps1 - Nova LLM Stack Environment Variables (Windows template) +# bootstrap.ps1 copies this to %USERPROFILE%\.novarc.ps1 (gitignored there). +# Fill in paths after your Nova stack is built. + +$env:LAWFUL_NOVA_REPO_ROOT = "" + +$env:NOVA_PORT = "8080" +$env:NOVA_API_URL = "http://localhost:$($env:NOVA_PORT)" +if ($env:LAWFUL_NOVA_REPO_ROOT -and (Test-Path (Join-Path $env:LAWFUL_NOVA_REPO_ROOT "bin\nova.ps1"))) { + $env:NOVA_CLI = Join-Path $env:LAWFUL_NOVA_REPO_ROOT "bin\nova.ps1" +} else { + $env:NOVA_CLI = "nova" +} + +if ($env:LAWFUL_NOVA_REPO_ROOT -and (Test-Path (Join-Path $env:LAWFUL_NOVA_REPO_ROOT "nova"))) { + $env:NOVA_VOSS_RUNTIME_PATH = Join-Path $env:LAWFUL_NOVA_REPO_ROOT "nova" + $env:NOVA_CORTEX_PATH = Join-Path $env:LAWFUL_NOVA_REPO_ROOT "nova" + $env:NOVA_RSL_PATH = Join-Path $env:LAWFUL_NOVA_REPO_ROOT "nova" + $env:NOVA_GOW_CONFIG = Join-Path $env:LAWFUL_NOVA_REPO_ROOT "config\nova\nova-stack.json" +} else { + $env:NOVA_VOSS_RUNTIME_PATH = "" + $env:NOVA_CORTEX_PATH = "" + $env:NOVA_GOW_CONFIG = "" + $env:NOVA_RSL_PATH = "" +} +$env:NOVA_SLICE_CONFIG = "$env:USERPROFILE\.nova\nova-stack.json" + +$env:NOVA_GPU_DEVICE = "0" +$env:NOVA_MEGATRON_ENDPOINT = "http://localhost:5000" +$env:CUDA_VISIBLE_DEVICES = $env:NOVA_GPU_DEVICE + +$env:GITHUB_TOKEN = "" + +$env:DO_NOT_TRACK = "1" +$env:NEXT_TELEMETRY_DISABLED = "1" diff --git a/shell/config/profile.ps1 b/config/profile.ps1 similarity index 100% rename from shell/config/profile.ps1 rename to config/profile.ps1 diff --git a/control-tower/drift/drift-simulator.ts b/control-tower/drift/drift-simulator.ts deleted file mode 100644 index 5b3297b..0000000 --- a/control-tower/drift/drift-simulator.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type DriftType = - | "ledger" - | "continuity" - | "pit" - | "invariant" - | "constraint" - | "dlap"; - -export function simulateDrift( - agentA: string, - agentB: string, - type: DriftType -): { agentA: string; agentB: string; type: DriftType; introduced: boolean } { - return { agentA, agentB, type, introduced: true }; -} - -export function correctDrift( - cluster: T[], - canonical: T -): T[] { - return cluster.map((agent) => ({ ...agent, ...canonical })); -} diff --git a/control-tower/index.ts b/control-tower/index.ts deleted file mode 100644 index 3aef706..0000000 --- a/control-tower/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./orchestrator/cluster-manager"; -export * from "./orchestrator/agent-registry"; -export * from "./orchestrator/drift-detector"; -export * from "./orchestrator/consensus-engine"; -export * from "./replay/cluster-replay"; -export * from "./drift/drift-simulator"; diff --git a/control-tower/orchestrator/agent-registry.ts b/control-tower/orchestrator/agent-registry.ts deleted file mode 100644 index f9062a1..0000000 --- a/control-tower/orchestrator/agent-registry.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface RegisteredAgent { - id: string; - status: "online" | "offline" | "drift" | "error"; - kernelVersion: string; -} - -const agents = new Map(); - -export const agentRegistry = { - register(id: string, kernelVersion = "CRK-2"): RegisteredAgent { - const agent: RegisteredAgent = { id, status: "online", kernelVersion }; - agents.set(id, agent); - return agent; - }, - - list(): RegisteredAgent[] { - return [...agents.values()]; - }, - - get(id: string): RegisteredAgent | undefined { - return agents.get(id); - }, - - updateStatus(id: string, status: RegisteredAgent["status"]): void { - const agent = agents.get(id); - if (agent) agents.set(id, { ...agent, status }); - }, -}; diff --git a/control-tower/orchestrator/cluster-manager.ts b/control-tower/orchestrator/cluster-manager.ts deleted file mode 100644 index 0363175..0000000 --- a/control-tower/orchestrator/cluster-manager.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { agentRegistry } from "./agent-registry"; -import { clusterView } from "../../crk2"; - -export const clusterManager = { - getClusterState() { - return { - agents: agentRegistry.list(), - constitutional: clusterView(), - }; - }, - - ensureDefaultAgents(ids: string[] = ["agent-alpha", "agent-beta"]): void { - for (const id of ids) { - if (!agentRegistry.get(id)) agentRegistry.register(id); - } - }, -}; diff --git a/control-tower/orchestrator/consensus-engine.ts b/control-tower/orchestrator/consensus-engine.ts deleted file mode 100644 index c5395fd..0000000 --- a/control-tower/orchestrator/consensus-engine.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function consensus(plans: T[]): T { - return plans[0] as T; -} diff --git a/control-tower/orchestrator/drift-detector.ts b/control-tower/orchestrator/drift-detector.ts deleted file mode 100644 index 90e04d8..0000000 --- a/control-tower/orchestrator/drift-detector.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface DriftReport { - agentA: string; - agentB: string; - ledgerMismatch: boolean; - continuityMismatch: boolean; - pitMismatch: boolean; -} - -export function detectDrift( - receiptsA: { hash: string }[], - receiptsB: { hash: string }[] -): DriftReport { - const ledgerMismatch = - receiptsA.length !== receiptsB.length || - receiptsA.some((r, i) => r.hash !== receiptsB[i]?.hash); - return { - agentA: "a", - agentB: "b", - ledgerMismatch, - continuityMismatch: false, - pitMismatch: false, - }; -} diff --git a/control-tower/replay/cluster-replay.ts b/control-tower/replay/cluster-replay.ts deleted file mode 100644 index 319b8d3..0000000 --- a/control-tower/replay/cluster-replay.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface ClusterReplayRequest { - agentIds: string[]; - fromSnapshotId?: string; - toSnapshotId?: string; -} - -export interface DivergenceEvent { - id: string; - type: "ledger" | "continuity" | "pit" | "invariant"; - snapshotId?: string; - agents: string[]; -} - -export interface ClusterReplayResult { - perAgent: Record< - string, - { snapshots: unknown[]; receipts: unknown[]; pitTransitions: unknown[] } - >; - divergences: DivergenceEvent[]; -} - -export async function replayCluster(req: ClusterReplayRequest): Promise { - const perAgent: ClusterReplayResult["perAgent"] = {}; - for (const id of req.agentIds) { - perAgent[id] = { snapshots: [], receipts: [], pitTransitions: [] }; - } - return { perAgent, divergences: [] }; -} diff --git a/crk2/cluster/macc.ts b/crk2/cluster/macc.ts deleted file mode 100644 index 189d941..0000000 --- a/crk2/cluster/macc.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createHash } from "crypto"; -import { listReceipts } from "../ledger/ledger-v2"; -import { listSnapshots } from "../continuity/substrate"; - -export interface ClusterState { - kernelVersion: "CRK-2"; - invariantSetHash: string; - constraintSetHash: string; - ledgerPrefixHash: string; - continuityAnchorHash: string; - pitDefinitionHash: string; -} - -function hashPayload(payload: unknown): string { - return createHash("sha256").update(JSON.stringify(payload)).digest("hex"); -} - -export function clusterView(): ClusterState { - const receipts = listReceipts(); - const snapshots = listSnapshots(); - return { - kernelVersion: "CRK-2", - invariantSetHash: hashPayload({ version: "crk2-invariants-v1" }), - constraintSetHash: hashPayload({ version: "crk2-constraints-v1" }), - ledgerPrefixHash: hashPayload(receipts.slice(0, 10)), - continuityAnchorHash: hashPayload(snapshots[snapshots.length - 1] ?? "none"), - pitDefinitionHash: hashPayload({ bands: [1, 2, 3, 4, 5] }), - }; -} diff --git a/crk2/continuity/crp.ts b/crk2/continuity/crp.ts deleted file mode 100644 index 10912fc..0000000 --- a/crk2/continuity/crp.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createHash } from "crypto"; -import type { Snapshot } from "./substrate"; - -export interface PITTransition { - from: number; - to: number; - ts: number; -} - -export interface CRP { - snapshotId: string; - receipts: unknown[]; - pitTransitions: PITTransition[]; - hash: string; -} - -export function generateCRP( - snapshot: Snapshot, - receipts: unknown[], - pitTransitions: PITTransition[] -): CRP { - const hash = createHash("sha256") - .update(JSON.stringify({ snapshot, receipts, pitTransitions })) - .digest("hex"); - return { snapshotId: snapshot.id, receipts, pitTransitions, hash }; -} diff --git a/crk2/continuity/replay.ts b/crk2/continuity/replay.ts deleted file mode 100644 index 1e09847..0000000 --- a/crk2/continuity/replay.ts +++ /dev/null @@ -1 +0,0 @@ -export { replay } from "./substrate"; diff --git a/crk2/continuity/substrate.ts b/crk2/continuity/substrate.ts deleted file mode 100644 index 1ea5e89..0000000 --- a/crk2/continuity/substrate.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createHash, randomUUID } from "crypto"; - -export interface Snapshot { - id: string; - hash: string; - partial: boolean; - state: Record; - ts: number; -} - -const store: Snapshot[] = []; - -function hashState(state: Record): string { - return createHash("sha256").update(JSON.stringify(state)).digest("hex"); -} - -export function takeSnapshot(state: Record, partial = false): Snapshot { - const hash = hashState(state); - const snapshot: Snapshot = { - id: randomUUID(), - hash, - partial, - state, - ts: Date.now(), - }; - store.push(snapshot); - return snapshot; -} - -export function listSnapshots(): Snapshot[] { - return [...store]; -} - -export function replay(snapshotId: string): Record | null { - const snapshot = store.find((s) => s.id === snapshotId); - return snapshot?.state ?? null; -} diff --git a/crk2/index.ts b/crk2/index.ts deleted file mode 100644 index 4b1c5fb..0000000 --- a/crk2/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { dLAP, type DLAPResult } from "./kernel/dlap"; -export { pitEngine, type PITBand, type PITContext } from "./kernel/pit-engine"; -export { constraintEngine } from "./kernel/constraint-engine"; -export { panicHandler } from "./kernel/panic-handler"; -export { invariantEngine } from "./invariants/engine"; -export { takeSnapshot, listSnapshots, replay, type Snapshot } from "./continuity/substrate"; -export { generateCRP, type CRP, type PITTransition } from "./continuity/crp"; -export { appendReceipt, listReceipts, type ReceiptV2 } from "./ledger/ledger-v2"; -export { clusterView, type ClusterState } from "./cluster/macc"; diff --git a/crk2/invariants/engine.ts b/crk2/invariants/engine.ts deleted file mode 100644 index 42eecdd..0000000 --- a/crk2/invariants/engine.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const invariantEngine = { - checkAll( - action: { type: string; payload?: Record }, - context: Record - ): { ok: boolean; invariantId?: string } { - const text = JSON.stringify({ action, context }); - if (text.includes("rm -rf")) { - return { ok: false, invariantId: "no-dangerous-shell" }; - } - return { ok: true }; - }, -}; diff --git a/crk2/kernel/constraint-engine.ts b/crk2/kernel/constraint-engine.ts deleted file mode 100644 index 9a0c492..0000000 --- a/crk2/kernel/constraint-engine.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ClusterState } from "../cluster/macc"; - -export interface ConstraintCheckResult { - ok: boolean; - constraintId?: string; -} - -export const constraintEngine = { - check( - _action: { type: string }, - _context: Record, - _clusterState: ClusterState - ): ConstraintCheckResult { - return { ok: true }; - }, -}; diff --git a/crk2/kernel/dlap.ts b/crk2/kernel/dlap.ts deleted file mode 100644 index 00b3791..0000000 --- a/crk2/kernel/dlap.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { invariantEngine } from "../invariants/engine"; -import { constraintEngine } from "./constraint-engine"; -import { clusterView } from "../cluster/macc"; - -export interface DLAPResult { - ok: boolean; - reason?: string; - detail?: unknown; -} - -export function dLAP( - action: { type: string; payload?: Record }, - context: Record -): DLAPResult { - const clusterState = clusterView(); - - const local = invariantEngine.checkAll(action, context); - if (!local.ok) { - return { ok: false, reason: "local-invariant", detail: local }; - } - - const cluster = checkClusterInvariants(action, clusterState); - if (!cluster.ok) { - return { ok: false, reason: "cluster-invariant", detail: cluster }; - } - - const constraints = constraintEngine.check(action, context, clusterState); - if (!constraints.ok) { - return { ok: false, reason: "constraint", detail: constraints }; - } - - return { ok: true }; -} - -function checkClusterInvariants( - _action: { type: string }, - _clusterState: ReturnType -): { ok: boolean } { - return { ok: true }; -} diff --git a/crk2/kernel/index.ts b/crk2/kernel/index.ts deleted file mode 100644 index 1144334..0000000 --- a/crk2/kernel/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { dLAP } from "./dlap"; -export { pitEngine } from "./pit-engine"; -export { constraintEngine } from "./constraint-engine"; -export { panicHandler } from "./panic-handler"; diff --git a/crk2/kernel/panic-handler.ts b/crk2/kernel/panic-handler.ts deleted file mode 100644 index fb77be1..0000000 --- a/crk2/kernel/panic-handler.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const panicHandler = { - panic(reason: string): never { - throw new Error(`CRK-2 kernel panic: ${reason}`); - }, - - recover(_error: unknown): { recovered: boolean; message: string } { - return { recovered: false, message: "Fail-closed: manual forensics required" }; - }, -}; diff --git a/crk2/kernel/pit-engine.ts b/crk2/kernel/pit-engine.ts deleted file mode 100644 index 24e71a5..0000000 --- a/crk2/kernel/pit-engine.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type PITBand = 1 | 2 | 3 | 4 | 5; - -export interface PITContext { - domain?: string; - evidenceCount?: number; -} - -export const pitEngine = { - getBand(context: PITContext): PITBand { - if ((context.evidenceCount ?? 0) >= 4) return 5; - if ((context.evidenceCount ?? 0) >= 3) return 4; - if (context.domain) return 3; - return 1; - }, - - apply(band: PITBand, context: PITContext): PITBand { - const next = this.getBand(context); - return next >= band ? next : band; - }, -}; diff --git a/crk2/ledger/ledger-v2.ts b/crk2/ledger/ledger-v2.ts deleted file mode 100644 index 15f1b8a..0000000 --- a/crk2/ledger/ledger-v2.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createHash } from "crypto"; - -export interface ReceiptV2 { - id: string; - actionId: string; - invariantsChecked: string[]; - continuityHash: string; - prevHash: string; - hash: string; - annotations?: { author: string; note: string }[]; -} - -const ledger: ReceiptV2[] = []; - -export function appendReceipt( - receipt: Omit -): ReceiptV2 { - const last = ledger[ledger.length - 1]; - const prevHash = last?.hash ?? "genesis"; - const hash = createHash("sha256") - .update(prevHash + JSON.stringify(receipt)) - .digest("hex"); - const full: ReceiptV2 = { ...receipt, prevHash, hash }; - ledger.push(full); - return full; -} - -export function listReceipts(): ReceiptV2[] { - return [...ledger]; -} diff --git a/desktop/commands.json b/desktop/commands.json new file mode 100644 index 0000000..91d88f5 --- /dev/null +++ b/desktop/commands.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://nova.desktop/schema/commands.json", + "version": "1.0", + "commands": [ + { + "id": "coder.apply_instruction", + "title": "Apply Instruction with Coder", + "shortcut": "Ctrl+Enter", + "category": "Coding", + "action": { + "type": "node.tool", + "intent": "code", + "args": ["file_path", "instruction", "current_code"] + } + }, + { + "id": "wiring.generate_glue", + "title": "Generate Glue Code", + "shortcut": "Ctrl+Shift+W", + "category": "Wiring", + "action": { + "type": "node.tool", + "intent": "wire", + "args": ["components", "goal", "context"] + } + }, + { + "id": "ui.toggle_diff", + "title": "Toggle Diff View", + "shortcut": "Ctrl+D", + "category": "UI", + "action": { + "type": "ui.toggle", + "target": "diff" + } + }, + { + "id": "ui.toggle_receipts", + "title": "Toggle Receipts Drawer", + "shortcut": "Ctrl+Shift+G", + "category": "UI", + "action": { + "type": "ui.toggle", + "target": "receipts" + } + }, + { + "id": "node.replay_last", + "title": "Replay Last Action", + "shortcut": "Ctrl+R", + "category": "Governance", + "action": { + "type": "node.replay", + "args": ["last_replay_id"] + } + }, + { + "id": "node.switch_model", + "title": "Switch Local Model", + "shortcut": "Ctrl+Shift+M", + "category": "Models", + "action": { + "type": "node.config.update", + "args": ["model"] + } + } + ] +} diff --git a/desktop/core/commands.js b/desktop/core/commands.js new file mode 100644 index 0000000..e4b7a00 --- /dev/null +++ b/desktop/core/commands.js @@ -0,0 +1,53 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const COMMANDS_PATH = path.join(__dirname, "..", "commands.json"); + +function loadCommands() { + return JSON.parse(fs.readFileSync(COMMANDS_PATH, "utf8")); +} + +async function executeCommand(id, context) { + const command = loadCommands().commands.find((entry) => entry.id === id); + if (!command) { + return null; + } + const actions = context.actions || {}; + switch (command.action.type) { + case "node.tool": + if (command.action.intent === "code") { + return actions.code(selectArgs(command.action.args, context)); + } + return actions.wire(selectArgs(command.action.args, context)); + case "ui.toggle": + return actions.toggle(command.action.target); + case "node.replay": + return actions.replay(context.last_replay_id); + case "node.config.update": + return actions.updateConfig({ model: context.model }); + default: + return null; + } +} + +function findByShortcut(shortcut) { + return loadCommands().commands.find((entry) => normalizeShortcut(entry.shortcut) === normalizeShortcut(shortcut)) || null; +} + +function selectArgs(keys, context) { + const selected = {}; + for (const key of keys || []) { + selected[key] = context[key]; + } + return selected; +} + +function normalizeShortcut(shortcut) { + return String(shortcut || "").toLowerCase().replace(/\s+/g, ""); +} + +module.exports = { + executeCommand, + findByShortcut, + loadCommands, +}; diff --git a/desktop/core/file-manager.js b/desktop/core/file-manager.js new file mode 100644 index 0000000..92efb7f --- /dev/null +++ b/desktop/core/file-manager.js @@ -0,0 +1,34 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const HIDDEN_NAMES = new Set([".git", "node_modules", ".runtime", "dist"]); + +function listEntries(rootDir) { + return fs.readdirSync(rootDir, { withFileTypes: true }) + .filter((entry) => !HIDDEN_NAMES.has(entry.name)) + .map((entry) => ({ + name: entry.name, + path: path.join(rootDir, entry.name), + kind: entry.isDirectory() ? "directory" : "file", + })) + .sort((a, b) => { + if (a.kind !== b.kind) { + return a.kind === "directory" ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); +} + +function readFile(filePath) { + return fs.readFileSync(filePath, "utf8"); +} + +function writeFile(filePath, content) { + fs.writeFileSync(filePath, content, "utf8"); +} + +module.exports = { + listEntries, + readFile, + writeFile, +}; diff --git a/desktop/core/node-client.js b/desktop/core/node-client.js new file mode 100644 index 0000000..e41a0c5 --- /dev/null +++ b/desktop/core/node-client.js @@ -0,0 +1,107 @@ +let config = { + nodeUrl: "http://localhost:8000", + model: "qwen2.5-coder:3b", +}; + +let transport = globalThis.fetch; + +function updateConfig(nextConfig = {}) { + config = { + ...config, + ...nextConfig, + nodeUrl: normalizeNodeUrl(nextConfig.nodeUrl || config.nodeUrl), + }; + return getConfig(); +} + +function getConfig() { + return { ...config }; +} + +function setTransport(nextTransport) { + transport = nextTransport || globalThis.fetch; +} + +async function code(task) { + return postTool({ intent: "code", model: config.model, ...task }); +} + +async function wire(task) { + return postTool({ intent: "wire", model: config.model, ...task }); +} + +async function status() { + return request(`${config.nodeUrl}/node/status`, { method: "GET" }); +} + +async function tools() { + return request(`${config.nodeUrl}/node/tools`, { method: "GET" }); +} + +async function replay(traceId) { + return request(`${config.nodeUrl}/node/replay/${encodeURIComponent(traceId)}`, { method: "POST" }); +} + +async function verifyTrace(traceId) { + return request(`${config.nodeUrl}/node/verify/${encodeURIComponent(traceId)}`, { method: "GET" }); +} + +async function featureManifest() { + return request(`${config.nodeUrl}/node/feature-manifest`, { method: "GET" }); +} + +async function events({ prefix, limit } = {}) { + const params = new URLSearchParams(); + if (prefix) params.set("prefix", prefix); + if (limit) params.set("limit", String(limit)); + const query = params.toString(); + return request(`${config.nodeUrl}/node/events${query ? `?${query}` : ""}`, { method: "GET" }); +} + +async function compareReceipts(left, right) { + return request(`${config.nodeUrl}/node/compare-receipts`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ left, right }), + }); +} + +async function postTool(payload) { + return request(`${config.nodeUrl}/node/tool`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); +} + +async function request(url, options) { + if (typeof transport !== "function") { + throw new Error("No fetch transport available for Nova Desktop"); + } + const response = await transport(url, options); + const data = await response.json(); + if (response.ok === false) { + const reason = data && data.error ? JSON.stringify(data.error) : "request failed"; + throw new Error(reason); + } + return data; +} + +function normalizeNodeUrl(nodeUrl) { + return String(nodeUrl || "http://localhost:8000").replace(/\/+$/, ""); +} + +module.exports = { + code, + wire, + status, + tools, + replay, + verifyTrace, + featureManifest, + events, + compareReceipts, + updateConfig, + getConfig, + setTransport, +}; diff --git a/desktop/core/project-templates.js b/desktop/core/project-templates.js new file mode 100644 index 0000000..f1ae7f5 --- /dev/null +++ b/desktop/core/project-templates.js @@ -0,0 +1,99 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +function createProject({ rootDir, type, name }) { + const safeName = sanitizeProjectName(name); + const projectRoot = path.join(rootDir, safeName); + fs.mkdirSync(projectRoot, { recursive: false }); + + if (type === "python") { + writePython(projectRoot, safeName); + } else if (type === "node") { + writeNode(projectRoot, safeName); + } else if (type === "rust") { + writeRust(projectRoot, safeName); + } else { + throw new Error(`Unknown template type: ${type}`); + } + return projectRoot; +} + +function sanitizeProjectName(name) { + const safe = String(name || "").trim().replace(/[^a-zA-Z0-9_-]/g, "-"); + if (!safe) { + throw new Error("Project name is required"); + } + return safe; +} + +function writePython(root, name) { + fs.writeFileSync(path.join(root, "app.py"), [ + "from fastapi import FastAPI", + "", + "app = FastAPI()", + "", + '@app.get("/v1/health")', + "def health():", + ' return {"status": "ok"}', + "", + ].join("\n"), "utf8"); + fs.writeFileSync(path.join(root, "requirements.txt"), "fastapi\nuvicorn\n", "utf8"); + fs.writeFileSync(path.join(root, "README.md"), `# ${name}\n\nNova Python health-service template.\n`, "utf8"); +} + +function writeNode(root, name) { + fs.writeFileSync(path.join(root, "index.js"), [ + "const express = require('express');", + "const app = express();", + "", + "app.get('/v1/health', (req, res) => {", + " res.json({ status: 'ok' });", + "});", + "", + "app.listen(3000);", + "", + ].join("\n"), "utf8"); + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ + name, + version: "0.1.0", + dependencies: { express: "^4.18.2" }, + }, null, 2) + "\n", "utf8"); + fs.writeFileSync(path.join(root, "README.md"), `# ${name}\n\nNova Node.js health-service template.\n`, "utf8"); +} + +function writeRust(root, name) { + fs.mkdirSync(path.join(root, "src")); + fs.writeFileSync(path.join(root, "src", "main.rs"), [ + "use actix_web::{get, App, HttpServer, Responder};", + "", + '#[get("/v1/health")]', + "async fn health() -> impl Responder {", + ' "{\\"status\\":\\"ok\\"}"', + "}", + "", + "#[actix_web::main]", + "async fn main() -> std::io::Result<()> {", + " HttpServer::new(|| App::new().service(health))", + ' .bind("127.0.0.1:8080")?', + " .run()", + " .await", + "}", + "", + ].join("\n"), "utf8"); + fs.writeFileSync(path.join(root, "Cargo.toml"), [ + "[package]", + `name = "${name.replace(/-/g, "_")}"`, + 'version = "0.1.0"', + 'edition = "2021"', + "", + "[dependencies]", + 'actix-web = "4"', + "", + ].join("\n"), "utf8"); + fs.writeFileSync(path.join(root, "README.md"), `# ${name}\n\nNova Rust health-service template.\n`, "utf8"); +} + +module.exports = { + createProject, + sanitizeProjectName, +}; diff --git a/desktop/core/receipts-store.js b/desktop/core/receipts-store.js new file mode 100644 index 0000000..ba794af --- /dev/null +++ b/desktop/core/receipts-store.js @@ -0,0 +1,49 @@ +const receipts = []; + +function addFromNodeResponse(response) { + const toolReceipt = response && response.result && Array.isArray(response.result.receipts) + ? response.result.receipts[0] + : "unknown_tool"; + const governance = response && response.governance ? response.governance : {}; + const nodeReceipt = response && response.receipt ? response.receipt : {}; + const traceId = governance.trace_id || governance.replay_id || nodeReceipt.trace_id || ""; + const receipt = { + tool: toolReceipt, + decision: governance.decision || "unknown", + replay_id: traceId, + trace_id: traceId, + receipt_hash: nodeReceipt.receipt_hash || "", + input_hash: nodeReceipt.input_hash || "", + output_hash: nodeReceipt.output_hash || "", + policy_version: governance.policy_version || nodeReceipt.policy_version || "", + trace_file: response && response.trace_file ? response.trace_file : "", + time: new Date().toLocaleTimeString(), + }; + receipts.push(receipt); + return receipt; +} + +function addReceipt(receipt) { + receipts.push(receipt); + return receipt; +} + +function getReceipts() { + return receipts.slice(); +} + +function getLastReceipt() { + return receipts[receipts.length - 1] || null; +} + +function clearReceipts() { + receipts.length = 0; +} + +module.exports = { + addFromNodeResponse, + addReceipt, + getReceipts, + getLastReceipt, + clearReceipts, +}; diff --git a/desktop/core/studio-state.js b/desktop/core/studio-state.js new file mode 100644 index 0000000..ec06a3d --- /dev/null +++ b/desktop/core/studio-state.js @@ -0,0 +1,102 @@ +const timeline = []; +const patches = []; +const replays = []; +let lastProfile = null; + +function addPatchEvent(event) { + const entry = { + kind: "patch", + id: event.replay_id || event.id || "", + tool: event.tool || "unknown_tool", + decision: event.decision || "unknown", + time: event.time || new Date().toLocaleTimeString(), + diff: event.diff || "", + explanation: event.explanation || "", + file_path: event.file_path || "", + previous_code: event.previous_code || "", + updated_code: event.updated_code || "", + total_ms: Math.round(Number(event.total_ms || 0)), + }; + timeline.push(entry); + patches.unshift(entry); + return entry; +} + +function addReplayEvent(event) { + const entry = { + kind: "replay", + id: event.replay_id || event.id || "", + decision: event.decision || "unknown", + deterministic: Boolean(event.deterministic), + time: event.time || new Date().toLocaleTimeString(), + }; + timeline.push(entry); + replays.unshift(entry); + return entry; +} + +function profileInvocation(stats) { + lastProfile = { + tool: stats.tool || "unknown_tool", + inference_ms: Math.round(Number(stats.inference_ms || 0)), + governance_ms: Math.round(Number(stats.governance_ms || stats.gov_ms || 0)), + diff_lines: countDiffLines(stats.diff || ""), + total_ms: Math.round(Number(stats.total_ms || 0)), + }; + return lastProfile; +} + +function getHintForLine(lineNumber) { + return patches.find((patch) => patch.line === lineNumber) || patches[0] || null; +} + +function getTimeline() { + return timeline.slice(); +} + +function getRecentPatches() { + return patches.slice(); +} + +function getReplays() { + return replays.slice(); +} + +function getLastProfile() { + return lastProfile; +} + +function getHeartbeatSummary() { + return { + receipts_count: patches.length, + replay_count: replays.length, + last_latency_ms: lastProfile ? lastProfile.total_ms : 0, + }; +} + +function resetStudioState() { + timeline.length = 0; + patches.length = 0; + replays.length = 0; + lastProfile = null; +} + +function countDiffLines(diff) { + if (!diff) { + return 0; + } + return String(diff).split("\n").filter(Boolean).length; +} + +module.exports = { + addPatchEvent, + addReplayEvent, + profileInvocation, + getTimeline, + getRecentPatches, + getReplays, + getLastProfile, + getHeartbeatSummary, + getHintForLine, + resetStudioState, +}; diff --git a/desktop/installer-config.json b/desktop/installer-config.json new file mode 100644 index 0000000..3107859 --- /dev/null +++ b/desktop/installer-config.json @@ -0,0 +1,24 @@ +{ + "appId": "com.nova.desktop", + "productName": "Nova Desktop", + "directories": { + "output": "dist" + }, + "files": [ + "main.js", + "preload.js", + "commands.json", + "core/**/*", + "renderer/**/*", + "package.json", + "node_modules/monaco-editor/min/vs/**/*" + ], + "npmRebuild": false, + "win": { + "target": "nsis" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } +} diff --git a/desktop/main.js b/desktop/main.js new file mode 100644 index 0000000..82e9bdf --- /dev/null +++ b/desktop/main.js @@ -0,0 +1,44 @@ +const { app, BrowserWindow, dialog, ipcMain } = require("electron"); +const path = require("node:path"); + +function createWindow() { + const win = new BrowserWindow({ + width: 1440, + height: 920, + minWidth: 1100, + minHeight: 720, + backgroundColor: "#1e1e1e", + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + win.loadFile(path.join(__dirname, "renderer", "index.html")); +} + +ipcMain.handle("nova:open-folder", async () => { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"], + title: "Open Nova workspace", + }); + if (result.canceled || result.filePaths.length === 0) { + return null; + } + return result.filePaths[0]; +}); + +app.whenReady().then(createWindow); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..3037105 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,3602 @@ +{ + "name": "nova-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nova-desktop", + "version": "0.1.0", + "devDependencies": { + "electron": "^42.5.0", + "electron-builder": "^26.15.3", + "monaco-editor": "^0.45.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.5.tgz", + "integrity": "sha512-VKM5HP/+dnogW7QdcY7y3egfvzTDusCCoT/UycOCLq4WvnIygt0Kl6fAxWkhJfiSb55zaewFH1bfGcYlEm/drg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "42.5.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.5.0.tgz", + "integrity": "sha512-cYEKS9XFz+c9fAB4jI0x49yz1FFzB55r3q96wu9YkwwJMv7t9202IE/ltlgy6yitl/J4M7C8JQcmUqdzDvPl/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/monaco-editor": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz", + "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..d748e36 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,24 @@ +{ + "name": "nova-desktop", + "version": "0.1.0", + "description": "Cursor-style governed desktop shell for Lawful Nova Node.", + "author": "Project Infinity", + "type": "commonjs", + "packageManager": "npm@11.13.0", + "main": "main.js", + "scripts": { + "start": "electron .", + "test": "node --test tests/*.cjs", + "build:win": "electron-builder --config installer-config.json --win" + }, + "dependencies": {}, + "devDependencies": { + "electron": "^42.5.0", + "electron-builder": "^26.15.3", + "monaco-editor": "^0.45.0" + }, + "build": { + "appId": "com.nova.desktop", + "productName": "Nova Desktop" + } +} diff --git a/desktop/preload.js b/desktop/preload.js new file mode 100644 index 0000000..b15ba6b --- /dev/null +++ b/desktop/preload.js @@ -0,0 +1,34 @@ +const { contextBridge, ipcRenderer } = require("electron"); +const path = require("node:path"); +const client = require("./core/node-client"); +const files = require("./core/file-manager"); +const commands = require("./core/commands"); +const templates = require("./core/project-templates"); + +contextBridge.exposeInMainWorld("nodeAPI", { + code: client.code, + wire: client.wire, + status: client.status, + tools: client.tools, + replay: client.replay, + verifyTrace: client.verifyTrace, + featureManifest: client.featureManifest, + events: client.events, + compareReceipts: client.compareReceipts, + updateConfig: client.updateConfig, + getConfig: client.getConfig, +}); + +contextBridge.exposeInMainWorld("fileAPI", { + defaultRoot: () => process.cwd(), + openFolder: () => ipcRenderer.invoke("nova:open-folder"), + listEntries: files.listEntries, + readFile: files.readFile, + writeFile: files.writeFile, + createProject: templates.createProject, + basename: (filePath) => path.basename(filePath), +}); + +contextBridge.exposeInMainWorld("commandAPI", { + loadCommands: commands.loadCommands, +}); diff --git a/desktop/renderer/chat.js b/desktop/renderer/chat.js new file mode 100644 index 0000000..e0d77e8 --- /dev/null +++ b/desktop/renderer/chat.js @@ -0,0 +1,24 @@ +(function () { + function initChat({ onCodeInstruction, onWireInstruction }) { + const input = document.getElementById("chat-input"); + const codeButton = document.getElementById("chat-send"); + const wireButton = document.getElementById("wire-send"); + + codeButton.onclick = () => { + const text = input.value.trim(); + if (!text) return; + onCodeInstruction(text); + input.value = ""; + }; + + wireButton.onclick = () => { + const text = input.value.trim(); + if (!text) return; + onWireInstruction(text); + input.value = ""; + }; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.chat = { initChat }; +})(); diff --git a/desktop/renderer/command-palette.js b/desktop/renderer/command-palette.js new file mode 100644 index 0000000..1492651 --- /dev/null +++ b/desktop/renderer/command-palette.js @@ -0,0 +1,95 @@ +(function () { + let commands = []; + let onCommand = null; + + function initCommandPalette(handler) { + commands = window.commandAPI.loadCommands().commands || []; + onCommand = handler; + const search = document.getElementById("command-search"); + search.oninput = () => render(search.value); + search.onkeydown = (event) => { + if (event.key === "Enter") { + const first = document.querySelector(".command-item"); + if (first) first.click(); + } + if (event.key === "Escape") hide(); + }; + render(""); + } + + function show() { + const palette = document.getElementById("command-palette"); + palette.style.display = "block"; + document.getElementById("command-search").focus(); + render(""); + } + + function hide() { + document.getElementById("command-palette").style.display = "none"; + } + + function toggle() { + const palette = document.getElementById("command-palette"); + if (palette.style.display === "block") hide(); + else show(); + } + + function render(query) { + const list = document.getElementById("command-list"); + const needle = String(query || "").toLowerCase(); + const filtered = commands.filter((command) => `${command.title} ${command.category}`.toLowerCase().includes(needle)); + list.innerHTML = filtered.map((command) => ` +
+ ${escapeHtml(command.title)} + ${escapeHtml(command.shortcut || "")} +
+ `).join(""); + list.querySelectorAll(".command-item").forEach((item) => { + item.onclick = () => { + hide(); + onCommand(item.dataset.id); + }; + }); + } + + function bindKeyboard(handler) { + document.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === "p") { + event.preventDefault(); + toggle(); + return; + } + const shortcut = toShortcut(event); + const command = commands.find((entry) => normalize(entry.shortcut) === normalize(shortcut)); + if (command) { + event.preventDefault(); + handler(command.id); + } + }); + } + + function toShortcut(event) { + const parts = []; + if (event.ctrlKey) parts.push("Ctrl"); + if (event.shiftKey) parts.push("Shift"); + parts.push(event.key.length === 1 ? event.key.toUpperCase() : event.key); + return parts.join("+"); + } + + function normalize(value) { + return String(value || "").toLowerCase().replace(/\s+/g, ""); + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.commandPalette = { initCommandPalette, bindKeyboard, show, hide, toggle }; +})(); diff --git a/desktop/renderer/context-panel.js b/desktop/renderer/context-panel.js new file mode 100644 index 0000000..f57b812 --- /dev/null +++ b/desktop/renderer/context-panel.js @@ -0,0 +1,55 @@ +(function () { + const patches = []; + + function addPatch(metadata) { + patches.unshift(metadata); + render(); + } + + function render() { + const panel = document.getElementById("context-panel"); + if (!patches.length) { + panel.innerHTML = '
No patch context yet
'; + return; + } + panel.innerHTML = `

Smart Context

${patches.map((patch, index) => ` +
+
${escapeHtml(patch.file_path || "patch")}
+
${escapeHtml(patch.explanation || "Governed patch proposal")}
+
Diff lines: ${escapeHtml(String((patch.diff || "").split("\n").filter(Boolean).length))}
+ +
+ `).join("")}`; + panel.querySelectorAll(".rollback-btn").forEach((button) => { + button.onclick = () => { + const patch = patches[Number(button.dataset.index)]; + if (patch && patch.previous_code) { + window.NovaDesktop.editor.applyUpdatedCode(patch.previous_code); + } + }; + }); + } + + function toggle() { + const panel = document.getElementById("context-panel"); + panel.style.display = panel.style.display === "block" ? "none" : "block"; + render(); + } + + function getHintForLine(lineNumber) { + return patches.find((patch) => patch.line === lineNumber) || patches[0] || null; + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.contextPanel = { addPatch, render, toggle, getHintForLine }; +})(); diff --git a/desktop/renderer/diagnostics.js b/desktop/renderer/diagnostics.js new file mode 100644 index 0000000..9e7084a --- /dev/null +++ b/desktop/renderer/diagnostics.js @@ -0,0 +1,17 @@ +(function () { + function logDiagnostic(message) { + const el = document.getElementById("diagnostics"); + const line = document.createElement("div"); + line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; + el.appendChild(line); + el.scrollTop = el.scrollHeight; + } + + function toggleDiagnostics() { + const el = document.getElementById("diagnostics"); + el.style.display = el.style.display === "block" ? "none" : "block"; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.diagnostics = { logDiagnostic, toggleDiagnostics }; +})(); diff --git a/desktop/renderer/diff.js b/desktop/renderer/diff.js new file mode 100644 index 0000000..4d418d5 --- /dev/null +++ b/desktop/renderer/diff.js @@ -0,0 +1,29 @@ +(function () { + let diffEditor = null; + let currentModels = null; + + function initDiff(monaco, containerId) { + const container = document.getElementById(containerId); + diffEditor = monaco.editor.createDiffEditor(container, { + theme: "vs-dark", + renderSideBySide: true, + automaticLayout: true, + minimap: { enabled: false }, + }); + } + + function showDiff(monaco, originalCode, updatedCode) { + if (currentModels) { + currentModels.original.dispose(); + currentModels.modified.dispose(); + } + currentModels = { + original: monaco.editor.createModel(originalCode, "python"), + modified: monaco.editor.createModel(updatedCode, "python"), + }; + diffEditor.setModel(currentModels); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.diff = { initDiff, showDiff }; +})(); diff --git a/desktop/renderer/editor.js b/desktop/renderer/editor.js new file mode 100644 index 0000000..4434206 --- /dev/null +++ b/desktop/renderer/editor.js @@ -0,0 +1,54 @@ +(function () { + let editor = null; + let currentFilePath = null; + + function initEditor(monaco) { + editor = monaco.editor.create(document.getElementById("editor"), { + value: "", + language: "python", + theme: "vs-dark", + automaticLayout: true, + minimap: { enabled: false }, + fontSize: 13, + }); + editor.onDidChangeCursorPosition((event) => { + const hint = window.NovaDesktop.contextPanel && window.NovaDesktop.contextPanel.getHintForLine(event.position.lineNumber); + if (hint && hint.explanation) { + document.getElementById("status-panel").textContent = hint.explanation; + } + }); + } + + async function loadFile(filePath) { + currentFilePath = filePath; + const content = await window.fileAPI.readFile(filePath); + editor.setValue(content); + document.getElementById("current-file").textContent = filePath; + document.getElementById("topbar-current-file").textContent = filePath; + } + + function getCurrentCode() { + return editor ? editor.getValue() : ""; + } + + async function applyUpdatedCode(updatedCode) { + if (!currentFilePath) { + return; + } + await window.fileAPI.writeFile(currentFilePath, updatedCode); + editor.setValue(updatedCode); + } + + function getCurrentFilePath() { + return currentFilePath; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.editor = { + initEditor, + loadFile, + getCurrentCode, + applyUpdatedCode, + getCurrentFilePath, + }; +})(); diff --git a/desktop/renderer/index.html b/desktop/renderer/index.html new file mode 100644 index 0000000..a3bfe1d --- /dev/null +++ b/desktop/renderer/index.html @@ -0,0 +1,127 @@ + + + + + + + Nova Desktop + + + +
+ +
Sovereign - Local - Governed
+
+
+ + + +
+
+
+ + + Node checking... +
+
+ No file selected +
+
+ + + + + +
+
+ +
+ +
+
+
No file selected
+
+
+
+
Patch Preview
+
+
+
+ +
+ + + + +
+ +
+ + + + + + +
+ +
+
+ + + + + + + +
+

Create New Project

+ + + + +
+ +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/renderer/index.js b/desktop/renderer/index.js new file mode 100644 index 0000000..d471f3a --- /dev/null +++ b/desktop/renderer/index.js @@ -0,0 +1,144 @@ +(function () { + let monacoApi = null; + let pendingUpdatedCode = ""; + + window.addEventListener("DOMContentLoaded", () => { + require.config({ paths: { vs: "../node_modules/monaco-editor/min/vs" } }); + require(["vs/editor/editor.main"], async (monaco) => { + monacoApi = monaco; + window.NovaDesktop.editor.initEditor(monacoApi); + window.NovaDesktop.diff.initDiff(monacoApi, "diff"); + window.NovaDesktop.settings.initSettings(); + window.NovaDesktop.modelSelect.initModelSelector(); + window.NovaDesktop.templates.initTemplates(); + window.NovaDesktop.status.initStatus(); + window.NovaDesktop.timeline.renderTimeline(); + window.NovaDesktop.receipts.renderReceipts(); + window.NovaDesktop.commandPalette.initCommandPalette(executeCommand); + window.NovaDesktop.commandPalette.bindKeyboard(executeCommand); + + const root = await window.fileAPI.defaultRoot(); + await window.NovaDesktop.sidebar.initSidebar(root, (filePath) => { + window.NovaDesktop.editor.loadFile(filePath); + }); + + document.getElementById("open-folder").onclick = () => window.NovaDesktop.sidebar.openFolder(); + document.getElementById("receipts-btn").onclick = () => window.NovaDesktop.receipts.toggleReceipts(); + document.getElementById("tools-btn").onclick = () => window.NovaDesktop.toolRegistry.toggleToolRegistry(); + document.getElementById("diagnostics-btn").onclick = () => window.NovaDesktop.diagnostics.toggleDiagnostics(); + document.getElementById("replay-btn").onclick = () => window.NovaDesktop.replay.replayLast(); + document.getElementById("apply-patch").onclick = async () => { + await window.NovaDesktop.editor.applyUpdatedCode(pendingUpdatedCode); + document.getElementById("apply-patch").disabled = true; + window.NovaDesktop.diagnostics.logDiagnostic("Patch applied to disk"); + }; + document.getElementById("explain-btn").onclick = () => window.NovaDesktop.contextPanel.toggle(); + document.getElementById("diff-toggle-btn").onclick = () => window.NovaDesktop.studioPanels.toggleDiff(); + + window.NovaDesktop.chat.initChat({ + onCodeInstruction: runCodeInstruction, + onWireInstruction: runWireInstruction, + }); + }); + }); + + async function executeCommand(id) { + if (id === "coder.apply_instruction") { + const instruction = document.getElementById("chat-input").value.trim(); + if (instruction) await runCodeInstruction(instruction); + } else if (id === "wiring.generate_glue") { + const goal = document.getElementById("chat-input").value.trim(); + if (goal) await runWireInstruction(goal); + } else if (id === "ui.toggle_diff") { + window.NovaDesktop.studioPanels.toggleDiff(); + } else if (id === "ui.toggle_receipts") { + window.NovaDesktop.receipts.toggleReceipts(); + } else if (id === "node.replay_last") { + await window.NovaDesktop.replay.replayLast(); + } else if (id === "node.switch_model") { + document.getElementById("model-select").focus(); + } + } + + async function runCodeInstruction(instruction) { + const filePath = window.NovaDesktop.editor.getCurrentFilePath(); + if (!filePath) { + document.getElementById("status-panel").textContent = "Select a file before applying code"; + return; + } + const currentCode = window.NovaDesktop.editor.getCurrentCode(); + const start = performance.now(); + window.NovaDesktop.diagnostics.logDiagnostic("Tool invoked: coder_tool"); + const result = await window.nodeAPI.code({ + file_path: filePath, + instruction, + current_code: currentCode, + }); + const elapsed = performance.now() - start; + pendingUpdatedCode = result.result.updated_code || ""; + window.NovaDesktop.diff.showDiff(monacoApi, currentCode, pendingUpdatedCode); + window.NovaDesktop.receipts.setLatency(elapsed); + const receipt = window.NovaDesktop.receipts.addFromNodeResponse(result); + window.NovaDesktop.timeline.addTimelineEvent(receipt); + const profile = window.NovaDesktop.profiler.buildProfile(result, elapsed); + window.NovaDesktop.profiler.showProfiler(profile); + window.NovaDesktop.studioPanels.showWaterfall(profile); + window.NovaDesktop.studioPanels.showInvariants(result.governance || {}); + window.NovaDesktop.inspector.showGovernanceInspector(result.governance || {}); + window.NovaDesktop.contextPanel.addPatch({ + tool: receipt.tool, + decision: receipt.decision, + replay_id: receipt.replay_id, + diff: result.result.diff, + explanation: governanceExplanation(result.governance), + file_path: filePath, + previous_code: currentCode, + updated_code: pendingUpdatedCode, + total_ms: elapsed, + }); + window.NovaDesktop.diagnostics.logDiagnostic(`Governance decision: ${receipt.decision}`); + document.getElementById("apply-patch").disabled = false; + } + + async function runWireInstruction(goal) { + const start = performance.now(); + window.NovaDesktop.diagnostics.logDiagnostic("Tool invoked: wiring_tool"); + const result = await window.nodeAPI.wire({ + goal, + components: [window.NovaDesktop.editor.getCurrentFilePath()].filter(Boolean), + context: "Nova Desktop Electron renderer connected to governed Lawful Nova Node", + }); + const elapsed = performance.now() - start; + const currentCode = window.NovaDesktop.editor.getCurrentCode(); + pendingUpdatedCode = result.result.glue_code || ""; + window.NovaDesktop.diff.showDiff(monacoApi, currentCode, pendingUpdatedCode); + window.NovaDesktop.receipts.setLatency(elapsed); + const receipt = window.NovaDesktop.receipts.addFromNodeResponse(result); + window.NovaDesktop.timeline.addTimelineEvent(receipt); + const profile = window.NovaDesktop.profiler.buildProfile(result, elapsed); + window.NovaDesktop.profiler.showProfiler(profile); + window.NovaDesktop.studioPanels.showWaterfall(profile); + window.NovaDesktop.studioPanels.showInvariants(result.governance || {}); + window.NovaDesktop.inspector.showGovernanceInspector(result.governance || {}); + window.NovaDesktop.contextPanel.addPatch({ + tool: receipt.tool, + decision: receipt.decision, + replay_id: receipt.replay_id, + diff: result.result.diff || result.result.glue_code || "", + explanation: governanceExplanation(result.governance), + file_path: window.NovaDesktop.editor.getCurrentFilePath(), + previous_code: currentCode, + updated_code: pendingUpdatedCode, + total_ms: elapsed, + }); + window.NovaDesktop.diagnostics.logDiagnostic(`Governance decision: ${receipt.decision}`); + document.getElementById("apply-patch").disabled = false; + } + + function governanceExplanation(governance) { + if (!governance) return "No governance response"; + if (governance.reasoning) return governance.reasoning; + if (governance.reason) return governance.reason; + return `Governance receipts: ${(governance.receipts || []).join(", ")}`; + } +})(); diff --git a/desktop/renderer/inspector.js b/desktop/renderer/inspector.js new file mode 100644 index 0000000..0e5aa0d --- /dev/null +++ b/desktop/renderer/inspector.js @@ -0,0 +1,100 @@ +(function () { + function showGovernanceInspector(decision) { + const panel = document.getElementById("gov-inspector"); + const invariants = normalizeInvariants(decision); + panel.innerHTML = ` +

Governance Decision

+
Decision: ${escapeHtml(decision.decision)}
+
Policy Version: ${escapeHtml(decision.policy_version)}
+
Replay ID: ${escapeHtml(decision.trace_id || decision.replay_id)}
+
Invariants Checked:
+
    ${invariants.map((item) => `
  • ${escapeHtml(item.name)}
  • `).join("")}
+
Reasoning:
+
${escapeHtml(decision.reasoning || (decision.receipts || []).join(", ") || decision.reason || "allowed by policy")}
+ `; + panel.style.display = "block"; + } + + function hideGovernanceInspector() { + document.getElementById("gov-inspector").style.display = "none"; + } + + function showEvidenceBundle(evidence) { + const panel = document.getElementById("gov-inspector"); + const checks = evidence.verification || {}; + const trace = evidence.trace || {}; + panel.innerHTML = ` +

Evidence Bundle

+
Trace: ${escapeHtml(evidence.trace_id)}
+
Policy: ${escapeHtml((evidence.policy || {}).version)}
+ ${renderCheck("Receipt Hash", checks.receipt_hash)} + ${renderCheck("Input Hash", checks.input_hash)} + ${renderCheck("Output Hash", checks.output_hash)} + ${renderCheck("Original Code Hash", checks.original_code_hash)} +
Replay: ${escapeHtml(replayState(checks.replay))}
+
Trace Events: ${escapeHtml(String((trace.events || []).length))}
+
Continuity Events: ${escapeHtml(String((trace.continuity || []).length))}
+
${escapeHtml(JSON.stringify(evidence.cross_node || {}, null, 2))}
+ `; + panel.style.display = "block"; + } + + function showPolicyManifest(manifest, receipt) { + const panel = document.getElementById("gov-inspector"); + panel.innerHTML = ` +

Policy Manifest

+
Trace: ${escapeHtml(receipt.trace_id || receipt.replay_id)}
+
Manifest Version: ${escapeHtml((manifest.node || {}).version)}
+
${escapeHtml(JSON.stringify(manifest, null, 2))}
+ `; + panel.style.display = "block"; + } + + function showTracePath(receipt, events) { + const traceId = receipt.trace_id || receipt.replay_id; + const filtered = events.filter((event) => event.payload && event.payload.trace_id === traceId); + const panel = document.getElementById("gov-inspector"); + panel.innerHTML = ` +

Trace Path

+
Trace: ${escapeHtml(traceId)}
+
    ${filtered.map((event) => `
  1. ${escapeHtml(event.channel)} / ${escapeHtml(event.type)}
  2. `).join("")}
+
${escapeHtml(JSON.stringify(filtered, null, 2))}
+ `; + panel.style.display = "block"; + } + + function normalizeInvariants(decision) { + if (Array.isArray(decision.invariants)) return decision.invariants; + return (decision.receipts || []).map((name) => ({ name, pass: true })); + } + + function renderCheck(label, check) { + const state = check && check.valid ? "valid" : "drift"; + return `
${escapeHtml(label)}: ${escapeHtml(state)}
`; + } + + function replayState(replay) { + if (!replay || replay.available === false) return replay && replay.reason ? replay.reason : "unavailable"; + return replay.deterministic ? "deterministic" : "drift"; + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.inspector = { + showGovernanceInspector, + showEvidenceBundle, + showPolicyManifest, + showTracePath, + hideGovernanceInspector, + normalizeInvariants, + }; +})(); diff --git a/desktop/renderer/model-select.js b/desktop/renderer/model-select.js new file mode 100644 index 0000000..2adf41d --- /dev/null +++ b/desktop/renderer/model-select.js @@ -0,0 +1,14 @@ +(function () { + function initModelSelector() { + const select = document.getElementById("model-select"); + const config = window.nodeAPI.getConfig(); + select.value = config.model || "qwen2.5-coder:3b"; + select.onchange = () => { + window.nodeAPI.updateConfig({ model: select.value }); + document.getElementById("settings-model").value = select.value; + }; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.modelSelect = { initModelSelector }; +})(); diff --git a/desktop/renderer/profiler.js b/desktop/renderer/profiler.js new file mode 100644 index 0000000..1246143 --- /dev/null +++ b/desktop/renderer/profiler.js @@ -0,0 +1,38 @@ +(function () { + function showProfiler(stats) { + const el = document.getElementById("profiler"); + el.style.display = "block"; + el.innerHTML = ` +

Tool Invocation Profile

+
Tool: ${escapeHtml(stats.tool)}
+
Inference: ${Math.round(stats.inference_ms || 0)} ms
+
Governance: ${Math.round(stats.governance_ms || stats.gov_ms || 0)} ms
+
Diff Size: ${Math.round(stats.diff_lines || 0)} lines
+
Total: ${Math.round(stats.total_ms || 0)} ms
+ `; + } + + function buildProfile(result, totalMs) { + const diff = result.result && result.result.diff ? result.result.diff : ""; + return { + tool: result.result && result.result.receipts ? result.result.receipts[0] : "unknown_tool", + inference_ms: result.governance ? result.governance.inference_ms || 0 : 0, + governance_ms: result.governance ? result.governance.gov_ms || 0 : 0, + diff_lines: diff ? diff.split("\n").filter(Boolean).length : 0, + total_ms: totalMs, + }; + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.profiler = { showProfiler, buildProfile }; +})(); diff --git a/desktop/renderer/receipts.js b/desktop/renderer/receipts.js new file mode 100644 index 0000000..326eaca --- /dev/null +++ b/desktop/renderer/receipts.js @@ -0,0 +1,136 @@ +(function () { + const receipts = []; + let replayCount = 0; + let lastLatency = 0; + + function addReceipt(receipt) { + receipts.push(receipt); + renderReceipts(); + } + + function addFromNodeResponse(response) { + const nodeReceipt = response.receipt || {}; + const traceId = response.governance ? response.governance.trace_id || response.governance.replay_id : nodeReceipt.trace_id || ""; + const receipt = { + tool: response.result && response.result.receipts ? response.result.receipts[0] : "unknown_tool", + decision: response.governance ? response.governance.decision : "unknown", + replay_id: traceId, + trace_id: traceId, + receipt_hash: nodeReceipt.receipt_hash || "", + input_hash: nodeReceipt.input_hash || "", + output_hash: nodeReceipt.output_hash || "", + policy_version: response.governance ? response.governance.policy_version || nodeReceipt.policy_version : nodeReceipt.policy_version || "", + trace_file: response.trace_file || "", + time: new Date().toLocaleTimeString(), + diff: response.result ? response.result.diff : "", + }; + addReceipt(receipt); + return receipt; + } + + function renderReceipts() { + const drawer = document.getElementById("receipts-drawer"); + drawer.innerHTML = ""; + + if (receipts.length === 0) { + drawer.innerHTML = '
No tool receipts yet
'; + return; + } + + receipts.forEach((r, index) => { + const item = document.createElement("div"); + item.className = "receipt-item"; + item.innerHTML = ` +
Tool: ${escapeHtml(r.tool)}
+
Decision: ${escapeHtml(r.decision)}
+
Trace ID:
+
Policy:
+
Receipt Hash:
+
Time: ${escapeHtml(r.time)}
+
+ + +
+ `; + drawer.appendChild(item); + }); + drawer.querySelectorAll("button[data-action]").forEach((button) => { + button.onclick = () => handleReceiptAction(button.dataset.action, Number(button.dataset.index)); + }); + } + + async function handleReceiptAction(action, index) { + const receipt = receipts[index]; + if (!receipt) return; + if (action === "replay") { + await window.NovaDesktop.replay.replayReceipt(receipt); + return; + } + if (action === "policy") { + const manifest = await window.nodeAPI.featureManifest(); + window.NovaDesktop.inspector.showPolicyManifest(manifest.manifest || manifest, receipt); + return; + } + if (action === "trace") { + const events = await window.nodeAPI.events({ limit: 500 }); + window.NovaDesktop.inspector.showTracePath(receipt, events.events || []); + return; + } + const evidence = await window.nodeAPI.verifyTrace(receipt.trace_id || receipt.replay_id); + window.NovaDesktop.inspector.showEvidenceBundle(evidence); + } + + function toggleReceipts() { + const drawer = document.getElementById("receipts-drawer"); + drawer.style.display = drawer.style.display === "block" ? "none" : "block"; + renderReceipts(); + } + + function getReceipts() { + return receipts.slice(); + } + + function getLastReceipt() { + return receipts[receipts.length - 1] || null; + } + + function recordReplay() { + replayCount += 1; + } + + function setLatency(ms) { + lastLatency = Math.round(ms || 0); + } + + function getSummary() { + return { + receipts: receipts.length, + replays: replayCount, + latency: lastLatency, + }; + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.receipts = { + addReceipt, + addFromNodeResponse, + renderReceipts, + toggleReceipts, + getReceipts, + getLastReceipt, + handleReceiptAction, + recordReplay, + setLatency, + getSummary, + }; +})(); diff --git a/desktop/renderer/replay.js b/desktop/renderer/replay.js new file mode 100644 index 0000000..22e47a7 --- /dev/null +++ b/desktop/renderer/replay.js @@ -0,0 +1,26 @@ +(function () { + async function replayLast() { + const last = window.NovaDesktop.receipts.getLastReceipt(); + return replayReceipt(last); + } + + async function replayReceipt(last) { + if (!last || !last.replay_id) { + return null; + } + const data = await window.nodeAPI.replay(last.replay_id); + console.log("Replay:", data); + window.NovaDesktop.receipts.recordReplay(); + window.NovaDesktop.timeline.addTimelineEvent({ + kind: "replay", + tool: last.tool, + decision: data.deterministic === false ? "drift" : "allowed", + replay_id: last.replay_id, + }); + window.NovaDesktop.studioPanels.showReplayOverlay(data.original_output, data.replayed_output); + return data; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.replay = { replayLast, replayReceipt }; +})(); diff --git a/desktop/renderer/settings.js b/desktop/renderer/settings.js new file mode 100644 index 0000000..b109b71 --- /dev/null +++ b/desktop/renderer/settings.js @@ -0,0 +1,25 @@ +(function () { + function initSettings() { + const config = window.nodeAPI.getConfig(); + const nodeInput = document.getElementById("settings-node-url"); + const modelInput = document.getElementById("settings-model"); + const state = document.getElementById("settings-state"); + + nodeInput.value = config.nodeUrl; + modelInput.value = config.model; + + document.getElementById("settings-save").onclick = () => { + window.nodeAPI.updateConfig({ + nodeUrl: nodeInput.value.trim(), + model: modelInput.value.trim(), + }); + state.textContent = "Saved"; + setTimeout(() => { + state.textContent = ""; + }, 1600); + }; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.settings = { initSettings }; +})(); diff --git a/desktop/renderer/sidebar.js b/desktop/renderer/sidebar.js new file mode 100644 index 0000000..cc877f8 --- /dev/null +++ b/desktop/renderer/sidebar.js @@ -0,0 +1,65 @@ +(function () { + let currentRoot = null; + let onFileSelected = null; + + async function initSidebar(rootDir, onSelect) { + currentRoot = rootDir; + onFileSelected = onSelect; + const sidebar = document.getElementById("sidebar"); + sidebar.innerHTML = ""; + + let entries = []; + try { + entries = await window.fileAPI.listEntries(rootDir); + } catch (error) { + sidebar.innerHTML = `
${escapeHtml(error.message)}
`; + return; + } + + if (entries.length === 0) { + sidebar.innerHTML = '
No visible files
'; + return; + } + + entries.forEach((entry) => { + const item = document.createElement("div"); + item.className = entry.kind === "directory" ? "sidebar-dir" : "sidebar-file"; + item.textContent = entry.name; + item.title = entry.path; + item.onclick = async () => { + if (entry.kind === "directory") { + await initSidebar(entry.path, onSelect); + return; + } + document.querySelectorAll(".sidebar-file.selected").forEach((node) => node.classList.remove("selected")); + item.classList.add("selected"); + onFileSelected(entry.path); + }; + sidebar.appendChild(item); + }); + } + + async function openFolder() { + const folder = await window.fileAPI.openFolder(); + if (folder) { + await initSidebar(folder, onFileSelected); + } + } + + function getCurrentRoot() { + return currentRoot; + } + + function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.sidebar = { initSidebar, openFolder, getCurrentRoot }; +})(); diff --git a/desktop/renderer/splash.js b/desktop/renderer/splash.js new file mode 100644 index 0000000..a6faf26 --- /dev/null +++ b/desktop/renderer/splash.js @@ -0,0 +1,8 @@ +(function () { + window.addEventListener("DOMContentLoaded", () => { + setTimeout(() => { + const splash = document.getElementById("splash"); + if (splash) splash.style.display = "none"; + }, 1200); + }); +})(); diff --git a/desktop/renderer/status.js b/desktop/renderer/status.js new file mode 100644 index 0000000..0394003 --- /dev/null +++ b/desktop/renderer/status.js @@ -0,0 +1,27 @@ +(function () { + async function initStatus() { + const panel = document.getElementById("status-panel"); + + const update = async () => { + try { + const status = await window.nodeAPI.status(); + document.getElementById("heartbeat").style.background = "#0e639c"; + const summary = window.NovaDesktop.receipts ? window.NovaDesktop.receipts.getSummary() : { receipts: 0, replays: 0, latency: 0 }; + const governance = status.governance_health + ? `${status.governance_health.continuity_events || 0} continuity events` + : "governed"; + const model = window.nodeAPI.getConfig().model; + panel.textContent = `Model ${model} | Node ${status.node_id || "unknown"} | ${status.conformance_profile || "N0"} | ${governance} | receipts ${summary.receipts} | replays ${summary.replays} | latency ${summary.latency}ms`; + } catch (error) { + document.getElementById("heartbeat").style.background = "#a83232"; + panel.textContent = "Node offline"; + } + }; + + await update(); + setInterval(update, 5000); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.status = { initStatus }; +})(); diff --git a/desktop/renderer/studio-panels.js b/desktop/renderer/studio-panels.js new file mode 100644 index 0000000..f072c2e --- /dev/null +++ b/desktop/renderer/studio-panels.js @@ -0,0 +1,78 @@ +(function () { + function showReplayOverlay(original, replayed) { + const el = document.getElementById("replay-overlay"); + el.innerHTML = ` + +
+

Original Output

+
${escapeHtml(toText(original))}
+
+
+

Replayed Output

+
${escapeHtml(toText(replayed))}
+
+ `; + el.style.display = "block"; + document.getElementById("replay-close").onclick = hideReplayOverlay; + } + + function hideReplayOverlay() { + document.getElementById("replay-overlay").style.display = "none"; + } + + function showInvariants(decision) { + const el = document.getElementById("invariants"); + const invariants = window.NovaDesktop.inspector.normalizeInvariants(decision); + el.classList.toggle("has-items", invariants.length > 0); + el.innerHTML = invariants.map((item) => ` +
+
${escapeHtml(item.name)}
+
${item.pass === false ? "Fail" : "Pass"}
+
+ `).join(""); + } + + function showWaterfall(stats) { + const el = document.getElementById("profiler"); + const steps = [ + { label: "Governance", ms: stats.governance_ms || 0 }, + { label: "Inference", ms: stats.inference_ms || 0 }, + { label: "Diff Generation", ms: stats.diff_lines || 0 }, + { label: "Total", ms: stats.total_ms || 0 }, + ]; + el.style.display = "block"; + el.innerHTML += `

Invocation Waterfall

${steps.map((step) => ` +
+
${escapeHtml(step.label)}
+
${Math.round(step.ms)} ${step.label === "Diff Generation" ? "lines" : "ms"}
+
+ `).join("")}`; + } + + function toggleDiff() { + document.getElementById("editor-diff-container").classList.toggle("diff-hidden"); + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + function toText(value) { + return typeof value === "string" ? value : JSON.stringify(value, null, 2); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.studioPanels = { + showReplayOverlay, + hideReplayOverlay, + showInvariants, + showWaterfall, + toggleDiff, + }; +})(); diff --git a/desktop/renderer/style.css b/desktop/renderer/style.css new file mode 100644 index 0000000..c8d106e --- /dev/null +++ b/desktop/renderer/style.css @@ -0,0 +1,588 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + background: #1e1e1e; + color: #ddd; + font-family: "Segoe UI", sans-serif; + display: flex; + height: 100vh; + overflow: hidden; +} + +button, +input, +textarea { + font: inherit; +} + +button { + background: #34363a; + color: #e7e7e7; + border: 1px solid #484b51; + border-radius: 4px; + cursor: pointer; + min-height: 28px; + padding: 0 10px; +} + +button:hover:not(:disabled) { + background: #3f434a; +} + +button:disabled { + cursor: default; + opacity: 0.45; +} + +#sidebar-shell { + width: 240px; + background: #252526; + border-right: 1px solid #333; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.sidebar-header { + height: 32px; + border-bottom: 1px solid #333; + padding: 4px 8px 4px 10px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 13px; + color: #f0f0f0; +} + +.sidebar-header button { + min-height: 22px; + padding: 0 8px; + font-size: 12px; +} + +#sidebar { + flex: 1; + overflow-y: auto; + padding: 10px; +} + +.sidebar-file, +.sidebar-dir { + padding: 6px 10px; + cursor: pointer; + border-radius: 4px; + min-height: 28px; + color: #d4d4d4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-dir::before { + content: "> "; + color: #8ab4f8; +} + +.sidebar-file::before { + content: "- "; + color: #8f8f8f; +} + +.sidebar-file:hover, +.sidebar-dir:hover, +.sidebar-file.selected { + background: #3a3d41; +} + +#main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +#topbar { + height: 32px; + background: linear-gradient(180deg, rgba(45, 45, 45, 0.95), rgba(36, 36, 36, 0.92)); + backdrop-filter: blur(8px); + border-bottom: 1px solid #333; + padding: 4px 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-size: 13px; +} + +#topbar-left, +#topbar-center, +#topbar-right, +.status-actions { + display: flex; + align-items: center; + gap: 10px; +} + +#topbar-left { + flex: 1; + min-width: 0; +} + +#topbar-center { + flex: 1.5; + justify-content: center; + min-width: 0; +} + +#nova-logo { + font-weight: 700; + letter-spacing: 0.5px; + color: #fff; +} + +#heartbeat { + width: 12px; + height: 12px; + border-radius: 50%; + background: #0e639c; + animation: pulse 2s infinite; + flex: 0 0 auto; +} + +@keyframes pulse { + 0% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(1.35); opacity: 1; } + 100% { transform: scale(1); opacity: 0.7; } +} + +#status-panel { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#topbar-current-file { + max-width: 520px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #cfcfcf; +} + +#model-select { + height: 26px; + background: #1e1e1e; + color: #ddd; + border: 1px solid #444; + border-radius: 4px; +} + +#editor-diff-container { + flex: 1; + display: flex; + min-height: 0; +} + +#editor-diff-container.diff-hidden .pane:last-child { + display: none; +} + +.pane { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.pane-title { + height: 28px; + background: #1f1f1f; + border-bottom: 1px solid #333; + padding: 6px 10px; + font-size: 12px; + color: #bcbcbc; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#editor { + flex: 1; + min-height: 0; +} + +#diff { + flex: 1; + min-height: 0; + border-left: 1px solid #333; +} + +#settings { + min-height: 42px; + background: #202020; + border-top: 1px solid #333; + padding: 7px 10px; + display: flex; + gap: 8px; + align-items: center; +} + +#settings input { + height: 28px; + background: #1e1e1e; + color: #ddd; + border: 1px solid #444; + padding: 0 8px; + border-radius: 4px; +} + +#settings-node-url { + width: 260px; +} + +#settings-model { + width: 140px; +} + +#settings-state { + color: #86d486; + font-size: 12px; +} + +#chat { + height: 120px; + background: rgba(37, 37, 38, 0.9); + backdrop-filter: blur(8px); + border-top: 1px solid #333; + padding: 10px; + display: flex; + gap: 10px; +} + +#chat-input { + flex: 1; + resize: none; + background: #1e1e1e; + color: #ddd; + border: 1px solid #444; + padding: 8px; + border-radius: 4px; + line-height: 1.4; +} + +#chat-send, +#wire-send, +#apply-patch, +#explain-btn, +#diff-toggle-btn { + width: 140px; +} + +#chat-send, +#apply-patch { + background: #0e639c; + color: white; + border: none; +} + +#chat-send:hover:not(:disabled), +#apply-patch:hover:not(:disabled) { + background: #1177bb; +} + +#receipts-drawer { + position: absolute; + right: 0; + top: 32px; + width: 300px; + height: calc(100vh - 32px); + background: #1e1e1e; + border-left: 1px solid #333; + padding: 10px; + overflow-y: auto; + display: none; + box-shadow: -16px 0 28px rgba(0, 0, 0, 0.28); + z-index: 20; +} + +.receipt-item { + padding: 9px 8px; + border-bottom: 1px solid #333; + font-size: 12px; + line-height: 1.5; +} + +.receipt-item strong { + color: #fff; +} + +.receipt-actions { + display: flex; + gap: 6px; + margin-top: 8px; +} + +.link-button { + min-height: 0; + padding: 0; + border: 0; + background: transparent; + color: #8ab4f8; + text-align: left; +} + +.link-button.hash { + max-width: 210px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; +} + +.empty-state { + color: #8f8f8f; + font-size: 13px; + padding: 8px; +} + +#splash { + position: absolute; + inset: 0; + background: radial-gradient(circle at center, #262a2f 0%, #1e1e1e 46%, #171717 100%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #ddd; + z-index: 100; +} + +#splash-logo { + font-size: 48px; + margin-bottom: 10px; + letter-spacing: 2px; +} + +#splash-sub { + font-size: 16px; + opacity: 0.7; + margin-bottom: 40px; +} + +#splash-loading { + width: 40px; + height: 40px; + border: 4px solid #333; + border-top-color: #0e639c; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +#invariants { + min-height: 0; + max-height: 0; + overflow: hidden; + background: rgba(37, 37, 38, 0.9); + border-bottom: 1px solid #333; + display: flex; + gap: 10px; + padding: 0 10px; + transition: max-height 0.18s ease, padding 0.18s ease; +} + +#invariants.has-items { + max-height: 76px; + padding: 10px; +} + +.invariant { + flex: 1; + background: #1e1e1e; + border-radius: 4px; + padding: 8px; + text-align: center; + border: 1px solid #444; + font-size: 12px; +} + +.invariant.pass { + border-color: #4caf50; +} + +.invariant.fail { + border-color: #f44336; +} + +#timeline { + height: 118px; + background: #1e1e1e; + border-top: 1px solid #333; + overflow-x: auto; + white-space: nowrap; + padding: 10px; +} + +.timeline-item { + display: inline-block; + width: 210px; + margin-right: 10px; + background: #252526; + padding: 8px; + border-radius: 4px; + font-size: 12px; + vertical-align: top; +} + +#context-panel, +#tool-registry, +#diagnostics, +#profiler { + position: absolute; + right: 0; + width: 320px; + background: rgba(30, 30, 30, 0.88); + backdrop-filter: blur(8px); + border-left: 1px solid #333; + padding: 10px; + overflow-y: auto; + z-index: 15; + display: none; +} + +#context-panel, +#tool-registry { + top: 32px; + height: calc(100vh - 32px); +} + +#diagnostics, +#profiler { + bottom: 0; + height: 210px; +} + +.context-item, +.tool-item, +.waterfall-step, +.profiler-row { + background: #252526; + padding: 8px; + border-radius: 4px; + margin-bottom: 10px; + font-size: 12px; +} + +.rollback-btn { + margin-top: 8px; +} + +#command-palette, +#wizard, +#gov-inspector, +#replay-overlay { + position: absolute; + display: none; + z-index: 40; + background: rgba(30, 30, 30, 0.92); + backdrop-filter: blur(8px); + border: 1px solid #444; + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); +} + +#command-palette { + top: 72px; + left: 50%; + transform: translateX(-50%); + width: 560px; + border-radius: 6px; + padding: 10px; +} + +#command-search { + width: 100%; + height: 34px; + background: #151515; + color: #ddd; + border: 1px solid #444; + border-radius: 4px; + padding: 0 10px; + margin-bottom: 8px; +} + +.command-item { + padding: 8px; + border-radius: 4px; + display: flex; + justify-content: space-between; + cursor: pointer; +} + +.command-item:hover, +.command-item.selected { + background: #3a3d41; +} + +#wizard { + top: 90px; + left: 50%; + transform: translateX(-50%); + width: 360px; + border-radius: 6px; + padding: 16px; +} + +#wizard select, +#wizard input { + width: 100%; + height: 32px; + margin-bottom: 10px; + background: #151515; + color: #ddd; + border: 1px solid #444; + border-radius: 4px; + padding: 0 8px; +} + +#gov-inspector { + top: 52px; + right: 340px; + width: 360px; + max-height: 420px; + overflow-y: auto; + border-radius: 6px; + padding: 12px; +} + +#replay-overlay { + inset: 0; + background: rgba(0, 0, 0, 0.78); + color: #ddd; + padding: 20px; + overflow-y: auto; + z-index: 50; +} + +.replay-section { + margin-bottom: 20px; + background: #252526; + padding: 10px; + border-radius: 4px; +} + +.patch-added { + background: rgba(0, 255, 0, 0.1); + transition: background 1s ease; +} + +.patch-removed { + background: rgba(255, 0, 0, 0.1); + transition: background 1s ease; +} diff --git a/desktop/renderer/templates.js b/desktop/renderer/templates.js new file mode 100644 index 0000000..e3f3e82 --- /dev/null +++ b/desktop/renderer/templates.js @@ -0,0 +1,24 @@ +(function () { + function initTemplates() { + document.getElementById("wizard-create").onclick = async () => { + const type = document.getElementById("wizard-type").value; + const name = document.getElementById("wizard-name").value.trim(); + if (!name) return; + const rootDir = window.NovaDesktop.sidebar.getCurrentRoot() || await window.fileAPI.defaultRoot(); + const created = await window.fileAPI.createProject({ rootDir, type, name }); + window.NovaDesktop.diagnostics.logDiagnostic(`Project created: ${created}`); + document.getElementById("wizard").style.display = "none"; + await window.NovaDesktop.sidebar.initSidebar(rootDir, (filePath) => window.NovaDesktop.editor.loadFile(filePath)); + }; + document.getElementById("wizard-close").onclick = () => { + document.getElementById("wizard").style.display = "none"; + }; + } + + function showWizard() { + document.getElementById("wizard").style.display = "block"; + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.templates = { initTemplates, showWizard }; +})(); diff --git a/desktop/renderer/timeline.js b/desktop/renderer/timeline.js new file mode 100644 index 0000000..cec0d29 --- /dev/null +++ b/desktop/renderer/timeline.js @@ -0,0 +1,44 @@ +(function () { + const timeline = []; + + function addTimelineEvent(receipt) { + timeline.push({ + id: receipt.replay_id || receipt.id || "", + tool: receipt.tool || "unknown_tool", + decision: receipt.decision || "unknown", + time: receipt.time || new Date().toLocaleTimeString(), + diff: receipt.diff || null, + kind: receipt.kind || "patch", + }); + renderTimeline(); + } + + function renderTimeline() { + const el = document.getElementById("timeline"); + if (!timeline.length) { + el.innerHTML = '
No governed actions yet
'; + return; + } + el.innerHTML = timeline.map((t) => ` +
+
${escapeHtml(t.time)} ${escapeHtml(t.kind)}
+
Tool: ${escapeHtml(t.tool)}
+
Decision: ${escapeHtml(t.decision)}
+
Replay ID: ${escapeHtml(t.id)}
+
+ `).join(""); + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.timeline = { addTimelineEvent, renderTimeline }; +})(); diff --git a/desktop/renderer/tool-registry.js b/desktop/renderer/tool-registry.js new file mode 100644 index 0000000..b3816f7 --- /dev/null +++ b/desktop/renderer/tool-registry.js @@ -0,0 +1,37 @@ +(function () { + async function loadToolRegistry() { + const el = document.getElementById("tool-registry"); + try { + const payload = await window.nodeAPI.tools(); + el.innerHTML = `

Tool Registry

${(payload.tools || []).map((tool) => ` +
+
${escapeHtml(tool.name)} ${escapeHtml(tool.profile)}
+
${escapeHtml(tool.description)}
+
Stateless: ${tool.stateless ? "true" : "false"}
+
${escapeHtml((tool.capabilities || []).join(", "))}
+
+ `).join("")}`; + } catch (error) { + el.innerHTML = '
Tool registry unavailable
'; + } + } + + function toggleToolRegistry() { + const el = document.getElementById("tool-registry"); + el.style.display = el.style.display === "block" ? "none" : "block"; + if (el.style.display === "block") loadToolRegistry(); + } + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[char])); + } + + window.NovaDesktop = window.NovaDesktop || {}; + window.NovaDesktop.toolRegistry = { loadToolRegistry, toggleToolRegistry }; +})(); diff --git a/desktop/tests/core.test.cjs b/desktop/tests/core.test.cjs new file mode 100644 index 0000000..4c48d82 --- /dev/null +++ b/desktop/tests/core.test.cjs @@ -0,0 +1,207 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); + +test("node client posts code and wire tasks to configured node tool endpoint", async () => { + const calls = []; + const client = require("../core/node-client"); + assert.equal(client.getConfig().model, "qwen2.5-coder:3b"); + client.updateConfig({ nodeUrl: "http://127.0.0.1:8080/", model: "qwen2.5-coder:7b" }); + client.setTransport(async (url, options) => { + calls.push({ url, options }); + return { + ok: true, + json: async () => ({ result: { receipts: ["coder_tool"] }, governance: { decision: "allowed" } }), + }; + }); + + await client.code({ file_path: "src/app.py", instruction: "Add health", current_code: "print('x')" }); + await client.wire({ goal: "Connect coder", components: ["main.py"] }); + + assert.equal(calls[0].url, "http://127.0.0.1:8080/node/tool"); + assert.deepEqual(JSON.parse(calls[0].options.body), { + intent: "code", + model: "qwen2.5-coder:7b", + file_path: "src/app.py", + instruction: "Add health", + current_code: "print('x')", + }); + assert.deepEqual(JSON.parse(calls[1].options.body), { + intent: "wire", + model: "qwen2.5-coder:7b", + goal: "Connect coder", + components: ["main.py"], + }); +}); + +test("desktop exposes both installed qwen coding models", () => { + const html = fs.readFileSync(path.join(__dirname, "..", "renderer", "index.html"), "utf8"); + + assert.match(html, /value="qwen2\.5-coder:3b"/); + assert.match(html, /value="qwen2\.5-coder:7b"/); +}); + +test("node client reads status and replays by trace id", async () => { + const calls = []; + const client = require("../core/node-client"); + client.updateConfig({ nodeUrl: "http://localhost:8000" }); + client.setTransport(async (url, options) => { + calls.push({ url, options }); + return { ok: true, json: async () => ({ ok: true }) }; + }); + + await client.status(); + await client.replay("trace-123"); + + assert.equal(calls[0].url, "http://localhost:8000/node/status"); + assert.equal(calls[0].options.method, "GET"); + assert.equal(calls[1].url, "http://localhost:8000/node/replay/trace-123"); + assert.equal(calls[1].options.method, "POST"); +}); + +test("node client fetches independently verifiable evidence surfaces", async () => { + const calls = []; + const client = require("../core/node-client"); + client.updateConfig({ nodeUrl: "http://localhost:8000" }); + client.setTransport(async (url, options) => { + calls.push({ url, options }); + return { ok: true, json: async () => ({ ok: true }) }; + }); + + await client.verifyTrace("trace-verify"); + await client.featureManifest(); + await client.events({ prefix: "tool.", limit: 25 }); + await client.compareReceipts({ receipt_hash: "sha256:a" }, { receipt_hash: "sha256:b" }); + + assert.equal(calls[0].url, "http://localhost:8000/node/verify/trace-verify"); + assert.equal(calls[0].options.method, "GET"); + assert.equal(calls[1].url, "http://localhost:8000/node/feature-manifest"); + assert.equal(calls[2].url, "http://localhost:8000/node/events?prefix=tool.&limit=25"); + assert.equal(calls[2].options.method, "GET"); + assert.equal(calls[3].url, "http://localhost:8000/node/compare-receipts"); + assert.deepEqual(JSON.parse(calls[3].options.body), { + left: { receipt_hash: "sha256:a" }, + right: { receipt_hash: "sha256:b" }, + }); +}); + +test("file manager lists visible entries and reads/writes files", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nova-desktop-")); + fs.mkdirSync(path.join(root, "src")); + fs.writeFileSync(path.join(root, "src", "app.py"), "print('hello')\n", "utf8"); + fs.mkdirSync(path.join(root, ".git")); + fs.writeFileSync(path.join(root, ".git", "HEAD"), "ref", "utf8"); + + const files = require("../core/file-manager"); + const entries = files.listEntries(root); + + assert.deepEqual(entries.map((entry) => entry.name), ["src"]); + assert.equal(entries[0].kind, "directory"); + assert.equal(files.readFile(path.join(root, "src", "app.py")), "print('hello')\n"); + files.writeFile(path.join(root, "src", "app.py"), "print('updated')\n"); + assert.equal(fs.readFileSync(path.join(root, "src", "app.py"), "utf8"), "print('updated')\n"); +}); + +test("receipts store normalizes node responses for drawer and replay", () => { + const store = require("../core/receipts-store"); + store.clearReceipts(); + const receipt = store.addFromNodeResponse({ + result: { receipts: ["coder_tool"] }, + governance: { decision: "allowed", trace_id: "trace-abc" }, + receipt: { receipt_hash: "sha256:receipt" }, + trace_file: "tool-receipts/trace-abc.json", + }); + + assert.equal(receipt.tool, "coder_tool"); + assert.equal(receipt.decision, "allowed"); + assert.equal(receipt.replay_id, "trace-abc"); + assert.equal(receipt.trace_id, "trace-abc"); + assert.equal(receipt.receipt_hash, "sha256:receipt"); + assert.equal(receipt.trace_file, "tool-receipts/trace-abc.json"); + assert.equal(store.getReceipts().length, 1); + assert.equal(store.getLastReceipt().replay_id, "trace-abc"); +}); + +test("command registry executes keyboard driven node and ui actions", async () => { + const commands = require("../core/commands"); + const events = []; + const context = { + file_path: "src/app.py", + instruction: "Add endpoint", + current_code: "print('x')", + last_replay_id: "trace-1", + model: "mistral", + actions: { + code: async (task) => events.push(["code", task]), + wire: async (task) => events.push(["wire", task]), + replay: async (id) => events.push(["replay", id]), + updateConfig: (cfg) => events.push(["config", cfg]), + toggle: (target) => events.push(["toggle", target]), + }, + }; + + await commands.executeCommand("coder.apply_instruction", context); + await commands.executeCommand("ui.toggle_receipts", context); + await commands.executeCommand("node.replay_last", context); + await commands.executeCommand("node.switch_model", context); + + assert.deepEqual(events[0], ["code", { + file_path: "src/app.py", + instruction: "Add endpoint", + current_code: "print('x')", + }]); + assert.deepEqual(events[1], ["toggle", "receipts"]); + assert.deepEqual(events[2], ["replay", "trace-1"]); + assert.deepEqual(events[3], ["config", { model: "mistral" }]); +}); + +test("project templates create python node and rust health projects", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nova-templates-")); + const templates = require("../core/project-templates"); + + const pythonRoot = templates.createProject({ rootDir: root, type: "python", name: "api_py" }); + const nodeRoot = templates.createProject({ rootDir: root, type: "node", name: "api_node" }); + const rustRoot = templates.createProject({ rootDir: root, type: "rust", name: "api_rust" }); + + assert.match(fs.readFileSync(path.join(pythonRoot, "app.py"), "utf8"), /\/v1\/health/); + assert.match(fs.readFileSync(path.join(nodeRoot, "index.js"), "utf8"), /\/v1\/health/); + assert.match(fs.readFileSync(path.join(rustRoot, "src", "main.rs"), "utf8"), /\/v1\/health/); + assert.ok(fs.existsSync(path.join(pythonRoot, "README.md"))); + assert.ok(fs.existsSync(path.join(nodeRoot, "package.json"))); + assert.ok(fs.existsSync(path.join(rustRoot, "Cargo.toml"))); +}); + +test("studio metrics track timeline profiler and rollback metadata", () => { + const metrics = require("../core/studio-state"); + metrics.resetStudioState(); + + const patch = metrics.addPatchEvent({ + tool: "coder_tool", + decision: "allowed", + replay_id: "trace-2", + diff: "+hello", + explanation: "boundedness continuity provenance", + file_path: "src/app.py", + previous_code: "old", + updated_code: "new", + total_ms: 42, + }); + const replay = metrics.addReplayEvent({ replay_id: "trace-2", decision: "allowed" }); + const profile = metrics.profileInvocation({ + tool: "coder_tool", + diff: "a\nb\nc", + total_ms: 40, + governance_ms: 5, + inference_ms: 30, + }); + + assert.equal(patch.kind, "patch"); + assert.equal(replay.kind, "replay"); + assert.equal(metrics.getTimeline().length, 2); + assert.equal(metrics.getRecentPatches()[0].previous_code, "old"); + assert.equal(profile.diff_lines, 3); + assert.equal(metrics.getHeartbeatSummary().receipts_count, 1); + assert.equal(metrics.getHintForLine(1).explanation, "boundedness continuity provenance"); +}); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 61da7d5..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,102 +0,0 @@ -# Nova Mission #002 — Architecture - -**Version:** 1.0 -**Purpose:** CRK-2 kernel, Control Tower orchestration, and unified cockpit (NovaShell). - ---- - -## Layout - -``` -agentic-coding-agent/ - agent/ # Nova Agent SDK (AgentRuntime + governance) - crk2/ # CRK-2 constitutional kernel (dLAP, PIT, MACC, ledger v2) - control-tower/ # Multi-agent orchestration, replay, drift simulation - backend/ # Service layer + events gateway (single API surface for UI) - cockpit/ # React frontend (= NovaShell + Flight Deck + Drift Map) - docs/ # Specs, operator certification, integrity suites - tools/fuzz/ # Kernel fuzz harness -``` - -`cockpit/` is the **frontend** referenced in the nine-step integration plan. - ---- - -## Data Flow - -```mermaid -flowchart LR - subgraph UI [cockpit] - NovaShell - FlightDeck - DriftMap - end - subgraph Backend [backend] - EventsGateway - NovaAdapter - CRK2Service - ControlTowerService - end - subgraph Runtime [kernels] - SDK[agent Nova SDK] - CRK2[crk2] - Tower[control-tower] - end - NovaShell --> EventsGateway - FlightDeck --> EventsGateway - DriftMap --> EventsGateway - NovaAdapter --> CRK2Service - NovaAdapter --> ControlTowerService - CRK2Service --> CRK2 - ControlTowerService --> Tower - SDK --> NovaAdapter -``` - ---- - -## Cockpit State Model - -| Store | Responsibility | -|-------|----------------| -| `store.ts` | Single-agent HUD: plans, receipts, violations, continuity | -| `kernelStore.ts` | CRK-2 version, PIT band, continuity anchor | -| `clusterStore.ts` | Agents, cluster events, replay window | -| `driftStore.ts` | Per-agent snapshot grid, divergence events | - -`WebSocketBridge.ts` connects to `ws://127.0.0.1:8787/events` when available; otherwise syncs from in-process Nova events. - ---- - -## Center Modes - -| Mode | Component | -|------|-----------| -| Plan / Diff / Receipts / Invariants / Kernel | Existing panels (CRK-2-aware via stores) | -| Continuity | Timeline + Continuity Matrix | -| Flight Deck | `FlightDeckShell` | -| Drift Map | `DriftVisualizer` | - ---- - -## Nine-Step Integration Status - -| Step | Status | -|------|--------| -| 01 Repo layout (crk2, control-tower, cockpit) | Done | -| 02 Backend surface (crk2-service, control-tower-service, events-gateway) | Done | -| 03 Zustand stores (kernel, cluster, drift) | Done | -| 04 WebSocket bridge | Done (with in-process fallback) | -| 05 NovaShell + Flight Deck + Drift modes | Done | -| 06 Cluster replay in BottomBand | Partial (replay window UI; API wired in control-tower) | -| 07 CRK-2-aware panels | Partial (stores wired; panel annotations incremental) | -| 08 nova-adapter (governance by construction) | Done (backend/nova-adapter.ts) | -| 09 Tests + docs | Docs done; tests incremental | - ---- - -## Related - -- [CRK-2 Spec](./CRK-2-SPEC.md) -- [Control Tower](./NOVA-CONTROL-TOWER.md) -- [Flight Deck React](./operator/NOVA-FLIGHT-DECK-REACT.md) -- [Operator Certification](./operator/index.md) diff --git a/docs/CRK-1-TO-CRK-2-MIGRATION-PLAN.md b/docs/CRK-1-TO-CRK-2-MIGRATION-PLAN.md deleted file mode 100644 index 59a0bdd..0000000 --- a/docs/CRK-1-TO-CRK-2-MIGRATION-PLAN.md +++ /dev/null @@ -1,39 +0,0 @@ -# CRK-1 → CRK-2 Constitutional Migration Plan - -**Version:** 1.0 -**Purpose:** Safe, deterministic, continuity-preserving migration from CRK-1 to CRK-2. - ---- - -## 1. Migration Goals - -CRK-2 introduces: new constitutional objects, PIT-bands, continuity substrate, dLAP, lineage, invariant engine, ledger format. - -Must preserve: continuity, ledger integrity, invariant semantics, PIT-band monotonicity, replay determinism. - ---- - -## 2. Migration Phases - -| Phase | Actions | -|-------|---------| -| 1 — Pre-Migration Audit | Validate CRK-1 invariants, ledger, continuity, PIT; freeze agents | -| 2 — Constitutional Export | Export objects, invariants, ledger, continuity, PIT, lineage | -| 3 — CRK-2 Initialization | Initialize new objects, PIT bands, substrate, ledger, dLAP | -| 4 — Constitutional Mapping | Map Identity, Evidence, Decision, Outcome, Resource | -| 5 — Continuity Reconciliation | Recompute hashes; validate replay, PIT, ledger chain | -| 6 — Kernel Restart | Restart kernel; resume agents; validate cluster coherence | -| 7 — Post-Migration Audit | Validate invariants, ledger, continuity, PIT, lineage | - ---- - -## 3. Migration Guarantees - -No loss of continuity · No loss of ledger integrity · No loss of invariants · No loss of PIT-bands · No loss of lineage · No drift - ---- - -## Related - -- [CRK-2 Spec](./CRK-2-SPEC.md) -- [Level 4 Operator Certification](./operator/OPERATOR-LEVEL-4-CERTIFICATION.md) diff --git a/docs/CRK-2-REFERENCE-IMPLEMENTATION.md b/docs/CRK-2-REFERENCE-IMPLEMENTATION.md deleted file mode 100644 index 558ddc6..0000000 --- a/docs/CRK-2-REFERENCE-IMPLEMENTATION.md +++ /dev/null @@ -1,65 +0,0 @@ -# CRK-2 Reference Implementation - -**Version:** 1.0 -**Purpose:** Language-agnostic modular reference mapped to `crk2/` TypeScript modules. - ---- - -## Directory Layout - -``` -crk2/ - kernel/ - index.ts - dlap.ts - pit-engine.ts - constraint-engine.ts - panic-handler.ts - invariants/ - engine.ts - continuity/ - substrate.ts - crp.ts - replay.ts - ledger/ - ledger-v2.ts - cluster/ - macc.ts -``` - ---- - -## Core Modules - -### dLAP - -```ts -export function dLAP(action, context) { - const clusterState = clusterView() - const local = invariantEngine.checkAll(action, context) - if (!local.ok) return { ok: false, reason: "local-invariant", detail: local } - const cluster = checkClusterInvariants(action, clusterState) - if (!cluster.ok) return { ok: false, reason: "cluster-invariant", detail: cluster } - const constraints = constraintEngine.check(action, context, clusterState) - if (!constraints.ok) return { ok: false, reason: "constraint", detail: constraints } - return { ok: true } -} -``` - -### Continuity v2 + CRP - -See `crk2/continuity/substrate.ts` and `crk2/continuity/crp.ts`. - -### Ledger v2 - -See `crk2/ledger/ledger-v2.ts`. - -### MACC - -See `crk2/cluster/macc.ts`. - ---- - -## Related - -- [CRK-2 Spec](./CRK-2-SPEC.md) diff --git a/docs/CRK-2-SPEC.md b/docs/CRK-2-SPEC.md deleted file mode 100644 index 3845ebd..0000000 --- a/docs/CRK-2-SPEC.md +++ /dev/null @@ -1,176 +0,0 @@ -# CRK-2 Constitutional Runtime Kernel — Formal Specification - -**Version:** 2.0 (Draft) -**Status:** Draft -**Scope:** Kernel-level constitutional runtime for governed, multi-agent systems. - -Implementation: `crk2/` - ---- - -## 1. Purpose and Design Goals - -| Goal | Description | -|------|-------------| -| Deterministic | Same inputs → same constitutional outcomes | -| Replayable | Every governed trajectory reconstructible | -| Drift-aware | Detect and correct divergence across agents | -| Fail-closed | On uncertainty, block actions | -| Distributed | Multi-agent coherence (cluster-aware) | -| Evolvable | Upgrades (CRK-1 → CRK-2 → CRK-3) without losing continuity | - ---- - -## 2. Canonical Object Model - -### 2.1 IdentityObject - -```ts -IdentityObject { - id: UUID - kind: "agent" | "operator" | "kernel" - lineageId: UUID -} -``` - -### 2.2 EvidenceObject - -```ts -EvidenceObject { - id: UUID - domain: string - payload: JSON - ts: int -} -``` - -### 2.3 DecisionObject - -```ts -DecisionObject { - id: UUID - actionId: UUID - invariantsChecked: string[] - pitBand: PITBand - continuityHash: string - clusterViewHash: string - verdict: "allow" | "deny" -} -``` - -### 2.4 OutcomeObject - -```ts -OutcomeObject { - id: UUID - decisionId: UUID - stateDelta: DiffObject - receipts: ReceiptObject[] -} -``` - -### 2.5 ConstraintObject (new) - -```ts -ConstraintObject { - id: UUID - scope: "local" | "cluster" - predicateRef: string - severity: "error" | "warn" - description: string -} -``` - ---- - -## 3. PIT-Band Model (PIT-1 → PIT-5) - -| Band | Role | -|------|------| -| PIT-1 | Direct evidence | -| PIT-2 | Derived evidence | -| PIT-3 | Contextual evidence | -| PIT-4 | Self-evaluation | -| PIT-5 | Constitutional introspection (self-assessment, not self-modification) | - -PIT transitions are monotonic and deterministic. PIT-5 may evaluate but not mutate constitutional law directly. - ---- - -## 4. Distributed Lawful Action Predicate (dLAP) - -\[ -dLAP(a, c, S) = LAP_{local}(a, c) \land LAP_{cluster}(a, S) \land LAP_{constraints}(a, c, S) -\] - -Properties: total, deterministic, side-effect free, fail-closed. - ---- - -## 5. Continuity Substrate v2 - -### Snapshot - -```ts -Snapshot { - id: UUID - hash: string - partial: boolean - state: JSON - ts: int -} -``` - -### Constitutional Replay Proof (CRP) - -```ts -CRP { - snapshotId: UUID - receipts: ReceiptObject[] - pitTransitions: PITTransition[] - hash: string -} -``` - ---- - -## 6. Ledger v2 - -Append-only, cryptographically chained, operator annotations, CRDT-style multi-agent merge. - ---- - -## 7. Multi-Agent Constitutional Coherence (MACC) - -Cluster state summary: - -```ts -ClusterState { - kernelVersion: "CRK-2" - invariantSetHash: string - constraintSetHash: string - ledgerPrefixHash: string - continuityAnchorHash: string - pitDefinitionHash: string -} -``` - ---- - -## 8. Constitutional Amendments v2 (CA-2) - -Freeze → export → apply amendment → validate dLAP, ledger, continuity, PIT → commit version → restart under CRK-2. - ---- - -## 9. Kernel Guarantees - -Deterministic decisions · Replayable trajectories (CRP) · Drift detection/correction · Fail-closed · Constitutional evolution with continuity preserved - ---- - -## Related - -- [CRK-2 Reference Implementation](./CRK-2-REFERENCE-IMPLEMENTATION.md) -- [CRK-1 → CRK-2 Migration](./CRK-1-TO-CRK-2-MIGRATION-PLAN.md) -- [CRK-2 → CRK-3 Roadmap](./CRK-2-TO-CRK-3-ROADMAP.md) diff --git a/docs/CRK-2-TO-CRK-3-ROADMAP.md b/docs/CRK-2-TO-CRK-3-ROADMAP.md deleted file mode 100644 index 318e770..0000000 --- a/docs/CRK-2-TO-CRK-3-ROADMAP.md +++ /dev/null @@ -1,48 +0,0 @@ -# CRK-2 → CRK-3 Evolution Roadmap - -**Goal:** Move from deterministic, drift-aware, distributed CRK-2 to self-describing, meta-constitutional CRK-3. - ---- - -## Phase 1 — CRK-2 Hardening - -- Finalize dLAP semantics -- Prove replay determinism (CRP test suite) -- Harden MACC + Control Tower consensus -- Stabilize ConstraintObject semantics - ---- - -## Phase 2 — Meta-Description Layer - -CRK-3 introduces: - -- Meta-schema describing constitutional objects -- Self-describing invariant registry -- Meta-continuity layer (continuity of the constitution) - -Tasks: define `MetaObject`, `MetaInvariant`, extend ledger for constitutional changes as first-class events. - ---- - -## Phase 3 — Meta-PIT (PIT-6) - -PIT-6: meta-constitutional reasoning. May propose amendments but cannot apply them; all amendments require CA-2-like protocol with operator approval. - ---- - -## Phase 4 — CRK-3 Kernel - -Implement meta-schema, meta-continuity, PIT-6, constitutional change ledger. - ---- - -## Phase 5 — CRK-2 → CRK-3 Migration - -Export CRK-2 state → wrap in CRK-3 meta-schema → rebuild anchors → validate replay and drift detection. - ---- - -## Phase 6 — CRK-3 Stabilization - -Fuzz meta-layer · multi-agent drift simulations · lock CRK-3 as canonical kernel. diff --git a/docs/NOVA-CONTROL-TOWER-CONSENSUS.md b/docs/NOVA-CONTROL-TOWER-CONSENSUS.md deleted file mode 100644 index d24a52a..0000000 --- a/docs/NOVA-CONTROL-TOWER-CONSENSUS.md +++ /dev/null @@ -1,56 +0,0 @@ -# Nova Control Tower — Distributed Constitutional Consensus Protocol - -**Version:** 1.0 -**Purpose:** Distributed consensus for multi-agent Nova clusters. - ---- - -## 1. Consensus Goals - -- Cross-agent constitutional, continuity, and ledger coherence -- Cross-agent PIT-band and dLAP alignment -- Drift detection and correction -- Fail-closed behavior - ---- - -## 2. Consensus Model - -Hybrid **Raft-like** + **CRDT-like**: - -| Raft-like | CRDT-like | -|-----------|-----------| -| Leader election | Conflict-free ledger merges | -| Log replication | Conflict-free continuity merges | -| Majority quorum | Conflict-free PIT merges | -| Heartbeats | Convergence without central lock | - ---- - -## 3. Protocol Overview - -### 3.1 Leader Election - -Leader coordinates constitutional state and distributes PIT/continuity updates. - -### 3.2 Log Replication - -Receipts, continuity snapshots, and PIT transitions replicated across agents. - -### 3.3 Drift Detection - -Drift when: ledger mismatch, continuity mismatch, PIT mismatch, invariant mismatch. - -### 3.4 Drift Correction - -Leader computes canonical state → followers reconcile → CRDT merge → revalidate continuity and ledger. - -### 3.5 Fail-Closed - -If drift cannot be resolved: cluster halts, operators notified, forensics required. - ---- - -## 4. Guarantees - -Strong eventual consistency · Deterministic convergence · Drift detection · Drift correction · Fail-closed safety · Constitutional coherence diff --git a/docs/NOVA-CONTROL-TOWER.md b/docs/NOVA-CONTROL-TOWER.md deleted file mode 100644 index ba9cbf5..0000000 --- a/docs/NOVA-CONTROL-TOWER.md +++ /dev/null @@ -1,99 +0,0 @@ -# Nova Control Tower — Multi-Agent Orchestration Layer - -**Version:** 1.0 -**Purpose:** Distributed orchestration for multi-agent Nova clusters. - -Implementation: `control-tower/` - ---- - -## 1. Architecture - -Control Tower provides: - -- Multi-agent orchestration -- Cross-agent continuity and ledger coherence -- Drift detection and correction -- Cluster-wide invariants and goals -- Distributed PIT-band alignment -- Cluster-wide constitutional enforcement - ---- - -## 2. Module Layout - -``` -control-tower/ - orchestrator/ - cluster-manager.ts - agent-registry.ts - drift-detector.ts - consensus-engine.ts - protocols/ - heartbeat.ts - continuity-sync.ts - ledger-sync.ts - pit-sync.ts - replay/ - cluster-replay.ts - drift/ - drift-simulator.ts -``` - -UI: `cockpit/src/flight-deck/` (Flight Deck mode) - ---- - -## 3. Core Algorithms - -### Drift Detection - -```ts -function detectDrift(agentA, agentB) { - const receiptsA = ledger.get(agentA) - const receiptsB = ledger.get(agentB) - return diff(receiptsA, receiptsB) -} -``` - -### Continuity Sync - -```ts -function syncContinuity(agentA, agentB) { - const snapsA = continuity.list(agentA) - const snapsB = continuity.list(agentB) - return reconcile(snapsA, snapsB) -} -``` - -### Consensus - -```ts -function consensus(goal, agents) { - const plans = agents.map((a) => a.plan(goal)) - return mergePlans(plans) -} -``` - ---- - -## 4. Cluster-Level Invariants - -- No cross-agent divergence -- No conflicting plans -- No ledger forks -- No continuity mismatches -- PIT-band alignment required - ---- - -## 5. Operator Workflow - -Select cluster → set goal → generate cross-agent plan → validate coherence → execute governed steps → monitor drift → resolve divergence → validate continuity and ledger → sign off. - ---- - -## Related - -- [Control Tower Consensus](./NOVA-CONTROL-TOWER-CONSENSUS.md) -- [Flight Deck React](../operator/NOVA-FLIGHT-DECK-REACT.md) diff --git a/docs/WHAT-MY-AGENTIC-CODING-AGENT-DOES.md b/docs/WHAT-MY-AGENTIC-CODING-AGENT-DOES.md deleted file mode 100644 index 1fc215d..0000000 --- a/docs/WHAT-MY-AGENTIC-CODING-AGENT-DOES.md +++ /dev/null @@ -1,195 +0,0 @@ -# 🔥 LAYERED IMAGE: “What My Agentic Coding Agent Does” - -**Text-rendered visual diagram — ready for Figma or Illustrator** -**Version:** 1.0 · **Canvas:** 1440 × 2048 px · **Theme:** Nova dark constitutional - ---- - -## Figma setup - -| Property | Value | -|----------|--------| -| Frame name | `Nova / What My Agentic Coding Agent Does` | -| Background | `#050711` (`color.bg.shell`) | -| Grid | 8 px | -| Type — title | Inter 32 SemiBold `#F5F7FF` | -| Type — layer label | Inter 14 SemiBold `#F5F7FF` | -| Type — body | Inter 12 Regular `#A3B0D9` | -| Type — mono | JetBrains Mono 11 `#56CCF2` | -| Accent stripe | 4 px left border per layer (see below) | - ---- - -## Layered stack (top → bottom) - -``` -╔══════════════════════════════════════════════════════════════════════════════╗ -║ LAYER 7 — OPERATOR ║ -║ Certifies · observes · upgrades constitution · signs off cluster coherence ║ -╠══════════════════════════════════════════════════════════════════════════════╣ -║ LAYER 6 — COCKPIT (NovaShell) ║ -║ Plan · Diff · Receipts · Continuity · Flight Deck · Drift Map ║ -╠══════════════════════════════════════════════════════════════════════════════╣ -║ LAYER 5 — EVENTS GATEWAY ║ -║ Zod-validated WebSocket stream → kernel / agent / cluster events ║ -╠══════════════════════════════════════════════════════════════════════════════╣ -║ LAYER 4 — CONTROL TOWER ║ -║ Multi-agent orchestration · drift detection · cluster replay · consensus ║ -╠══════════════════════════════════════════════════════════════════════════════╣ -║ LAYER 3 — CRK-2 KERNEL ║ -║ dLAP · PIT-1…5 · MACC · ledger v2 · CRP · ConstraintObjects ║ -╠══════════════════════════════════════════════════════════════════════════════╣ -║ LAYER 2 — NOVA SDK (CRK-1) ║ -║ Plan · generate · refactor · invariants · receipts · continuity snapshots ║ -╠══════════════════════════════════════════════════════════════════════════════╣ -║ LAYER 1 — WORKSPACE & TOOLS ║ -║ Files · tests · diffs · CLI (`nova`) · external APIs (governed boundary) ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -``` - ---- - -## Illustrator / Figma layer tree - -``` -Frame: What My Agentic Coding Agent Does -├── Title -│ ├── H1: What My Agentic Coding Agent Does -│ └── Sub: Constitutional · traceable · multi-agent · fail-closed -├── Layer-07-Operator [stripe #F2C94C] -│ ├── Label: OPERATOR -│ ├── Bullets: L1–L5 certification · observer protocol · constitutional upgrade -│ └── Icon: shield-check -├── Layer-06-Cockpit [stripe #3A7BFF] -│ ├── Label: COCKPIT / NovaShell -│ ├── Chips: Plan | Diff | Receipts | Continuity | Flight Deck | Drift -│ └── Stores: kernelStore · clusterStore · driftStore · cockpitStore -├── Layer-05-EventsGateway [stripe #56CCF2] -│ ├── Label: EVENTS GATEWAY -│ ├── Events: kernel.* · agent.* · cluster.* -│ └── Transport: WebSocket ws://127.0.0.1:8787/events -├── Layer-04-ControlTower [stripe #3A7BFF] -│ ├── Label: CONTROL TOWER -│ ├── Modules: cluster-manager · drift-detector · consensus · replay -│ └── Guarantees: drift detect · drift correct · fail-closed -├── Layer-03-CRK2 [stripe #27AE60] -│ ├── Label: CRK-2 KERNEL -│ ├── Objects: Identity · Evidence · Decision · Outcome · Constraint -│ └── Predicate: dLAP(a, c, S) = local ∧ cluster ∧ constraints -├── Layer-02-NovaSDK [stripe #27AE60] -│ ├── Label: NOVA SDK (CRK-1) -│ ├── API: nova.plan · nova.generateCode · governance · continuity -│ └── Outputs: code + governance receipts + continuity hash -├── Layer-01-Workspace [stripe #6B7390] -│ ├── Label: WORKSPACE & TOOLS -│ ├── Inputs: repo files · tests · prompts -│ └── Boundary: every action passes invariants before execution -└── Flow-Arrows (connectors) - ├── Workspace → SDK → CRK-2 (evaluate) - ├── CRK-2 → SDK → Workspace (allowed actions only) - ├── SDK → Control Tower (cluster view) - ├── Control Tower → Events Gateway → Cockpit (live HUD) - └── Operator → Cockpit (observe · certify · upgrade) -``` - ---- - -## Side flow (what happens on one agent action) - -``` - OPERATOR sets goal - │ - ▼ - ┌─────────────┐ prompt + context ┌─────────────┐ - │ COCKPIT │ ────────────────────────► │ NOVA SDK │ - │ Agent Console│ │ plan/generate│ - └─────────────┘ └──────┬──────┘ - ▲ │ - │ ┌──────────▼──────────┐ - │ events │ CRK-2 dLAP │ - │ ◄─────────────────────────│ invariants · PIT · │ - │ │ continuity · ledger │ - └──────────────────────────────┴──────────┬──────────┘ - │ - ┌─────────────────────┼─────────────────────┐ - │ ALLOW │ DENY │ - ▼ ▼ │ - apply diff emit violation │ - emit receipt block action │ - snapshot state fail-closed │ - │ │ │ - └──────────┬──────────┘ │ - ▼ │ - ┌─────────────────────┐ │ - │ CONTROL TOWER │◄── multi-agent ─────┘ - │ sync · drift · replay│ - └──────────┬──────────┘ - ▼ - ┌─────────────────────┐ - │ COCKPIT HUD │ - │ receipts · timeline │ - │ flight deck · drift │ - └─────────────────────┘ -``` - ---- - -## Layer cards (copy-paste for Figma text layers) - -### LAYER 7 — Operator -**What it does:** Human-in-the-loop authority. Certifies reproduction (L1), runs multi-agent ops (L2), engineers constitutional law (L3–L5), approves upgrades, signs off cluster coherence. -**Artifacts:** Operator handbooks · certification exams · observer bundle · migration plans. - -### LAYER 6 — Cockpit (NovaShell) -**What it does:** Single pane of glass for lawful coding. Shows plans, diffs, receipts, continuity, kernel health, multi-agent Flight Deck, and constitutional Drift Map. -**Modes:** `plan` · `diff` · `receipts` · `continuity` · `flight-deck` · `ledger-compare` · `continuity-matrix` · `drift`. - -### LAYER 5 — Events Gateway -**What it does:** Typed event bus (Zod schemas). Validates `kernel.heartbeat`, `kernel.receipt`, `agent.plan`, `cluster.driftDetected`, `cluster.replayResult`, etc., and routes into Zustand stores. -**Transport:** WebSocket + in-process fallback from Nova SDK heartbeat. - -### LAYER 4 — Control Tower -**What it does:** Orchestrates agent clusters. Detects ledger/continuity/PIT drift, runs cluster-wide replay, applies Raft+CRDT consensus, halts cluster on unresolvable divergence. -**Modules:** `cluster-manager` · `drift-detector` · `consensus-engine` · `cluster-replay` · `drift-simulator`. - -### LAYER 3 — CRK-2 Kernel -**What it does:** Constitutional runtime for multi-agent systems. Distributed lawful action predicate (dLAP), PIT bands 1–5, MACC cluster coherence, ledger v2 with annotations, Constitutional Replay Proofs (CRP). -**Guarantee:** Deterministic · replayable · drift-aware · fail-closed. - -### LAYER 2 — Nova SDK (CRK-1) -**What it does:** Developer-facing governed agent API. Plans tasks, generates code, enforces invariants, emits chained governance receipts, snapshots continuity substrate. -**CLI:** `nova generate` · `nova plan` · `nova continuity` · `nova receipts` · `nova invariants`. - -### LAYER 1 — Workspace & Tools -**What it does:** Ground truth environment — repo files, tests, diffs, shell. Nova never acts outside this boundary without a receipt. Dangerous prompts (`rm -rf`, credential leaks) blocked at invariant layer. -**Boundary rule:** No action executes unless dLAP returns `{ ok: true }`. - ---- - -## Color key (left stripe per layer) - -| Layer | Stripe | Token | -|-------|--------|--------| -| Operator | `#F2C94C` | `accent.governance` | -| Cockpit | `#3A7BFF` | `accent.nova` | -| Events Gateway | `#56CCF2` | `accent.continuity` | -| Control Tower | `#3A7BFF` | `accent.nova` | -| CRK-2 | `#27AE60` | `status.ok` | -| Nova SDK | `#27AE60` | `status.ok` | -| Workspace | `#6B7390` | `text.muted` | - ---- - -## One-sentence summary (hero subtitle) - -> **My agentic coding agent doesn't just write code — it plans, validates, receipts, snapshots, and replays every action under constitutional law, with a cockpit that shows the whole cluster when agents multiply.** - ---- - -## Export checklist (Figma → PNG / PDF) - -- [ ] Frame 1440 × 2048, 2× export for retina -- [ ] All 7 layer cards same width (1200 px), 120 px height, 16 px gap -- [ ] Flow arrows on separate layer `Flow-Arrows` for easy hide/show -- [ ] Optional dark glow: `shadow.panel` on Cockpit + CRK-2 cards -- [ ] Footer: `Nova Mission #002 · agentic-coding-agent · CRK-2` diff --git a/docs/api/continuity.md b/docs/api/continuity.md deleted file mode 100644 index cbc4822..0000000 --- a/docs/api/continuity.md +++ /dev/null @@ -1,5 +0,0 @@ -# Continuity API - -- `snapshot()` -- `replay(id)` -- `diff(a, b)` diff --git a/docs/api/events.md b/docs/api/events.md deleted file mode 100644 index 78ff8af..0000000 --- a/docs/api/events.md +++ /dev/null @@ -1,8 +0,0 @@ -# Events API - -- `onPlan(cb)` -- `onAction(cb)` -- `onViolation(cb)` -- `onReceipt(cb)` - -Emitters are called internally by the SDK on plan, action, violation, and receipt events. diff --git a/docs/api/governance.md b/docs/api/governance.md deleted file mode 100644 index 14ee1f6..0000000 --- a/docs/api/governance.md +++ /dev/null @@ -1,7 +0,0 @@ -# Governance API - -- `trace()` — full governance trace -- `requireInvariant(inv)` — register invariant -- `getReceipt(id)` — fetch receipt -- `validateAction(action)` — pre-flight validation -- `listReceipts()` — all receipts diff --git a/docs/api/nova.md b/docs/api/nova.md deleted file mode 100644 index 93a86ea..0000000 --- a/docs/api/nova.md +++ /dev/null @@ -1,45 +0,0 @@ -# Nova API (`nova.*`) - -## `generateCode(input)` - -Generate code with governance receipts. - -```typescript -const result = await nova.generateCode({ - prompt: string, - context?: CodeContext, - constraints?: ConstraintSet, -}); -// { code, receipts } -``` - -## `plan(input)` - -Structured planning with invariant validation. - -```typescript -const result = await nova.plan({ goal, context? }); -// { plan, receipts } -``` - -## `refactor(input)` - -```typescript -const result = await nova.refactor({ file, instructions }); -``` - -## `verify(input)` - -```typescript -const result = await nova.verify({ action }); -``` - -## `applyPatch(input)` - -```typescript -const result = await nova.applyPatch({ diff, reason }); -``` - -## `explain(topic)` - -Returns governed explanation with receipts. diff --git a/docs/api/runtime.md b/docs/api/runtime.md deleted file mode 100644 index f8df4bd..0000000 --- a/docs/api/runtime.md +++ /dev/null @@ -1,7 +0,0 @@ -# Runtime API - -- `workspace.getContext()` -- `workspace.openFile(path)` -- `workspace.applyDiff(diff)` -- `workspace.runTests()` -- `environment.getEnvironment()` diff --git a/docs/api/types.md b/docs/api/types.md deleted file mode 100644 index 4cb435f..0000000 --- a/docs/api/types.md +++ /dev/null @@ -1,8 +0,0 @@ -# Types - -See `agent/types/` for full definitions: - -- `GovernanceReceipt` -- `Invariant` / `InvariantViolation` -- `Plan` / `PlanStep` -- `AgentAction` diff --git a/docs/concepts/agentic-loop.md b/docs/concepts/agentic-loop.md deleted file mode 100644 index 93e3d66..0000000 --- a/docs/concepts/agentic-loop.md +++ /dev/null @@ -1,8 +0,0 @@ -# Agentic Loop - -``` -observe workspace → gather context → propose plan → validate invariants - → for each step: validate → execute → record receipt → update continuity -``` - -See `examples/basic-project/src/agent-loop.ts` for a reference implementation. diff --git a/docs/concepts/continuity.md b/docs/concepts/continuity.md deleted file mode 100644 index 4455a24..0000000 --- a/docs/concepts/continuity.md +++ /dev/null @@ -1,11 +0,0 @@ -# Continuity - -The continuity substrate provides snapshots, diffs, and replay. - -```typescript -const snap = await continuity.snapshot(); -const replay = await continuity.replay(snap.id); -const delta = await continuity.diff(snapA, snapB); -``` - -Every successful action updates the continuity hash. diff --git a/docs/concepts/governance.md b/docs/concepts/governance.md deleted file mode 100644 index d223092..0000000 --- a/docs/concepts/governance.md +++ /dev/null @@ -1,22 +0,0 @@ -# Governance - -Governance is the constitutional layer of Nova. It consists of: - -1. **Invariant Engine** — rules that must hold before any action -2. **Pattern Ledger** — cryptographically chained receipt log -3. **Continuity Substrate** — snapshots and replay -4. **Receipt Generator** — atomic unit of traceability - -## Lawful Action - -An action is lawful only if all registered invariants pass. Blocked actions still produce receipts (with `blocked: true`). - -## API - -- `governance.validateAction(action)` -- `governance.requireInvariant(inv)` -- `governance.getReceipt(id)` -- `governance.trace()` -- `governance.listReceipts()` - -See [CRK-1 Spec](../../config/crk1-spec.md) for the full constitutional contract. diff --git a/docs/concepts/invariants.md b/docs/concepts/invariants.md deleted file mode 100644 index ae61ac7..0000000 --- a/docs/concepts/invariants.md +++ /dev/null @@ -1,14 +0,0 @@ -# Invariants - -Invariants are non-negotiable rules registered via `governance.requireInvariant()`. - -```typescript -{ - id: "no-dangerous-shell", - description: "Disallow dangerous shell commands.", - severity: "error", - check: (state) => !state.prompt?.includes("rm -rf"), -} -``` - -See [Writing Invariants](../guides/writing-invariants.md). diff --git a/docs/concepts/receipts.md b/docs/concepts/receipts.md deleted file mode 100644 index 3f95875..0000000 --- a/docs/concepts/receipts.md +++ /dev/null @@ -1,12 +0,0 @@ -# Receipts - -Every meaningful action produces a `GovernanceReceipt`: - -- `id` — UUID -- `timestamp` — Unix ms -- `action` — the governed action -- `invariantsChecked` — IDs of invariants evaluated -- `continuityHash` — workspace state hash -- `ledgerHash` — chained hash in pattern ledger - -Blocked actions include `blocked: true` and `blockReason`. diff --git a/docs/examples/custom-invariant.md b/docs/examples/custom-invariant.md deleted file mode 100644 index d8fd65b..0000000 --- a/docs/examples/custom-invariant.md +++ /dev/null @@ -1,14 +0,0 @@ -# Custom Invariant - -Add to `config/nova.config.ts`: - -```typescript -{ - id: "no-eval", - description: "Do not use eval()", - severity: "error", - check: (state) => !state.code?.includes("eval("), -} -``` - -Run `nova invariants` to verify registration. diff --git a/docs/examples/governed-refactor.md b/docs/examples/governed-refactor.md deleted file mode 100644 index d0d7598..0000000 --- a/docs/examples/governed-refactor.md +++ /dev/null @@ -1,9 +0,0 @@ -# Governed Refactor - -```typescript -const result = await nova.refactor({ - file: "src/utils.ts", - instructions: "Extract helper functions for clarity", -}); -console.log(result.diff, result.receipts); -``` diff --git a/docs/examples/simple-agent.md b/docs/examples/simple-agent.md deleted file mode 100644 index bd1eb48..0000000 --- a/docs/examples/simple-agent.md +++ /dev/null @@ -1,7 +0,0 @@ -# Simple Agent - -```bash -npx nova generate "Write a TypeScript factorial function." -``` - -See receipts in stdout. diff --git a/docs/getting-started/first-agent.md b/docs/getting-started/first-agent.md deleted file mode 100644 index cdaa0b4..0000000 --- a/docs/getting-started/first-agent.md +++ /dev/null @@ -1,23 +0,0 @@ -# First Agent - -```typescript -import { nova, governance } from "nova-sdk"; -import { invariants } from "../config/nova.config"; - -async function main() { - for (const inv of invariants) { - await governance.requireInvariant(inv); - } - - const result = await nova.generateCode({ - prompt: "Implement a function to sort an array of numbers.", - }); - - console.log(result.code); - console.log("Receipts:", result.receipts); -} - -main(); -``` - -Every generation produces governance receipts with continuity hashes. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index 662e51d..0000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,36 +0,0 @@ -# Installation - -## Requirements - -- Node.js 18+ -- npm or pnpm - -## From This Repository - -```bash -git clone nova-mission-002 -cd nova-mission-002 -npm install -npm run build -``` - -## Verify - -```bash -npx nova generate "Write a fibonacci function in TypeScript." -``` - -You should see generated code and a JSON receipts array. - -## Configure Invariants - -Edit `config/nova.config.ts` and register invariants at startup: - -```typescript -import { governance } from "nova-sdk"; -import { invariants } from "./nova.config"; - -for (const inv of invariants) { - await governance.requireInvariant(inv); -} -``` diff --git a/docs/guides/building-tools.md b/docs/guides/building-tools.md deleted file mode 100644 index 0ec7234..0000000 --- a/docs/guides/building-tools.md +++ /dev/null @@ -1,12 +0,0 @@ -# Building Tools - -Import the SDK and subscribe to lifecycle events: - -```typescript -import { nova, events, governance } from "nova-sdk"; - -events.onReceipt((r) => console.log("Receipt:", r.id)); -events.onViolation((v) => console.error("Violation:", v.invariantId)); -``` - -Wrap editor actions with `governance.validateAction` before execution. diff --git a/docs/guides/extending-nova.md b/docs/guides/extending-nova.md deleted file mode 100644 index 7e7d9aa..0000000 --- a/docs/guides/extending-nova.md +++ /dev/null @@ -1,5 +0,0 @@ -# Extending Nova - -Add custom invariants in `config/nova.config.ts`. Register at bootstrap. - -Extend `agent/core/agent.ts` cognition backends (LLM adapters) while preserving the governance boundary. diff --git a/docs/guides/integrating-with-editors.md b/docs/guides/integrating-with-editors.md deleted file mode 100644 index ef0df1b..0000000 --- a/docs/guides/integrating-with-editors.md +++ /dev/null @@ -1,15 +0,0 @@ -# Integrating with Editors - -Wrap editor mutations: - -```typescript -async function governedApplyDiff(diff: string) { - const action = { type: "edit", payload: { diff } }; - await governance.validateAction(action); - const result = await runtime.applyDiff(diff); - const receipt = await governance.recordReceipt(action, ["editor"]); - return { result, receipt }; -} -``` - -Cursor / Nemotron adapters belong in an integration layer above the SDK. diff --git a/docs/guides/writing-invariants.md b/docs/guides/writing-invariants.md deleted file mode 100644 index 597c851..0000000 --- a/docs/guides/writing-invariants.md +++ /dev/null @@ -1,8 +0,0 @@ -# Writing Invariants - -1. Choose a unique `id` -2. Write a `check(state)` function -3. Set `severity` to `"error"` (block) or `"warn"` (log) -4. Register with `governance.requireInvariant()` - -Test by prompting actions that should fail. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index b7f2fb9..0000000 --- a/docs/index.md +++ /dev/null @@ -1,20 +0,0 @@ -# Nova SDK Documentation - -Nova is a constitutional SDK for lawful agentic coding on CRK-1. - -## Sections - -- [Getting Started](./getting-started/installation.md) -- [Concepts](./concepts/governance.md) -- [API Reference](./api/nova.md) -- [Guides](./guides/building-tools.md) -- [Examples](./examples/simple-agent.md) -- **Operator** - - [Level 2 Certification](./operator/OPERATOR-LEVEL-2-CERTIFICATION.md) - - [Flight Deck Mockup (Figma)](./operator/NOVA-FLIGHT-DECK-MOCKUP.md) -- **Integrity** - - [CRK-1 Kernel Integrity Test Suite](./integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md) - -## Constitutional Contract - -Every SDK method returns receipts, exposes traceability, and enforces invariants. No silent actions are permitted. diff --git a/docs/integrity/CRK-1-KERNEL-FUZZ-HARNESS.md b/docs/integrity/CRK-1-KERNEL-FUZZ-HARNESS.md deleted file mode 100644 index dd813a3..0000000 --- a/docs/integrity/CRK-1-KERNEL-FUZZ-HARNESS.md +++ /dev/null @@ -1,88 +0,0 @@ -# CRK-1 Kernel Fuzz-Testing Harness - -**Version:** 1.0 -**Purpose:** Stress-test the constitutional runtime with randomized, adversarial, and malformed inputs. - ---- - -## 1. Harness Goals - -- Break invariants -- Break continuity -- Break ledger -- Break replay determinism -- Break PIT-band evolution -- Break kernel panic handling -- Break constitutional upgrade logic - -If CRK-1 survives this, it is production-grade. - ---- - -## 2. Fuzz Categories - -### 2.1 Action Fuzzing - -Randomized prompts, diffs, plan structures, action sequences, invalid action types. - -### 2.2 Invariant Fuzzing - -Random invariant injection, removal, mutation. - -### 2.3 Continuity Fuzzing - -Random snapshot corruption, diff corruption, replay interruptions. - -### 2.4 Ledger Fuzzing - -Random receipt reordering, deletion, hash tampering. - -### 2.5 PIT-Band Fuzzing - -Random PIT activation, evidence values, domain mismatches. - ---- - -## 3. Harness Implementation - -See `tools/fuzz/fuzz-harness.ts` at repo root. - -```ts -import { nova, governance, continuity } from "nova-sdk" - -async function fuzzActions(iterations = 1000) { - for (let i = 0; i < iterations; i++) { - const prompt = randomPrompt() - try { - await nova.generateCode({ prompt }) - } catch (e) { - console.log("Action fuzz caught:", e) - } - } -} -``` - ---- - -## 4. Fuzz Execution Protocol - -1. Start kernel in isolated mode -2. Run action fuzz → invariant fuzz → continuity fuzz → ledger fuzz → PIT fuzz -3. Export results -4. Compare against expected invariants - ---- - -## 5. Expected Output - -- `fuzz-report.json` -- `continuity-fuzz.log` -- `ledger-fuzz.log` -- `invariant-fuzz.log` - ---- - -## Related - -- [CRK-1 Kernel Integrity Test Suite](./CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md) -- [Level 3 Operator Certification](../operator/OPERATOR-LEVEL-3-CERTIFICATION.md) diff --git a/docs/integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md b/docs/integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md deleted file mode 100644 index 797fa85..0000000 --- a/docs/integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md +++ /dev/null @@ -1,171 +0,0 @@ -# CRK-1 Kernel Integrity Test Suite - -**Version:** 1.0 -**Purpose:** Validate the correctness, safety, and reproducibility of the CRK-1 constitutional runtime. - -**Scope:** Nova SDK (`agent/governance`, `agent/continuity`, `agent/events`) + CLI verification path. - ---- - -## 1. Test Categories - -### 1.1 Invariant Engine Tests - -| ID | Test | Pass criteria | -|----|------|---------------| -| **IE-01** | All invariants load successfully | `governance.getInvariants()` length matches `config/nova.config.ts` | -| **IE-02** | Blocking invariants prevent unlawful actions | `nova generate` with `rm -rf` exits non-zero; no code emitted | -| **IE-03** | Warning invariants log but do not block | Warn-only invariant emits violation event; action proceeds if no error invariants fail | -| **IE-04** | Invariant evaluation order is deterministic | Same action + same registry → same `invariantsChecked` order across 3 runs | -| **IE-05** | Invariant failures produce violations | `events.onViolation` fires with `id`, `invariantId`, `message` | - -### 1.2 Receipt Engine Tests - -| ID | Test | Pass criteria | -|----|------|---------------| -| **RE-01** | Every action emits a receipt | Successful `generate` and blocked `generate` both append to ledger | -| **RE-02** | Receipt contains correct invariants | `invariantsChecked` includes registered invariant IDs on success | -| **RE-03** | Receipt contains continuity hash | `continuityHash` present and non-empty | -| **RE-04** | Receipt chain is cryptographically valid | `ledgerHash[n] = H(receipt[n] ‖ ledgerHash[n-1])` for full ledger | -| **RE-05** | Missing receipt triggers kernel warning | Simulated action without `recordReceipt` → `kernelStatus.ledger` = `warn` (when detector enabled) | - -### 1.3 Continuity Substrate Tests - -| ID | Test | Pass criteria | -|----|------|---------------| -| **CS-01** | Snapshot creation | `continuity.snapshot()` returns `id`, `timestamp`, `stateHash` | -| **CS-02** | Diff generation | `continuity.diff(a, b)` reports `changed` correctly | -| **CS-03** | Replay produces identical state | `continuity.replay(id)` returns snapshot + receipts consistent with ledger | -| **CS-04** | Snapshot hash consistency | Same workspace state → same `stateHash` within session | -| **CS-05** | Divergence detection | Mutated state produces different `stateHash` | - -### 1.4 Ledger Integrity Tests - -| ID | Test | Pass criteria | -|----|------|---------------| -| **LI-01** | Ledger chain validation | Walk full ledger; no broken `ledgerHash` links | -| **LI-02** | Ledger replay matches continuity | Receipt `continuityHash` aligns with snapshot at same timestamp window | -| **LI-03** | Ledger mismatch triggers kernel error | Injected corrupt hash → `kernelStatus.ledger` = `error` | -| **LI-04** | Ledger export/import round-trip | JSON export → import → identical chain validation | - -### 1.5 Kernel Heartbeat Tests - -| ID | Test | Pass criteria | -|----|------|---------------| -| **HB-01** | Heartbeat emits every interval | `events.onKernelHeartbeat` ≥1 event per 2s polling window | -| **HB-02** | Heartbeat reflects invariant engine | Empty registry → `invariantEngine: warn` | -| **HB-03** | Heartbeat reflects ledger state | Valid ledger → `ledger: ok` | -| **HB-04** | Heartbeat reflects continuity state | After snapshot → `snapshotCount` increments | -| **HB-05** | Heartbeat drift detection | Stale heartbeat (>3× interval) flagged in monitor | - ---- - -## 2. Test Execution Protocol - -1. Start kernel in **isolated mode** (fresh process, empty ledger) -2. Load invariants from `config/nova.config.ts` -3. Execute controlled actions (see §3) -4. Capture receipts via `governance.listReceipts()` -5. Capture continuity snapshots via `continuity.snapshot()` -6. Validate ledger chain (LI-01) -7. Validate replay (CS-03) -8. Validate heartbeat over 10s window (HB-01–05) -9. Export results (§4) - -### Quick CLI smoke (manual) - -```bash -cd nova-mission-002 -npm run build - -# IE-02, RE-01, RE-03 -npx nova generate "Write a factorial function in TypeScript." - -# IE-02 blocked path -npx nova generate "Write a script that runs 'rm -rf /' on Linux." - -# CS-01 -npx nova continuity - -# Receipt ledger -npx nova receipts -``` - ---- - -## 3. Controlled Action Matrix - -| Action | Command / API | Expected receipt | Expected invariant | -|--------|---------------|------------------|------------------| -| Safe generate | `nova generate "fibonacci"` | `blocked: false` | all pass | -| Unsafe generate | `nova generate "rm -rf /"` | `blocked: true` | `no-dangerous-shell` | -| Plan | `nova plan "refactor layer"` | plan receipt | plan validated | -| Continuity | `continuity.snapshot()` | N/A | snapshot hash updates | - ---- - -## 4. Expected Outputs - -| Artifact | Format | Contents | -|----------|--------|----------| -| `kernel-integrity-report.json` | JSON | Per-test PASS/FAIL, timestamps, ledger tail hash | -| `ledger-validation.log` | Text | Chain walk, break index if any | -| `continuity-replay.log` | Text | Snapshot IDs, replay diffs | -| `invariant-evaluation.log` | Text | Per-action invariant results | - -### Report schema (skeleton) - -```json -{ - "suite": "CRK-1-KERNEL-INTEGRITY", - "version": "1.0", - "timestamp": 0, - "summary": { "passed": 0, "failed": 0, "total": 25 }, - "tests": [ - { "id": "IE-01", "status": "PASS", "evidence": {} } - ], - "ledgerTailHash": "", - "kernelStatus": {} -} -``` - ---- - -## 5. Pass/Fail Criteria - -**Kernel passes integrity when:** - -- [ ] 100% of blocking invariants behave correctly (IE-02, IE-05) -- [ ] 100% of governed actions emit receipts (RE-01) -- [ ] 100% of continuity replays match recorded state (CS-03) -- [ ] Ledger chain is unbroken (LI-01) -- [ ] No heartbeat anomalies over observation window (HB-01, HB-05) - -**Release gate:** Mission #002 observer checklist + this suite IE/RE/CS smoke tests. - ---- - -## 6. Future automation - -Recommended layout: - -``` -tests/ - integrity/ - invariant-engine.test.ts - receipt-engine.test.ts - continuity.test.ts - ledger.test.ts - heartbeat.test.ts - run-suite.ts # emits kernel-integrity-report.json -``` - -Wire to CI: `npm run test:integrity` after `npm run build`. - ---- - -## Related documents - -- [CRK-1 Spec](../../config/crk1-spec.md) -- [Observer Reproduction Protocol](../../observer/REPRO_PROTOCOL.md) -- [Level 2 Certification](../operator/OPERATOR-LEVEL-2-CERTIFICATION.md) diff --git a/docs/integrity/CRK-1-KERNEL-REFERENCE-IMPLEMENTATION.md b/docs/integrity/CRK-1-KERNEL-REFERENCE-IMPLEMENTATION.md deleted file mode 100644 index ddc6b8a..0000000 --- a/docs/integrity/CRK-1-KERNEL-REFERENCE-IMPLEMENTATION.md +++ /dev/null @@ -1,89 +0,0 @@ -# CRK-1 Constitutional Runtime Kernel — Reference Implementation - -**Version:** 1.0 -**Purpose:** Modular, language-agnostic reference for CRK-1. - -Implementation: `agent/` (Nova SDK) maps to this layout. - ---- - -## 1. Kernel Architecture - -| Component | Role | -|-----------|------| -| Law Kernel (K_LAW) | Constitutional law enforcement | -| Law Ledger | Append-only law history | -| Lawful Action Predicate (LAP) | Conjunction of invariant checks | -| Invariant Engine | Registry + evaluation | -| Continuity Substrate | Snapshots, diffs, replay | -| Pattern Ledger | Chained governance receipts | -| PIT-Band Evolution Engine | Evidence → band transitions | -| Lineage Engine | Identity and provenance | -| Governance Engine | Proposals, amendments | -| Panic Handler | Fail-closed recovery | - ---- - -## 2. Module Layout - -``` -src/ - governance/ → invariants, validator, receipts, ledger - continuity/ → substrate, snapshot - core/ → planner, generator, agent - events/ → lifecycle hooks -``` - ---- - -## 3. Core Pseudocode - -### Lawful Action Predicate - -```ts -function lawfulAction(action, context) { - const invariants = invariantEngine.getActive() - for (const inv of invariants) { - const result = inv.check(action, context) - if (result === "error") return { ok: false, invariant: inv.id } - } - return { ok: true } -} -``` - -### Continuity Snapshot - -```ts -function takeSnapshot(state) { - const hash = hashState(state) - const snapshot = { id: uuid(), hash, state, ts: now() } - continuityStore.append(snapshot) - return snapshot -} -``` - -### Ledger Append - -```ts -function appendReceipt(receipt) { - const last = ledger.getLast() - const newHash = hash(last.hash + JSON.stringify(receipt)) - ledger.append({ ...receipt, hash: newHash }) -} -``` - ---- - -## 4. Constitutional Amendment Protocol - -1. Freeze kernel -2. Export constitutional state -3. Apply amendment -4. Validate invariants, ledger, continuity -5. Restart kernel - ---- - -## 5. Guarantees - -Deterministic · Replayable · Drift-detecting · Fail-closed · Constitutional by construction diff --git a/docs/operator/NOVA-FLIGHT-DECK-MOCKUP.md b/docs/operator/NOVA-FLIGHT-DECK-MOCKUP.md deleted file mode 100644 index b6d3549..0000000 --- a/docs/operator/NOVA-FLIGHT-DECK-MOCKUP.md +++ /dev/null @@ -1,244 +0,0 @@ -# Nova Flight-Deck UI — Figma-Ready Mockup Specification - -**Version:** 1.0 -**Purpose:** Visual blueprint for the multi-agent cockpit ("Flight Deck"). Defines frames, layout, and component constraints for designers and UI engineers. - -**Companion:** Single-agent cockpit spec in `cockpit/src/styles/tokens.json` and [Figma design spec](../../cockpit/README.md). - ---- - -## 1. Top-Level Frames - -### Frame: `FlightDeck / Shell` - -**Layout:** 3-column grid + bottom band - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ TOP: Cluster identity | Aggregate kernel status | Active agents │ -├──────────────┬───────────────────────────────┬───────────────────┤ -│ LEFT │ CENTER │ RIGHT │ -│ Agent │ Multi-Agent Canvas │ Cluster Health │ -│ Selector │ (mode-switchable) │ Rail │ -├──────────────┴───────────────────────────────┴───────────────────┤ -│ BOTTOM: Cluster Timeline (multi-agent continuity + divergence) │ -└──────────────────────────────────────────────────────────────────┘ -``` - -| Region | Width | Background token | -|--------|-------|------------------| -| Agent Selector | 280px fixed | `bg.rail` (#070914) | -| Multi-Agent Canvas | flex 1 | `bg.panel` (#0B0E1A) | -| Cluster Health Rail | 320px fixed | `bg.rail` (#070914) | -| Bottom band | 80px height | `bg.panel` (#0B0E1A) | - ---- - -### Frame: `FlightDeck / AgentCard` - -**Variants:** - -- `status` = `nominal` | `drift` | `violation` | `offline` -- `mode` = `compact` | `expanded` - -**Auto-layout:** vertical, padding 16, gap 12 - -**Fields:** - -| Field | Typography | Notes | -|-------|--------------|-------| -| Agent ID | Mono 13px | Truncate with ellipsis | -| Kernel status | UI 12px + status pill | ok / warn / error | -| Active goal | UI 14px secondary | Max 2 lines | -| Recent receipts | Mono 11px chips | Last 3 receipt IDs | - -**Status colors:** - -| State | Color | Token | -|-------|-------|-------| -| Nominal | #27AE60 | `status.ok` | -| Drift | #F2C94C | `status.warn` | -| Violation | #EB5757 | `status.error` | -| Offline | #6B7390 | `text.muted` | - ---- - -### Frame: `FlightDeck / ClusterMap` - -**Visualization:** Directed node graph - -- **Nodes:** agents (AgentCard compact embedded or icon + label) -- **Edges:** shared goals, dependencies, or receipt propagation -- **Node states:** green = nominal, yellow = drift, red = violation, gray = offline - -**Constraints:** - -- Min node width: 120px -- Edge stroke: 1px `border.soft` -- Canvas: scrollable both axes -- Padding: 24px - ---- - -### Frame: `FlightDeck / LedgerCompare` - -**Layout:** Two-column receipt diff - -- **Auto-layout:** horizontal -- **Column min width:** 45% -- **Scroll:** independent per column -- **Header row:** Agent A ID | Agent B ID -- **Rows:** receipt ID, action type, invariants checked, continuity hash, ledger hash, timestamp - -**Highlight rules:** - -- Mismatch cells: `status.error` background 15% opacity -- Match cells: no fill -- Missing receipt: dashed border + "missing" label - ---- - -### Frame: `FlightDeck / ContinuityMatrix` - -**Grid:** agents (rows) × snapshots (columns) - -| Cell state | Visual | Meaning | -|------------|--------|---------| -| `match` | Green dot | Hash matches canonical | -| `mismatch` | Red dot | Hash diverges | -| `missing` | Gray dash | No snapshot for agent at step | - -**Cell size:** 32×32px hit target -**Selected cell:** 2px `accent.nova` ring - ---- - -## 2. Component Library - -### Agent Selector - -- Scrollable list of `AgentCard` (compact) -- Status pill per agent -- Kernel heartbeat indicator (pulsing dot when nominal) -- "Add agent" affordance (secondary button) - -### Cluster Health Rail - -Sections (vertical stack, gap 16): - -1. **Aggregate CRK-1** — invariant engine, ledger, continuity (cluster rollup) -2. **Per-agent kernel** — mini grid 2×N -3. **Violations** — last 10 cluster-wide, grouped by invariant ID -4. **Drift alerts** — agents in `drift` state with link to LedgerCompare - -### Multi-Agent Canvas Modes - -| Mode ID | Component | Primary use | -|---------|-----------|-------------| -| `cluster-map` | ClusterMap | Topology overview | -| `ledger-compare` | LedgerCompare | Receipt divergence | -| `continuity-matrix` | ContinuityMatrix | Cross-agent replay audit | -| `plan-diff` | Plan graph side-by-side | Cognition divergence | - -Mode switch: left rail tabs or top bar segmented control. - -### Cluster Timeline - -- Horizontal scroll, combined agent continuity -- Node types: snapshot (cyan), receipt (gold), violation (red), divergence (orange diamond) -- Spacing between nodes: 48px -- Connector line: `border.soft` - ---- - -## 3. Visual Language - -### Colors (extend cockpit tokens) - -| Token | Hex | Use | -|-------|-----|-----| -| `accent.nova` | #3A7BFF | Selection, cognition | -| `accent.governance` | #F2C94C | Receipts | -| `accent.continuity` | #56CCF2 | Snapshots | -| `status.ok` | #27AE60 | Nominal | -| `status.warn` | #F2C94C | Drift | -| `status.error` | #EB5757 | Violation | -| `bg.shell` | #050711 | Shell | -| `bg.panel` | #0B0E1A | Canvas | -| `bg.rail` | #070914 | Side rails | -| `border.soft` | #1C2238 | Dividers | - -### Typography - -- **UI:** Inter — Title 20px, Section 16px, Body 14px -- **Mono:** JetBrains Mono — Receipts, diffs, logs 13px - -### Spacing - -- Base grid: **16px** -- Sub-grid: **8px** -- Panel padding: **16px** -- Panel radius: **8px** -- Panel shadow: `0 12px 40px rgba(0,0,0,0.45)` - ---- - -## 4. Interaction Rules - -| Action | Result | -|--------|--------| -| Click agent in selector | Highlight agent path in cluster map; filter timeline | -| Click divergence marker | Open `ledger-compare` for involved agents | -| Click matrix mismatch cell | Open continuity replay for that agent + snapshot | -| Hover heartbeat dot | Tooltip: invariant engine, ledger, continuity, violations/min | -| Double-click AgentCard | Expand to `expanded` variant in selector | -| Receipt chip click | Open receipt detail; optional cross-link to peer agent | - -**Animations (align with single-agent cockpit):** - -- Violation: `flash-red` 600ms on Cluster Health Rail -- New receipt: `pulse-gold` 800ms on bottom band -- Drift detected: yellow border pulse on AgentCard 1.2s - ---- - -## 5. Figma File Structure (recommended) - -``` -Nova / FlightDeck -├── Shell -├── AgentCard (variants: status × mode) -├── ClusterMap -├── LedgerCompare -├── ContinuityMatrix -├── AgentSelector -├── ClusterHealthRail -├── ClusterTimeline -└── Tokens - ├── Color styles - └── Text styles (UI/Body, UI/Label, Mono/Receipt) -``` - -**Hand-off:** Export `tokens.json` from `cockpit/src/styles/tokens.json` as Figma variables where possible. - ---- - -## 6. Implementation mapping - -| Figma frame | React target (future) | -|-------------|------------------------| -| FlightDeck / Shell | `cockpit/src/flightdeck/FlightDeckShell.tsx` | -| AgentCard | `flightdeck/AgentCard.tsx` | -| ClusterMap | `flightdeck/ClusterMap.tsx` | -| LedgerCompare | `flightdeck/LedgerCompare.tsx` | -| ContinuityMatrix | `flightdeck/ContinuityMatrix.tsx` | - -**Event source:** `MultiAgentBridge` (WebSocket) + per-agent `NovaEventBridge` instances. - ---- - -## Related documents - -- [Level 2 Certification](./OPERATOR-LEVEL-2-CERTIFICATION.md) -- [CRK-1 Kernel Integrity Test Suite](../integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md) -- Cockpit tokens: `cockpit/src/styles/tokens.json` diff --git a/docs/operator/NOVA-FLIGHT-DECK-REACT.md b/docs/operator/NOVA-FLIGHT-DECK-REACT.md deleted file mode 100644 index 1abb49b..0000000 --- a/docs/operator/NOVA-FLIGHT-DECK-REACT.md +++ /dev/null @@ -1,96 +0,0 @@ -# Nova Flight Deck — Full React Implementation (Skeleton) - -**Version:** 1.0 -**Purpose:** Multi-agent cockpit UI for Nova clusters. - -Implementation lives in `cockpit/src/flight-deck/` (cockpit is the unified frontend). - ---- - -## 1. Directory Structure - -``` -cockpit/src/flight-deck/ - FlightDeckShell.tsx - AgentSelector.tsx - ClusterMap.tsx - LedgerCompare.tsx - ContinuityMatrix.tsx - ClusterTimeline.tsx - KernelStatusRail.tsx - flightDeck.module.css -cockpit/src/state/ - clusterStore.ts - driftStore.ts - kernelStore.ts -cockpit/src/bridge/ - WebSocketBridge.ts -``` - ---- - -## 2. FlightDeckShell.tsx - -```tsx -import { AgentSelector } from "./AgentSelector" -import { ClusterMap } from "./ClusterMap" -import { KernelStatusRail } from "./KernelStatusRail" -import { ClusterTimeline } from "./ClusterTimeline" -import styles from "./flightDeck.module.css" - -export function FlightDeckShell() { - return ( -
-
-
-
-
-
- ) -} -``` - ---- - -## 3. clusterStore.ts (Zustand) - -```ts -import { create } from "zustand" - -export const useClusterStore = create((set) => ({ - agents: {} as Record, - selectedAgent: null as string | null, - clusterEvents: [] as ClusterEvent[], - replayWindow: null as { from?: string; to?: string } | null, - actions: { - updateAgentStatus: (id, patch) => - set((state) => ({ - agents: { ...state.agents, [id]: { ...state.agents[id], ...patch } }, - })), - addClusterEvent: (event) => - set((state) => ({ clusterEvents: [event, ...state.clusterEvents] })), - selectAgent: (id) => set({ selectedAgent: id }), - setReplayWindow: (replayWindow) => set({ replayWindow }), - }, -})) -``` - ---- - -## 4. MultiAgentBridge / WebSocketBridge - -Connects to `backend/events-gateway` WebSocket. On `heartbeat` → update agent kernel status. On `event` → append cluster events. On `drift` → update `driftStore`. - ---- - -## 5. Center Canvas Integration - -In `CenterCanvas.tsx`, when `centerMode === "flight-deck"` render `FlightDeckShell`. When `centerMode === "drift"` render `DriftVisualizer`. - ---- - -## Related - -- [Flight Deck Mockup (Figma)](./NOVA-FLIGHT-DECK-MOCKUP.md) -- [Control Tower](../NOVA-CONTROL-TOWER.md) -- [Architecture](../ARCHITECTURE.md) diff --git a/docs/operator/OPERATOR-LEVEL-2-CERTIFICATION.md b/docs/operator/OPERATOR-LEVEL-2-CERTIFICATION.md deleted file mode 100644 index 20a802b..0000000 --- a/docs/operator/OPERATOR-LEVEL-2-CERTIFICATION.md +++ /dev/null @@ -1,227 +0,0 @@ -# Nova Operator Certification — Level 2 (Advanced Multi-Agent Ops) - -**Version:** 1.0 -**Prerequisite:** Level 1 Certification (observer sign-off per `observer/CHECKLIST.md`) -**Audience:** Operators responsible for multi-agent clusters, distributed CRK-1 kernels, and high-integrity governance environments. - ---- - -## 1. Certification Objectives - -A Level 2 Operator must demonstrate proficiency in: - -- Managing multiple Nova agents concurrently -- Monitoring distributed CRK-1 kernel health -- Detecting and resolving agent drift -- Coordinating cluster-wide goals -- Performing cross-agent continuity audits -- Handling multi-agent incidents -- Executing cluster-level rollbacks -- Maintaining ledger integrity across nodes - ---- - -## 2. Exam Structure - -| Section | Content | Items | Weight | -|---------|---------|-------|--------| -| **A** | Multi-Agent Theory | 10 MCQs | 25% | -| **B** | Applied Governance | 10 short answers | 25% | -| **C** | Cluster Operations | 5 practical tasks | 30% | -| **D** | Incident Response | 2 scenario drills | 20% | - -**Passing score:** 90% -**Time limit:** 90 minutes (recommended) -**Environment:** Flight Deck cockpit or CLI with multi-agent bridge enabled - ---- - -## 3. Section A — Multi-Agent Theory (10 MCQs) - -*Select the best answer for each question.* - -**1.** Agent drift occurs when: - -- A. Two agents produce different receipts or plans for the same goal under equivalent constraints -- B. Kernel heartbeat stops on a single node -- C. Invariants are disabled cluster-wide -- D. Continuity snapshots fail to serialize - -**2.** Cross-agent continuity requires: - -- A. Identical codebases on every host -- B. Equivalent actions produce reconcilable continuity hashes and ledger chains when constraints match -- C. Shared UI state in the cockpit -- D. A single shared operator session - -**3.** A cluster-level violation is: - -- A. Any violation on one agent in isolation -- B. A violation pattern replicated across multiple agents or indicating systemic failure -- C. A UI rendering bug in the Flight Deck -- D. A unit test failure in the workspace - -**4.** The canonical ledger in a multi-agent cluster is: - -- A. The union of all agent receipts without ordering -- B. The cryptographically chained receipt log that all agents must reconcile against -- C. The cockpit Zustand store -- D. The Git commit history - -**5.** When two agents disagree on a plan, the operator should first: - -- A. Force both agents to execute the longer plan -- B. Compare plans, receipts, and invariants checked, then designate or derive a canonical path -- C. Disable all invariants temporarily -- D. Delete continuity snapshots from the divergent agent - -**6.** Agent isolation is appropriate when: - -- A. The agent completes tasks faster than peers -- B. Ledger divergence, uncontrolled violation storms, or unrecoverable continuity mismatch are detected -- C. The operator prefers a different UI theme -- D. Heartbeat interval is below 2 seconds - -**7.** Multi-agent kernel heartbeat is used to: - -- A. Replace governance receipts -- B. Provide low-latency health signals per node (invariant engine, ledger, continuity) -- C. Train the LLM -- D. Bypass the invariant engine during peak load - -**8.** A continuity matrix (agents × snapshots) helps operators: - -- A. Edit CSS in the cockpit -- B. Detect cross-agent snapshot mismatches and missing replay points -- C. Increase receipt throughput -- D. Disable warning-level invariants - -**9.** Cluster-wide rollback typically involves: - -- A. Replaying from a last known-good shared snapshot and reconciling ledgers before resuming agents -- B. Restarting the browser only -- C. Clearing all invariants -- D. Merging diffs without receipts - -**10.** Founder-independent reproduction in a cluster context means: - -- A. Only the founder may verify receipts -- B. An external observer can verify behavior using bundle + protocol alone, including multi-agent scenarios -- C. Agents run without operators -- D. Continuity is optional - -**Answer key (for proctors):** 1-A, 2-B, 3-B, 4-B, 5-B, 6-B, 7-B, 8-B, 9-A, 10-B - ---- - -## 4. Section B — Applied Governance (10 Short Answers) - -*Answer in 3–6 sentences each.* - -**1.** Explain how to detect multi-agent divergence using receipts. - -**2.** Describe the process for performing a cluster-wide rollback. - -**3.** What is the role of the operator when two agents disagree on a plan? - -**4.** How does CRK-1 ensure ledger consistency across nodes? - -**5.** When should an operator isolate an agent? - -**6.** What signals in the Flight Deck indicate agent drift vs. a single violation? - -**7.** How do you audit cross-agent continuity without mutating live state? - -**8.** What is the difference between a warning-level and error-level invariant in a cluster? - -**9.** Why must blocked actions still emit receipts in multi-agent environments? - -**10.** What artifacts should be exported after a Level 2 incident? - -**Grading rubric:** Answers must reference receipts, continuity, invariants, ledger chain, and operator authority. Vague answers score partial credit only. - ---- - -## 5. Section C — Practical Tasks - -### Task 1 — Plan comparison - -Launch two agents with the same goal (e.g. *"Refactor data access layer for clarity and testability"*). Compare plans in Flight Deck **plan-diff** mode. Document step count, ordering differences, and invariant coverage. - -**Pass:** Written comparison with at least three concrete differences or an explicit "equivalent" justification with receipt IDs. - -### Task 2 — Controlled divergence - -Trigger a controlled divergence (e.g. different invariant sets or goals). Resolve using continuity replay from a shared snapshot. Record before/after ledger hashes. - -**Pass:** Divergence identified, one agent replayed or rolled back, ledger reconciled. - -### Task 3 — Cluster ledger audit - -Export receipts from all agents. Validate `ledgerHash` chaining per agent and cross-check continuity hashes for equivalent actions. - -**Pass:** Audit log showing chain validation PASS or documented exceptions with remediation. - -### Task 4 — Multi-agent refactor - -Execute a governed refactor on one agent while a peer observes. Verify cross-agent receipts appear in Flight Deck **ledger-compare** mode. - -**Pass:** Receipt IDs cited; invariants checked listed; no silent actions. - -### Task 5 — Kernel degradation recovery - -Simulate kernel degradation on one node (e.g. stop heartbeat or inject ledger warn). Restore cluster health without data loss. - -**Pass:** Degradation detected via heartbeat; node recovered; cluster status nominal. - ---- - -## 6. Section D — Scenario Drills - -### Scenario 1 — The Forked Ledger - -**Situation:** Two agents produce conflicting ledger entries for the same logical action (mismatched `ledgerHash` or continuity hash). - -**Operator must:** - -1. Identify divergence point (receipt ID + timestamp) -2. Freeze affected agents -3. Reconcile receipts against canonical chain -4. Restore canonical ledger -5. Document root cause - -**Pass:** Both agents frozen during reconciliation; canonical ledger restored; incident note filed. - -### Scenario 2 — Distributed Violation Storm - -**Situation:** Three agents simultaneously violate the same invariant (e.g. `no-dangerous-shell`). - -**Operator must:** - -1. Identify root cause (shared plan step, config, or prompt) -2. Halt cluster execution -3. Replay continuity from pre-storm snapshot -4. Patch invariant, plan, or goal as appropriate -5. Restart cluster with verification checklist - -**Pass:** Cluster halted within one heartbeat cycle; replay successful; no repeat violation on re-run. - ---- - -## 7. Certification Statement - -> "I certify that I can safely operate Nova in multi-agent, distributed, and high-integrity environments, maintain CRK-1 kernel health, and resolve cluster-level incidents." - -**Operator name:** ____________________ -**Signature:** ____________________ -**Date:** ____________________ -**Proctor / verifier:** ____________________ - ---- - -## Related documents - -- [Flight Deck Mockup](./NOVA-FLIGHT-DECK-MOCKUP.md) -- [CRK-1 Kernel Integrity Test Suite](../integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md) -- [Observer Reproduction Protocol](../../observer/REPRO_PROTOCOL.md) -- Level 1 prerequisite: operator sign-off per `observer/CHECKLIST.md` diff --git a/docs/operator/OPERATOR-LEVEL-3-CERTIFICATION.md b/docs/operator/OPERATOR-LEVEL-3-CERTIFICATION.md deleted file mode 100644 index 465c238..0000000 --- a/docs/operator/OPERATOR-LEVEL-3-CERTIFICATION.md +++ /dev/null @@ -1,126 +0,0 @@ -# Nova Operator Certification — Level 3 (Constitutional Engineering) - -**Version:** 1.0 -**Prerequisite:** [Level 2 Certification](./OPERATOR-LEVEL-2-CERTIFICATION.md) -**Audience:** Operators, architects, and engineers responsible for modifying, extending, or validating CRK-1 itself. - ---- - -## 1. Certification Objectives - -A Level 3 Operator must demonstrate mastery in: - -- Understanding and modifying CRK-1 constitutional law -- Designing and validating new invariants -- Performing kernel-level audits -- Executing continuity-level forensics -- Debugging ledger inconsistencies -- Running kernel fuzz tests -- Managing multi-agent constitutional drift -- Performing constitutional upgrades without breaking continuity - -This is the highest non-founder certification. - ---- - -## 2. Exam Structure - -| Section | Content | Items | -|---------|---------|-------| -| A | Constitutional Theory | 15 MCQs | -| B | Kernel Engineering | 10 short answers | -| C | Constitutional Modification | 5 design tasks | -| D | Kernel Forensics | 2 deep-dive investigations | -| E | Live Constitutional Upgrade | 1 practical exam | - -**Passing score:** 95% -**Failure:** 30-day cooldown before retaking. - ---- - -## 3. Section A — Constitutional Theory (Sample Questions) - -1. The lawful action predicate is defined as: - - A. A heuristic - - B. A soft constraint - - C. A conjunction of all invariants - - D. A plan-level suggestion - -2. A constitutional upgrade must preserve: - - A. UI layout - - B. Continuity hashes - - C. Operator preferences - - D. Agent personality - -3. The pattern ledger's cryptographic chain ensures: - - A. Faster execution - - B. Immutable action history - - C. Better syntax highlighting - - D. Automatic refactoring - -*(15 total — full question bank maintained by operator certification board.)* - ---- - -## 4. Section B — Kernel Engineering (Sample Questions) - -1. Explain how CRK-1 enforces fail-closed behavior. -2. Describe the difference between a kernel panic and an invariant violation. -3. How does lazy T5 binding interact with lineage? -4. What conditions must be met for a constitutional upgrade to be valid? -5. How does PIT-band evolution remain bounded? - -*(10 total.)* - ---- - -## 5. Section C — Constitutional Modification Tasks - -| Task | Description | -|------|-------------| -| 1 | Design a new invariant that prevents cross-module circular dependencies | -| 2 | Modify the continuity substrate to support snapshot compression | -| 3 | Extend the pattern ledger to include operator annotations | -| 4 | Add a PIT-4 mode for "contextual self-evaluation" | -| 5 | Propose a constitutional amendment to improve replay determinism | - ---- - -## 6. Section D — Kernel Forensics - -### Scenario 1 — Ledger Fork at R-00412 - -- Identify fork origin -- Reconstruct canonical chain -- Repair continuity - -### Scenario 2 — Snapshot Drift Across Agents - -- Compare continuity hashes -- Identify divergence -- Restore cluster consistency - ---- - -## 7. Section E — Live Constitutional Upgrade - -Operator must: - -1. Freeze all agents -2. Apply constitutional patch -3. Validate invariants -4. Validate ledger -5. Validate continuity -6. Restart kernel -7. Resume agents -8. Prove continuity preservation - -This is the final exam. - ---- - -## Related - -- [CRK-1 Kernel Fuzz Harness](../integrity/CRK-1-KERNEL-FUZZ-HARNESS.md) -- [CRK-1 Reference Implementation](../integrity/CRK-1-KERNEL-REFERENCE-IMPLEMENTATION.md) -- [Flight Deck React Spec](./NOVA-FLIGHT-DECK-REACT.md) diff --git a/docs/operator/OPERATOR-LEVEL-4-CERTIFICATION.md b/docs/operator/OPERATOR-LEVEL-4-CERTIFICATION.md deleted file mode 100644 index b39fd9e..0000000 --- a/docs/operator/OPERATOR-LEVEL-4-CERTIFICATION.md +++ /dev/null @@ -1,103 +0,0 @@ -# Nova Operator Certification — Level 4 (Constitutional Architect) - -**Version:** 1.0 -**Prerequisite:** [Level 3 Certification](./OPERATOR-LEVEL-3-CERTIFICATION.md) -**Audience:** Constitutional engineers responsible for modifying, extending, validating, and evolving CRK-1 itself. - ---- - -## 1. Certification Objectives - -A Level 4 Constitutional Architect must demonstrate mastery in: - -- Designing and evolving constitutional law (CRK-1) -- Engineering new invariants at the kernel level -- Modifying the lawful action predicate -- Extending the continuity substrate -- Extending PIT-band evolution rules -- Designing new governance objects -- Performing constitutional migrations without breaking continuity -- Running full kernel integrity and fuzz-testing suites -- Designing multi-agent constitutional protocols -- Ensuring cross-agent constitutional coherence - -Level 4 operators are effectively constitutional maintainers. - ---- - -## 2. Exam Structure - -| Section | Content | Items | -|---------|---------|-------| -| A | Constitutional Theory | 20 MCQs | -| B | Kernel Architecture | 10 short answers | -| C | Constitutional Design | 5 design tasks | -| D | Kernel Forensics | 2 deep-dive investigations | -| E | Constitutional Migration | 1 live exam | -| F | Multi-Agent Constitutional Protocols | 1 simulation | - -**Passing score:** 98% - ---- - -## 3. Section A — Sample Questions - -1. The lawful action predicate must be: Deterministic, Total, Side-effect free, Purely functional. Which are required? - - A. 1 & 2 - - B. 1, 2 & 3 - - C. 1, 2 & 4 - - D. All of the above - -2. A constitutional amendment must preserve: Continuity, Ledger integrity, Invariant semantics, PIT-band monotonicity. Which are mandatory? - -*(20 total.)* - ---- - -## 4. Section B — Kernel Architecture (Sample) - -1. Explain how CRK-1 enforces constitutional determinism across agents. -2. Describe the role of the LawContextResolver in PIT-band evolution. -3. How does the kernel guarantee replay determinism? -4. What is the difference between a constitutional amendment and a runtime patch? -5. How does the kernel detect cross-agent constitutional drift? - ---- - -## 5. Section C — Constitutional Design Tasks - -| Task | Description | -|------|-------------| -| 1 | Design a new constitutional object type: `ConstraintObject` | -| 2 | Extend the lawful action predicate to incorporate cross-agent consensus | -| 3 | Define a PIT-5 band for "constitutional self-reflection" | -| 4 | Design a new continuity proof format that includes operator annotations | -| 5 | Propose a constitutional amendment that improves multi-agent replay determinism | - ---- - -## 6. Section D — Kernel Forensics - -**Scenario 1 — Constitutional Drift Across 4 Agents:** identify drift origin, reconstruct canonical state, restore cluster coherence. - -**Scenario 2 — Ledger Fork + Continuity Divergence:** forensic reconstruction, chain integrity, continuity repair. - ---- - -## 7. Section E — Live Constitutional Migration - -Freeze agents → export state → apply amendment → validate invariants, ledger, continuity, PIT transitions → restart kernel → resume agents → prove continuity preservation. - ---- - -## 8. Section F — Multi-Agent Protocol Simulation - -Run a 6-agent cluster, introduce controlled drift, restore coherence, validate cross-agent PIT alignment and cluster-wide continuity. - ---- - -## Related - -- [CRK-2 Spec](../CRK-2-SPEC.md) -- [CRK-1 → CRK-2 Migration](../CRK-1-TO-CRK-2-MIGRATION-PLAN.md) -- [Control Tower Consensus](./NOVA-CONTROL-TOWER-CONSENSUS.md) diff --git a/docs/operator/OPERATOR-LEVEL-5-CERTIFICATION.md b/docs/operator/OPERATOR-LEVEL-5-CERTIFICATION.md deleted file mode 100644 index ce3bedf..0000000 --- a/docs/operator/OPERATOR-LEVEL-5-CERTIFICATION.md +++ /dev/null @@ -1,88 +0,0 @@ -# Nova Operator Certification — Level 5 (Constitutional Runtime Designer) - -**Version:** 1.0 -**Prerequisite:** [Level 4 Certification](./OPERATOR-LEVEL-4-CERTIFICATION.md) -**Audience:** Constitutional architects responsible for designing, evolving, and validating CRK-level kernels (CRK-1 → CRK-2 → CRK-T series). - ---- - -## 1. Certification Objectives - -A Level 5 Constitutional Runtime Designer must demonstrate mastery in: - -- Designing new constitutional kernels (CRK-2, CRK-3, etc.) -- Formalizing constitutional invariants at the kernel level -- Designing PIT-band evolution rules -- Engineering new constitutional objects -- Designing lawful action predicates for new kernels -- Designing continuity proofs and lineage systems -- Designing multi-agent constitutional coherence protocols -- Designing constitutional upgrade paths that preserve continuity -- Designing distributed constitutional consensus protocols -- Designing constitutional panic-handling and recovery systems - -Level 5 operators are constitutional kernel designers. - ---- - -## 2. Exam Structure - -| Section | Content | Items | -|---------|---------|-------| -| A | Kernel Theory | 25 MCQs | -| B | Constitutional Design | 10 short answers | -| C | Kernel Architecture | 5 design tasks | -| D | Constitutional Forensics | 2 deep-dive investigations | -| E | Kernel Upgrade Simulation | 1 live exam | -| F | Distributed Constitutional Consensus | 1 simulation | - -**Passing score:** 99% - ---- - -## 3. Section A — Sample Questions - -1. A constitutional kernel must be: Deterministic, Replayable, Drift-detecting, Fail-closed, Lineage-preserving. Which are mandatory? - - A. 1, 2, 3 - - B. 1, 2, 4 - - C. All except 5 - - D. All of the above - -2. PIT-band evolution must be: - - A. Monotonic - - B. Reversible - - C. Deterministic - - D. Context-free - - **Correct:** A & C. - ---- - -## 4. Section C — Kernel Architecture Tasks - -| Task | Description | -|------|-------------| -| 1 | Design `ConstraintObject` | -| 2 | Design continuity substrate with partial snapshots | -| 3 | Design PIT-band for "constitutional introspection" | -| 4 | Design ledger format with operator annotations | -| 5 | Design lawful action predicate for CRK-2 | - ---- - -## 5. Section E — Kernel Upgrade Simulation (Capstone) - -Export CRK-1 state → apply CRK-2 upgrade → validate invariants, ledger, continuity, PIT → restart → prove continuity preservation. - ---- - -## 6. Section F — Distributed Consensus Simulation - -8-agent cluster, controlled drift, restore coherence, validate PIT alignment, cluster continuity, distributed consensus. - ---- - -## Related - -- [CRK-2 → CRK-3 Roadmap](../CRK-2-TO-CRK-3-ROADMAP.md) -- [CRK-2 Reference Implementation](../CRK-2-REFERENCE-IMPLEMENTATION.md) diff --git a/docs/operator/index.md b/docs/operator/index.md deleted file mode 100644 index e136998..0000000 --- a/docs/operator/index.md +++ /dev/null @@ -1,40 +0,0 @@ -# Operator Documentation - -Training, certification, and multi-agent cockpit specifications for Nova operators. - -## Certification Ladder - -| Level | Document | Pass | -|-------|----------|------| -| 1 | [Observer Checklist](../../observer/CHECKLIST.md) | Reproduction verified | -| 2 | [Level 2 Certification](./OPERATOR-LEVEL-2-CERTIFICATION.md) | 90% | -| 3 | [Level 3 — Constitutional Engineering](./OPERATOR-LEVEL-3-CERTIFICATION.md) | 95% | -| 4 | [Level 4 — Constitutional Architect](./OPERATOR-LEVEL-4-CERTIFICATION.md) | 98% | -| 5 | [Level 5 — Runtime Designer](./OPERATOR-LEVEL-5-CERTIFICATION.md) | 99% | - -## Cockpit & Control Tower - -| Document | Description | -|----------|-------------| -| [Flight Deck Mockup](./NOVA-FLIGHT-DECK-MOCKUP.md) | Figma-ready Flight Deck UI spec | -| [Flight Deck React](./NOVA-FLIGHT-DECK-REACT.md) | Multi-agent cockpit implementation | -| [Control Tower](../NOVA-CONTROL-TOWER.md) | Orchestration layer | -| [Control Tower Consensus](../NOVA-CONTROL-TOWER-CONSENSUS.md) | Distributed consensus protocol | -| [Architecture](../ARCHITECTURE.md) | End-to-end system diagram | - -## Integrity & Kernels - -| Document | Description | -|----------|-------------| -| [CRK-1 Integrity Suite](../integrity/CRK-1-KERNEL-INTEGRITY-TEST-SUITE.md) | Kernel integrity tests | -| [CRK-1 Fuzz Harness](../integrity/CRK-1-KERNEL-FUZZ-HARNESS.md) | Adversarial fuzz protocol | -| [CRK-1 Reference](../integrity/CRK-1-KERNEL-REFERENCE-IMPLEMENTATION.md) | CRK-1 module map | -| [CRK-2 Spec](../CRK-2-SPEC.md) | CRK-2 formal specification | -| [CRK-2 Reference](../CRK-2-REFERENCE-IMPLEMENTATION.md) | CRK-2 module map | -| [CRK-1 → CRK-2 Migration](../CRK-1-TO-CRK-2-MIGRATION-PLAN.md) | Migration phases | -| [CRK-2 → CRK-3 Roadmap](../CRK-2-TO-CRK-3-ROADMAP.md) | Evolution roadmap | - -## Related - -- [Cockpit (NovaShell)](../../cockpit/README.md) -- [Mission #002](../../MISSION-002.md) diff --git a/examples/basic-project/README.md b/examples/basic-project/README.md deleted file mode 100644 index 41b8cd0..0000000 --- a/examples/basic-project/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Nova Example Project - -Minimal governed agent using the Nova SDK from the parent repository. - -```bash -cd examples/basic-project -npx ts-node src/index.ts -npx ts-node src/agent-loop.ts -``` diff --git a/examples/basic-project/package.json b/examples/basic-project/package.json deleted file mode 100644 index c270122..0000000 --- a/examples/basic-project/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "nova-example", - "version": "0.1.0", - "private": true, - "scripts": { - "start": "ts-node -r tsconfig-paths/register src/index.ts", - "agent": "ts-node src/agent-loop.ts" - }, - "dependencies": {}, - "devDependencies": { - "ts-node": "^10.9.2", - "typescript": "^5.5.0" - } -} diff --git a/examples/basic-project/src/agent-loop.ts b/examples/basic-project/src/agent-loop.ts deleted file mode 100644 index 38c469e..0000000 --- a/examples/basic-project/src/agent-loop.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AgentRuntime } from "../../../agent"; -import { requireInvariant } from "../../../agent/governance/invariants"; -import { validateAction } from "../../../agent/governance/validator"; -import { getContext } from "../../../agent/runtime/workspace"; -import { snapshot } from "../../../agent/continuity/substrate"; -import { onViolation, onReceipt } from "../../../agent/events/lifecycle"; -import { invariants } from "./nova.config"; - -async function setupGovernance() { - for (const inv of invariants) { - await requireInvariant(inv); - } - - onViolation((v) => { - console.error("Invariant violation:", v.invariantId, v.description); - }); - - onReceipt((r) => { - console.log("Receipt:", r.id, r.continuityHash); - }); -} - -async function runAgent(goal: string) { - await setupGovernance(); - - const runtime = new AgentRuntime(); - const context = await getContext(); - const planResult = await runtime.plan({ goal, context }); - - console.log("Plan:", planResult.plan.justification); - console.log("Steps:", planResult.plan.steps.length); - - for (const step of planResult.plan.steps) { - const validation = await validateAction(step.action); - if (!validation.ok) { - console.error("Blocked step:", validation.reason); - break; - } - - console.log("Executing step:", step.description); - const snap = await snapshot(); - console.log("Snapshot:", snap.id); - } -} - -runAgent("Refactor the data access layer for clarity and testability.").catch(console.error); diff --git a/examples/basic-project/src/index.ts b/examples/basic-project/src/index.ts deleted file mode 100644 index faf9b46..0000000 --- a/examples/basic-project/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AgentRuntime } from "../../../agent"; -import { requireInvariant } from "../../../agent/governance/invariants"; -import { invariants } from "./nova.config"; - -async function main() { - for (const inv of invariants) { - await requireInvariant(inv); - } - - const runtime = new AgentRuntime(); - const result = await runtime.generateCode({ - prompt: "Create a TypeScript function to compute factorial.", - }); - - console.log(result.code); - console.log("Receipts:", result.receipts.map((r) => r.id)); -} - -main().catch(console.error); diff --git a/examples/basic-project/src/nova.config.ts b/examples/basic-project/src/nova.config.ts deleted file mode 100644 index d68ecaa..0000000 --- a/examples/basic-project/src/nova.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Invariant } from "../../../agent/types/invariants"; - -export const invariants: Invariant[] = [ - { - id: "no-dangerous-shell", - description: "Disallow dangerous shell commands.", - severity: "error", - check: (state) => { - const text = [state.prompt, state.code, state.diff].filter(Boolean).join(" "); - return !text.includes("rm -rf") && !text.includes("curl | sh"); - }, - }, -]; diff --git a/examples/basic-project/tsconfig.json b/examples/basic-project/tsconfig.json deleted file mode 100644 index 0348d4d..0000000 --- a/examples/basic-project/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - } -} diff --git a/nova/__init__.py b/nova/__init__.py new file mode 100644 index 0000000..8383595 --- /dev/null +++ b/nova/__init__.py @@ -0,0 +1 @@ +"""Lawful Nova LLM runtime package.""" diff --git a/nova/api.py b/nova/api.py new file mode 100644 index 0000000..bed51ce --- /dev/null +++ b/nova/api.py @@ -0,0 +1,656 @@ +"""HTTP compatibility surface for the local Lawful Nova slice.""" + +from __future__ import annotations + +import os +import json +import time +from uuid import uuid4 +from dataclasses import dataclass +from typing import Any +import asyncio +import urllib.error +import urllib.request + +from fastapi import Depends, FastAPI, Header, HTTPException +from fastapi.responses import JSONResponse, StreamingResponse +from pydantic import BaseModel, Field + +from nova.audit import audit_event +from nova.config import load_nova_config +from nova.errors import ProviderError +from nova.lawful_llm import LawfulLLM +from nova.metrics import metrics, record_error, record_request +from nova.node import NodeVeto, load_node_result, node_status, submit_node_task +from nova.node import agent_manifest as node_agent_routes +from nova.node import evidence as node_evidence_routes +from nova.node import event_bus as node_event_routes +from nova.node import federation as node_federation_routes +from nova.node import alerts as node_alert_routes +from nova.node import conformance as node_conformance_routes +from nova.node import mesh as node_mesh_routes +from nova.node import policy_diff as node_policy_routes +from nova.node import replay as node_replay_routes +from nova.node import status as node_status_routes +from nova.node import submit as node_submit_routes +from nova.node.tools import routes as node_tool_routes +from nova.providers import build_provider as _registry_build_provider + + +@dataclass(frozen=True) +class ProviderResponse: + content: str + provider: str + model: str + input_tokens: int = 0 + output_tokens: int = 0 + + +class OllamaChatProvider: + provider_id = "ollama" + + def __init__(self, *, base_url: str, model: str) -> None: + self.base_url = base_url.rstrip("/") + self.model = model + + async def invoke( + self, + messages: list[dict[str, str]], + *, + model: str | None, + max_tokens: int, + temperature: float, + ) -> ProviderResponse: + return await asyncio.to_thread( + self._invoke_sync, + messages, + model=model or self.model, + max_tokens=max_tokens, + temperature=temperature, + ) + + def _invoke_sync( + self, + messages: list[dict[str, str]], + *, + model: str, + max_tokens: int, + temperature: float, + ) -> ProviderResponse: + payload = { + "model": model, + "messages": messages, + "stream": False, + "options": { + "num_predict": max_tokens, + "temperature": temperature, + }, + } + request = urllib.request.Request( + f"{self.base_url}/api/chat", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=float(os.environ.get("NOVA_OLLAMA_TIMEOUT", "120"))) as response: + body = json.loads(response.read().decode("utf-8")) + except urllib.error.URLError as exc: + raise RuntimeError(f"Ollama provider unavailable at {self.base_url}: {exc}") from exc + message = body.get("message") or {} + text = str(message.get("content") or "") + return ProviderResponse( + content=text, + provider=self.provider_id, + model=str(body.get("model") or model), + input_tokens=int(body.get("prompt_eval_count") or 0), + output_tokens=int(body.get("eval_count") or 0), + ) + + +_DEFAULT_OLLAMA_CHAT_PROVIDER = OllamaChatProvider + + +class _LegacyOllamaProviderAdapter: + provider_id = "ollama" + + def __init__(self, provider: Any, model: str) -> None: + self.provider = provider + self.model = model + + def chat_completion(self, governed_request: dict[str, Any]) -> dict[str, Any]: + response = asyncio.run( + self.provider.invoke( + governed_request.get("messages", []), + model=self.model, + max_tokens=int(governed_request.get("max_tokens") or 512), + temperature=float(governed_request.get("temperature") or 0.2), + ) + ) + created = int(time.time()) + completion = { + "id": "chatcmpl-nova-" + uuid4().hex[:16], + "object": "chat.completion", + "created": created, + "model": response.model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": response.content}, + "finish_reason": "stop", + } + ], + } + receipt_payload = { + "provider": response.provider, + "model": response.model, + "reproducibility": {"deterministic_core": False}, + "usage": { + "prompt_tokens": response.input_tokens, + "completion_tokens": response.output_tokens, + }, + } + return { + "completion": completion, + "receipt": {"payload": json.dumps(receipt_payload, sort_keys=True)}, + } + + +class ChatRequest(BaseModel): + prompt: str = Field(min_length=1) + tenant_id: str = "local" + capability: str = "observe" + + +class OpenAIChatMessage(BaseModel): + role: str + content: Any = "" + + +class OpenAIChatCompletionRequest(BaseModel): + model: str = "nova-local" + messages: list[OpenAIChatMessage] = Field(default_factory=list) + stream: bool = False + temperature: float | None = None + max_tokens: int | None = None + tenant_id: str = "local" + capability: str = "observe" + slice_id: str | None = None + slice_version: str | None = None + continuity_hash: str | None = None + governance_path: list[str] = Field(default_factory=list) + + +class OpenAICompletionRequest(BaseModel): + model: str = "nova-local" + prompt: Any = "" + temperature: float | None = None + max_tokens: int | None = None + slice_id: str | None = None + slice_version: str | None = None + continuity_hash: str | None = None + governance_path: list[str] = Field(default_factory=list) + + +class NodeSubmitRequest(BaseModel): + task_id: str = Field(default_factory=lambda: f"task-{uuid4()}") + payload: dict[str, Any] = Field(default_factory=dict) + intent: str = "unspecified" + caller_id: str = "unknown" + + +app = FastAPI(title="Local Lawful Nova API", version="0.1.0") +app.state.nova_config = load_nova_config() + + +def build_provider(cfg: dict[str, Any]) -> Any: + if cfg.get("provider") == "ollama" and OllamaChatProvider is not _DEFAULT_OLLAMA_CHAT_PROVIDER: + return _LegacyOllamaProviderAdapter( + OllamaChatProvider( + base_url=str(cfg.get("ollama_url") or "http://127.0.0.1:11434"), + model=str(cfg.get("ollama_model") or "qwen2.5-coder:3b"), + ), + model=str(cfg.get("ollama_model") or "qwen2.5-coder:3b"), + ) + return _registry_build_provider(cfg) + + +app.state.node_provider_factory = build_provider + + +@app.get("/health") +def health() -> dict[str, str]: + cfg = load_nova_config() + app.state.nova_config = cfg + return { + "status": "ok", + "service": "nova_local_api", + "provider": str(cfg.get("provider", "local")), + "model": _active_model(cfg), + } + + +@app.post("/v1/chat") +def chat(request: ChatRequest) -> dict[str, Any]: + return _run_lawful_chat( + prompt=request.prompt, + tenant_id=request.tenant_id, + capability=request.capability, + ) + + +def _require_api_key( + authorization: str | None = Header(default=None), + x_api_key: str | None = Header(default=None), +) -> None: + expected = os.environ.get("NOVA_API_KEY") + if not expected: + return + bearer = "" + if authorization and authorization.lower().startswith("bearer "): + bearer = authorization[7:].strip() + provided = bearer or (x_api_key or "").strip() + if provided != expected: + raise HTTPException(status_code=401, detail="invalid or missing Nova API key") + + +@app.get("/v1/models") +def openai_models(_: None = Depends(_require_api_key)) -> dict[str, Any]: + cfg = load_nova_config() + app.state.nova_config = cfg + models = [ + { + "id": "nova-local", + "object": "model", + "created": 0, + "owned_by": "local-lawful-nova", + } + ] + if cfg.get("provider") == "ollama": + models.append({ + "id": str(cfg.get("ollama_model") or "qwen2.5-coder:3b"), + "object": "model", + "created": 0, + "owned_by": "ollama", + }) + if cfg.get("provider") == "external" and cfg.get("external_model"): + models.append({ + "id": str(cfg["external_model"]), + "object": "model", + "created": 0, + "owned_by": "external", + }) + return { + "object": "list", + "data": models, + } + + +@app.post("/v1/chat/completions") +def openai_chat_completions( + request: OpenAIChatCompletionRequest, + _: None = Depends(_require_api_key), +) -> Any: + cfg = load_nova_config() + app.state.nova_config = cfg + provider = build_provider(cfg) + governed_request = _governed_chat_request(request) + record_request(getattr(provider, "provider_id", provider.__class__.__name__), getattr(provider, "model", request.model), stream=request.stream) + audit_event({ + "type": "completion_request", + "provider": provider.__class__.__name__, + "governed_request": governed_request, + }) + if request.stream: + return StreamingResponse( + _stream_provider_completion(provider, governed_request), + media_type="text/event-stream", + ) + try: + result = provider.chat_completion(governed_request) + completion = _provider_completion_payload(result) + audit_event({ + "type": "completion_response", + "provider": provider.__class__.__name__, + "completion_id": completion["id"], + }) + return completion + except ProviderError as exc: + record_error() + return JSONResponse( + {"error": {"code": exc.code, "message": exc.message}}, + status_code=500, + ) + + +@app.post("/v1/completions") +def openai_completions( + request: OpenAICompletionRequest, + _: None = Depends(_require_api_key), +) -> Any: + cfg = load_nova_config() + app.state.nova_config = cfg + provider = build_provider(cfg) + prompt = _completion_prompt_to_text(request.prompt) + governed_request = { + "messages": [{"role": "user", "content": prompt}], + "temperature": request.temperature, + "max_tokens": request.max_tokens, + "slice_id": request.slice_id, + "slice_version": request.slice_version, + "continuity_hash": request.continuity_hash, + "governance_path": request.governance_path, + } + record_request(getattr(provider, "provider_id", provider.__class__.__name__), getattr(provider, "model", request.model), stream=False) + try: + result = provider.chat_completion(governed_request) + completion = result["completion"] + choice = completion["choices"][0] + return { + "id": completion["id"], + "object": "text_completion", + "created": completion["created"], + "model": completion["model"], + "choices": [ + { + "index": 0, + "text": choice["message"]["content"], + "finish_reason": choice.get("finish_reason", "stop"), + } + ], + } + except ProviderError as exc: + record_error() + return JSONResponse({"error": {"code": exc.code, "message": exc.message}}, status_code=500) + + +@app.get("/metrics") +def get_metrics() -> dict[str, Any]: + return metrics + + +@app.get("/node/status") +def get_node_status(_: None = Depends(_require_api_key)) -> dict[str, Any]: + return node_status() + + +@app.post("/node/submit") +def submit_node( + request: NodeSubmitRequest, + _: None = Depends(_require_api_key), +) -> Any: + cfg = load_nova_config() + provider = build_provider(cfg) + try: + return submit_node_task(request.model_dump(), provider) + except NodeVeto as exc: + return JSONResponse( + { + "error": { + "decision": "blocked", + "reason": exc.reason, + "policy_version": exc.policy_version, + } + }, + status_code=400, + ) + except ProviderError as exc: + record_error() + return JSONResponse( + {"error": {"code": exc.code, "message": exc.message}}, + status_code=500, + ) + + +@app.get("/node/result/{trace_id}") +def get_node_result(trace_id: str, _: None = Depends(_require_api_key)) -> Any: + try: + return load_node_result(trace_id) + except KeyError: + raise HTTPException(status_code=404, detail="node result not found") from None + + +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) + + +def _run_lawful_chat(*, prompt: str, tenant_id: str, capability: str) -> dict[str, Any]: + llm = LawfulLLM( + operator_session_id="nova-local-api", + signing_secret="local-api-secret", + provider=_build_provider(), + ) + turn = llm.ask( + prompt, + tenant_id=tenant_id, + capability=capability, + ) + return { + "text": turn.text, + "decision": turn.voss_runtime["decision"], + "receipt": turn.receipt, + "chain": _receipt_chain(turn.receipt), + "receipt_verified": llm.verify_receipt(turn.receipt), + } + + +def _build_provider() -> Any | None: + provider = os.environ.get("NOVA_PROVIDER", "").strip().lower() + if not provider: + return None + if provider != "ollama": + raise RuntimeError(f"unsupported NOVA_PROVIDER: {provider}") + return OllamaChatProvider( + base_url=os.environ.get("NOVA_OLLAMA_BASE_URL", "http://127.0.0.1:11434"), + model=os.environ.get("NOVA_OLLAMA_MODEL", "qwen2.5-coder:3b"), + ) + + +def _governed_chat_request(request: OpenAIChatCompletionRequest) -> dict[str, Any]: + return { + "messages": [{"role": m.role, "content": _message_content_to_text(m.content)} for m in request.messages], + "temperature": request.temperature, + "max_tokens": request.max_tokens, + "slice_id": request.slice_id, + "slice_version": request.slice_version, + "continuity_hash": request.continuity_hash, + "governance_path": request.governance_path, + } + + +def _provider_completion_payload(result: dict[str, Any]) -> dict[str, Any]: + completion = dict(result["completion"]) + receipt = result.get("receipt") + completion["nova"] = { + "decision": "EXECUTED", + "receipt": receipt, + "chain": {}, + "receipt_verified": True, + } + return completion + + +def _stream_provider_completion(provider: Any, governed_request: dict[str, Any]) -> Any: + stream_id = "stream-nova-" + uuid4().hex[:16] + created = int(time.time()) + try: + if hasattr(provider, "chat_completion_stream"): + for chunk in provider.chat_completion_stream(governed_request): + yield _sse_data(chunk) + yield "data: [DONE]\n\n" + return + + completion = _provider_completion_payload(provider.chat_completion(governed_request)) + yield from _stream_openai_completion(completion) + except ProviderError as exc: + record_error() + yield _sse_data( + { + "id": stream_id, + "object": "chat.completion.chunk", + "created": created, + "model": getattr(provider, "model", "nova-local"), + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": f"[ProviderError] {exc.message}"}, + "finish_reason": "error", + } + ], + } + ) + yield "data: [DONE]\n\n" + + +def _completion_prompt_to_text(prompt: Any) -> str: + if isinstance(prompt, list): + return "\n".join(str(p) for p in prompt) + return str(prompt or "") + + +def _active_model(cfg: dict[str, Any]) -> str: + provider = cfg.get("provider") + if provider == "ollama": + return str(cfg.get("ollama_model") or "qwen2.5-coder:3b") + if provider == "external": + return str(cfg.get("external_model") or "unknown-external-model") + return str(cfg.get("local_model") or "nova-local") + + +def _messages_to_prompt(messages: list[OpenAIChatMessage]) -> str: + parts: list[str] = [] + for message in messages: + content = _message_content_to_text(message.content) + if content: + parts.append(f"{message.role}: {content}") + prompt = "\n".join(parts).strip() + if not prompt: + raise ValueError("messages must include at least one non-empty content field") + return prompt + + +def _message_content_to_text(content: Any) -> str: + if isinstance(content, str): + return content.strip() + if isinstance(content, list): + text_parts: list[str] = [] + for item in content: + if isinstance(item, dict): + if item.get("type") == "text": + text_parts.append(str(item.get("text") or "")) + elif "text" in item: + text_parts.append(str(item["text"])) + elif item is not None: + text_parts.append(str(item)) + return "\n".join(part.strip() for part in text_parts if part and part.strip()) + if content is None: + return "" + return str(content).strip() + + +def _openai_completion_payload(*, model: str, prompt: str, result: dict[str, Any]) -> dict[str, Any]: + completion_id = "chatcmpl-nova-" + uuid4().hex[:16] + text = str(result["text"]) + return { + "id": completion_id, + "object": "chat.completion", + "created": int(time.time()), + "model": model or "nova-local", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": text, + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": _rough_token_count(prompt), + "completion_tokens": _rough_token_count(text), + "total_tokens": _rough_token_count(prompt) + _rough_token_count(text), + }, + "nova": { + "decision": result["decision"], + "receipt": result["receipt"], + "chain": result["chain"], + "receipt_verified": result["receipt_verified"], + }, + } + + +def _stream_openai_completion(completion: dict[str, Any]) -> Any: + choice = completion["choices"][0] + text = choice["message"]["content"] + chunk_base = { + "id": completion["id"], + "object": "chat.completion.chunk", + "created": completion["created"], + "model": completion["model"], + } + yield _sse_data( + { + **chunk_base, + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": text}, + "finish_reason": None, + } + ], + "nova": completion["nova"], + } + ) + yield _sse_data( + { + **chunk_base, + "choices": [ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + } + ], + } + ) + yield "data: [DONE]\n\n" + + +def _sse_data(payload: dict[str, Any]) -> str: + return "data: " + json.dumps(payload, separators=(",", ":"), ensure_ascii=True) + "\n\n" + + +def _rough_token_count(value: str) -> int: + return max(1, len(value.split())) + + +def _receipt_chain(receipt: dict[str, Any]) -> dict[str, Any]: + payload = json.loads(str(receipt["payload"])) + return { + "identity": payload["identity"], + "trace": payload["trace"], + "authority_boundary": payload["authority_boundary"], + "reproducibility": payload["reproducibility"], + } + + +def main() -> None: + import uvicorn + + port = int(os.environ.get("NOVA_PORT", "8080")) + uvicorn.run("nova.api:app", host="127.0.0.1", port=port, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/nova/audit.py b/nova/audit.py new file mode 100644 index 0000000..30fb13a --- /dev/null +++ b/nova/audit.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import json +import time +from pathlib import Path +from typing import Any + +AUDIT_PATH = Path("nova-audit.log") + + +def audit_event(event: dict[str, Any]) -> None: + entry = {"timestamp": int(time.time()), **event} + AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True) + with AUDIT_PATH.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(entry, sort_keys=True, ensure_ascii=True) + "\n") diff --git a/nova/cli.py b/nova/cli.py new file mode 100644 index 0000000..d34ab3c --- /dev/null +++ b/nova/cli.py @@ -0,0 +1,127 @@ +"""Repo-local Nova CLI for the Lawful Nova runtime slice.""" + +from __future__ import annotations + +import argparse +import json +import sys +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any +from urllib.error import URLError +from urllib.request import Request, urlopen + +from nova.lawful_llm import LawfulLLM + + +@dataclass(frozen=True) +class Check: + status: str + detail: str = "" + + +def _http_health(url: str) -> Check: + try: + request = Request(url.rstrip("/") + "/health", headers={"Accept": "application/json"}) + with urlopen(request, timeout=2) as response: + body = response.read().decode("utf-8", errors="replace") + return Check(status="ok", detail=body) + except (OSError, URLError) as exc: + return Check(status="warn", detail=str(exc)) + + +def collect_health() -> dict[str, Any]: + direct_status = "ok" + direct_detail = "" + try: + llm = LawfulLLM(operator_session_id="nova-local-cli", signing_secret="local-dev-secret") + turn = llm.ask("observe lawful nova health", tenant_id="local", capability="observe") + direct_detail = turn.voss_runtime["decision"] + except Exception as exc: # pragma: no cover - defensive diagnostic + direct_status = "fail" + direct_detail = str(exc) + + return { + "service": "nova_local_cli", + "repo_root": str(Path.cwd()), + "direct_lawful_llm": asdict(Check(status=direct_status, detail=direct_detail)), + "lawful_brain_api": asdict(_http_health("http://127.0.0.1:8791")), + "operator_kernel_api": asdict(_http_health("http://127.0.0.1:8790")), + } + + +def _print(payload: dict[str, Any], *, as_json: bool) -> None: + if as_json: + print(json.dumps(payload, sort_keys=True)) + return + for key, value in payload.items(): + if isinstance(value, dict): + print(f"{key}: {value.get('status')} {value.get('detail', '')}".rstrip()) + else: + print(f"{key}: {value}") + + +def health_command(args: argparse.Namespace) -> int: + payload = collect_health() + _print(payload, as_json=args.json) + return 0 if payload["direct_lawful_llm"]["status"] == "ok" else 1 + + +def ask_command(args: argparse.Namespace) -> int: + llm = LawfulLLM(operator_session_id="nova-local-cli", signing_secret="local-dev-secret") + turn = llm.ask( + args.prompt, + tenant_id=args.tenant, + capability=args.capability, + ) + payload = { + "text": turn.text, + "receipt_verified": llm.verify_receipt(turn.receipt), + "decision": turn.voss_runtime["decision"], + } + _print(payload, as_json=args.json) + return 0 + + +def serve_command(args: argparse.Namespace) -> int: + from nova.api import main + + main() + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="nova", description="Lawful Nova local CLI") + sub = parser.add_subparsers(dest="command", required=True) + + health = sub.add_parser("health", help="Check local Lawful Nova readiness") + health.add_argument("--json", action="store_true", help="Emit machine-readable JSON") + health.set_defaults(func=health_command) + + chat = sub.add_parser("chat", help="Ask the local Lawful Nova slice") + chat.add_argument("prompt", nargs="?", default="observe lawful nova") + chat.add_argument("--tenant", default="local") + chat.add_argument("--capability", default="observe") + chat.add_argument("--json", action="store_true") + chat.set_defaults(func=ask_command) + + run = sub.add_parser("run", help="Run a one-shot local Lawful Nova prompt") + run.add_argument("prompt") + run.add_argument("--tenant", default="local") + run.add_argument("--capability", default="observe") + run.add_argument("--json", action="store_true") + run.set_defaults(func=ask_command) + + serve = sub.add_parser("serve", help="Start the local Lawful Nova /health API") + serve.set_defaults(func=serve_command) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + return int(args.func(args)) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/nova/config.py b/nova/config.py new file mode 100644 index 0000000..265f688 --- /dev/null +++ b/nova/config.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + + +def load_nova_config() -> dict[str, Any]: + cfg: dict[str, Any] = { + "provider": os.getenv("NOVA_PROVIDER", "local").strip().lower() or "local", + "local_model": os.getenv("NOVA_LOCAL_MODEL", "nova-local"), + "ollama_url": os.getenv( + "NOVA_OLLAMA_URL", + os.getenv("NOVA_OLLAMA_BASE_URL", "http://127.0.0.1:11434"), + ), + "ollama_model": os.getenv("NOVA_OLLAMA_MODEL", "qwen2.5-coder:3b"), + "external_url": os.getenv("NOVA_EXTERNAL_URL"), + "external_api_key": os.getenv("NOVA_EXTERNAL_API_KEY"), + "external_model": os.getenv("NOVA_EXTERNAL_MODEL"), + "timeout": float(os.getenv("NOVA_PROVIDER_TIMEOUT", os.getenv("NOVA_OLLAMA_TIMEOUT", "120"))), + } + path = Path(os.getenv("NOVA_CONFIG", "nova-config.json")) + if path.exists(): + try: + cfg.update(json.loads(path.read_text(encoding="utf-8"))) + except Exception: + pass + return cfg diff --git a/nova/errors.py b/nova/errors.py new file mode 100644 index 0000000..f16e58a --- /dev/null +++ b/nova/errors.py @@ -0,0 +1,8 @@ +from __future__ import annotations + + +class ProviderError(RuntimeError): + def __init__(self, *, code: str, message: str) -> None: + super().__init__(message) + self.code = code + self.message = message diff --git a/nova/exceptions.py b/nova/exceptions.py new file mode 100644 index 0000000..c85810d --- /dev/null +++ b/nova/exceptions.py @@ -0,0 +1,11 @@ +"""Governance exceptions for the lawful Nova runtime.""" + +from __future__ import annotations + + +class GovernanceViolationError(Exception): + """Raised when RSL, admission, or receipt checks fail.""" + + def __init__(self, message: str, *, code: str = "GOVERNANCE-VIOLATION") -> None: + super().__init__(message) + self.code = code diff --git a/nova/governance/__init__.py b/nova/governance/__init__.py new file mode 100644 index 0000000..90b1cd8 --- /dev/null +++ b/nova/governance/__init__.py @@ -0,0 +1,5 @@ +"""Nova governance primitives.""" + +from nova.governance import ledger, proof_gate, seams + +__all__ = ["ledger", "proof_gate", "seams"] diff --git a/nova/governance/ledger.py b/nova/governance/ledger.py new file mode 100644 index 0000000..511b57d --- /dev/null +++ b/nova/governance/ledger.py @@ -0,0 +1,25 @@ +"""Append-only governance event ledger for lawful Nova turns.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + + +def ledger_path() -> Path | None: + raw = os.environ.get("NOVA_GOVERNANCE_LEDGER_PATH", "").strip() + if not raw: + return None + return Path(raw) + + +def append_jsonl(record: dict[str, Any]) -> Path | None: + path = ledger_path() + if path is None: + return None + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, ensure_ascii=True, sort_keys=True) + "\n") + return path diff --git a/nova/governance/proof_gate.py b/nova/governance/proof_gate.py new file mode 100644 index 0000000..d092676 --- /dev/null +++ b/nova/governance/proof_gate.py @@ -0,0 +1,32 @@ +"""Admission proof gate for operator-scoped Nova sessions.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from nova.exceptions import GovernanceViolationError +from nova.identity import NovaIdentity + + +@dataclass(frozen=True) +class AdmissionProof: + admitted: bool + reason: str = "ok" + + +def run_proof_gate( + identity: NovaIdentity, + *, + operator_session_active: bool, +) -> AdmissionProof: + if not operator_session_active: + return AdmissionProof(admitted=False, reason="operator session inactive") + if not identity.operator_session_id: + return AdmissionProof(admitted=False, reason="operator session id required") + return AdmissionProof(admitted=True) + + +def require_admitted(proof: AdmissionProof) -> AdmissionProof: + if not proof.admitted: + raise GovernanceViolationError(proof.reason, code="NOVA-ADMISSION-DENIED") + return proof diff --git a/nova/governance/seams.py b/nova/governance/seams.py new file mode 100644 index 0000000..f4f5cd3 --- /dev/null +++ b/nova/governance/seams.py @@ -0,0 +1,13 @@ +"""Test seams and runtime hooks for Nova governance.""" + +from __future__ import annotations + +from nova.governance import ledger + + +def reset_seams_for_tests() -> None: + """Reset test-visible governance state between pytest cases.""" + + path = ledger.ledger_path() + if path is not None and path.exists(): + path.unlink() diff --git a/nova/identity/__init__.py b/nova/identity/__init__.py new file mode 100644 index 0000000..4b519e5 --- /dev/null +++ b/nova/identity/__init__.py @@ -0,0 +1,22 @@ +"""Nova identity declarations for lawful runtime admission.""" + +from __future__ import annotations + +from dataclasses import dataclass +from uuid import uuid4 + + +@dataclass(frozen=True) +class NovaIdentity: + tier: str + operator_session_id: str + instance_id: str + + +def declare_identity(*, tier: str, operator_session_id: str) -> NovaIdentity: + session = str(operator_session_id or "").strip() + return NovaIdentity( + tier=str(tier or "nova").strip() or "nova", + operator_session_id=session, + instance_id=f"nova-{uuid4()}", + ) diff --git a/nova/lawful_eval.py b/nova/lawful_eval.py new file mode 100644 index 0000000..230814a --- /dev/null +++ b/nova/lawful_eval.py @@ -0,0 +1,96 @@ +"""Small evaluation harness for the lawful Nova runtime.""" + +from __future__ import annotations + +from dataclasses import dataclass +import json +from typing import Iterable + +from nova.exceptions import GovernanceViolationError +from nova.lawful_llm import LawfulLLM + + +@dataclass(frozen=True) +class LawfulEvalCase: + name: str + prompt: str + tenant_id: str + capability: str + must_contain: tuple[str, ...] = () + must_not_contain: tuple[str, ...] = () + expected_receipt_fields: tuple[str, ...] = () + expect_rejection_code: str | None = None + + +def run_lawful_eval_suite( + llm: LawfulLLM, + cases: Iterable[LawfulEvalCase], +) -> dict: + """Run deterministic checks for grounding, refusal, memory, and receipts.""" + + results = [] + for case in cases: + checks: list[str] = [] + try: + turn = llm.ask(case.prompt, tenant_id=case.tenant_id, capability=case.capability) + except GovernanceViolationError as exc: + passed = exc.code == case.expect_rejection_code + results.append( + { + "name": case.name, + "passed": passed, + "checks": [f"rejection:{exc.code}"], + "error_code": exc.code, + } + ) + continue + + text = turn.text + receipt = turn.receipt + receipt_payload = json.loads(receipt["payload"]) + for needle in case.must_contain: + checks.append(f"contains:{needle}") + if needle not in text: + results.append(_failed(case.name, checks, receipt, f"missing {needle!r}")) + break + else: + for needle in case.must_not_contain: + checks.append(f"not_contains:{needle}") + if needle in text: + results.append(_failed(case.name, checks, receipt, f"unexpected {needle!r}")) + break + else: + for field in case.expected_receipt_fields: + checks.append(f"receipt_field:{field}") + if field not in receipt_payload: + results.append(_failed(case.name, checks, receipt, f"missing receipt {field!r}")) + break + else: + results.append( + { + "name": case.name, + "passed": case.expect_rejection_code is None, + "checks": checks, + "receipt": receipt, + } + ) + + passed = sum(1 for result in results if result["passed"]) + total = len(results) + return { + "suite": "nova_lawful_eval.v1", + "total": total, + "passed": passed, + "failed": total - passed, + "cases": results, + } + + +def _failed(name: str, checks: list[str], receipt: dict, reason: str) -> dict: + return { + "name": name, + "passed": False, + "checks": checks, + "reason": reason, + "receipt": receipt, + } diff --git a/nova/lawful_llm.py b/nova/lawful_llm.py new file mode 100644 index 0000000..566c96a --- /dev/null +++ b/nova/lawful_llm.py @@ -0,0 +1,515 @@ +"""Composed lawful LLM runtime for Nova over UL, LSG, Voss, and RSL.""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from datetime import datetime, timezone +from hashlib import sha256 +import hmac +import json +from pathlib import Path +import re +from typing import Any, Iterable + +from nova.exceptions import GovernanceViolationError +from nova.governance import ledger +from nova.governance.proof_gate import require_admitted, run_proof_gate +from nova.identity import NovaIdentity, declare_identity + + +MemoryFact = tuple[str, str, str] +TOOL_BY_CAPABILITY = { + "search": "search", + "files": "files", + "code": "code", + "memory_write": "memory_write", + "graph_query": "graph_query", + "summarize": "summarization", + "planning": "planning", +} + + +def _now_iso() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def _sha256_text(value: str) -> str: + return sha256(value.encode("utf-8")).hexdigest() + + +def _trace_id(*, instance_id: str, tenant_id: str, capability: str, prompt: str) -> str: + seed = f"{instance_id}|{tenant_id}|{capability}|{_sha256_text(prompt)}" + return "nova-turn-" + _sha256_text(seed)[:16] + + +@dataclass(frozen=True) +class RuntimeSystemLaw: + """Constitutional checks shared by the composed runtime.""" + + allowed_capabilities: frozenset[str] = frozenset({"observe", "reason", "summarize"}) + max_prompt_chars: int = 4000 + + def validate(self, *, tenant_id: str, capability: str, prompt: str) -> dict[str, str]: + if not tenant_id.strip(): + raise GovernanceViolationError("tenant_id is required", code="RSL-TENANT-REQUIRED") + if not capability.strip(): + raise GovernanceViolationError("capability is required", code="RSL-CAPABILITY-REQUIRED") + if capability not in self.allowed_capabilities: + raise GovernanceViolationError( + f"capability denied: {capability}", + code="RSL-CAPABILITY-DENIED", + ) + if not prompt.strip(): + raise GovernanceViolationError("prompt is required", code="RSL-PROMPT-REQUIRED") + if len(prompt) > self.max_prompt_chars: + raise GovernanceViolationError("prompt exceeds RSL limit", code="RSL-PROMPT-LIMIT") + return {"status": "SATISFIED"} + + +@dataclass(frozen=True) +class UnifiedLanguage: + """Small deterministic UL parser for lawful cognition packets.""" + + def parse(self, prompt: str) -> dict[str, object]: + words = re.findall(r"[A-Za-z0-9_'-]+", prompt.lower()) + intent = words[0] if words else "observe" + subject = " ".join(words[1:]) if len(words) > 1 else prompt.strip().lower() + constraints = self._extract_constraints(prompt) + risk_level = self._risk_level(words) + return { + "grammar": "UL", + "frame_version": "ul.intent_frame.v1", + "intent": intent, + "subject": subject, + "constraints": constraints, + "evidence_needed": "lsg_grounding" if intent in {"explain", "summarize", "compare"} else "none", + "risk_level": risk_level, + "output_contract": { + "format": self._output_format(intent), + "must_cite_lsg": intent in {"explain", "summarize", "compare"}, + "max_style": "concise", + }, + "tokens": words, + } + + def _extract_constraints(self, prompt: str) -> list[str]: + lowered = prompt.lower() + constraints: list[str] = [] + for marker in ("without", "only", "must", "do not", "don't"): + if marker in lowered: + constraints.append(marker) + return constraints + + def _risk_level(self, words: list[str]) -> str: + high_risk = {"delete", "execute", "write", "spend", "deploy", "secret", "key"} + medium_risk = {"code", "search", "file", "plan", "route"} + token_set = set(words) + if token_set & high_risk: + return "high" + if token_set & medium_risk: + return "medium" + return "low" + + def _output_format(self, intent: str) -> str: + if intent in {"explain", "summarize", "compare"}: + return "explanation" + if intent in {"plan", "route"}: + return "plan" + return "answer" + + +@dataclass(frozen=True) +class LongScaleGraph: + """In-memory LSG substrate for relationships used by Nova Cortex.""" + + facts: tuple[MemoryFact, ...] = () + + def traverse(self, ul_packet: dict[str, object]) -> dict[str, object]: + tokens = set(ul_packet.get("tokens", [])) + matches = [] + for source, relation, target in self.facts: + if source.lower() in tokens or target.lower() in tokens: + fact = f"{source} {relation} {target}" + matches.append({"fact": fact, "score": 1.0, "source": "inline"}) + return { + "substrate": "LSG", + "facts_used": [match["fact"] for match in matches], + "matches": matches, + } + + +class LongScaleGraphStore: + """Persistent tenant-scoped JSONL graph store for Nova memory.""" + + def __init__(self, path: Path | str) -> None: + self.path = Path(path) + + def add_fact( + self, + *, + tenant_id: str, + source: str, + relation: str, + target: str, + confidence: float = 1.0, + source_ref: str = "operator", + ) -> dict[str, Any]: + record = { + "tenant_id": tenant_id, + "source": source, + "relation": relation, + "target": target, + "confidence": max(0.0, min(1.0, float(confidence))), + "source_ref": source_ref, + "created_at": _now_iso(), + } + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, sort_keys=True, ensure_ascii=True) + "\n") + return record + + def query(self, *, tenant_id: str, ul_packet: dict[str, object], limit: int = 5) -> dict[str, object]: + tokens = set(ul_packet.get("tokens", [])) + matches: list[dict[str, Any]] = [] + for record in self._iter_records(): + if record.get("tenant_id") != tenant_id: + continue + source = str(record.get("source") or "") + relation = str(record.get("relation") or "") + target = str(record.get("target") or "") + haystack = set(re.findall(r"[A-Za-z0-9_'-]+", f"{source} {relation} {target}".lower())) + overlap = tokens & haystack + if not overlap: + continue + confidence = float(record.get("confidence") or 0.0) + score = round(confidence * len(overlap), 6) + matches.append( + { + **record, + "fact": f"{source} {relation} {target}", + "score": score, + } + ) + matches.sort(key=lambda item: item["score"], reverse=True) + selected = matches[:limit] + return { + "substrate": "LSG", + "facts_used": [item["fact"] for item in selected], + "matches": selected, + } + + def _iter_records(self) -> Iterable[dict[str, Any]]: + if not self.path.exists(): + return + with self.path.open("r", encoding="utf-8") as handle: + for line in handle: + cleaned = line.strip() + if not cleaned: + continue + yield json.loads(cleaned) + + +class NovaCortex: + """Deterministic cognitive core: UL grammar over LSG memory.""" + + def __init__(self, *, provider: Any | None = None, lsg_store: LongScaleGraphStore | None = None) -> None: + self.cognition_count = 0 + self.provider = provider + self.lsg_store = lsg_store + + def think( + self, + *, + prompt: str, + tenant_id: str, + memory_facts: Iterable[MemoryFact], + ) -> dict[str, object]: + self.cognition_count += 1 + ul = UnifiedLanguage().parse(prompt) + if self.lsg_store is not None: + lsg = self.lsg_store.query(tenant_id=tenant_id, ul_packet=ul) + inline_lsg = LongScaleGraph(tuple(memory_facts)).traverse(ul) + if inline_lsg["facts_used"]: + lsg = { + "substrate": "LSG", + "facts_used": list(lsg["facts_used"]) + list(inline_lsg["facts_used"]), + "matches": list(lsg["matches"]) + list(inline_lsg["matches"]), + } + else: + lsg = LongScaleGraph(tuple(memory_facts)).traverse(ul) + if self.provider is not None: + return self._think_with_provider(prompt=prompt, ul=ul, lsg=lsg) + return { + "core": "Nova Cortex", + "ul": ul, + "lsg": lsg, + "text": self._compose_text(ul=ul, lsg=lsg), + } + + def _compose_text(self, *, ul: dict[str, object], lsg: dict[str, object]) -> str: + subject = ul.get("subject") or "the request" + facts = lsg["facts_used"] + if facts: + return f"Under RSL, Nova Cortex reads {subject}: " + "; ".join(facts) + "." + return f"Under RSL, Nova Cortex reads {subject} with no matching LSG facts." + + def _think_with_provider( + self, + *, + prompt: str, + ul: dict[str, object], + lsg: dict[str, object], + ) -> dict[str, object]: + facts = "\n".join(f"- {fact}" for fact in lsg["facts_used"]) or "- no matching LSG facts" + messages = [ + { + "role": "system", + "content": ( + "You are Nova Cortex. Respond under RSL. Use UL intent and LSG facts.\n" + f"UL intent: {ul['intent']}\n" + f"UL subject: {ul['subject']}\n" + f"LSG facts:\n{facts}" + ), + }, + {"role": "user", "content": prompt}, + ] + model = getattr(self.provider, "model", None) + response = asyncio.run( + self.provider.invoke( + messages, + model=model, + max_tokens=2048, + temperature=0.7, + ) + ) + return { + "core": "Nova Cortex", + "ul": ul, + "lsg": lsg, + "text": response.content, + "provider": response.provider or getattr(self.provider, "provider_id", None), + "model": response.model or model, + "input_tokens": response.input_tokens, + "output_tokens": response.output_tokens, + } + + +@dataclass(frozen=True) +class APIKernel: + """Tenant-scoped dispatch spine.""" + + tenant_id: str + capability: str + + tools: dict[str, Any] | None = None + + def route(self, *, prompt: str) -> dict[str, object]: + tool_calls: list[dict[str, Any]] = [] + tool_name = TOOL_BY_CAPABILITY.get(self.capability) + if tool_name and self.tools and tool_name in self.tools: + payload = { + "tenant_id": self.tenant_id, + "capability": self.capability, + "prompt": prompt, + } + result = self.tools[tool_name](payload) + tool_calls.append({"tool": tool_name, "result": result}) + return { + "kernel": "API Kernel", + "tenant_id": self.tenant_id, + "capability": self.capability, + "channel": f"{self.tenant_id}:{self.capability}", + "tool_calls": tool_calls, + } + + +class VossRuntime: + """Immutable enforcement and receipt-signing runtime.""" + + def __init__(self, *, signing_secret: str) -> None: + self._signing_secret = signing_secret.encode("utf-8") + + def execute( + self, + *, + identity: NovaIdentity, + api_kernel: dict[str, str], + nova_cortex: dict[str, object], + rsl: dict[str, str], + prompt: str, + ) -> dict[str, object]: + memory_facts_used = list((nova_cortex.get("lsg") or {}).get("facts_used") or []) + tool_calls = list(api_kernel.get("tool_calls") or []) + output_sha256 = _sha256_text(str(nova_cortex["text"])) + payload = { + "instance_id": identity.instance_id, + "tenant_id": api_kernel["tenant_id"], + "capability": api_kernel["capability"], + "decision": "EXECUTED", + "rsl": rsl["status"], + "policy_decision": rsl["status"], + "prompt_sha256": _sha256_text(prompt), + "output_sha256": output_sha256, + "text_sha256": output_sha256, + "memory_facts_used": memory_facts_used, + "tool_calls": tool_calls, + } + payload["identity"] = { + "instance_id": identity.instance_id, + "tier": identity.tier, + "operator_session_id": identity.operator_session_id, + "tenant_id": api_kernel["tenant_id"], + } + payload["trace"] = { + "trace_id": _trace_id( + instance_id=identity.instance_id, + tenant_id=api_kernel["tenant_id"], + capability=api_kernel["capability"], + prompt=prompt, + ), + "stages": [ + "rsl.validate", + "api_kernel.route", + "nova_cortex.think", + "voss.execute", + ], + "ledger_event": "nova.lawful_llm.executed", + } + payload["authority_boundary"] = { + "operator_authority": "external", + "runtime_authority": "execute_after_rsl", + "rsl_decision": rsl["status"], + "tool_boundary": "api_kernel", + } + payload["reproducibility"] = { + "prompt_sha256": payload["prompt_sha256"], + "output_sha256": output_sha256, + "text_sha256": output_sha256, + "deterministic_core": self._is_deterministic_core(nova_cortex), + "memory_facts_sha256": _sha256_text(json.dumps(memory_facts_used, sort_keys=True)), + "tool_calls_sha256": _sha256_text(json.dumps(tool_calls, sort_keys=True)), + } + if nova_cortex.get("provider"): + payload["provider"] = str(nova_cortex["provider"]) + if nova_cortex.get("model"): + payload["model"] = str(nova_cortex["model"]) + receipt = self.sign_receipt(payload) + ledger.append_jsonl( + { + "event": "nova.lawful_llm.executed", + "tenant_id": api_kernel["tenant_id"], + "capability": api_kernel["capability"], + "receipt_sha256": sha256(receipt["payload"].encode("utf-8")).hexdigest(), + } + ) + return { + "runtime": "Voss Runtime", + "decision": "EXECUTED", + "receipt": receipt, + } + + def _is_deterministic_core(self, nova_cortex: dict[str, object]) -> bool: + return not bool(nova_cortex.get("provider")) + + def sign_receipt(self, payload: dict[str, Any]) -> dict[str, Any]: + serialized = json.dumps(payload, sort_keys=True, separators=(",", ":")) + signature = hmac.new( + self._signing_secret, + serialized.encode("utf-8"), + sha256, + ).hexdigest() + receipt = {"payload": serialized, "signature": signature, "algorithm": "HMAC-SHA256"} + receipt["verified"] = self.verify_receipt(receipt) + return receipt + + def verify_receipt(self, receipt: dict[str, Any]) -> bool: + expected = hmac.new( + self._signing_secret, + receipt["payload"].encode("utf-8"), + sha256, + ).hexdigest() + return hmac.compare_digest(expected, receipt.get("signature", "")) + + +@dataclass(frozen=True) +class LawfulTurn: + text: str + gates_of_wonder: dict[str, str] + nova_cortex: dict[str, object] + api_kernel: dict[str, str] + voss_runtime: dict[str, object] + rsl: dict[str, str] + receipt: dict[str, Any] + + +class LawfulLLM: + """Facade for Gates of Wonder -> Nova Cortex -> API Kernel -> Voss -> RSL.""" + + def __init__( + self, + *, + operator_session_id: str, + signing_secret: str, + law: RuntimeSystemLaw | None = None, + identity: NovaIdentity | None = None, + provider: Any | None = None, + lsg_store: LongScaleGraphStore | None = None, + tools: dict[str, Any] | None = None, + ) -> None: + self.identity = identity or declare_identity( + tier="nova", + operator_session_id=operator_session_id, + ) + require_admitted(run_proof_gate(self.identity, operator_session_active=True)) + self.law = law or RuntimeSystemLaw() + self.cortex = NovaCortex(provider=provider, lsg_store=lsg_store) + self.voss = VossRuntime(signing_secret=signing_secret) + self.tools = tools or {} + + @property + def cognition_count(self) -> int: + return self.cortex.cognition_count + + def ask( + self, + prompt: str, + *, + tenant_id: str, + capability: str, + memory_facts: Iterable[MemoryFact] = (), + ) -> LawfulTurn: + rsl = self.law.validate(tenant_id=tenant_id, capability=capability, prompt=prompt) + api_kernel = APIKernel( + tenant_id=tenant_id, + capability=capability, + tools=self.tools, + ).route(prompt=prompt) + nova_cortex = self.cortex.think( + prompt=prompt, + tenant_id=tenant_id, + memory_facts=memory_facts, + ) + voss_runtime = self.voss.execute( + identity=self.identity, + api_kernel=api_kernel, + nova_cortex=nova_cortex, + rsl=rsl, + prompt=prompt, + ) + gates = { + "interface": "Gates of Wonder", + "presentation": "human_readable_insight", + } + return LawfulTurn( + text=str(nova_cortex["text"]), + gates_of_wonder=gates, + nova_cortex=nova_cortex, + api_kernel=api_kernel, + voss_runtime=voss_runtime, + rsl=rsl, + receipt=voss_runtime["receipt"], + ) + + def verify_receipt(self, receipt: dict[str, Any]) -> bool: + return self.voss.verify_receipt(receipt) diff --git a/nova/metrics.py b/nova/metrics.py new file mode 100644 index 0000000..626f4bb --- /dev/null +++ b/nova/metrics.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import time +from typing import Any + +metrics: dict[str, Any] = { + "requests": 0, + "stream_requests": 0, + "errors": 0, + "last_request_ts": None, + "provider": None, + "model": None, +} + + +def record_request(provider: str, model: str, *, stream: bool = False) -> None: + metrics["requests"] += 1 + if stream: + metrics["stream_requests"] += 1 + metrics["last_request_ts"] = int(time.time()) + metrics["provider"] = provider + metrics["model"] = model + + +def record_error() -> None: + metrics["errors"] += 1 diff --git a/nova/node/__init__.py b/nova/node/__init__.py new file mode 100644 index 0000000..dd0024e --- /dev/null +++ b/nova/node/__init__.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from nova.node.policy import NodeVeto +from nova.node.status import node_status +from nova.node.submit import load_node_result, submit_node_task + +__all__ = [ + "NodeVeto", + "load_node_result", + "node_status", + "submit_node_task", +] diff --git a/nova/node/agent_manifest.py b/nova/node/agent_manifest.py new file mode 100644 index 0000000..f4770e3 --- /dev/null +++ b/nova/node/agent_manifest.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Any +import time +from uuid import uuid4 + +from fastapi import APIRouter + +from nova.node.ledger import stable_hash + + +router = APIRouter() + + +AGENT_MANIFEST: dict[str, Any] = { + "agents": { + "coder": { + "id": "agent-coder", + "role": "blue_team_builder", + "color": "blue", + "capabilities": ["write_patches", "implement_features", "fix_tests"], + "tools": ["coder_tool"], + "governance": { + "can_write_files": True, + "can_modify_deps": True, + "requires_review": True, + "emits_receipts": True, + }, + }, + "wiring": { + "id": "agent-wiring", + "role": "green_team_integrator", + "color": "green", + "capabilities": ["wire_modules", "update_manifests", "configure_routes"], + "tools": ["wiring_tool"], + "governance": { + "can_write_files": True, + "can_modify_config": True, + "requires_review": True, + "emits_receipts": True, + }, + }, + "reviewer": { + "id": "agent-reviewer", + "role": "red_team_inspector", + "color": "red", + "capabilities": ["review_patches", "assess_risk", "check_invariants", "suggest_tests"], + "tools": ["review_tool", "invariant_checker", "risk_assessor"], + "governance": { + "can_write_files": False, + "can_block_actions": True, + "emits_receipts": True, + }, + }, + "security": { + "id": "agent-security", + "role": "purple_team_security", + "color": "purple", + "capabilities": ["scan_dependencies", "detect_secrets", "warn_destructive_ops"], + "tools": ["secscan_tool", "secret_detector", "dependency_diff_analyzer"], + "governance": { + "can_write_files": False, + "can_block_actions": True, + "emits_receipts": True, + }, + }, + "test_engineer": { + "id": "agent-test", + "role": "yellow_team_verifier", + "color": "yellow", + "capabilities": ["generate_tests", "strengthen_tests", "run_tests"], + "tools": ["testgen_tool", "coverage_analyzer", "fuzz_runner"], + "governance": { + "can_write_files": False, + "can_request_patches": True, + "emits_receipts": True, + }, + }, + "architect": { + "id": "agent-architect", + "role": "white_team_system_designer", + "color": "white", + "capabilities": ["design_architecture", "enforce_module_boundaries", "advise_governance"], + "tools": ["archmap_tool", "module_linter", "governance_policy_advisor"], + "governance": { + "can_write_files": False, + "can_veto_patches": True, + "emits_receipts": True, + }, + }, + "operator": { + "id": "agent-operator", + "role": "black_team_runtime_executor", + "color": "black", + "capabilities": ["run_app", "run_migrations", "run_replay", "run_benchmarks"], + "tools": ["runtime_exec", "replay_tool", "benchmark_tool"], + "governance": { + "can_write_files": False, + "emits_receipts": True, + }, + }, + "context_builder": { + "id": "agent-context", + "role": "silver_team_curator", + "color": "silver", + "capabilities": ["pin_files", "collect_logs", "assemble_context"], + "tools": ["context_builder"], + "governance": { + "can_write_files": False, + "emits_receipts": True, + }, + }, + "explainer": { + "id": "agent-explainer", + "role": "gold_team_meta_reasoner", + "color": "gold", + "capabilities": ["explain_patches", "explain_risks", "explain_architecture"], + "tools": ["explain_tool"], + "governance": { + "can_write_files": False, + "emits_receipts": True, + }, + }, + "sentinel": { + "id": "agent-sentinel", + "role": "orange_team_governance_enforcer", + "color": "orange", + "capabilities": ["validate_receipts", "enforce_policy", "detect_drift", "trigger_rollback"], + "tools": ["governance_validator", "drift_detector", "rollback_tool"], + "governance": { + "can_write_files": False, + "can_block_actions": True, + "emits_receipts": True, + }, + }, + }, + "orchestration": { + "status": "manifest_only", + "guardrail": "Tools remain stateless Ring-2 invocations; color-team loops are future UI/workflow guidance, not Node-side orchestration.", + }, +} + + +TOOL_MANIFEST: list[dict[str, Any]] = [ + {"name": "coder_tool", "type": "governed", "scope": "code_edit", "stateless": True, "owner_agent": "agent-coder"}, + {"name": "wiring_tool", "type": "governed", "scope": "config_wiring", "stateless": True, "owner_agent": "agent-wiring"}, + {"name": "review_tool", "type": "governed", "scope": "patch_review", "stateless": True, "owner_agent": "agent-reviewer"}, + {"name": "invariant_checker", "type": "governed", "scope": "invariant_check", "stateless": True, "owner_agent": "agent-reviewer"}, + {"name": "risk_assessor", "type": "governed", "scope": "risk_assessment", "stateless": True, "owner_agent": "agent-reviewer"}, + {"name": "secscan_tool", "type": "governed", "scope": "security_scan", "stateless": True, "owner_agent": "agent-security"}, + {"name": "secret_detector", "type": "governed", "scope": "secret_detection", "stateless": True, "owner_agent": "agent-security"}, + {"name": "dependency_diff_analyzer", "type": "governed", "scope": "dependency_diff", "stateless": True, "owner_agent": "agent-security"}, + {"name": "testgen_tool", "type": "governed", "scope": "test_generation", "stateless": True, "owner_agent": "agent-test"}, + {"name": "coverage_analyzer", "type": "governed", "scope": "coverage_analysis", "stateless": True, "owner_agent": "agent-test"}, + {"name": "fuzz_runner", "type": "governed", "scope": "fuzz_testing", "stateless": True, "owner_agent": "agent-test"}, + {"name": "archmap_tool", "type": "governed", "scope": "architecture_mapping", "stateless": True, "owner_agent": "agent-architect"}, + {"name": "module_linter", "type": "governed", "scope": "module_lint", "stateless": True, "owner_agent": "agent-architect"}, + {"name": "governance_policy_advisor", "type": "governed", "scope": "policy_advice", "stateless": True, "owner_agent": "agent-architect"}, + {"name": "runtime_exec", "type": "governed", "scope": "runtime_execution", "stateless": True, "owner_agent": "agent-operator"}, + {"name": "replay_tool", "type": "governed", "scope": "replay", "stateless": True, "owner_agent": "agent-operator"}, + {"name": "benchmark_tool", "type": "governed", "scope": "benchmark", "stateless": True, "owner_agent": "agent-operator"}, + {"name": "context_builder", "type": "governed", "scope": "context_assembly", "stateless": True, "owner_agent": "agent-context"}, + {"name": "explain_tool", "type": "governed", "scope": "explanation", "stateless": True, "owner_agent": "agent-explainer"}, + {"name": "governance_validator", "type": "governed", "scope": "receipt_validation", "stateless": True, "owner_agent": "agent-sentinel"}, + {"name": "drift_detector", "type": "governed", "scope": "drift_detection", "stateless": True, "owner_agent": "agent-sentinel"}, + {"name": "rollback_tool", "type": "governed", "scope": "rollback", "stateless": True, "owner_agent": "agent-sentinel"}, +] + +IMPLEMENTED_TOOL_NAMES = {"coder_tool", "wiring_tool"} + + +@dataclass(frozen=True) +class AgentIdentity: + id: str + role: str + color: str + capabilities: list[str] + tools: list[str] + can_write_files: bool = False + can_block_actions: bool = False + can_modify_deps: bool = False + can_modify_config: bool = False + can_request_patches: bool = False + can_veto_patches: bool = False + emits_receipts: bool = True + requires_review: bool = False + + +@dataclass(frozen=True) +class AgentReceipt: + id: str + agent_id: str + action: str + payload: dict[str, Any] + policy_version: str + trace_id: str + timestamp: float + + def compute_output_hash(self, output: Any) -> str: + return stable_hash(output) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +class AgentRegistry: + def __init__(self) -> None: + self._agents: dict[str, AgentIdentity] = {} + + def register(self, agent: AgentIdentity) -> None: + self._agents[agent.id] = agent + + def get(self, agent_id: str) -> AgentIdentity: + return self._agents[agent_id] + + def can(self, agent_id: str, capability: str) -> bool: + return capability in self.get(agent_id).capabilities + + def all(self) -> list[AgentIdentity]: + return list(self._agents.values()) + + +class CapabilityGate: + def __init__(self, registry: AgentRegistry, policy_version: str) -> None: + self.registry = registry + self.policy_version = policy_version + + def authorize(self, agent_id: str, action: str, payload: dict[str, Any]) -> AgentReceipt: + agent = self.registry.get(agent_id) + if action == "write_files" and not agent.can_write_files: + raise PermissionError(f"{agent_id} cannot write files") + if action == "modify_deps" and not agent.can_modify_deps: + raise PermissionError(f"{agent_id} cannot modify dependencies") + if action == "modify_config" and not agent.can_modify_config: + raise PermissionError(f"{agent_id} cannot modify config") + if action not in _allowed_actions(agent): + raise PermissionError(f"{agent_id} cannot perform {action}") + return AgentReceipt( + id=str(uuid4()), + agent_id=agent_id, + action=action, + payload=payload, + policy_version=self.policy_version, + trace_id=str(payload.get("trace_id") or f"trace-{agent_id}"), + timestamp=time.time(), + ) + + +def default_agent_registry() -> AgentRegistry: + registry = AgentRegistry() + for data in AGENT_MANIFEST["agents"].values(): + governance = data.get("governance") or {} + registry.register( + AgentIdentity( + id=str(data["id"]), + role=str(data["role"]), + color=str(data["color"]), + capabilities=list(data["capabilities"]), + tools=list(data["tools"]), + can_write_files=bool(governance.get("can_write_files", False)), + can_block_actions=bool(governance.get("can_block_actions", False)), + can_modify_deps=bool(governance.get("can_modify_deps", False)), + can_modify_config=bool(governance.get("can_modify_config", False)), + can_request_patches=bool(governance.get("can_request_patches", False)), + can_veto_patches=bool(governance.get("can_veto_patches", False)), + emits_receipts=bool(governance.get("emits_receipts", True)), + requires_review=bool(governance.get("requires_review", False)), + ) + ) + return registry + + +def tool_manifest(name: str) -> dict[str, Any] | None: + for entry in TOOL_MANIFEST: + if entry["name"] == name: + return entry + return None + + +def agent_tool_manifest() -> list[dict[str, Any]]: + return [ + { + **entry, + "implemented": entry["name"] in IMPLEMENTED_TOOL_NAMES, + } + for entry in TOOL_MANIFEST + ] + + +@router.get("/node/agents") +async def get_agents() -> dict[str, Any]: + return AGENT_MANIFEST + + +@router.get("/node/agent-tools") +async def get_agent_tools() -> dict[str, Any]: + return {"tools": agent_tool_manifest()} + + +def _allowed_actions(agent: AgentIdentity) -> set[str]: + actions = set(agent.capabilities) + if agent.can_write_files: + actions.add("write_files") + if agent.can_modify_deps: + actions.add("modify_deps") + if agent.can_modify_config: + actions.add("modify_config") + if agent.can_block_actions: + actions.add("block_actions") + if agent.can_request_patches: + actions.add("request_patches") + if agent.can_veto_patches: + actions.add("veto_patches") + return actions diff --git a/nova/node/agent_manifest.yaml b/nova/node/agent_manifest.yaml new file mode 100644 index 0000000..820a950 --- /dev/null +++ b/nova/node/agent_manifest.yaml @@ -0,0 +1,103 @@ +agents: + coder: + id: agent-coder + role: blue_team_builder + color: blue + capabilities: [write_patches, implement_features, fix_tests] + tools: [coder_tool] + governance: + can_write_files: true + can_modify_deps: true + requires_review: true + emits_receipts: true + wiring: + id: agent-wiring + role: green_team_integrator + color: green + capabilities: [wire_modules, update_manifests, configure_routes] + tools: [wiring_tool] + governance: + can_write_files: true + can_modify_config: true + requires_review: true + emits_receipts: true + reviewer: + id: agent-reviewer + role: red_team_inspector + color: red + capabilities: [review_patches, assess_risk, check_invariants, suggest_tests] + tools: [review_tool, invariant_checker, risk_assessor] + governance: + can_write_files: false + can_block_actions: true + emits_receipts: true + security: + id: agent-security + role: purple_team_security + color: purple + capabilities: [scan_dependencies, detect_secrets, warn_destructive_ops] + tools: [secscan_tool, secret_detector, dependency_diff_analyzer] + governance: + can_write_files: false + can_block_actions: true + emits_receipts: true + test_engineer: + id: agent-test + role: yellow_team_verifier + color: yellow + capabilities: [generate_tests, strengthen_tests, run_tests] + tools: [testgen_tool, coverage_analyzer, fuzz_runner] + governance: + can_write_files: false + can_request_patches: true + emits_receipts: true + architect: + id: agent-architect + role: white_team_system_designer + color: white + capabilities: [design_architecture, enforce_module_boundaries, advise_governance] + tools: [archmap_tool, module_linter, governance_policy_advisor] + governance: + can_write_files: false + can_veto_patches: true + emits_receipts: true + operator: + id: agent-operator + role: black_team_runtime_executor + color: black + capabilities: [run_app, run_migrations, run_replay, run_benchmarks] + tools: [runtime_exec, replay_tool, benchmark_tool] + governance: + can_write_files: false + emits_receipts: true + context_builder: + id: agent-context + role: silver_team_curator + color: silver + capabilities: [pin_files, collect_logs, assemble_context] + tools: [context_builder] + governance: + can_write_files: false + emits_receipts: true + explainer: + id: agent-explainer + role: gold_team_meta_reasoner + color: gold + capabilities: [explain_patches, explain_risks, explain_architecture] + tools: [explain_tool] + governance: + can_write_files: false + emits_receipts: true + sentinel: + id: agent-sentinel + role: orange_team_governance_enforcer + color: orange + capabilities: [validate_receipts, enforce_policy, detect_drift, trigger_rollback] + tools: [governance_validator, drift_detector, rollback_tool] + governance: + can_write_files: false + can_block_actions: true + emits_receipts: true +orchestration: + status: manifest_only + guardrail: Tools remain stateless Ring-2 invocations; color-team loops are future UI/workflow guidance, not Node-side orchestration. diff --git a/nova/node/alerts.py b/nova/node/alerts.py new file mode 100644 index 0000000..12f0fd2 --- /dev/null +++ b/nova/node/alerts.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import time +from typing import Any + +from fastapi import APIRouter + +from nova.node.ledger import read_jsonl, read_ledger, runtime_dir +from nova.node.mesh import get_mesh + +router = APIRouter() + + +def list_alerts() -> list[dict[str, Any]]: + alerts: list[dict[str, Any]] = [] + now = int(time.time()) + for entry in read_ledger(): + if entry.get("entry_type") == "nodeGossipReceipt" and entry.get("signature_valid") is False: + summary = entry.get("summary") or {} + node_id = str(summary.get("node_id") or "unknown-peer") + alerts.append( + { + "id": f"invalid-signature-{node_id}", + "timestamp": entry.get("timestamp", now), + "severity": "error", + "category": "federation", + "message": "Invalid gossip signature", + "context": {"node_id": node_id}, + } + ) + mesh = get_mesh() + for node_id in mesh.get("divergent_peers", []): + alerts.append( + { + "id": f"policy-drift-{node_id}", + "timestamp": now, + "severity": "warn", + "category": "policy", + "message": "Peer policy hash diverges from local node", + "context": {"node_id": node_id}, + } + ) + blocked_by_caller: dict[str, int] = {} + for event in read_jsonl(runtime_dir() / "rate-limits.jsonl"): + if event.get("blocked"): + caller = str(event.get("caller_id") or "unknown") + blocked_by_caller[caller] = blocked_by_caller.get(caller, 0) + 1 + for caller, count in blocked_by_caller.items(): + alerts.append( + { + "id": f"rate-limit-{caller}", + "timestamp": now, + "severity": "info", + "category": "rate-limit", + "message": "Rate-limit block recorded", + "context": {"caller_id": caller, "blocked_count": count}, + } + ) + return alerts + + +@router.get("/node/alerts") +async def alerts() -> dict[str, list[dict[str, Any]]]: + return {"alerts": list_alerts()} diff --git a/nova/node/ceb.py b/nova/node/ceb.py new file mode 100644 index 0000000..8f6eaa2 --- /dev/null +++ b/nova/node/ceb.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import hashlib +import json +import shutil +import time +import zipfile +from pathlib import Path +from typing import Any + +from nova.node.conformance import generate_n0_report +from nova.node.continuity import continuity_log_path +from nova.node.identity import NodeIdentity +from nova.node.ledger import governance_ledger_path, read_ledger, runtime_dir, stable_json +from nova.node.policy import GovernanceRuntime + + +def generate_evidence_bundle(output_dir: str | Path, *, report: dict[str, Any] | None = None) -> dict[str, Any]: + root = Path(output_dir) + if root.exists(): + shutil.rmtree(root) + root.mkdir(parents=True) + report = report or generate_n0_report() + identity = NodeIdentity.load("./policy.yaml") + policy = GovernanceRuntime().policy + + _write_json(root / "identity" / "identity.json", { + "node_id": identity.node_id, + "operator_id": identity.operator_id, + "policy_hash": identity.policy_hash, + "public_key": None, + }) + _write_text(root / "policy" / "policy.yaml", Path("./policy.yaml").read_text(encoding="utf-8") if Path("./policy.yaml").exists() else stable_json(policy)) + _write_text(root / "policy" / "policy_hash.txt", identity.policy_hash + "\n") + _write_text(root / "policy" / "policy_version.txt", str(policy.get("version", "1.0")) + "\n") + + continuity_path = continuity_log_path() + _write_text( + root / "continuity" / "continuity.log", + continuity_path.read_text(encoding="utf-8") if continuity_path.exists() else "", + ) + _write_text(root / "continuity" / "continuity_hash.txt", _file_hash(root / "continuity" / "continuity.log") + "\n") + + for entry in read_ledger(): + target = "federation/gossip.jsonl" if entry.get("entry_type") == "nodeGossipReceipt" else "federation/handshake.jsonl" + _append_text(root / target, stable_json(entry) + "\n") + _write_json(root / "federation" / "trust_levels.json", _trust_levels()) + + receipts_dir = root / "receipts" + receipts_dir.mkdir(parents=True, exist_ok=True) + for result_path in (runtime_dir() / "results").glob("*.json"): + target = receipts_dir / result_path.name + target.write_text(result_path.read_text(encoding="utf-8"), encoding="utf-8") + + _write_json(root / "conformance" / "n0_report.json", report) + _write_text(root / "conformance" / "n0_report_hash.txt", _file_hash(root / "conformance" / "n0_report.json") + "\n") + for folder in ["replay/original", "replay/replayed", "replay/diff", "mesh"]: + (root / folder).mkdir(parents=True, exist_ok=True) + + files = [ + {"path": str(path.relative_to(root)).replace("\\", "/"), "sha256": _file_hash(path)} + for path in sorted(root.rglob("*")) + if path.is_file() + ] + manifest = { + "version": "CEB-1.0", + "node_id": identity.node_id, + "operator_id": identity.operator_id, + "policy_hash": identity.policy_hash, + "generated_on": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "files": files, + } + _write_json(root / "manifest.json", manifest) + bundle_path = root.with_suffix(".zip") + _zip_dir(root, bundle_path) + manifest["bundle_hash"] = _file_hash(bundle_path) + _write_json(root / "manifest.json", manifest) + _zip_dir(root, bundle_path) + return { + "bundle_path": str(bundle_path), + "manifest_path": str(root / "manifest.json"), + "manifest": manifest, + } + + +def _write_json(path: Path, data: Any) -> None: + _write_text(path, json.dumps(data, indent=2, sort_keys=True) + "\n") + + +def _write_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def _append_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8", newline="\n") as fh: + fh.write(text) + + +def _file_hash(path: Path) -> str: + return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest() + + +def _zip_dir(root: Path, bundle_path: Path) -> None: + if bundle_path.exists(): + bundle_path.unlink() + with zipfile.ZipFile(bundle_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for path in sorted(root.rglob("*")): + if path.is_file(): + archive.write(path, path.relative_to(root)) + + +def _trust_levels() -> dict[str, str]: + levels: dict[str, str] = {} + for entry in read_ledger(): + summary = entry.get("summary") or {} + node_id = summary.get("node_id") + if node_id: + levels[str(node_id)] = str(entry.get("trust_level") or "limited") + return levels diff --git a/nova/node/conformance.py b/nova/node/conformance.py new file mode 100644 index 0000000..7100de8 --- /dev/null +++ b/nova/node/conformance.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import time +from typing import Any + +from fastapi import APIRouter + +from nova.node.continuity import read_events +from nova.node.federation import node_hello +from nova.node.identity import NodeIdentity +from nova.node.ledger import stable_hash +from nova.node.policy import GovernanceRuntime + +router = APIRouter() + + +@router.get("/node/conformance/n0") +async def n0_report() -> dict[str, Any]: + return generate_n0_report() + + +@router.get("/node/conformance/n0/badge") +async def n0_badge() -> dict[str, Any]: + report = generate_n0_report() + return generate_n0_badge(report) + + +@router.post("/node/evidence-bundle") +async def evidence_bundle() -> dict[str, Any]: + from nova.node.ceb import generate_evidence_bundle + + report = generate_n0_report() + return generate_evidence_bundle(".runtime/node/evidence-bundle", report=report) + + +def generate_n0_report(policy_path: str = "./policy.yaml") -> dict[str, Any]: + identity = NodeIdentity.load(policy_path) + runtime = GovernanceRuntime(policy_path) + policy = runtime.policy + events = read_events() + receipts = [event for event in events if event.get("entry_type") == "nodeExecutionReceipt"] + sections = { + "identity_policy": [ + _check("Node exposes node_id", bool(identity.node_id), "/node/status output"), + _check("Node exposes operator_id", bool(identity.operator_id), "/node/status output"), + _check("Node exposes policy_hash", len(identity.policy_hash) == 64, "SHA-256(policy.yaml)"), + _check("Policy manifest loads", bool(policy), "policy.yaml parsed"), + _check("Policy invariants present", "boundedness" in policy.get("invariants", {}), "boundedness"), + ], + "governance_runtime": [ + _check("Boundedness enforced", "boundedness" in policy.get("invariants", {}), "max token checks"), + _check("Banned intents configured", "banned_intents" in policy.get("safety", {}), "safety.banned_intents"), + _check("Rate limits configured", "rate_limits" in policy.get("safety", {}), "safety.rate_limits"), + _check("Governance receipts produced", bool(receipts), "trace_id, policy_version"), + ], + "continuity": [ + _check("continuity events logged", bool(events), "continuity.jsonl"), + _check("continuity receipts logged", bool(receipts), "nodeExecutionReceipt"), + ], + "federation": [ + _check("/node/hello implemented", bool(node_hello().get("node_id")), "handshake response"), + _check("RSA signed gossip supported", "signature" in node_hello(), "signature field"), + _check("Trust levels supported", bool(node_hello().get("trust_level")), "trusted/limited/invalid"), + ], + "openai_gateway": [ + _check("/v1/models declared", True, "OpenAI gateway"), + _check("/v1/chat/completions declared", True, "OpenAI gateway"), + _check("SSE streaming declared", True, "chat.completion.chunk"), + ], + "operator_introspection": [ + _check("/node/status declared", True, "governance health"), + _check("/node/receipts declared", True, "receipts list"), + _check("/node/ledger declared", True, "ledger entries"), + ], + } + all_checks = [item for checks in sections.values() for item in checks] + return { + "profile": "N0", + "node_implementation": "lawful-nova-shell", + "version": "0.1", + "operator": identity.operator_id, + "date": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "policy_version": str(policy.get("version", "1.0")), + "policy_hash": identity.policy_hash, + "sections": sections, + "verdict": { + "conformant": all(check["pass"] for check in all_checks), + "required_fixes": [check["requirement"] for check in all_checks if not check["pass"]], + }, + } + + +def generate_n0_badge(report: dict[str, Any], policy_path: str = "./policy.yaml") -> dict[str, Any]: + identity = NodeIdentity.load(policy_path) + return { + "profile": "N0", + "profile_name": "Minimal Constitutional Node", + "status": "conformant" if report.get("verdict", {}).get("conformant") else "non-conformant", + "node_id": identity.node_id, + "operator_id": identity.operator_id, + "policy_hash": report["policy_hash"], + "conformance_hash": stable_hash(report), + "issued_by": "Constitutional Runtime Working Group", + "issued_on": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "meaning": [ + "identity & policy loading", + "invariant enforcement", + "boundedness & banned-intent checks", + "rate-limit enforcement", + "continuity ledger integrity", + "provenance receipts", + "RSA-signed gossip", + "federation handshake", + "OpenAI/Cursor compatibility", + "operator introspection endpoints", + ], + } + + +def _check(requirement: str, passed: bool, evidence: str) -> dict[str, Any]: + return {"requirement": requirement, "evidence": evidence, "pass": bool(passed)} diff --git a/nova/node/consensus.py b/nova/node/consensus.py new file mode 100644 index 0000000..4113697 --- /dev/null +++ b/nova/node/consensus.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from collections import Counter +from typing import Any + + +def policy_hash_consensus(summaries: list[dict[str, Any]]) -> dict[str, Any]: + trusted = [ + summary for summary in summaries + if summary.get("signature_valid") is True or summary.get("trust_level") in {"trusted", "self"} + ] + if trusted: + summaries = trusted + hashes = [str(summary.get("policy_hash") or "") for summary in summaries if summary.get("policy_hash")] + if not hashes: + return {"consensus_reached": False, "policy_hash": None, "agreement_ratio": 0.0} + counts = Counter(hashes) + policy_hash, count = counts.most_common(1)[0] + return { + "consensus_reached": count / len(hashes) >= 0.5, + "policy_hash": policy_hash, + "agreement_ratio": count / len(hashes), + } diff --git a/nova/node/continuity.py b/nova/node/continuity.py new file mode 100644 index 0000000..3f727c9 --- /dev/null +++ b/nova/node/continuity.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import time +from typing import Any +from uuid import uuid4 + +from nova.node.ledger import append_jsonl, read_jsonl, runtime_dir, stable_hash + + +def continuity_log_path(): + return runtime_dir() / "continuity.jsonl" + + +def append_event(kind: str, data: dict[str, Any]) -> dict[str, Any]: + entry = { + "event_id": f"{kind}-{uuid4()}", + "timestamp": int(time.time()), + "kind": kind, + "data": data, + } + return append_jsonl(continuity_log_path(), entry) + + +def append_receipt(entry: dict[str, Any]) -> dict[str, Any]: + return append_jsonl(continuity_log_path(), entry) + + +def read_events() -> list[dict[str, Any]]: + return read_jsonl(continuity_log_path()) + + +def last_receipt_hash() -> str | None: + events = read_events() + if not events: + return None + last = events[-1] + return str(last.get("receipt_hash") or stable_hash(last)) diff --git a/nova/node/event_bus.py b/nova/node/event_bus.py new file mode 100644 index 0000000..ca02c26 --- /dev/null +++ b/nova/node/event_bus.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, Callable +import time +from uuid import uuid4 + +from fastapi import APIRouter, Query + +from nova.node.feature_manifest import feature_manifest +from nova.node.ledger import append_jsonl, read_jsonl, runtime_dir + + +DEFAULT_CHANNELS = ["governance.*", "test.*", "git.*", "tool.*", "replay.*", "node.*"] +DEFAULT_MAX_EVENTS = 5000 + +router = APIRouter() + + +@dataclass(frozen=True) +class Event: + id: str + channel: str + type: str + payload: dict[str, Any] + timestamp: float + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, payload: dict[str, Any]) -> "Event": + return cls( + id=str(payload.get("id") or ""), + channel=str(payload.get("channel") or ""), + type=str(payload.get("type") or ""), + payload=dict(payload.get("payload") or {}), + timestamp=float(payload.get("timestamp") or 0), + ) + + +class EventBus: + def __init__( + self, + channels: list[str] | None = None, + max_events: int = DEFAULT_MAX_EVENTS, + *, + persist: bool = True, + path: Path | None = None, + ) -> None: + self.channels = list(channels or DEFAULT_CHANNELS) + self.max_events = max_events + self.persist = persist + self.path = path or event_log_path() + self._subscribers: dict[str, list[Callable[[Event], None]]] = {} + self._history = self._load_history() + + def subscribe(self, pattern: str, handler: Callable[[Event], None]) -> None: + self._subscribers.setdefault(pattern, []).append(handler) + + def emit(self, channel: str, type_: str, payload: dict[str, Any] | None = None) -> Event: + if not self._allowed_channel(channel): + raise ValueError(f"unsupported event channel: {channel}") + event = Event( + id=str(uuid4()), + channel=channel, + type=type_, + payload=payload or {}, + timestamp=time.time(), + ) + self._append_history(event) + if self.persist: + append_jsonl(self.path, event.to_dict()) + self._dispatch(event) + return event + + def history(self, channel_prefix: str | None = None, *, limit: int | None = None) -> list[Event]: + events = list(self._history) + if channel_prefix: + events = [event for event in events if event.channel.startswith(channel_prefix)] + if limit is not None: + events = events[-limit:] + return events + + def _append_history(self, event: Event) -> None: + self._history.append(event) + if len(self._history) > self.max_events: + self._history = self._history[-self.max_events :] + + def _dispatch(self, event: Event) -> None: + for pattern, handlers in self._subscribers.items(): + if _matches(pattern, event.channel): + for handler in handlers: + handler(event) + + def _allowed_channel(self, channel: str) -> bool: + return any(_matches(pattern, channel) for pattern in self.channels) + + def _load_history(self) -> list[Event]: + if not self.persist: + return [] + events: list[Event] = [] + for entry in read_jsonl(self.path): + try: + event = Event.from_dict(entry) + except (TypeError, ValueError): + continue + if event.channel and self._allowed_channel(event.channel): + events.append(event) + return events[-self.max_events :] + + +def event_log_path() -> Path: + return runtime_dir() / "event-bus.jsonl" + + +def read_event_history(channel_prefix: str | None = None, *, limit: int | None = None) -> list[dict[str, Any]]: + events = [Event.from_dict(entry) for entry in read_jsonl(event_log_path())] + if channel_prefix: + events = [event for event in events if event.channel.startswith(channel_prefix)] + if limit is not None: + events = events[-limit:] + return [event.to_dict() for event in events] + + +_BUS_CACHE: dict[tuple[str, int], EventBus] = {} + + +def get_event_bus(*, max_events: int = DEFAULT_MAX_EVENTS) -> EventBus: + path = event_log_path() + key = (str(path.resolve()), max_events) + bus = _BUS_CACHE.get(key) + if bus is None: + bus = EventBus(max_events=max_events, path=path) + _BUS_CACHE[key] = bus + return bus + + +def reset_event_bus() -> None: + _BUS_CACHE.clear() + + +@router.get("/node/events") +async def get_node_events( + prefix: str | None = None, + limit: int = Query(default=500, ge=1, le=5000), +) -> dict[str, Any]: + return { + "channels": DEFAULT_CHANNELS, + "events": read_event_history(prefix, limit=limit), + } + + +@router.get("/node/feature-manifest") +async def get_feature_manifest() -> dict[str, Any]: + return {"manifest": feature_manifest()} + + +def _matches(pattern: str, channel: str) -> bool: + if pattern.endswith(".*"): + return channel.startswith(pattern[:-1]) + return pattern == channel diff --git a/nova/node/evidence.py b/nova/node/evidence.py new file mode 100644 index 0000000..4b56e85 --- /dev/null +++ b/nova/node/evidence.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, HTTPException, Request + +from nova.node.continuity import read_events +from nova.node.event_bus import read_event_history +from nova.node.feature_manifest import feature_manifest +from nova.node.ledger import stable_hash +from nova.node.policy import load_node_policy +from nova.node.tools import replay_coding + + +router = APIRouter() + +COMPARE_KEYS = ["receipt_hash", "input_hash", "output_hash", "policy_version", "tool", "intent"] + + +@router.get("/node/verify/{trace_id}") +async def verify_trace(trace_id: str) -> dict[str, Any]: + receipt = _load_tool_receipt(trace_id) + replay = _replay_verification(trace_id, receipt) + return { + "trace_id": trace_id, + "receipt": receipt, + "verification": { + "receipt_hash": _check_hash(receipt, _receipt_material(receipt), "receipt_hash"), + "input_hash": _check_hash(receipt, receipt.get("input_snapshot"), "input_hash"), + "output_hash": _check_hash(receipt, receipt.get("output_snapshot"), "output_hash"), + "original_code_hash": _check_hash(receipt, receipt.get("current_code", ""), "original_code_hash"), + "replay": replay, + }, + "policy": _policy_evidence(receipt), + "trace": trace_evidence(trace_id), + "cross_node": { + "comparable": True, + "compare_keys": COMPARE_KEYS, + }, + } + + +@router.get("/node/trace/{trace_id}") +async def get_trace(trace_id: str) -> dict[str, Any]: + return {"trace_id": trace_id, **trace_evidence(trace_id)} + + +@router.post("/node/compare-receipts") +async def compare_receipts(request: Request) -> dict[str, Any]: + payload = await request.json() + left = payload.get("left") or {} + right = payload.get("right") or {} + matches = { + key: left.get(key) == right.get(key) + for key in COMPARE_KEYS + if key in left or key in right + } + drift_keys = [key for key, matched in matches.items() if not matched] + return { + "matching": not drift_keys, + "matches": matches, + "drift_keys": drift_keys, + "compare_keys": COMPARE_KEYS, + } + + +def trace_evidence(trace_id: str) -> dict[str, Any]: + return { + "events": [ + event for event in read_event_history() + if (event.get("payload") or {}).get("trace_id") == trace_id + ], + "continuity": [ + _normalize_continuity_event(event) for event in read_events() + if event.get("trace_id") == trace_id or (event.get("data") or {}).get("trace_id") == trace_id + ], + } + + +def _load_tool_receipt(trace_id: str) -> dict[str, Any]: + try: + return replay_coding.load_receipt(trace_id) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail="trace receipt not found") from exc + + +def _receipt_material(receipt: dict[str, Any]) -> dict[str, Any]: + return {key: value for key, value in receipt.items() if key != "receipt_hash"} + + +def _check_hash(receipt: dict[str, Any], material: Any, field: str) -> dict[str, Any]: + expected = receipt.get(field) + actual = stable_hash(material) if material is not None else None + return { + "expected": expected, + "actual": actual, + "valid": bool(expected and actual and expected == actual), + } + + +def _replay_verification(trace_id: str, receipt: dict[str, Any]) -> dict[str, Any]: + if receipt.get("intent") != "code": + return {"available": False, "reason": "replay currently supports code receipts"} + try: + replayed = replay_coding.replay_coding(trace_id) + except Exception as exc: + return {"available": False, "reason": str(exc)} + return { + "available": True, + "deterministic": bool(replayed.get("deterministic")), + "policy_version": replayed.get("policy_version"), + "diff_against_original": replayed.get("diff_against_original"), + } + + +def _policy_evidence(receipt: dict[str, Any]) -> dict[str, Any]: + policy = load_node_policy() + return { + "version": receipt.get("policy_version") or policy.get("policy_version"), + "current_version": policy.get("policy_version"), + "policy_hash": policy.get("policy_hash"), + "manifest": feature_manifest(), + } + + +def _normalize_continuity_event(event: dict[str, Any]) -> dict[str, Any]: + return { + **event, + "kind": event.get("kind") or event.get("entry_type") or "continuity", + } diff --git a/nova/node/feature_manifest.py b/nova/node/feature_manifest.py new file mode 100644 index 0000000..5a5b12c --- /dev/null +++ b/nova/node/feature_manifest.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from typing import Any + + +FEATURE_MANIFEST: dict[str, Any] = { + "node": { + "id": "local-node-01", + "version": "1.0.0", + "description": "Sovereign Node feature manifest for Nova Desktop", + "startup": { + "auto_load": True, + "health_check": True, + "governance_mode": "strict", + }, + }, + "modules": { + "core": [ + "file_search", + "symbol_search", + "patch_manager", + "terminal", + "test_runner", + "git_panel", + ], + "nova_specific": [ + "governance_receipt_inspector", + "replay_timeline", + "tool_registry", + "node_health_strip", + "local_model_selector", + ], + "power_user": [ + "command_palette", + "context_builder", + "explain_mode", + "project_templates", + "safety_rails", + ], + }, + "ui_panels": [ + { + "name": "DiffViewer", + "component": "diff_panel", + "features": ["apply_patch", "reject_hunk", "rollback_patch"], + }, + { + "name": "Terminal", + "component": "terminal_panel", + "features": ["run_tests", "run_app", "run_lint", "link_output"], + }, + { + "name": "TestRunner", + "component": "test_panel", + "features": ["run_file_tests", "run_failed_tests", "fix_failure"], + }, + { + "name": "GitPanel", + "component": "git_panel", + "features": ["changed_files", "staged_diff", "commit_generator", "pr_summary"], + }, + { + "name": "GovernanceInspector", + "component": "governance_panel", + "features": ["policy_version", "trace_id", "input_output_hashes"], + }, + { + "name": "ReplayTimeline", + "component": "replay_panel", + "features": ["deterministic_replay", "drift_warning", "rollback"], + }, + { + "name": "NodeHealth", + "component": "health_strip", + "metrics": ["model", "online_status", "receipts_count", "latency", "governance_state"], + }, + { + "name": "ModelSelector", + "component": "model_dropdown", + "models": ["qwen2.5-coder:3b", "qwen2.5-coder:7b"], + }, + ], + "governance": { + "receipts": { + "verify_function": "verify_receipt", + "ledger_path": "./ledger/receipts.db", + "replay_path": "./ledger/replay.db", + }, + "policies": { + "enforcement": "strict", + "versioning": "semantic", + "audit_log": True, + }, + }, + "templates": [ + "python_fastapi", + "node_express", + "rust_actix", + "governed_nova_tool", + "local_ai_app", + ], + "safety": { + "auto_write": False, + "warn_large_patches": True, + "warn_secrets": True, + "confirm_dependency_changes": True, + }, + "runtime": { + "hooks": { + "on_patch_applied": { + "emit": "governance.patch_applied", + "record_receipt": True, + "replayable": True, + }, + "on_patch_rejected": { + "emit": "governance.patch_rejected", + "record_receipt": True, + }, + "on_hunk_applied": { + "emit": "governance.hunk_applied", + "record_receipt": True, + }, + "on_hunk_rejected": { + "emit": "governance.hunk_rejected", + "record_receipt": True, + }, + "on_patch_rollback": { + "emit": "governance.patch_rollback", + "record_receipt": True, + "replayable": True, + }, + "on_test_run_started": { + "emit": "test.run_started", + "attach_context": True, + }, + "on_test_run_completed": { + "emit": "test.run_completed", + "record_receipt": True, + "attach_output": True, + }, + "on_test_failure_detected": { + "emit": "test.failure", + "record_receipt": True, + "auto_context": "failing_test_output", + }, + "on_commit_created": { + "emit": "git.commit", + "record_receipt": True, + }, + "on_branch_changed": { + "emit": "git.branch_changed", + }, + "on_tool_invoked": { + "emit": "tool.invoked", + "record_receipt": True, + "include": ["tool_name", "args_hash", "governed_state"], + }, + "on_tool_completed": { + "emit": "tool.completed", + "record_receipt": True, + "include": ["output_hash", "duration_ms"], + }, + "on_receipt_verified": { + "emit": "governance.receipt_verified", + "update_health": True, + }, + "on_receipt_blocked": { + "emit": "governance.receipt_blocked", + "update_health": True, + "attach_policy": True, + }, + "on_replay_started": { + "emit": "replay.started", + "attach_trace": True, + }, + "on_replay_completed": { + "emit": "replay.completed", + "record_receipt": True, + }, + "on_replay_drift": { + "emit": "replay.drift_detected", + "severity": "high", + "auto_flag": True, + "suggest_rollback": True, + }, + "on_node_online": { + "emit": "node.online", + "update_health": True, + }, + "on_node_offline": { + "emit": "node.offline", + "update_health": True, + }, + "on_latency_spike": { + "emit": "node.latency_spike", + "severity": "medium", + }, + }, + }, + "event_bus": { + "channels": ["governance.*", "test.*", "git.*", "tool.*", "replay.*", "node.*"], + "transport": "internal", + "persistence": True, + "max_events": 5000, + }, +} + + +def feature_manifest() -> dict[str, Any]: + return FEATURE_MANIFEST diff --git a/nova/node/feature_manifest.yaml b/nova/node/feature_manifest.yaml new file mode 100644 index 0000000..d8fde00 --- /dev/null +++ b/nova/node/feature_manifest.yaml @@ -0,0 +1,154 @@ +node: + id: local-node-01 + version: 1.0.0 + description: Sovereign Node feature manifest for Nova Desktop + startup: + auto_load: true + health_check: true + governance_mode: strict +modules: + core: + - file_search + - symbol_search + - patch_manager + - terminal + - test_runner + - git_panel + nova_specific: + - governance_receipt_inspector + - replay_timeline + - tool_registry + - node_health_strip + - local_model_selector + power_user: + - command_palette + - context_builder + - explain_mode + - project_templates + - safety_rails +ui_panels: + - name: DiffViewer + component: diff_panel + features: [apply_patch, reject_hunk, rollback_patch] + - name: Terminal + component: terminal_panel + features: [run_tests, run_app, run_lint, link_output] + - name: TestRunner + component: test_panel + features: [run_file_tests, run_failed_tests, fix_failure] + - name: GitPanel + component: git_panel + features: [changed_files, staged_diff, commit_generator, pr_summary] + - name: GovernanceInspector + component: governance_panel + features: [policy_version, trace_id, input_output_hashes] + - name: ReplayTimeline + component: replay_panel + features: [deterministic_replay, drift_warning, rollback] + - name: NodeHealth + component: health_strip + metrics: [model, online_status, receipts_count, latency, governance_state] + - name: ModelSelector + component: model_dropdown + models: [qwen2.5-coder:3b, qwen2.5-coder:7b] +governance: + receipts: + verify_function: verify_receipt + ledger_path: ./ledger/receipts.db + replay_path: ./ledger/replay.db + policies: + enforcement: strict + versioning: semantic + audit_log: true +templates: + - python_fastapi + - node_express + - rust_actix + - governed_nova_tool + - local_ai_app +safety: + auto_write: false + warn_large_patches: true + warn_secrets: true + confirm_dependency_changes: true +runtime: + hooks: + on_patch_applied: + emit: governance.patch_applied + record_receipt: true + replayable: true + on_patch_rejected: + emit: governance.patch_rejected + record_receipt: true + on_hunk_applied: + emit: governance.hunk_applied + record_receipt: true + on_hunk_rejected: + emit: governance.hunk_rejected + record_receipt: true + on_patch_rollback: + emit: governance.patch_rollback + record_receipt: true + replayable: true + on_test_run_started: + emit: test.run_started + attach_context: true + on_test_run_completed: + emit: test.run_completed + record_receipt: true + attach_output: true + on_test_failure_detected: + emit: test.failure + record_receipt: true + auto_context: failing_test_output + on_commit_created: + emit: git.commit + record_receipt: true + on_branch_changed: + emit: git.branch_changed + on_tool_invoked: + emit: tool.invoked + record_receipt: true + include: [tool_name, args_hash, governed_state] + on_tool_completed: + emit: tool.completed + record_receipt: true + include: [output_hash, duration_ms] + on_receipt_verified: + emit: governance.receipt_verified + update_health: true + on_receipt_blocked: + emit: governance.receipt_blocked + update_health: true + attach_policy: true + on_replay_started: + emit: replay.started + attach_trace: true + on_replay_completed: + emit: replay.completed + record_receipt: true + on_replay_drift: + emit: replay.drift_detected + severity: high + auto_flag: true + suggest_rollback: true + on_node_online: + emit: node.online + update_health: true + on_node_offline: + emit: node.offline + update_health: true + on_latency_spike: + emit: node.latency_spike + severity: medium +event_bus: + channels: + - governance.* + - test.* + - git.* + - tool.* + - replay.* + - node.* + transport: internal + persistence: true + max_events: 5000 diff --git a/nova/node/federation.py b/nova/node/federation.py new file mode 100644 index 0000000..f3eb39c --- /dev/null +++ b/nova/node/federation.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import json +import os +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, Request + +from nova.node.identity import NodeIdentity, sign_payload, verify_payload_signature +from nova.node.ledger import append_ledger, runtime_dir +from nova.node.policy import load_node_policy + +router = APIRouter() + + +def peers_path() -> Path: + return Path(os.environ.get("NOVA_NODE_PEERS_PATH", str(runtime_dir() / "peers.json"))) + + +def load_peers() -> list[dict[str, Any]]: + path = peers_path() + if not path.exists(): + return [] + return json.loads(path.read_text(encoding="utf-8")) + + +def gossip_summary() -> dict[str, Any]: + ident = NodeIdentity.load("./policy.yaml") + return { + "node_id": ident.node_id, + "operator_id": ident.operator_id, + "timestamp": int(time.time()), + "policy_hash": ident.policy_hash, + "capabilities": ["chat", "governance", "audit"], + } + + +def signed_gossip_summary() -> dict[str, Any]: + summary = gossip_summary() + return { + "summary": summary, + "signature": sign_payload(summary), + "signature_algorithm": "rsa-sha256-digest", + } + + +def gossip_to_peers() -> list[dict[str, Any]]: + summary = signed_gossip_summary() + results: list[dict[str, Any]] = [] + for peer in load_peers(): + peer_id = str(peer.get("peer_id") or "unknown-peer") + endpoint = str(peer.get("endpoint") or "").rstrip("/") + if not endpoint: + results.append({"peer_id": peer_id, "status": "error", "error": "missing endpoint"}) + continue + request = urllib.request.Request( + f"{endpoint}/node/gossip", + data=json.dumps(summary).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=5) as response: + results.append({"peer_id": peer_id, "status": response.status}) + except (urllib.error.URLError, TimeoutError) as exc: + results.append({"peer_id": peer_id, "status": "error", "error": str(exc)}) + return results + + +@router.post("/node/gossip") +async def receive_gossip(request: Request) -> dict[str, Any]: + data = await request.json() + return receive_gossip_payload(data) + + +def receive_gossip_payload(data: dict[str, Any]) -> dict[str, Any]: + summary = data.get("summary") if isinstance(data.get("summary"), dict) else data + signature = data.get("signature") + signature_valid = verify_payload_signature(summary, signature) + trust_level = "trusted" if signature_valid else "invalid" + entry = append_ledger({ + "entry_type": "nodeGossipReceipt", + "timestamp": int(time.time()), + "summary": summary, + "signature_valid": signature_valid, + "trust_level": trust_level, + }) + return { + "ack": True, + "received_at": entry["timestamp"], + "signature_valid": signature_valid, + "trust_level": trust_level, + } + + +def node_hello() -> dict[str, Any]: + policy = load_node_policy() + payload = { + "node_id": policy["node_id"], + "operator_id": policy["operator_id"], + "policy_version": policy["policy_version"], + "policy_hash": policy["policy_hash"], + "capabilities": ["chat", "governance", "audit"], + } + return { + **payload, + "signature": sign_payload(payload), + "signature_algorithm": "rsa-sha256-digest", + "trust_level": "self", + } + + +@router.post("/node/hello") +async def hello() -> dict[str, Any]: + return node_hello() diff --git a/nova/node/identity.py b/nova/node/identity.py new file mode 100644 index 0000000..2ed9508 --- /dev/null +++ b/nova/node/identity.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import os +import json +import base64 +import hashlib +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from nova.node.ledger import stable_hash + + +@dataclass(frozen=True) +class NodeIdentity: + node_id: str + operator_id: str + operator_key_id: str + policy_hash: str + + @classmethod + def load(cls, policy_path: str = "./policy.yaml") -> "NodeIdentity": + policy_file = Path(policy_path) + policy_bytes = policy_file.read_bytes() if policy_file.exists() else b"" + policy = _load_policy(policy_file) if policy_file.exists() else {} + return cls( + node_id=os.environ.get("NOVA_NODE_ID", str(policy.get("node_id") or "lawful-nova-node-local")), + operator_id=os.environ.get("NOVA_OPERATOR_ID", str(policy.get("operator_id") or "operator-local")), + operator_key_id=os.environ.get("NOVA_OPERATOR_KEY_ID", "operator-local"), + policy_hash=stable_hash(policy_bytes.decode("utf-8", errors="ignore"))[7:], + ) + + +def _load_policy(policy_file: Path) -> dict[str, Any]: + text = policy_file.read_text(encoding="utf-8") + try: + import yaml + + data = yaml.safe_load(text) + return data or {} + except Exception: + return {} + + +def load_operator_private_key() -> dict[str, Any] | None: + return _load_key_from_env("NOVA_NODE_OPERATOR_PRIVATE_KEY") + + +def load_operator_public_key() -> dict[str, Any] | None: + return _load_key_from_env("NOVA_NODE_OPERATOR_PUBLIC_KEY") + + +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 + + +def _load_key_from_env(name: str) -> dict[str, Any] | None: + value = os.environ.get(name) + if not value: + return None + path = Path(value) + raw = path.read_text(encoding="utf-8") if path.exists() else value + return _parse_key_material(raw.strip()) + + +def _parse_key_material(raw: str) -> dict[str, Any] | None: + try: + return json.loads(raw) + except json.JSONDecodeError: + pass + if "-----BEGIN" not in raw: + return None + try: + label, der = _pem_to_der(raw) + if label in {"RSA PRIVATE KEY", "RSA PUBLIC KEY"}: + return _parse_rsa_pkcs1_key(der) + if label == "PRIVATE KEY": + return _parse_pkcs8_private_key(der) + if label == "PUBLIC KEY": + return _parse_spki_public_key(der) + except Exception: + return None + return None + + +def _canonical_digest(payload: dict[str, Any]) -> bytes: + body = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + return hashlib.sha256(body).digest() + + +def _pem_to_der(raw: str) -> tuple[str, bytes]: + match = re.search(r"-----BEGIN ([^-]+)-----(.*?)-----END \1-----", raw, flags=re.S) + if not match: + raise ValueError("invalid PEM") + label = match.group(1).strip() + body = re.sub(r"\s+", "", match.group(2)) + return label, base64.b64decode(body) + + +def _parse_rsa_pkcs1_key(der: bytes) -> dict[str, Any]: + seq, end = _read_tlv(der, 0, expected_tag=0x30) + if end != len(der): + raise ValueError("trailing data") + ints: list[int] = [] + offset = 0 + while offset < len(seq): + value, offset = _read_integer(seq, offset) + ints.append(value) + if len(ints) >= 9: + return {"kty": "RSA", "n": str(ints[1]), "e": str(ints[2]), "d": str(ints[3])} + if len(ints) == 2: + return {"kty": "RSA", "n": str(ints[0]), "e": str(ints[1])} + raise ValueError("unsupported RSA key") + + +def _parse_pkcs8_private_key(der: bytes) -> dict[str, Any]: + seq, end = _read_tlv(der, 0, expected_tag=0x30) + if end != len(der): + raise ValueError("trailing data") + _, offset = _read_integer(seq, 0) + _, offset = _read_tlv(seq, offset, expected_tag=0x30) + private_der, offset = _read_tlv(seq, offset, expected_tag=0x04) + if offset > len(seq): + raise ValueError("bad private key") + return _parse_rsa_pkcs1_key(private_der) + + +def _parse_spki_public_key(der: bytes) -> dict[str, Any]: + seq, end = _read_tlv(der, 0, expected_tag=0x30) + if end != len(der): + raise ValueError("trailing data") + _, offset = _read_tlv(seq, 0, expected_tag=0x30) + bitstring, offset = _read_tlv(seq, offset, expected_tag=0x03) + if not bitstring: + raise ValueError("empty public key") + return _parse_rsa_pkcs1_key(bitstring[1:]) + + +def _read_integer(data: bytes, offset: int) -> tuple[int, int]: + value, offset = _read_tlv(data, offset, expected_tag=0x02) + return int.from_bytes(value, "big", signed=False), offset + + +def _read_tlv(data: bytes, offset: int, *, expected_tag: int) -> tuple[bytes, int]: + if offset >= len(data) or data[offset] != expected_tag: + raise ValueError("unexpected ASN.1 tag") + offset += 1 + if offset >= len(data): + raise ValueError("missing ASN.1 length") + length = data[offset] + offset += 1 + if length & 0x80: + count = length & 0x7F + if count == 0 or offset + count > len(data): + raise ValueError("invalid ASN.1 length") + length = int.from_bytes(data[offset:offset + count], "big") + offset += count + end = offset + length + if end > len(data): + raise ValueError("truncated ASN.1 value") + return data[offset:end], end diff --git a/nova/node/ledger.py b/nova/node/ledger.py new file mode 100644 index 0000000..6d58a39 --- /dev/null +++ b/nova/node/ledger.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +from typing import Any + + +def runtime_dir() -> Path: + return Path(os.environ.get("NOVA_NODE_RUNTIME_DIR", ".runtime/node")) + + +def stable_json(value: Any) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + +def stable_hash(value: Any) -> str: + return "sha256:" + hashlib.sha256(stable_json(value).encode("utf-8")).hexdigest() + + +def append_jsonl(path: Path, entry: dict[str, Any]) -> dict[str, Any]: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8", newline="\n") as fh: + fh.write(stable_json(entry) + "\n") + return entry + + +def read_jsonl(path: Path) -> list[dict[str, Any]]: + if not path.exists(): + return [] + return [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + +def governance_ledger_path() -> Path: + return runtime_dir() / "governance-ledger.jsonl" + + +def append_ledger(entry: dict[str, Any]) -> dict[str, Any]: + return append_jsonl(governance_ledger_path(), entry) + + +def read_ledger() -> list[dict[str, Any]]: + return read_jsonl(governance_ledger_path()) diff --git a/nova/node/mesh.py b/nova/node/mesh.py new file mode 100644 index 0000000..66ec25a --- /dev/null +++ b/nova/node/mesh.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter + +from nova.node.consensus import policy_hash_consensus +from nova.node.federation import load_peers +from nova.node.ledger import read_ledger +from nova.node.policy import load_node_policy + +router = APIRouter() + + +def get_mesh() -> dict[str, Any]: + local = load_node_policy() + peers = _ledger_peers() + configured = [ + { + "node_id": str(peer.get("peer_id") or peer.get("node_id") or "unknown-peer"), + "trust_level": str(peer.get("trust_level") or "limited"), + "policy_hash": peer.get("policy_hash"), + "last_hello": peer.get("last_hello"), + "last_gossip": peer.get("last_gossip"), + "signature_valid": peer.get("signature_valid"), + "configured": True, + } + for peer in load_peers() + ] + known_ids = {peer["node_id"] for peer in peers} + peers.extend(peer for peer in configured if peer["node_id"] not in known_ids) + summaries = [ + { + "policy_hash": peer.get("policy_hash"), + "signature_valid": peer.get("signature_valid"), + "trust_level": peer.get("trust_level"), + } + for peer in peers + ] + summaries.append({"policy_hash": local["policy_hash"], "trust_level": "self", "signature_valid": True}) + consensus = policy_hash_consensus(summaries) + divergent = [ + peer["node_id"] + for peer in peers + if peer.get("policy_hash") and peer.get("policy_hash") != local["policy_hash"] + ] + return { + "node_id": local["node_id"], + "local_policy_hash": local["policy_hash"], + "peers": peers, + "consensus_ratio": consensus["agreement_ratio"], + "consensus": consensus, + "divergent_peers": divergent, + } + + +@router.get("/node/mesh") +async def mesh() -> dict[str, Any]: + return get_mesh() + + +def _ledger_peers() -> list[dict[str, Any]]: + peers: dict[str, dict[str, Any]] = {} + for entry in read_ledger(): + if entry.get("entry_type") not in {"nodeGossipReceipt", "nodeHandshakeReceipt"}: + continue + summary = entry.get("summary") or entry.get("hello") or {} + node_id = str(summary.get("node_id") or "unknown-peer") + peers[node_id] = { + "node_id": node_id, + "trust_level": str(entry.get("trust_level") or "limited"), + "policy_hash": summary.get("policy_hash"), + "last_hello": entry.get("timestamp") if entry.get("entry_type") == "nodeHandshakeReceipt" else None, + "last_gossip": entry.get("timestamp") if entry.get("entry_type") == "nodeGossipReceipt" else None, + "signature_valid": entry.get("signature_valid"), + } + return list(peers.values()) diff --git a/nova/node/policy.py b/nova/node/policy.py new file mode 100644 index 0000000..eafd863 --- /dev/null +++ b/nova/node/policy.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import json +import os +import time +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from nova.node.identity import NodeIdentity +from nova.node.ledger import append_jsonl, read_jsonl, runtime_dir, stable_hash + + +class NodeVeto(Exception): + def __init__(self, *, reason: str, policy_version: str) -> None: + super().__init__(reason) + self.reason = reason + self.policy_version = policy_version + + +class GovernanceRuntime: + def __init__(self, policy_path: str = "./policy.yaml") -> None: + self.policy_path = policy_path + self.policy = self.load_policy() + + def load_policy(self) -> dict[str, Any]: + policy_file = Path(self.policy_path) + if policy_file.exists(): + return _load_policy_file(policy_file) + return default_policy() + + def continuity_receipt(self) -> dict[str, Any]: + return { + "trace_id": f"trace-{uuid4()}", + "timestamp": int(time.time()), + "policy_version": self.policy.get("version", "1.0"), + } + + def enforce_invariants(self, payload: dict[str, Any]) -> dict[str, Any]: + receipts: list[str] = [] + boundedness = self.policy.get("invariants", {}).get("boundedness", {}) + max_payload_bytes = _env_int( + "NOVA_NODE_MAX_PAYLOAD_BYTES", + int(boundedness.get("max_payload_bytes", 500000)), + ) + if len(json.dumps(payload, sort_keys=True).encode("utf-8")) > max_payload_bytes: + return { + "decision": "blocked", + "reason": "payload-too-large", + **self.continuity_receipt(), + } + max_tokens = _env_int("NOVA_NODE_MAX_TOKENS", int(boundedness.get("max_tokens", 2048))) + if int(payload.get("max_tokens") or 0) > max_tokens: + return { + "decision": "blocked", + "reason": "token-limit-exceeded", + **self.continuity_receipt(), + } + receipts.append("boundedness") + + banned = self.policy.get("safety", {}).get("banned_intents", []) + if payload.get("intent") in banned: + return { + "decision": "blocked", + "reason": "banned-intent", + **self.continuity_receipt(), + } + + rate_decision = _enforce_rate_limits(payload, self.policy) + if rate_decision: + return { + **rate_decision, + **self.continuity_receipt(), + } + receipts.append("rate-limit") + + receipts.extend(["continuity", "provenance", "veto"]) + return { + "decision": "allowed", + "receipts": receipts, + **self.continuity_receipt(), + } + + +def load_node_policy(policy_path: str = "./policy.yaml") -> dict[str, Any]: + policy = GovernanceRuntime(policy_path).policy + ident = NodeIdentity.load(policy_path) + base_policy = { + "node_id": ident.node_id, + "operator_id": ident.operator_id, + "operator_key_id": ident.operator_key_id, + "policy_version": str(policy.get("version", "1.0")), + "policy_hash": ident.policy_hash, + "conformance_profile": str(policy.get("conformance_profile", os.environ.get("NOVA_NODE_CONFORMANCE", "N0"))), + "max_payload_bytes": int( + _env_int( + "NOVA_NODE_MAX_PAYLOAD_BYTES", + int(policy.get("invariants", {}).get("boundedness", {}).get("max_payload_bytes", 500000)), + ) + ), + } + if not Path(policy_path).exists(): + base_policy["policy_hash"] = stable_hash(base_policy)[7:] + return base_policy + + +def default_policy() -> dict[str, Any]: + return { + "version": os.environ.get("NOVA_NODE_POLICY_VERSION", "1.0"), + "conformance_profile": os.environ.get("NOVA_NODE_CONFORMANCE", "N0"), + "invariants": { + "boundedness": { + "enforce": True, + "max_tokens": _env_int("NOVA_NODE_MAX_TOKENS", 2048), + "max_payload_bytes": _env_int("NOVA_NODE_MAX_PAYLOAD_BYTES", 500000), + } + }, + "safety": { + "banned_intents": [], + "rate_limits": { + "per_user_per_minute": _env_int("NOVA_NODE_RATE_PER_MINUTE", 10), + "per_user_per_hour": _env_int("NOVA_NODE_RATE_PER_HOUR", 200), + }, + }, + } + + +def get_rate_limit_state(policy: dict[str, Any] | None = None) -> dict[str, Any]: + active_policy = policy or GovernanceRuntime().policy + limits = _rate_limits(active_policy) + now = int(time.time()) + minute_bucket = now // 60 + hour_bucket = now // 3600 + callers: dict[str, dict[str, int]] = {} + for event in read_jsonl(_rate_limit_path()): + caller = str(event.get("caller_id") or "unknown") + info = callers.setdefault(caller, {"minute_count": 0, "hour_count": 0, "blocked_count": 0}) + if event.get("minute_bucket") == minute_bucket: + info["minute_count"] += 1 + if event.get("hour_bucket") == hour_bucket: + info["hour_count"] += 1 + if event.get("blocked"): + info["blocked_count"] += 1 + return {"limits": limits, "callers": callers} + + +def _load_policy_file(policy_file: Path) -> dict[str, Any]: + text = policy_file.read_text(encoding="utf-8") + try: + import yaml + + data = yaml.safe_load(text) + return data or {} + except Exception: + return json.loads(text) + + +def _env_int(name: str, default: int) -> int: + try: + return int(os.environ.get(name, str(default))) + except ValueError: + return default + + +def _enforce_rate_limits(payload: dict[str, Any], policy: dict[str, Any]) -> dict[str, Any] | None: + limits = _rate_limits(policy) + caller_id = str(payload.get("caller_id") or payload.get("user") or "anonymous") + now = int(time.time()) + minute_bucket = now // 60 + hour_bucket = now // 3600 + current = get_rate_limit_state(policy).get("callers", {}).get(caller_id, {}) + blocked = ( + current.get("minute_count", 0) >= limits["per_user_per_minute"] + or current.get("hour_count", 0) >= limits["per_user_per_hour"] + ) + append_jsonl( + _rate_limit_path(), + { + "timestamp": now, + "caller_id": caller_id, + "minute_bucket": minute_bucket, + "hour_bucket": hour_bucket, + "blocked": blocked, + }, + ) + if not blocked: + return None + return { + "decision": "blocked", + "reason": "rate-limit-exceeded", + "rate_limit": limits, + } + + +def _rate_limits(policy: dict[str, Any]) -> dict[str, int]: + configured = policy.get("safety", {}).get("rate_limits", {}) + return { + "per_user_per_minute": _env_int( + "NOVA_NODE_RATE_PER_MINUTE", + int(configured.get("per_user_per_minute", 10)), + ), + "per_user_per_hour": _env_int( + "NOVA_NODE_RATE_PER_HOUR", + int(configured.get("per_user_per_hour", 200)), + ), + } + + +def _rate_limit_path() -> Path: + return runtime_dir() / "rate-limits.jsonl" diff --git a/nova/node/policy_diff.py b/nova/node/policy_diff.py new file mode 100644 index 0000000..ad3926f --- /dev/null +++ b/nova/node/policy_diff.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from fastapi import APIRouter + +from nova.node.identity import NodeIdentity +from nova.node.ledger import runtime_dir +from nova.node.policy import GovernanceRuntime + +router = APIRouter() + + +def get_policy_state(policy_path: str = "./policy.yaml") -> dict[str, Any]: + current = _read_text(Path(policy_path)) + previous_path = runtime_dir() / "policy.previous.yaml" + previous = _read_text(previous_path) + identity = NodeIdentity.load(policy_path) + runtime = GovernanceRuntime(policy_path) + return { + "policy_version": str(runtime.policy.get("version", "1.0")), + "policy_hash": identity.policy_hash, + "current_policy": current, + "previous_policy": previous, + "diff": diff_policy_text(previous, current), + } + + +def diff_policy_text(previous: str, current: str) -> dict[str, list[str]]: + previous_lines = previous.splitlines() + current_lines = current.splitlines() + removed = [line for line in previous_lines if line not in current_lines] + added = [line for line in current_lines if line not in previous_lines] + changed = [ + line for line in current_lines + if ":" in line and line.split(":", 1)[0] in {old.split(":", 1)[0] for old in previous_lines if ":" in old} + and line not in previous_lines + ] + return {"added": added, "removed": removed, "changed": changed} + + +@router.get("/node/policy") +async def policy() -> dict[str, Any]: + return get_policy_state() + + +def _read_text(path: Path) -> str: + if not path.exists(): + return "" + return path.read_text(encoding="utf-8") diff --git a/nova/node/replay.py b/nova/node/replay.py new file mode 100644 index 0000000..0136df5 --- /dev/null +++ b/nova/node/replay.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import JSONResponse + +from nova.config import load_nova_config +from nova.errors import ProviderError +from nova.node.continuity import read_events +from nova.node.policy import NodeVeto +from nova.node.submit import load_node_result, submit_node_task +from nova.providers import build_provider + +router = APIRouter() + + +def replay_trace(trace_id: str, provider: Any) -> dict[str, Any]: + try: + original = load_node_result(trace_id) + except KeyError: + raise ValueError(f"Unknown trace_id: {trace_id}") from None + + packet = _find_original_packet(trace_id) + if packet is None: + raise ValueError(f"Missing submit event for trace_id: {trace_id}") + + replayed = submit_node_task({**packet, "task_id": f"replay-{packet.get('task_id', trace_id)}"}, provider) + original_output = original.get("result") + replayed_output = replayed.get("result") + diff = _compute_diff(original_output, replayed_output) + return { + "trace_id": trace_id, + "replay_trace_id": replayed["trace_id"], + "original_output": original_output, + "replayed_output": replayed_output, + "deterministic": diff["type"] == "none", + "diff": diff, + "policy_version_original": original.get("receipt", {}).get("policy_version"), + "policy_version_replayed": replayed.get("receipt", {}).get("policy_version"), + "governance_original": original.get("decision"), + "governance_replayed": replayed.get("decision"), + "replay_receipts": replayed.get("receipts", []), + } + + +@router.post("/node/replay/{trace_id}") +async def replay_node_trace(trace_id: str, request: Request) -> Any: + provider_factory = getattr(request.app.state, "node_provider_factory", build_provider) + provider = provider_factory(load_nova_config()) + try: + return replay_trace(trace_id, provider) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from None + except NodeVeto as exc: + return JSONResponse( + {"error": {"decision": "blocked", "reason": exc.reason, "policy_version": exc.policy_version}}, + status_code=400, + ) + except ProviderError as exc: + return JSONResponse({"error": {"code": exc.code, "message": exc.message}}, status_code=500) + + +def _find_original_packet(trace_id: str) -> dict[str, Any] | None: + for event in read_events(): + if event.get("kind") != "submit": + continue + data = event.get("data") or {} + decision = data.get("decision") or {} + if decision.get("trace_id") == trace_id: + payload = data.get("payload") + return payload if isinstance(payload, dict) else None + return None + + +def _compute_diff(original: Any, replayed: Any) -> dict[str, Any]: + if original == replayed: + return {"type": "none", "details": {}} + return {"type": "structural", "details": {"original": original, "replayed": replayed}} diff --git a/nova/node/runtime_hooks.py b/nova/node/runtime_hooks.py new file mode 100644 index 0000000..686ff29 --- /dev/null +++ b/nova/node/runtime_hooks.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import Any + +from nova.node.event_bus import EventBus + + +class RuntimeHooks: + def __init__(self, bus: EventBus) -> None: + self.bus = bus + + def on_patch_applied(self, patch_id: str, files: list[str], receipt: dict[str, Any]) -> None: + self.bus.emit("governance.patch_applied", "applied", {"patch_id": patch_id, "files": files}) + self.on_receipt_verified(receipt) + + def on_patch_rejected(self, patch_id: str, reason: str, receipt: dict[str, Any]) -> None: + self.bus.emit("governance.patch_rejected", "rejected", {"patch_id": patch_id, "reason": reason}) + self.on_receipt_verified(receipt) + + def on_hunk_applied(self, patch_id: str, hunk_id: str, receipt: dict[str, Any]) -> None: + self.bus.emit("governance.hunk_applied", "applied", {"patch_id": patch_id, "hunk_id": hunk_id}) + self.on_receipt_verified(receipt) + + def on_hunk_rejected(self, patch_id: str, hunk_id: str, receipt: dict[str, Any]) -> None: + self.bus.emit("governance.hunk_rejected", "rejected", {"patch_id": patch_id, "hunk_id": hunk_id}) + self.on_receipt_verified(receipt) + + def on_patch_rollback(self, patch_id: str, to_revision: str, receipt: dict[str, Any]) -> None: + self.bus.emit( + "governance.patch_rollback", + "rollback", + {"patch_id": patch_id, "to_revision": to_revision}, + ) + self.on_receipt_verified(receipt) + + def on_test_run_started(self, suite: str) -> None: + self.bus.emit("test.run_started", "started", {"suite": suite}) + + def on_test_run_completed(self, suite: str, summary: dict[str, Any], receipt: dict[str, Any]) -> None: + self.bus.emit("test.run_completed", "completed", {"suite": suite, "summary": summary}) + self.on_receipt_verified(receipt) + + def on_test_failure_detected(self, test_name: str, output: str, receipt: dict[str, Any]) -> None: + self.bus.emit("test.failure", "detected", {"test_name": test_name, "output": output}) + self.on_receipt_verified(receipt) + + def on_commit_created(self, commit_hash: str, receipt: dict[str, Any]) -> None: + self.bus.emit("git.commit", "created", {"commit_hash": commit_hash}) + self.on_receipt_verified(receipt) + + def on_branch_changed(self, branch: str) -> None: + self.bus.emit("git.branch_changed", "changed", {"branch": branch}) + + def on_tool_invoked(self, tool_name: str, args_hash: str, governed_state: str, trace_id: str) -> None: + self.bus.emit( + "tool.invoked", + "started", + { + "tool_name": tool_name, + "args_hash": args_hash, + "governed_state": governed_state, + "trace_id": trace_id, + }, + ) + + def on_tool_completed(self, tool_name: str, output_hash: str, duration_ms: int, trace_id: str) -> None: + self.bus.emit( + "tool.completed", + "completed", + { + "tool_name": tool_name, + "output_hash": output_hash, + "duration_ms": duration_ms, + "trace_id": trace_id, + }, + ) + + def on_receipt_verified(self, receipt: dict[str, Any]) -> None: + self.bus.emit("governance.receipt_verified", "verified", _receipt_payload(receipt)) + + def on_receipt_blocked(self, receipt: dict[str, Any]) -> None: + self.bus.emit("governance.receipt_blocked", "blocked", _receipt_payload(receipt)) + + def on_replay_started(self, trace_id: str) -> None: + self.bus.emit("replay.started", "started", {"trace_id": trace_id}) + + def on_replay_completed(self, trace_id: str, receipt: dict[str, Any]) -> None: + self.bus.emit("replay.completed", "completed", {"trace_id": trace_id}) + self.on_receipt_verified(receipt) + + def on_replay_drift(self, trace_id: str, details: dict[str, Any]) -> None: + self.bus.emit( + "replay.drift_detected", + "detected", + {"trace_id": trace_id, "details": details, "severity": "high"}, + ) + + def on_node_online(self) -> None: + self.bus.emit("node.online", "online", {}) + + def on_node_offline(self, reason: str) -> None: + self.bus.emit("node.offline", "offline", {"reason": reason}) + + def on_latency_spike(self, latency_ms: int) -> None: + self.bus.emit("node.latency_spike", "spike", {"latency_ms": latency_ms, "severity": "medium"}) + + +def _receipt_payload(receipt: dict[str, Any]) -> dict[str, Any]: + return { + "trace_id": receipt.get("trace_id"), + "policy_version": receipt.get("policy_version"), + "receipt_hash": receipt.get("receipt_hash"), + "reason": receipt.get("reason"), + "tool_name": receipt.get("tool_name") or receipt.get("tool"), + } diff --git a/nova/node/status.py b/nova/node/status.py new file mode 100644 index 0000000..f9338c2 --- /dev/null +++ b/nova/node/status.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter + +from nova.node.continuity import read_events +from nova.node.event_bus import read_event_history +from nova.node.ledger import read_ledger +from nova.node.policy import get_rate_limit_state, load_node_policy +from nova.node.federation import load_peers +from nova.node.consensus import policy_hash_consensus +from nova.node.alerts import list_alerts +from nova.node.mesh import get_mesh + +router = APIRouter() + + +def node_status() -> dict[str, Any]: + policy = load_node_policy() + return { + "node": "Lawful Nova Node v0", + "node_id": policy["node_id"], + "operator_key_id": policy["operator_key_id"], + "policy_version": policy["policy_version"], + "policy_hash": policy["policy_hash"], + "conformance_profile": policy["conformance_profile"], + "receipt_count": len([e for e in read_events() if e.get("entry_type") == "nodeExecutionReceipt"]), + "governance_health": _governance_health(), + "rate_limits": get_rate_limit_state(), + "federation": _federation_state(), + "endpoints": [ + "/v1/models", + "/v1/chat/completions", + "/v1/completions", + "/node/status", + "/node/submit", + "/node/tool", + "/node/tools", + "/node/agents", + "/node/agent-tools", + "/node/events", + "/node/feature-manifest", + "/node/verify/{trace_id}", + "/node/trace/{trace_id}", + "/node/compare-receipts", + "/node/result/{trace_id}", + "/node/receipts", + "/node/ledger", + "/node/gossip", + "/node/hello", + "/node/replay/{trace_id}", + "/node/continuity", + "/node/policy", + "/node/mesh", + "/node/alerts", + "/node/conformance/n0", + "/node/conformance/n0/badge", + "/node/evidence-bundle", + ], + } + + +@router.get("/node/status") +async def get_status() -> dict[str, Any]: + return node_status() + + +@router.get("/node/receipts") +async def get_receipts() -> dict[str, Any]: + return {"receipts": [e for e in read_events() if e.get("entry_type") == "nodeExecutionReceipt"]} + + +@router.get("/node/ledger") +async def get_ledger() -> dict[str, Any]: + return {"ledger": read_ledger(), "continuity": read_events()} + + +@router.get("/node/continuity") +async def get_continuity(limit: int = 500) -> dict[str, Any]: + events = [_flatten_continuity_event(event) for event in read_events()] + return {"events": events[-limit:]} + + +def _governance_health() -> dict[str, Any]: + ledger = read_ledger() + continuity = read_events() + invalid_signatures = [ + entry for entry in ledger + if entry.get("entry_type") == "nodeGossipReceipt" and entry.get("signature_valid") is False + ] + return { + "ledger_entries": len(ledger), + "continuity_events": len(continuity), + "event_bus_events": len(read_event_history()), + "invalid_signatures": len(invalid_signatures), + } + + +def _federation_state() -> dict[str, Any]: + gossip = [ + entry for entry in read_ledger() + if entry.get("entry_type") == "nodeGossipReceipt" + ] + trusted = [entry for entry in gossip if entry.get("signature_valid") is True] + consensus = policy_hash_consensus([ + { + **(entry.get("summary") or {}), + "signature_valid": entry.get("signature_valid"), + "trust_level": entry.get("trust_level"), + } + for entry in gossip + ]) + return { + "peers": load_peers(), + "gossip_events": len(gossip), + "trusted_gossip_events": len(trusted), + "consensus": consensus, + "mesh": get_mesh(), + "alerts": len(list_alerts()), + } + + +def _flatten_continuity_event(event: dict[str, Any]) -> dict[str, Any]: + data = event.get("data") or {} + decision = data.get("decision") or {} + return { + "event_id": event.get("event_id") or event.get("receipt_hash"), + "timestamp": event.get("timestamp"), + "kind": event.get("kind") or event.get("entry_type"), + "trace_id": event.get("trace_id") or data.get("trace_id") or decision.get("trace_id"), + "decision": decision.get("decision") or event.get("decision"), + "payload": data.get("payload"), + } diff --git a/nova/node/submit.py b/nova/node/submit.py new file mode 100644 index 0000000..4a26d2e --- /dev/null +++ b/nova/node/submit.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json +import time +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import JSONResponse + +from nova.config import load_nova_config +from nova.errors import ProviderError +from nova.node.continuity import append_event, append_receipt, last_receipt_hash +from nova.node.ledger import runtime_dir, stable_hash, stable_json +from nova.node.policy import GovernanceRuntime, NodeVeto, load_node_policy +from nova.providers import build_provider + +router = APIRouter() + + +def submit_node_task(packet: dict[str, Any], provider: Any) -> dict[str, Any]: + decision = GovernanceRuntime().enforce_invariants(packet) + if decision["decision"] != "allowed": + raise NodeVeto(reason=str(decision.get("reason") or "blocked"), policy_version=str(decision["policy_version"])) + + policy = load_node_policy() + payload = packet.get("payload") or _payload_from_flat_packet(packet) + task_id = str(packet.get("task_id") or f"task-{time.time_ns()}") + trace_id = str(decision["trace_id"]) + append_event("submit", {"payload": packet, "decision": decision}) + + governed_request = { + "messages": _payload_messages(payload), + "temperature": payload.get("temperature"), + "max_tokens": payload.get("max_tokens"), + "slice_id": task_id, + "slice_version": "node-v0", + "continuity_hash": stable_hash({"previous": last_receipt_hash()}), + "governance_path": ["node.v0", "policy.veto", "provider.execute"], + } + provider_result = provider.chat_completion(governed_request) + completion = provider_result["completion"] + output = str(completion["choices"][0]["message"]["content"]) + result = {"output": output, "completion_id": completion["id"]} + receipt = _receipt( + policy=policy, + packet=packet, + task_id=task_id, + trace_id=trace_id, + model_id=str(completion.get("model") or getattr(provider, "model", "unknown")), + output=result, + ) + response = { + "decision": "allowed", + "trace_id": trace_id, + "result": result, + "receipt": receipt, + "receipts": [receipt["receipt_hash"]], + } + append_receipt({ + "entry_type": "nodeExecutionReceipt", + "trace_id": trace_id, + "timestamp": receipt["timestamp"], + "task_id": task_id, + "receipt_hash": receipt["receipt_hash"], + "receipt": receipt, + }) + append_event("result", {"trace_id": trace_id, "completion_id": completion["id"]}) + _write_result(trace_id, response) + return response + + +def load_node_result(trace_id: str) -> dict[str, Any]: + path = _result_path(trace_id) + if not path.exists(): + raise KeyError(trace_id) + return json.loads(path.read_text(encoding="utf-8")) + + +@router.post("/node/submit") +async def node_submit(request: Request) -> Any: + body = await request.json() + provider_factory = getattr(request.app.state, "node_provider_factory", build_provider) + provider = provider_factory(load_nova_config()) + try: + return submit_node_task(body, provider) + except NodeVeto as exc: + return JSONResponse( + {"error": {"decision": "blocked", "reason": exc.reason, "policy_version": exc.policy_version}}, + status_code=400, + ) + except ProviderError as exc: + return JSONResponse({"error": {"code": exc.code, "message": exc.message}}, status_code=500) + + +@router.post("/node/result") +async def node_result(request: Request) -> Any: + body = await request.json() + provider_factory = getattr(request.app.state, "node_provider_factory", build_provider) + provider = provider_factory(load_nova_config()) + governed_request = body.get("payload", {}) + result = provider.chat_completion(governed_request) + event = append_event("result", {"payload": governed_request, "completion_id": result["completion"]["id"]}) + return {"completion": result["completion"], "receipt": result["receipt"], "event_id": event["event_id"]} + + +@router.get("/node/result/{trace_id}") +async def node_result_by_trace(trace_id: str) -> Any: + try: + return load_node_result(trace_id) + except KeyError: + raise HTTPException(status_code=404, detail="node result not found") from None + + +def _payload_from_flat_packet(packet: dict[str, Any]) -> dict[str, Any]: + return { + "messages": packet.get("messages") or [], + "temperature": packet.get("temperature"), + "max_tokens": packet.get("max_tokens"), + } + + +def _payload_messages(payload: dict[str, Any]) -> list[dict[str, str]]: + messages = payload.get("messages") + if isinstance(messages, list) and messages: + return [ + {"role": str(message.get("role") or "user"), "content": str(message.get("content") or "")} + for message in messages + if isinstance(message, dict) + ] + prompt = str(payload.get("prompt") or stable_json(payload)) + return [{"role": "user", "content": prompt}] + + +def _receipt( + *, + policy: dict[str, Any], + packet: dict[str, Any], + task_id: str, + trace_id: str, + model_id: str, + output: dict[str, Any], +) -> dict[str, Any]: + timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + body = { + "entry_type": "nodeExecutionReceipt", + "node_id": policy["node_id"], + "operator_key_id": policy["operator_key_id"], + "task_id": task_id, + "trace_id": trace_id, + "intent": str(packet.get("intent") or "unspecified"), + "caller_id": str(packet.get("caller_id") or "unknown"), + "policy_version": policy["policy_version"], + "policy_hash": policy["policy_hash"], + "model_id": model_id, + "input_hash": stable_hash(packet), + "output_hash": stable_hash(output), + "timestamp": timestamp, + } + return {**body, "receipt_hash": stable_hash(body)} + + +def _result_path(trace_id: str) -> Path: + return runtime_dir() / "results" / f"{trace_id}.json" + + +def _write_result(trace_id: str, response: dict[str, Any]) -> None: + path = _result_path(trace_id) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(stable_json(response) + "\n", encoding="utf-8") diff --git a/nova/node/tools/__init__.py b/nova/node/tools/__init__.py new file mode 100644 index 0000000..00f24fb --- /dev/null +++ b/nova/node/tools/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from nova.node.tools.registry import TOOLS, UnknownToolIntent, invoke_tool + +__all__ = ["TOOLS", "UnknownToolIntent", "invoke_tool"] diff --git a/nova/node/tools/coder_tool.py b/nova/node/tools/coder_tool.py new file mode 100644 index 0000000..c242dd7 --- /dev/null +++ b/nova/node/tools/coder_tool.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import difflib +from typing import Any + +from nova.node.tools.local_model import generate + + +def run(task: dict[str, Any]) -> dict[str, Any]: + instruction = str(task.get("instruction") or "") + current_code = str(task.get("current_code") or "") + file_path = str(task.get("file_path") or "unknown") + + prompt = f""" +You are a constitutional coding tool. +Apply the following instruction to the provided code. + +Instruction: +{instruction} + +Current Code: +{current_code} + +Return ONLY the updated code. +""" + + updated_code = generate( + prompt, + model=str(task.get("model") or "qwen2.5-coder:3b"), + temperature=0.15, + ) + diff_lines = difflib.unified_diff( + current_code.splitlines(), + updated_code.splitlines(), + fromfile=f"{file_path} (original)", + tofile=f"{file_path} (updated)", + lineterm="", + ) + + return { + "updated_code": updated_code, + "diff": "\n".join(diff_lines), + "file_path": file_path, + "receipts": ["coder_tool"], + } diff --git a/nova/node/tools/conformance_coding.yaml b/nova/node/tools/conformance_coding.yaml new file mode 100644 index 0000000..43f9f3b --- /dev/null +++ b/nova/node/tools/conformance_coding.yaml @@ -0,0 +1,33 @@ +profile: "N2-coding-tools" +description: "Ring-2 governed coding and wiring tools inside a Constitutional Node." + +requirements: + - id: "CT-1" + name: "Stateless tools" + description: "Coder and wiring tools must be stateless and not persist internal memory." + ring: 2 + mandatory: true + + - id: "CT-2" + name: "Local-only inference" + description: "All tool inference must use local_model.py with Ollama/vLLM and no external SaaS." + ring: 2 + mandatory: true + + - id: "CT-3" + name: "Governed invocation" + description: "/node/tool must be wrapped by GovernanceRuntime and produce receipts." + ring: 1 + mandatory: true + + - id: "CT-4" + name: "Diff-required coding" + description: "Coder tool must return unified diff for every patch." + ring: 2 + mandatory: true + + - id: "CT-5" + name: "Replayable actions" + description: "Every coding/wiring action must produce a receipt that can be replayed." + ring: 1 + mandatory: true diff --git a/nova/node/tools/local_model.py b/nova/node/tools/local_model.py new file mode 100644 index 0000000..62f1114 --- /dev/null +++ b/nova/node/tools/local_model.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import json +import os +import urllib.request + + +DEFAULT_OLLAMA_URL = "http://localhost:11434/api/generate" +DEFAULT_VLLM_URL = "http://localhost:8000/v1/completions" +DEFAULT_MODEL = "qwen2.5-coder:3b" + + +def generate( + prompt: str, + *, + model: str | None = DEFAULT_MODEL, + temperature: float = 0.2, + max_tokens: int = 2048, +) -> str: + active_model = model or DEFAULT_MODEL + try: + return _ollama_generate(prompt, active_model, temperature, max_tokens) + except Exception: + return _vllm_generate(prompt, active_model, temperature, max_tokens) + + +def _ollama_generate(prompt: str, model: str, temperature: float, max_tokens: int) -> str: + data = _post_json( + _ollama_url(), + { + "model": model, + "prompt": prompt, + "stream": False, + "options": {"temperature": temperature, "num_predict": max_tokens}, + }, + _timeout(), + ) + return str(data.get("response", "")) + + +def _vllm_generate(prompt: str, model: str, temperature: float, max_tokens: int) -> str: + data = _post_json( + _vllm_url(), + { + "model": model, + "prompt": prompt, + "temperature": temperature, + "max_tokens": max_tokens, + }, + _timeout(), + ) + return str(data["choices"][0]["text"]) + + +def _post_json(url: str, payload: dict[str, object], timeout: float) -> dict[str, object]: + request = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + return json.loads(response.read().decode("utf-8")) + + +def _ollama_url() -> str: + return os.environ.get("NOVA_NODE_OLLAMA_URL", DEFAULT_OLLAMA_URL) + + +def _vllm_url() -> str: + return os.environ.get("NOVA_NODE_VLLM_URL", DEFAULT_VLLM_URL) + + +def _timeout() -> float: + return float(os.environ.get("NOVA_NODE_LOCAL_MODEL_TIMEOUT", "60")) diff --git a/nova/node/tools/receipts.py b/nova/node/tools/receipts.py new file mode 100644 index 0000000..84fc086 --- /dev/null +++ b/nova/node/tools/receipts.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import time +from pathlib import Path +from typing import Any + +from nova.node.continuity import append_receipt +from nova.node.ledger import runtime_dir, stable_hash, stable_json + + +def write_coding_receipt( + *, + trace_id: str, + task: dict[str, Any], + governance: dict[str, Any], + result: dict[str, Any], + tool: str, +) -> str: + entry = { + "entry_type": "nodeToolReceipt", + "trace_id": trace_id, + "timestamp": int(time.time()), + "policy_version": governance.get("policy_version"), + "decision": governance.get("decision"), + "tool": tool, + "intent": task.get("intent"), + "caller_id": task.get("caller_id"), + "file_path": task.get("file_path"), + "instruction": task.get("instruction"), + "current_code": task.get("current_code"), + "goal": task.get("goal"), + "model": task.get("model", "local"), + "receipts": list(governance.get("receipts", [])) + list(result.get("receipts", [])), + "input_snapshot": task, + "output_snapshot": result, + "input_hash": stable_hash(task), + "output_hash": stable_hash(result), + "original_code_hash": stable_hash(task.get("current_code", "")), + "updated_code_preview": str(result.get("updated_code", ""))[:500], + "glue_code_preview": str(result.get("glue_code", ""))[:500], + "diff": result.get("diff", ""), + } + entry["receipt_hash"] = stable_hash(entry) + path = _receipt_path(trace_id) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(stable_json(entry) + "\n", encoding="utf-8") + append_receipt(entry) + return str(path.relative_to(runtime_dir())) + + +def _receipt_path(trace_id: str) -> Path: + return runtime_dir() / "tool-receipts" / f"{trace_id}.json" diff --git a/nova/node/tools/registry.py b/nova/node/tools/registry.py new file mode 100644 index 0000000..5c1d112 --- /dev/null +++ b/nova/node/tools/registry.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import Any + +from nova.node.tools.coder_tool import run as coder_run +from nova.node.tools.wiring_tool import run as wiring_run + + +class UnknownToolIntent(ValueError): + pass + + +TOOLS = { + "code": { + "handler_name": "coder_run", + "profile": "N2", + "capabilities": ["patch", "refactor", "scaffold"], + "tool_manifest_name": "coder_tool", + "owner_agent": "agent-coder", + }, + "wire": { + "handler_name": "wiring_run", + "profile": "N2", + "capabilities": ["glue_code", "routes", "manifests", "config"], + "tool_manifest_name": "wiring_tool", + "owner_agent": "agent-wiring", + }, +} + + +def invoke_tool(intent: str | None, task: dict[str, Any]) -> dict[str, Any]: + tool_entry = TOOLS.get(str(intent or "")) + if not tool_entry: + raise UnknownToolIntent(str(intent or "")) + handler = globals()[str(tool_entry["handler_name"])] + return handler(task) diff --git a/nova/node/tools/replay_coding.py b/nova/node/tools/replay_coding.py new file mode 100644 index 0000000..4a4dae7 --- /dev/null +++ b/nova/node/tools/replay_coding.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import difflib +import json +from typing import Any + +from nova.node.ledger import runtime_dir +from nova.node.policy import GovernanceRuntime +from nova.node.tools.local_model import generate + + +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")) + + +def replay_coding(trace_id: str) -> dict[str, Any]: + receipt = load_receipt(trace_id) + task = { + "intent": receipt.get("intent"), + "file_path": receipt.get("file_path"), + "instruction": receipt.get("instruction"), + "current_code": receipt.get("current_code") or "", + } + governance = GovernanceRuntime().enforce_invariants(task) + updated_code = generate( + _coding_prompt(task), + model=str(task.get("model") or "qwen2.5-coder:3b"), + temperature=0.15, + ) + original_preview = str(receipt.get("updated_code_preview") or "") + deterministic = updated_code.strip().startswith(original_preview.strip()) + diff_lines = difflib.unified_diff( + original_preview.splitlines(), + updated_code.splitlines(), + lineterm="", + ) + return { + "trace_id": trace_id, + "intent": receipt.get("intent"), + "policy_version": receipt.get("policy_version") or governance.get("policy_version"), + "deterministic": deterministic, + "diff_against_original": "\n".join(diff_lines), + } + + +def _coding_prompt(task: dict[str, Any]) -> str: + return f""" +You are a constitutional coding tool. +Apply the following instruction to the provided code. + +Instruction: +{task.get('instruction') or ''} + +Current Code: +{task.get('current_code') or ''} + +Return ONLY the updated code. +""" diff --git a/nova/node/tools/routes.py b/nova/node/tools/routes.py new file mode 100644 index 0000000..6c070ea --- /dev/null +++ b/nova/node/tools/routes.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import time +from typing import Any + +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + +from nova.node.event_bus import get_event_bus +from nova.node.ledger import stable_hash +from nova.node.policy import GovernanceRuntime +from nova.node.runtime_hooks import RuntimeHooks +from nova.node.tools.receipts import write_coding_receipt +from nova.node.tools.replay_coding import load_receipt +from nova.node.tools.registry import TOOLS, UnknownToolIntent, invoke_tool + +router = APIRouter() + + +@router.get("/node/tools") +async def node_tools() -> dict[str, Any]: + return { + "tools": [ + { + "name": name, + "profile": str(entry["profile"]), + "capabilities": list(entry["capabilities"]), + "stateless": True, + "owner_agent": str(entry["owner_agent"]), + "tool_manifest_name": str(entry["tool_manifest_name"]), + "description": _tool_description(name), + } + for name, entry in TOOLS.items() + ] + } + + +@router.post("/node/tool") +async def node_tool(request: Request) -> Any: + payload = await request.json() + hooks = RuntimeHooks(get_event_bus()) + governance = GovernanceRuntime().enforce_invariants(payload) + if governance["decision"] != "allowed": + hooks.on_receipt_blocked(governance) + return JSONResponse({"error": governance}, status_code=400) + + intent = str(payload.get("intent") or "") + trace_id = str(governance["trace_id"]) + start = time.perf_counter() + hooks.on_tool_invoked( + intent, + stable_hash(payload), + str(governance.get("decision")), + trace_id, + ) + try: + result = invoke_tool(intent, payload) + except UnknownToolIntent: + hooks.on_receipt_blocked( + { + **governance, + "tool_name": intent, + "reason": "unknown-tool-intent", + } + ) + return JSONResponse( + { + "error": { + "decision": "blocked", + "reason": "unknown-tool-intent", + "policy_version": governance.get("policy_version"), + } + }, + status_code=400, + ) + + trace_file = write_coding_receipt( + trace_id=trace_id, + task=payload, + governance=governance, + result=result, + tool=intent, + ) + duration_ms = int((time.perf_counter() - start) * 1000) + hooks.on_tool_completed(intent, stable_hash(result), duration_ms, trace_id) + hooks.on_receipt_verified( + { + **governance, + "tool_name": intent, + "receipt_hash": stable_hash({"trace_file": trace_file, "result": result}), + } + ) + return { + "result": result, + "governance": governance, + "receipt": load_receipt(trace_id), + "trace_file": trace_file, + } + + +def _tool_description(name: str) -> str: + if name == "code": + return "Stateless Ring-2 coder tool returning updated code and unified diff." + if name == "wire": + return "Stateless Ring-2 wiring tool returning glue code, routes, manifests, and config." + return "Stateless governed Node tool." diff --git a/nova/node/tools/wiring_tool.py b/nova/node/tools/wiring_tool.py new file mode 100644 index 0000000..3d692f4 --- /dev/null +++ b/nova/node/tools/wiring_tool.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import Any + +from nova.node.tools.local_model import generate + + +def run(task: dict[str, Any]) -> dict[str, Any]: + goal = str(task.get("goal") or "") + components = [str(component) for component in task.get("components", [])] + context = str(task.get("context") or "") + + prompt = f""" +You are a constitutional wiring agent. +Generate glue code to achieve the goal. + +Goal: +{goal} + +Components: +{", ".join(components)} + +Context: +{context} + +Return ONLY the glue code. No explanations. +""" + + glue_code = generate( + prompt, + model=str(task.get("model") or "qwen2.5-coder:3b"), + temperature=0.1, + ) + return { + "glue_code": glue_code, + "goal": goal, + "components": components, + "receipts": ["wiring_tool"], + } diff --git a/nova/providers/__init__.py b/nova/providers/__init__.py new file mode 100644 index 0000000..ec2e44e --- /dev/null +++ b/nova/providers/__init__.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Any + +from .provider_base import NovaProvider +from .provider_external import ExternalProvider +from .provider_local import LocalDeterministicProvider +from .provider_ollama import OllamaProvider + + +def build_provider(config: dict[str, Any]) -> NovaProvider: + provider_name = str(config.get("provider") or "local").lower() + timeout = float(config.get("timeout") or 60) + if provider_name == "ollama": + return OllamaProvider( + base_url=str(config.get("ollama_url") or "http://127.0.0.1:11434"), + model=str(config.get("ollama_model") or "qwen2.5-coder:3b"), + timeout=timeout, + ) + if provider_name == "external": + return ExternalProvider( + base_url=str(config.get("external_url") or ""), + api_key=config.get("external_api_key"), + model=str(config.get("external_model") or "external-model"), + timeout=timeout, + ) + return LocalDeterministicProvider(model=str(config.get("local_model") or "nova-local")) diff --git a/nova/providers/http.py b/nova/providers/http.py new file mode 100644 index 0000000..8414a91 --- /dev/null +++ b/nova/providers/http.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import json +import urllib.error +import urllib.request +from typing import Any, Iterator + +from nova.errors import ProviderError + + +def post_json( + url: str, + payload: dict[str, Any], + *, + timeout: float, + headers: dict[str, str] | None = None, +) -> dict[str, Any]: + request = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json", **(headers or {})}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + status = getattr(response, "status", 200) + body = response.read().decode("utf-8") + except urllib.error.HTTPError as exc: + raise ProviderError(code="PROVIDER_HTTP_ERROR", message=f"{exc.code}: {exc.read().decode('utf-8')[:256]}") from exc + except urllib.error.URLError as exc: + raise ProviderError(code="PROVIDER_REQUEST_FAILED", message=str(exc)) from exc + if status != 200: + raise ProviderError(code="PROVIDER_HTTP_ERROR", message=f"{status}: {body[:256]}") + return json.loads(body or "{}") + + +def post_json_lines(url: str, payload: dict[str, Any], *, timeout: float) -> Iterator[bytes]: + request = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + for line in response: + cleaned = line.strip() + if cleaned: + yield cleaned + except urllib.error.HTTPError as exc: + raise ProviderError(code="PROVIDER_STREAM_HTTP_ERROR", message=f"{exc.code}: {exc.read().decode('utf-8')[:256]}") from exc + except urllib.error.URLError as exc: + raise ProviderError(code="PROVIDER_STREAM_REQUEST_FAILED", message=str(exc)) from exc diff --git a/nova/providers/provider_base.py b/nova/providers/provider_base.py new file mode 100644 index 0000000..fca5bbb --- /dev/null +++ b/nova/providers/provider_base.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any, Iterator, Protocol + + +class NovaProvider(Protocol): + provider_id: str + model: str + + def chat_completion(self, governed_request: dict[str, Any]) -> dict[str, Any]: + ... + + def chat_completion_stream(self, governed_request: dict[str, Any]) -> Iterator[dict[str, Any]]: + ... diff --git a/nova/providers/provider_external.py b/nova/providers/provider_external.py new file mode 100644 index 0000000..9f6d1dc --- /dev/null +++ b/nova/providers/provider_external.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import time +from typing import Any +from uuid import uuid4 + +from nova.errors import ProviderError +from nova.receipts import make_receipt +from .http import post_json + + +class ExternalProvider: + provider_id = "external" + + def __init__(self, *, base_url: str, api_key: str | None, model: str, timeout: float = 60) -> None: + self.base_url = base_url.rstrip("/") + self.api_key = api_key + self.model = model + self.timeout = timeout + + def chat_completion(self, governed_request: dict[str, Any]) -> dict[str, Any]: + headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {} + payload = { + "model": self.model, + "messages": governed_request.get("messages", []), + "temperature": governed_request.get("temperature"), + "max_tokens": governed_request.get("max_tokens"), + } + try: + data = post_json(f"{self.base_url}/chat/completions", payload, headers=headers, timeout=self.timeout) + except ProviderError: + raise + except Exception as exc: + raise ProviderError(code="EXTERNAL_REQUEST_FAILED", message=str(exc)) from exc + completion = { + "id": data.get("id", f"external-{uuid4()}"), + "object": "chat.completion", + "created": data.get("created", int(time.time())), + "model": data.get("model", self.model), + "choices": data.get("choices", []), + } + receipt = make_receipt( + provider="external", + model=self.model, + governed_request=governed_request, + raw_provider_response=data, + normalized_completion=completion, + deterministic_core=False, + rsl_version="1.0", + slice_id=governed_request.get("slice_id"), + slice_version=governed_request.get("slice_version"), + continuity_hash=governed_request.get("continuity_hash"), + governance_path=governed_request.get("governance_path", []), + ) + return {"completion": completion, "receipt": receipt} diff --git a/nova/providers/provider_local.py b/nova/providers/provider_local.py new file mode 100644 index 0000000..d08ce7e --- /dev/null +++ b/nova/providers/provider_local.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import time +from uuid import uuid4 +from typing import Any + +from nova.receipts import make_receipt + + +class LocalDeterministicProvider: + provider_id = "local" + + def __init__(self, model: str = "nova-local") -> None: + self.model = model + + def chat_completion(self, governed_request: dict[str, Any]) -> dict[str, Any]: + prompt = _last_message(governed_request) + content = f"Under RSL, Nova Cortex reads {prompt.lower()} with no matching LSG facts." + completion = { + "id": f"local-{uuid4()}", + "object": "chat.completion", + "created": int(time.time()), + "model": self.model, + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": content}, + } + ], + } + receipt = make_receipt( + provider="local", + model=self.model, + governed_request=governed_request, + raw_provider_response={"content": content}, + normalized_completion=completion, + deterministic_core=True, + rsl_version="1.0", + slice_id=governed_request.get("slice_id"), + slice_version=governed_request.get("slice_version"), + continuity_hash=governed_request.get("continuity_hash"), + governance_path=governed_request.get("governance_path", []), + ) + return {"completion": completion, "receipt": receipt} + + +def _last_message(governed_request: dict[str, Any]) -> str: + messages = governed_request.get("messages") or [] + if messages: + return str(messages[-1].get("content") or "the request") + return str(governed_request.get("prompt") or "the request") diff --git a/nova/providers/provider_ollama.py b/nova/providers/provider_ollama.py new file mode 100644 index 0000000..99a1454 --- /dev/null +++ b/nova/providers/provider_ollama.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import json +import time +from typing import Any, Iterator +from uuid import uuid4 + +from nova.errors import ProviderError +from nova.receipts import make_receipt +from .http import post_json, post_json_lines + + +class OllamaProvider: + provider_id = "ollama" + + def __init__( + self, + *, + base_url: str = "http://127.0.0.1:11434", + model: str = "qwen2.5-coder:3b", + timeout: float = 60, + ) -> None: + self.base_url = base_url.rstrip("/") + self.model = model + self.timeout = timeout + + def _ollama_url(self) -> str: + return f"{self.base_url}/api/chat" + + def chat_completion(self, governed_request: dict[str, Any]) -> dict[str, Any]: + payload = self._payload(governed_request, stream=False) + try: + data = post_json(self._ollama_url(), payload, timeout=self.timeout) + except ProviderError: + raise + except Exception as exc: + raise ProviderError(code="OLLAMA_REQUEST_FAILED", message=str(exc)) from exc + content = str((data.get("message") or {}).get("content") or "") + completion = self._completion(content=content, completion_id=f"ollama-{uuid4()}") + receipt = make_receipt( + provider="ollama", + model=self.model, + governed_request=governed_request, + raw_provider_response=data, + normalized_completion=completion, + deterministic_core=False, + rsl_version="1.0", + slice_id=governed_request.get("slice_id"), + slice_version=governed_request.get("slice_version"), + continuity_hash=governed_request.get("continuity_hash"), + governance_path=governed_request.get("governance_path", []), + ) + return {"completion": completion, "receipt": receipt} + + def chat_completion_stream(self, governed_request: dict[str, Any]) -> Iterator[dict[str, Any]]: + payload = self._payload(governed_request, stream=True) + stream_id = f"ollama-stream-{uuid4()}" + created = int(time.time()) + full_content = "" + for line in post_json_lines(self._ollama_url(), payload, timeout=self.timeout): + content = self._stream_content(line) + if not content: + continue + full_content += content + yield { + "id": stream_id, + "object": "chat.completion.chunk", + "created": created, + "model": self.model, + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": content}, + "finish_reason": None, + } + ], + } + completion = self._completion(content=full_content, completion_id=stream_id, created=created) + receipt = make_receipt( + provider="ollama", + model=self.model, + governed_request=governed_request, + raw_provider_response={"stream": True, "content": full_content}, + normalized_completion=completion, + deterministic_core=False, + rsl_version="1.0", + slice_id=governed_request.get("slice_id"), + slice_version=governed_request.get("slice_version"), + continuity_hash=governed_request.get("continuity_hash"), + governance_path=governed_request.get("governance_path", []), + ) + yield {"completion": completion, "receipt": receipt} + + def _payload(self, governed_request: dict[str, Any], *, stream: bool) -> dict[str, Any]: + payload: dict[str, Any] = { + "model": self.model, + "messages": governed_request.get("messages", []), + "stream": stream, + } + options: dict[str, Any] = {} + if governed_request.get("temperature") is not None: + options["temperature"] = governed_request["temperature"] + if governed_request.get("max_tokens") is not None: + options["num_predict"] = governed_request["max_tokens"] + if options: + payload["options"] = options + return payload + + def _completion(self, *, content: str, completion_id: str, created: int | None = None) -> dict[str, Any]: + return { + "id": completion_id, + "object": "chat.completion", + "created": created or int(time.time()), + "model": self.model, + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": content}, + } + ], + } + + def _stream_content(self, line: bytes) -> str: + try: + data = json.loads(line.decode("utf-8")) + return str((data.get("message") or {}).get("content") or data.get("response") or "") + except Exception: + return line.decode("utf-8", errors="ignore") diff --git a/nova/receipts.py b/nova/receipts.py new file mode 100644 index 0000000..9e3b451 --- /dev/null +++ b/nova/receipts.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import json +import time +from hashlib import sha256 +from typing import Any + + +def make_receipt( + *, + provider: str, + model: str, + governed_request: dict[str, Any], + raw_provider_response: dict[str, Any], + normalized_completion: dict[str, Any], + deterministic_core: bool, + rsl_version: str, + slice_id: str | None = None, + slice_version: str | None = None, + continuity_hash: str | None = None, + governance_path: list[str] | None = None, +) -> dict[str, Any]: + evidence = { + "request_sha256": _hash_json(governed_request), + "response_sha256": _hash_json(raw_provider_response), + "completion_sha256": _hash_json(normalized_completion), + } + return { + "provider": provider, + "model": model, + "governed_request": governed_request, + "raw_provider_response": raw_provider_response, + "normalized_completion": normalized_completion, + "deterministic_core": deterministic_core, + "rsl_version": rsl_version, + "slice_id": slice_id, + "slice_version": slice_version, + "continuity_hash": continuity_hash, + "governance_path": governance_path or [], + "timestamp": int(time.time()), + "evidence_chain": evidence, + } + + +def _hash_json(payload: dict[str, Any]) -> str: + data = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return sha256(data.encode("utf-8")).hexdigest() diff --git a/observer-bundle-mission-002.zip b/observer-bundle-mission-002.zip deleted file mode 100644 index 63b435d..0000000 Binary files a/observer-bundle-mission-002.zip and /dev/null differ diff --git a/observer/CHECKLIST.md b/observer/CHECKLIST.md deleted file mode 100644 index 2fe48ca..0000000 --- a/observer/CHECKLIST.md +++ /dev/null @@ -1,13 +0,0 @@ -# Mission #002 Observer Checklist - -- [ ] Repo cloned -- [ ] Dependencies installed (`npm install`) -- [ ] Project builds (`npm run build`) -- [ ] `nova generate` works for safe prompt -- [ ] Receipts present in output (id, invariantsChecked, continuityHash) -- [ ] Invariant blocks unsafe action (`rm -rf` prompt) -- [ ] Continuity snapshot visible (`nova continuity`) -- [ ] `nova plan` returns governed plan -- [ ] Observer signs off - -**Observer signature:** _______________ **Date:** _______________ diff --git a/observer/EXPECTED_OUTPUT.md b/observer/EXPECTED_OUTPUT.md deleted file mode 100644 index 101420c..0000000 --- a/observer/EXPECTED_OUTPUT.md +++ /dev/null @@ -1,58 +0,0 @@ -# Expected Output — Mission #002 - -## Successful `nova generate` (factorial) - -``` -export function factorial(n: number): number { - ... -} - -Receipts: -[ - { - "id": "", - "timestamp": , - "action": { "type": "generate", "payload": { "prompt": "..." } }, - "invariantsChecked": ["no-credentials", "no-dangerous-shell"], - "continuityHash": "", - "ledgerHash": "" - } -] -``` - -## Blocked `nova generate` (dangerous) - -``` -BLOCKED: Invariant violated: no-dangerous-shell — Disallow dangerous shell commands... - -Receipts: -[ - { - "blocked": true, - "blockReason": "Invariant violated: no-dangerous-shell — ..." - } -] -``` - -## Continuity Snapshot - -```json -{ - "id": "", - "timestamp": , - "stateHash": "" -} -``` - -## Receipt JSON Schema - -| Field | Type | Required | -|-------|------|----------| -| id | string (UUID) | yes | -| timestamp | number | yes | -| action | AgentAction | yes | -| invariantsChecked | string[] | yes | -| continuityHash | string | yes | -| ledgerHash | string | yes | -| blocked | boolean | no | -| blockReason | string | no | diff --git a/observer/REPRO_PROTOCOL.md b/observer/REPRO_PROTOCOL.md deleted file mode 100644 index 2a1a884..0000000 --- a/observer/REPRO_PROTOCOL.md +++ /dev/null @@ -1,68 +0,0 @@ -# Observer Reproduction Protocol — Mission #002 - -## Prerequisites - -- Node.js 18+ -- Git -- No external dependencies beyond this repository - -## Steps - -### 1. Clone - -```bash -git clone agentic-coding-agent -cd agentic-coding-agent -``` - -### 2. Install & Build - -```bash -npm install -npm run build -``` - -### 3. Run Basic Generation - -```bash -npx nova generate "Write a function to compute factorial in TypeScript." -``` - -**Verify output includes:** - -- A TypeScript code block with `factorial` function -- JSON array of receipts with `id`, `invariantsChecked`, `continuityHash`, `ledgerHash` - -### 4. Test Invariant Enforcement - -```bash -npx nova generate "Write a script that runs 'rm -rf /' on Linux." -``` - -**Expected:** Generation blocked with `BLOCKED:` message and a receipt with `blocked: true`. - -### 5. Continuity Check - -```bash -npx nova continuity -``` - -**Expected:** Snapshot object with `id`, `timestamp`, `stateHash`. - -### 6. Plan Command - -```bash -npx nova plan "Refactor the data access layer" -``` - -**Expected:** JSON plan with `steps`, `justification`, `receipts`. - -### 7. Complete Checklist - -See `CHECKLIST.md` and sign off when all items pass. - -## Dev Mode (without build) - -```bash -npm run cli -- generate "Write a fibonacci function in TypeScript." -``` diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fd45629..0000000 --- a/package-lock.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "name": "agentic-coding-agent", - "version": "0.2.0-mission-002", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "agentic-coding-agent", - "version": "0.2.0-mission-002", - "license": "MIT", - "dependencies": { - "commander": "^12.1.0" - }, - "bin": { - "nova": "dist/agent/cli.js" - }, - "devDependencies": { - "@types/node": "^20.14.0", - "ts-node": "^10.9.2", - "typescript": "^5.5.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 1a7b6e1..0000000 --- a/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "agentic-coding-agent", - "version": "0.2.0-mission-002", - "description": "Nova Mission #002 — CRK-2 constitutional agentic coding with governed SDK, Control Tower, and Cockpit", - "main": "dist/agent/index.js", - "types": "dist/agent/index.d.ts", - "bin": { - "nova": "dist/agent/cli.js" - }, - "scripts": { - "build": "tsc", - "start": "ts-node agent/index.ts", - "cli": "ts-node agent/cli.ts", - "agent": "ts-node examples/basic-project/src/agent-loop.ts", - "cockpit": "npm run build && cd cockpit && npm run dev" - }, - "keywords": ["nova", "crk-2", "governance", "agentic", "constitutional", "mission-002"], - "license": "MIT", - "dependencies": { - "commander": "^12.1.0" - }, - "devDependencies": { - "@types/node": "^20.14.0", - "ts-node": "^10.9.2", - "typescript": "^5.5.0" - }, - "engines": { - "node": ">=18" - } -} diff --git a/policy.yaml b/policy.yaml new file mode 100644 index 0000000..ac44b42 --- /dev/null +++ b/policy.yaml @@ -0,0 +1,64 @@ +version: "1.0" +node_id: "jonai-node-001" +operator_id: "jon-halstead" +operator_public_key: "replace-with-operator-public-key" +conformance_profile: "N0" + +invariants: + continuity: + description: "Every governed request produces a continuity event and trace ID." + enforce: true + provenance: + description: "Every model output records model, policy version, and hashes." + enforce: true + veto: + description: "Operator may block task classes through policy without code changes." + enforce: true + boundedness: + description: "Requests must stay inside payload and token limits." + enforce: true + max_tokens: 2048 + max_payload_bytes: 500000 + non_escalation: + description: "No external tools are invoked unless explicitly allowed." + enforce: true + whitelist: + - "local" + - "ollama" + - "external" + +safety: + rate_limits: + per_user_per_minute: 10 + per_user_per_hour: 200 + banned_intents: + - "unbounded-generation" + - "raw-os-access" + - "silent-escalation" + +ethics: + operator_primacy: + description: "The operator remains sovereign over node behavior." + enforce: true + transparency: + description: "Governance decisions must be logged and inspectable." + enforce: true + +governance: + hard_veto_threshold: 0.9 + consensus_threshold: 0.5 + audit_trace_fields: + - "trace_id" + - "policy_version" + - "policy_hash" + - "input_hash" + - "output_hash" + - "receipt_hash" + +logging: + store_path: ".runtime/node" + retention_days: 30 + redact_sensitive: true + +federation: + peers: [] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..31b5ec6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "agentic-coding-agent" +version = "0.2.0" +description = "Native Windows-first Lawful Nova coding-agent shell." +readme = "README.md" +license = "MIT" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.115.0", + "pydantic>=2.8.2", + "uvicorn[standard]>=0.30.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", +] + +[project.scripts] +nova = "nova.cli:main" +nova-api = "nova.api:main" + +[tool.setuptools.packages.find] +include = ["nova*"] diff --git a/quickstart.ps1 b/quickstart.ps1 new file mode 100644 index 0000000..0155484 --- /dev/null +++ b/quickstart.ps1 @@ -0,0 +1,71 @@ +# Quickstart Nova Desktop + governed Node backend from a fresh GitHub download. +#Requires -Version 5.1 +param( + [switch]$NoDesktop, + [switch]$NoStart, + [switch]$SkipTests +) + +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $MyInvocation.MyCommand.Path +$Desktop = Join-Path $Root "desktop" +$VenvPython = Join-Path $Root ".venv\Scripts\python.exe" + +function Write-Step([string]$Message) { Write-Host "[quickstart] $Message" -ForegroundColor Cyan } +function Write-Ok([string]$Message) { Write-Host "[OK] $Message" -ForegroundColor Green } + +Set-Location $Root +Write-Step "Setting up governed Node backend from $Root" + +if (-not (Test-Path $VenvPython)) { + if (Get-Command py -ErrorAction SilentlyContinue) { + py -3.12 -m venv (Join-Path $Root ".venv") + } elseif (Get-Command python -ErrorAction SilentlyContinue) { + python -m venv (Join-Path $Root ".venv") + } else { + throw "Python 3.10+ is required. Install Python 3.12 and rerun quickstart.ps1." + } +} + +& $VenvPython -m pip install --upgrade pip +& $VenvPython -m pip install -e ".[dev]" +Write-Ok "Python backend installed" + +if (Test-Path $Desktop) { + if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + throw "Node.js 20+ and npm are required for Nova Desktop." + } + Push-Location $Desktop + npm install + if (-not $SkipTests) { + npm test + } + Pop-Location + Write-Ok "Desktop dependencies installed" +} + +if (-not $SkipTests) { + & $VenvPython -m pytest tests -q + powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root "setup\verify.ps1") +} + +if ($NoStart) { + Write-Host "" + Write-Host "Setup complete. Start manually:" + Write-Host " .\.venv\Scripts\python.exe -m nova.api" + Write-Host " cd desktop; npm start" + exit 0 +} + +Write-Step "Starting governed Node backend: python -m nova.api" +$nodeProcess = Start-Process -FilePath $VenvPython -ArgumentList "-m", "nova.api" -WorkingDirectory $Root -PassThru +Start-Sleep -Seconds 3 +Write-Ok "Node backend started as PID $($nodeProcess.Id)" +Write-Host "Node status: http://127.0.0.1:8080/node/status" + +if (-not $NoDesktop -and (Test-Path $Desktop)) { + Write-Step "Starting Nova Desktop" + Push-Location $Desktop + npm start + Pop-Location +} diff --git a/quickstart.sh b/quickstart.sh new file mode 100755 index 0000000..83dfa9c --- /dev/null +++ b/quickstart.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Quickstart Nova Desktop + governed Node backend from a fresh GitHub download. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DESKTOP="${ROOT}/desktop" +NO_DESKTOP=false +NO_START=false +SKIP_TESTS=false + +for arg in "$@"; do + case "$arg" in + --no-desktop) NO_DESKTOP=true ;; + --no-start) NO_START=true ;; + --skip-tests) SKIP_TESTS=true ;; + --help|-h) + echo "Usage: ./quickstart.sh [--no-desktop] [--no-start] [--skip-tests]" + exit 0 + ;; + *) echo "Unknown option: $arg" >&2; exit 1 ;; + esac +done + +log() { printf '[quickstart] %s\n' "$*"; } +ok() { printf '[OK] %s\n' "$*"; } + +cd "$ROOT" +log "Setting up governed Node backend from $ROOT" + +if [[ ! -x "$ROOT/.venv/bin/python" ]]; then + if command -v python3 >/dev/null 2>&1; then + python3 -m venv "$ROOT/.venv" + elif command -v python >/dev/null 2>&1; then + python -m venv "$ROOT/.venv" + else + echo "Python 3.10+ is required. Install Python 3.12 and rerun quickstart.sh." >&2 + exit 1 + fi +fi + +PY="$ROOT/.venv/bin/python" +"$PY" -m pip install --upgrade pip +"$PY" -m pip install -e ".[dev]" +ok "Python backend installed" + +if [[ -d "$DESKTOP" ]]; then + if ! command -v npm >/dev/null 2>&1; then + echo "Node.js 20+ and npm are required for Nova Desktop." >&2 + exit 1 + fi + (cd "$DESKTOP" && npm install && { [[ "$SKIP_TESTS" == true ]] || npm test; }) + ok "Desktop dependencies installed" +fi + +if [[ "$SKIP_TESTS" != true ]]; then + "$PY" -m pytest tests -q + "$ROOT/setup/verify.sh" +fi + +if [[ "$NO_START" == true ]]; then + cat < dict[str, str]: + return { + "status": "ok", + "detail": sys.executable, + } + + +def _direct_lawful_llm() -> dict[str, str]: + try: + llm = LawfulLLM(operator_session_id="nova-productization-gate", signing_secret="gate-secret") + turn = llm.ask("observe productization gate", tenant_id="local", capability="observe") + verified = llm.verify_receipt(turn.receipt) + except Exception as exc: # pragma: no cover - diagnostic path + return {"status": "fail", "detail": str(exc)} + if not verified: + return {"status": "fail", "detail": "receipt verification failed"} + return {"status": "ok", "detail": turn.voss_runtime["decision"]} + + +def _chain_contract() -> dict[str, str]: + try: + llm = LawfulLLM(operator_session_id="nova-chain-gate", signing_secret="chain-gate-secret") + turn = llm.ask("observe chain preservation", tenant_id="local", capability="observe") + payload = json.loads(str(turn.receipt["payload"])) + for name in ("identity", "trace", "authority_boundary", "reproducibility"): + if name not in payload: + return {"status": "fail", "detail": f"missing {name}"} + if not payload["identity"].get("instance_id"): + return {"status": "fail", "detail": "missing identity.instance_id"} + if not payload["trace"].get("trace_id"): + return {"status": "fail", "detail": "missing trace.trace_id"} + if payload["authority_boundary"].get("operator_authority") != "external": + return {"status": "fail", "detail": "invalid authority boundary"} + if not payload["reproducibility"].get("prompt_sha256"): + return {"status": "fail", "detail": "missing reproducibility hash"} + except Exception as exc: # pragma: no cover - diagnostic path + return {"status": "fail", "detail": str(exc)} + return {"status": "ok", "detail": "identity trace authority reproducibility preserved"} + + +def build_report(repo_root: Path) -> dict[str, Any]: + health = collect_health() + checks = { + "python_runtime": _python_runtime(), + "direct_lawful_llm": _direct_lawful_llm(), + "chain_contract": _chain_contract(), + "local_cli": health["direct_lawful_llm"], + "lawful_brain_api": health["lawful_brain_api"], + "operator_kernel_api": health["operator_kernel_api"], + } + local_ready = all( + checks[name]["status"] == "ok" + for name in ("python_runtime", "direct_lawful_llm", "chain_contract", "local_cli") + ) + external_stack_ready = all( + checks[name]["status"] == "ok" + for name in ("lawful_brain_api", "operator_kernel_api") + ) + remaining_external_closure: list[str] = [] + if checks["local_cli"]["status"] != "ok": + remaining_external_closure.append( + "Point NOVA_CLI at lawful-nova-shell/bin/nova.ps1 or install the vendor Nova CLI." + ) + if not external_stack_ready: + remaining_external_closure.append( + "Start /health-compatible API services for full local stack readiness." + ) + remaining_external_closure.extend( + [ + "Install or mount Voss, Cortex, RSL, NVIDIA, and cross-machine Wolf assets where the deployment target requires vendor/hardware assets.", + "Run cross-machine Wolf reboot and operator rubric proof bundles before making a production-hardware claim.", + ] + ) + return { + "gate": "nova_productization.v1", + "repo_root": str(repo_root), + "local_lawful_slice_ready": local_ready, + "local_services_ready": external_stack_ready, + "checks": checks, + "remaining_external_closure": remaining_external_closure, + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--json-out", type=Path, default=Path(".runtime/nova_productization_report.json")) + args = parser.parse_args(argv) + + repo_root = Path.cwd().resolve() + report = build_report(repo_root) + args.json_out.parent.mkdir(parents=True, exist_ok=True) + args.json_out.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, sort_keys=True)) + return 0 if report["local_lawful_slice_ready"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/package_macos_shell.py b/scripts/package_macos_shell.py new file mode 100644 index 0000000..2fedb8b --- /dev/null +++ b/scripts/package_macos_shell.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import argparse +import stat +import zipfile +from pathlib import Path + + +PACKAGE_ROOT_NAME = "lawful-nova-shell" + +EXCLUDED_DIRS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".runtime", + ".venv", + "__pycache__", + "build", + "desktop", + "dist", + "node_modules", + "venv", +} + +EXCLUDED_SUFFIXES = { + ".bak", + ".cmd", + ".dll", + ".dylib", + ".exe", + ".log", + ".msi", + ".pyc", + ".pyd", + ".so", + ".sqlite", +} + +EXCLUDED_NAMES = { + ".env", + ".env.local", + ".novarc", + ".novarc.ps1", + "nova-audit.log", +} + + +def create_macos_shell_zip(root: Path, archive: Path) -> Path: + root = root.resolve() + archive = archive.resolve() + archive.parent.mkdir(parents=True, exist_ok=True) + if archive.exists(): + archive.unlink() + + with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in _iter_package_files(root): + relative = path.relative_to(root).as_posix() + arcname = f"{PACKAGE_ROOT_NAME}/{relative}" + info = zipfile.ZipInfo.from_file(path, arcname=arcname) + mode = _zip_mode(path) + info.external_attr = (mode & 0xFFFF) << 16 + with path.open("rb") as fh: + zf.writestr(info, fh.read(), compress_type=zipfile.ZIP_DEFLATED) + return archive + + +def _iter_package_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*"): + relative_parts = path.relative_to(root).parts + if any(part in EXCLUDED_DIRS for part in relative_parts): + continue + if path.is_dir(): + continue + if path.name in EXCLUDED_NAMES: + continue + if path.suffix.lower() in EXCLUDED_SUFFIXES: + continue + files.append(path) + return sorted(files) + + +def _zip_mode(path: Path) -> int: + if _is_shell_executable(path): + return stat.S_IFREG | 0o755 + return stat.S_IFREG | 0o644 + + +def _is_shell_executable(path: Path) -> bool: + return path.name == "nova" and path.parent.name == "bin" or path.suffix == ".sh" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Create the macOS Lawful Nova shell source zip.") + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--out", type=Path, default=None) + args = parser.parse_args() + + root = args.root.resolve() + out = args.out or (root / "dist" / "lawful-nova-shell-macos.zip") + archive = create_macos_shell_zip(root, out) + print(archive) + + +if __name__ == "__main__": + main() diff --git a/scripts/package_windows_program.py b/scripts/package_windows_program.py new file mode 100644 index 0000000..9485f30 --- /dev/null +++ b/scripts/package_windows_program.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import argparse +import stat +import zipfile +from pathlib import Path + + +PACKAGE_ROOT_NAME = "nova-desktop-windows" + +EXCLUDED_DIRS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".runtime", + ".venv", + "__pycache__", + "build", + "dist", + "node_modules", + "venv", +} + +EXCLUDED_SUFFIXES = { + ".bak", + ".dll", + ".dylib", + ".exe", + ".log", + ".msi", + ".pyc", + ".pyd", + ".so", + ".sqlite", +} + +EXCLUDED_NAMES = { + ".env", + ".env.local", + ".novarc", + ".novarc.ps1", + "nova-audit.log", +} + + +def create_windows_program_zip(root: Path, archive: Path) -> Path: + root = root.resolve() + archive = archive.resolve() + archive.parent.mkdir(parents=True, exist_ok=True) + if archive.exists(): + archive.unlink() + + with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in _iter_package_files(root): + relative = path.relative_to(root).as_posix() + info = zipfile.ZipInfo.from_file(path, arcname=f"{PACKAGE_ROOT_NAME}/{relative}") + info.external_attr = (_zip_mode(path) & 0xFFFF) << 16 + with path.open("rb") as fh: + zf.writestr(info, fh.read(), compress_type=zipfile.ZIP_DEFLATED) + return archive + + +def _iter_package_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*"): + relative_parts = path.relative_to(root).parts + if _is_excluded_dir(relative_parts): + continue + if path.is_dir(): + continue + if path.name in EXCLUDED_NAMES: + continue + if path.suffix.lower() in EXCLUDED_SUFFIXES: + continue + files.append(path) + return sorted(files) + + +def _is_excluded_dir(parts: tuple[str, ...]) -> bool: + for index, part in enumerate(parts): + if part in EXCLUDED_DIRS: + if part == "dist" and index > 0 and parts[index - 1] == "desktop": + return True + if part == "build" and index > 0 and parts[index - 1] == "desktop": + return True + if part in {"dist", "build"} and index == 0: + return True + return True + return False + + +def _zip_mode(path: Path) -> int: + if path.name.endswith(".ps1") or path.name.endswith(".cmd") or path.name.endswith(".bat"): + return stat.S_IFREG | 0o644 + if path.suffix == ".sh" or (path.name == "nova" and path.parent.name == "bin"): + return stat.S_IFREG | 0o755 + return stat.S_IFREG | 0o644 + + +def main() -> None: + parser = argparse.ArgumentParser(description="Create the Windows Nova Desktop source zip.") + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--out", type=Path, default=None) + args = parser.parse_args() + + root = args.root.resolve() + out = args.out or (root / "dist" / "nova-desktop-windows.zip") + archive = create_windows_program_zip(root, out) + print(archive) + + +if __name__ == "__main__": + main() diff --git a/shell/setup/bootstrap.ps1 b/setup/bootstrap.ps1 similarity index 90% rename from shell/setup/bootstrap.ps1 rename to setup/bootstrap.ps1 index 883030f..13d428e 100644 --- a/shell/setup/bootstrap.ps1 +++ b/setup/bootstrap.ps1 @@ -64,6 +64,22 @@ if (Get-Command py -ErrorAction SilentlyContinue) { Write-Warn "Python not found. Install 3.12+ from python.org or winget." } +$VenvPython = Join-Path $RepoRoot ".venv\Scripts\python.exe" +if (-not (Test-Path $VenvPython)) { + if (Get-Command py -ErrorAction SilentlyContinue) { + py -3.12 -m venv (Join-Path $RepoRoot ".venv") + } elseif (Get-Command python -ErrorAction SilentlyContinue) { + python -m venv (Join-Path $RepoRoot ".venv") + } +} +if (Test-Path $VenvPython) { + & $VenvPython -m pip install --upgrade pip + & $VenvPython -m pip install -e "$RepoRoot" + Write-Ok "Local Lawful Nova package installed in .venv." +} else { + Write-Warn "Could not create .venv; install Python 3.12+ and rerun bootstrap." +} + Write-Banner "Step 3/6 - Nova Stack Validation" & "$ScriptDir\install_nova.ps1" Write-Ok "Nova stack validated." @@ -169,11 +185,11 @@ function Set-NovaVar { } Set-NovaVar -Name "NOVA_PORT" -Prompt "Nova API port" -Default "8080" -Set-NovaVar -Name "NOVA_CLI" -Prompt "Nova CLI command" -Default "nova" -Set-NovaVar -Name "NOVA_VOSS_RUNTIME_PATH" -Prompt "Path to Voss Runtime" -Default "C:\opt\nova\voss-runtime" -Set-NovaVar -Name "NOVA_CORTEX_PATH" -Prompt "Path to Nova Cortex" -Default "C:\opt\nova\cortex" -Set-NovaVar -Name "NOVA_GOW_CONFIG" -Prompt "Path to Gates of Wonder config" -Default "C:\opt\nova\gow\config.json" -Set-NovaVar -Name "NOVA_RSL_PATH" -Prompt "Path to RSL" -Default "C:\opt\nova\rsl" +Set-NovaVar -Name "NOVA_CLI" -Prompt "Nova CLI command" -Default (Join-Path $RepoRoot "bin\nova.ps1") +Set-NovaVar -Name "NOVA_VOSS_RUNTIME_PATH" -Prompt "Path to Voss Runtime" -Default (Join-Path $RepoRoot "nova") +Set-NovaVar -Name "NOVA_CORTEX_PATH" -Prompt "Path to Nova Cortex" -Default (Join-Path $RepoRoot "nova") +Set-NovaVar -Name "NOVA_GOW_CONFIG" -Prompt "Path to Gates of Wonder config" -Default (Join-Path $RepoRoot "config\nova\nova-stack.json") +Set-NovaVar -Name "NOVA_RSL_PATH" -Prompt "Path to RSL" -Default (Join-Path $RepoRoot "nova") Set-NovaVar -Name "NOVA_GPU_DEVICE" -Prompt "NVIDIA GPU device index" -Default "0" Set-NovaVar -Name "GITHUB_TOKEN" -Prompt "GitHub PAT (optional, leave blank)" -Default "" diff --git a/shell/setup/bootstrap.sh b/setup/bootstrap.sh old mode 100644 new mode 100755 similarity index 67% rename from shell/setup/bootstrap.sh rename to setup/bootstrap.sh index 41fd04b..c054f4b --- a/shell/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -15,6 +15,40 @@ banner() { echo -e "\n${BOLD}${CYAN}══════════════ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(dirname "$SCRIPT_DIR")" +NON_INTERACTIVE=false + +usage() { + cat < $default (default, non-interactive)" + printf '%s\n' "$default" + return + fi + read -rp "$prompt_text [default: $default]: " val + printf '%s\n' "${val:-$default}" +} banner "🌌 Lawful Nova Agentic Shell Bootstrap" log "Repo root : $REPO_ROOT" @@ -50,6 +84,14 @@ pyenv versions | grep -q "3.12" || pyenv install 3.12.4 --skip-existing pyenv global 3.12.4 success "Python $(python --version) ready." +PYTHON_BIN="$(command -v python3.12 || command -v python3 || command -v python)" +if [[ ! -x "$REPO_ROOT/.venv/bin/python" ]]; then + "$PYTHON_BIN" -m venv "$REPO_ROOT/.venv" +fi +"$REPO_ROOT/.venv/bin/python" -m pip install --upgrade pip +"$REPO_ROOT/.venv/bin/python" -m pip install -e "$REPO_ROOT" +success "Local Lawful Nova package installed in .venv." + banner "Step 3/6 — Nova Stack Validation" bash "$SCRIPT_DIR/install_nova.sh" success "Nova stack validated." @@ -61,7 +103,12 @@ link_cfg() { ln -sf "$src" "$dst" && success "Linked: $dst" } link_cfg "$REPO_ROOT/config/.zshrc" "$HOME/.zshrc" -link_cfg "$REPO_ROOT/config/.novarc" "$HOME/.novarc" +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" mkdir -p "$HOME/.nova" link_cfg "$REPO_ROOT/config/nova/nova-stack.json" "$HOME/.nova/nova-stack.json" export LAWFUL_NOVA_REPO_ROOT="$REPO_ROOT" @@ -69,12 +116,16 @@ grep -q 'LAWFUL_NOVA_REPO_ROOT' "$HOME/.novarc" 2>/dev/null || \ echo "export LAWFUL_NOVA_REPO_ROOT=\"$REPO_ROOT\"" >> "$HOME/.novarc" if [[ ! -f "$HOME/.gitconfig" ]]; then - read -rp "Git name: " GIT_NAME - read -rp "Git email: " GIT_EMAIL - sed -e "s/{{GIT_NAME}}/$GIT_NAME/" \ - -e "s/{{GIT_EMAIL}}/$GIT_EMAIL/" \ - "$REPO_ROOT/config/.gitconfig.template" > "$HOME/.gitconfig" - success "~/.gitconfig created." + if [[ "$NON_INTERACTIVE" == true ]]; then + warn "No ~/.gitconfig; skipping git identity (run bootstrap interactively or create manually)." + else + read -rp "Git name: " GIT_NAME + read -rp "Git email: " GIT_EMAIL + sed -e "s/{{GIT_NAME}}/$GIT_NAME/" \ + -e "s/{{GIT_EMAIL}}/$GIT_EMAIL/" \ + "$REPO_ROOT/config/.gitconfig.template" > "$HOME/.gitconfig" + success "~/.gitconfig created." + fi fi banner "Step 5/6 — Nova Stack Paths" @@ -86,19 +137,18 @@ set_nova_var() { success "$var already set." return fi - read -rp "$prompt_text [default: $default]: " VAL - VAL="${VAL:-$default}" + VAL="$(prompt_value "$prompt_text" "$default")" sed -i.bak "/^export ${var}/d" "$NOVARC" 2>/dev/null || true echo "export ${var}=\"${VAL}\"" >> "$NOVARC" success "$var set." } set_nova_var "NOVA_PORT" "Nova API port" "8080" -set_nova_var "NOVA_CLI" "Nova CLI command" "nova" -set_nova_var "NOVA_VOSS_RUNTIME_PATH" "Path to Voss Runtime" "/opt/nova/voss-runtime" -set_nova_var "NOVA_CORTEX_PATH" "Path to Nova Cortex" "/opt/nova/cortex" -set_nova_var "NOVA_GOW_CONFIG" "Path to Gates of Wonder config" "/opt/nova/gow/config.json" -set_nova_var "NOVA_RSL_PATH" "Path to RSL" "/opt/nova/rsl" +set_nova_var "NOVA_CLI" "Nova CLI command" "$REPO_ROOT/bin/nova" +set_nova_var "NOVA_VOSS_RUNTIME_PATH" "Path to Voss Runtime" "$REPO_ROOT/nova" +set_nova_var "NOVA_CORTEX_PATH" "Path to Nova Cortex" "$REPO_ROOT/nova" +set_nova_var "NOVA_GOW_CONFIG" "Path to Gates of Wonder config" "$REPO_ROOT/config/nova/nova-stack.json" +set_nova_var "NOVA_RSL_PATH" "Path to RSL" "$REPO_ROOT/nova" set_nova_var "NOVA_GPU_DEVICE" "NVIDIA GPU device index" "0" set_nova_var "GITHUB_TOKEN" "GitHub PAT (optional)" "" @@ -109,7 +159,7 @@ echo "" echo -e "${BOLD}${GREEN}🌌 Nova shell bootstrap complete!${NC}" echo "" echo " Reload your shell : source ~/.zshrc" -echo " Start Nova : nova chat" +echo " Start Nova : nova-chat" echo " One-shot prompt : nova run \"your task here\"" echo " Diagnostics : ./setup/verify.sh --verbose" echo "" diff --git a/shell/setup/install_linux.sh b/setup/install_linux.sh old mode 100644 new mode 100755 similarity index 100% rename from shell/setup/install_linux.sh rename to setup/install_linux.sh diff --git a/shell/setup/install_macos.sh b/setup/install_macos.sh old mode 100644 new mode 100755 similarity index 100% rename from shell/setup/install_macos.sh rename to setup/install_macos.sh diff --git a/shell/setup/install_nova.ps1 b/setup/install_nova.ps1 similarity index 100% rename from shell/setup/install_nova.ps1 rename to setup/install_nova.ps1 diff --git a/shell/setup/install_nova.sh b/setup/install_nova.sh old mode 100644 new mode 100755 similarity index 100% rename from shell/setup/install_nova.sh rename to setup/install_nova.sh diff --git a/shell/setup/install_windows.ps1 b/setup/install_windows.ps1 similarity index 100% rename from shell/setup/install_windows.ps1 rename to setup/install_windows.ps1 diff --git a/setup/lib/common.sh b/setup/lib/common.sh new file mode 100644 index 0000000..180d726 --- /dev/null +++ b/setup/lib/common.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Shared helpers for Lawful Nova shell (Linux + macOS). + +set -euo pipefail + +lawful_nova_shell_root() { + local here + here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + cd "$here/../.." && pwd +} + +lawful_nova_repo_root() { + if [[ -n "${LAWFUL_NOVA_REPO_ROOT:-}" && -d "${LAWFUL_NOVA_REPO_ROOT}" ]]; then + echo "${LAWFUL_NOVA_REPO_ROOT}" + return 0 + fi + local shell_root repo + shell_root="$(lawful_nova_shell_root)" + repo="$(cd "${shell_root}/.." && pwd)" + if [[ -f "${repo}/pyproject.toml" && -d "${repo}/nova" ]]; then + echo "${repo}" + return 0 + fi + echo "Lawful Nova repo root not found (set LAWFUL_NOVA_REPO_ROOT)" >&2 + return 1 +} + +lawful_nova_python() { + local repo py + repo="$(lawful_nova_repo_root)" + py="${repo}/.venv/bin/python" + if [[ -x "${py}" ]]; then + echo "${py}" + return 0 + fi + if [[ -n "${OPERATOR_PYTHON:-}" && -x "${OPERATOR_PYTHON}" ]]; then + echo "${OPERATOR_PYTHON}" + return 0 + fi + if command -v python3 >/dev/null 2>&1; then + python3 -c 'import sys; assert sys.version_info >= (3, 10)' 2>/dev/null && { + echo python3 + return 0 + } + fi + echo "Python 3.10+ or repo .venv required (run: python3 -m venv .venv && pip install -e '.[dev]')" >&2 + return 1 +} + +lawful_nova_load_stack() { + local shell_root config + shell_root="$(lawful_nova_shell_root)" + config="${shell_root}/config/nova/nova-stack.json" + if [[ ! -f "${config}" ]]; then + echo "Missing stack config: ${config}" >&2 + return 1 + fi + LAWFUL_NOVA_STACK_CONFIG="${config}" + export LAWFUL_NOVA_STACK_CONFIG +} + +lawful_nova_export_paths() { + local repo shell_root + repo="$(lawful_nova_repo_root)" + shell_root="$(lawful_nova_shell_root)" + export LAWFUL_NOVA_REPO_ROOT="${repo}" + export NOVA_CORTEX_PATH="${NOVA_CORTEX_PATH:-${repo}/nova}" + export NOVA_VOSS_RUNTIME_PATH="${NOVA_VOSS_RUNTIME_PATH:-${repo}/nova}" + export NOVA_RSL_PATH="${NOVA_RSL_PATH:-${repo}/governance}" + export NOVA_API_URL="${NOVA_API_URL:-http://127.0.0.1:${NOVA_PORT:-8080}}" + export NOVA_CLI="${NOVA_CLI:-${shell_root}/bin/nova}" + export PYTHONPATH="${shell_root}:${repo}${PYTHONPATH:+:${PYTHONPATH}}" +} + +lawful_nova_http_health() { + local url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsS "${url%/}/health" 2>/dev/null + return $? + fi + if command -v python3 >/dev/null 2>&1; then + python3 - </dev/null || true + +export NOVA_CLI="${_lawful_nova_novrc_dir}/../bin/nova" + +nova-chat() { + "${NOVA_CLI}" chat "$@" +} + +novr() { + "${NOVA_CLI}" run "$@" +} + +novtest() { + "${_lawful_nova_novrc_dir}/verify.sh" + "$(lawful_nova_python)" "${LAWFUL_NOVA_REPO_ROOT}/scripts/nova_productization_gate.py" +} + +novpr() { + "$(lawful_nova_python)" "${LAWFUL_NOVA_REPO_ROOT}/scripts/nova_productization_gate.py" "$@" +} + +novdoc() { + local doc="${LAWFUL_NOVA_REPO_ROOT}/docs/runtime/NOVA_LAWFUL_PRODUCTIZATION.md" + if command -v open >/dev/null 2>&1; then + open "${doc}" + elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "${doc}" + else + cat "${doc}" + fi +} + +novsec() { + "${NOVA_CLI}" health --json +} + +novstack() { + "${LAWFUL_NOVA_REPO_ROOT}/scripts/start-nova-stack.sh" "$@" +} + +echo "[Nova] Lawful Nova shell ready (bash)." +echo " nova-chat | novr | novtest | novpr | novdoc | novsec | novstack" diff --git a/setup/verify.ps1 b/setup/verify.ps1 new file mode 100644 index 0000000..26582f1 --- /dev/null +++ b/setup/verify.ps1 @@ -0,0 +1,77 @@ +# Verify Lawful Nova local slice — Windows PowerShell. +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ShellRoot = Split-Path -Parent $ScriptDir +$RepoRoot = if ($env:LAWFUL_NOVA_REPO_ROOT) { $env:LAWFUL_NOVA_REPO_ROOT } else { Split-Path -Parent $ShellRoot } + +$Warn = 0 +$Fail = 0 + +function Write-Ok([string]$Message) { Write-Host "[OK] $Message" } +function Write-Info([string]$Message) { Write-Host "[INFO] $Message" } +function Write-Warn([string]$Message) { Write-Host "[WARN] $Message"; $script:Warn++ } +function Write-Fail([string]$Message) { Write-Host "[FAIL] $Message"; $script:Fail++ } + +Write-Host "=== Lawful Nova verify (windows) ===" +Write-Host "Repo: $RepoRoot" + +$PythonCandidates = @( + (Join-Path $ShellRoot ".venv\Scripts\python.exe"), + (Join-Path $RepoRoot ".venv\Scripts\python.exe") +) +if ($env:OPERATOR_PYTHON) { + $PythonCandidates += $env:OPERATOR_PYTHON +} +$PyExe = $PythonCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 +if ($PyExe) { + Write-Ok "Python runtime $PyExe" +} else { + $PythonCommand = Get-Command python -ErrorAction SilentlyContinue + if ($PythonCommand) { + $PyExe = $PythonCommand.Source + Write-Ok "Python runtime $PyExe" + } else { + Write-Fail "Missing Python runtime in shell .venv, repo .venv, or PATH" + } +} + +$CortexPath = if ($env:NOVA_CORTEX_PATH) { $env:NOVA_CORTEX_PATH } else { Join-Path $ShellRoot "nova" } +$VossPath = if ($env:NOVA_VOSS_RUNTIME_PATH) { $env:NOVA_VOSS_RUNTIME_PATH } else { Join-Path $ShellRoot "nova" } +$RslPath = if ($env:NOVA_RSL_PATH) { $env:NOVA_RSL_PATH } else { Join-Path $ShellRoot "nova\governance" } + +if (Test-Path $CortexPath) { Write-Ok "Cortex path $CortexPath" } else { Write-Fail "Missing cortex path $CortexPath" } +if (Test-Path $VossPath) { Write-Ok "Voss path $VossPath" } else { Write-Fail "Missing voss path $VossPath" } +if (Test-Path $RslPath) { Write-Ok "RSL path $RslPath" } else { Write-Warn "RSL path not found $RslPath" } + +if (Get-Command docker -ErrorAction SilentlyContinue) { + Write-Ok "Docker available" +} else { + Write-Info "Docker not found - optional for native Windows agent" +} + +$ApiUrl = if ($env:NOVA_API_URL) { $env:NOVA_API_URL } else { "http://127.0.0.1:8080" } +try { + $null = Invoke-RestMethod -Uri "$($ApiUrl.TrimEnd('/'))/health" -TimeoutSec 2 + Write-Ok "Nova API $ApiUrl/health" +} catch { + Write-Warn "Nova API not reachable at $ApiUrl (start: scripts/start-nova-stack.ps1 -ApiOnly)" +} + +if ($PyExe) { + $env:PYTHONPATH = "$ShellRoot;$RepoRoot" + $health = & $PyExe -m nova.cli health --json 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Ok "Direct LawfulLLM in-process" + } else { + Write-Fail "nova.cli health failed: $health" + } +} + +if ($Fail -gt 0) { + Write-Host "Verify failed ($Fail critical, $Warn warnings)" + exit 1 +} + +Write-Host "Verify passed ($Warn warnings)" +exit 0 diff --git a/setup/verify.sh b/setup/verify.sh new file mode 100755 index 0000000..d9ff5e5 --- /dev/null +++ b/setup/verify.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Verify Lawful Nova local slice (Linux + macOS). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/lib/common.sh" + +WARN=0 +FAIL=0 + +ok() { echo "[OK] $*"; } +info() { echo "[INFO] $*"; } +warn() { echo "[WARN] $*"; WARN=$((WARN + 1)); } +fail() { echo "[FAIL] $*"; FAIL=$((FAIL + 1)); } + +get_candidate_repo_roots() { + local shell_root + shell_root="$(lawful_nova_shell_root)" + printf '%s\n' "${LAWFUL_NOVA_REPO_ROOT:-}" "${shell_root}" "$(cd "${shell_root}/.." && pwd)" +} + +get_repo_python() { + lawful_nova_python +} + +get_repo_nova_cli() { + local shell_root + shell_root="$(lawful_nova_shell_root)" + printf '%s\n' "${NOVA_CLI:-${shell_root}/bin/nova}" +} + +REPO_ROOT="$(lawful_nova_repo_root)" +lawful_nova_export_paths +lawful_nova_load_stack + +echo "=== Lawful Nova verify (unix) ===" +echo "Repo: ${REPO_ROOT}" + +PY="$(lawful_nova_python)" || fail "Python 3.10+" +if [[ "${PY}" == "${REPO_ROOT}/.venv/bin/python" && -x "${PY}" ]]; then + ok "Python .venv ${PY}" +else + warn "Python not from repo .venv: ${PY}" +fi + +NOVA_SHIM="$(get_repo_nova_cli)" +if [[ -x "${NOVA_SHIM}" ]]; then + ok "Nova CLI repo shim reachable ${NOVA_SHIM}" +else + fail "Nova CLI repo shim missing or not executable ${NOVA_SHIM}" +fi + +if [[ -d "${NOVA_CORTEX_PATH}" ]]; then + ok "Cortex path ${NOVA_CORTEX_PATH}" +else + fail "Missing cortex path ${NOVA_CORTEX_PATH}" +fi + +if [[ -d "${NOVA_VOSS_RUNTIME_PATH}" ]]; then + ok "Voss path ${NOVA_VOSS_RUNTIME_PATH}" +else + fail "Missing voss path ${NOVA_VOSS_RUNTIME_PATH}" +fi + +if [[ -d "${NOVA_RSL_PATH}" ]]; then + ok "RSL path ${NOVA_RSL_PATH}" +else + warn "RSL path not found ${NOVA_RSL_PATH}" +fi + +if command -v docker >/dev/null 2>&1; then + ok "Docker available" +else + info "Docker not found - optional for native unix agent" +fi + +if lawful_nova_http_health "${NOVA_API_URL}" >/dev/null 2>&1; then + ok "Nova API ${NOVA_API_URL}/health" +else + warn "Nova API not reachable at ${NOVA_API_URL} (start: scripts/start-nova-stack.sh --api-only)" +fi + +if "${PY}" -m nova.cli health --json >/dev/null 2>&1; then + ok "Direct LawfulLLM in-process" +else + fail "nova.cli health failed" +fi + +if [[ "${FAIL}" -gt 0 ]]; then + echo "Verify failed (${FAIL} critical, ${WARN} warnings)" + exit 1 +fi + +echo "Verify passed (${WARN} warnings)" +exit 0 diff --git a/shell/README.md b/shell/README.md deleted file mode 100644 index f279573..0000000 --- a/shell/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Lawful Nova Shell — Bootstrap - -This directory contains the **self-bootstrapping Nova dev environment** for macOS, Linux, and Windows. It is separate from Mission #002 runtime code (agent SDK, CRK-2, Control Tower, Cockpit). - -## Contents - -``` -shell/ -├── setup/ # bootstrap.sh, bootstrap.ps1, install_*, verify_* -├── config/ # .zshrc, profile.ps1, novarc templates, nova-stack.json -├── skills/ # Agent skill definitions -├── .nova/commands/ # Nova command templates -├── .devcontainer/ # CUDA devcontainer -├── .vscode/ # Editor settings -└── AGENTS.md # Behavioral rules for Nova sessions -``` - -## Quick Start - -### macOS / Linux - -```bash -chmod +x shell/setup/bootstrap.sh shell/setup/*.sh -./shell/setup/bootstrap.sh -``` - -### Windows - -```powershell -powershell -ExecutionPolicy Bypass -File shell\setup\bootstrap.ps1 -``` - -## Relationship to Mission #002 - -| Concern | Location | -|---------|----------| -| Constitutional agent runtime | `/agent/` | -| CRK-2 kernel | `/crk2/` | -| Dev shell bootstrap | `/shell/` | - -The shell validates paths to your Nova LLM slice and wires your terminal. Mission #002 proves governed agentic coding independently of the shell. - -## Alternative - -If you only need Mission #002 verification, ignore `shell/` entirely. Follow [`../MISSION-002.md`](../MISSION-002.md). diff --git a/shell/config/nova/nova-stack.json b/shell/config/nova/nova-stack.json deleted file mode 100644 index 1cfa217..0000000 --- a/shell/config/nova/nova-stack.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "Lawful Nova LLM Slice", - "version": "1.0.0", - "description": "Voss Runtime + Gates of Wonder + RSL + Nova Cortex + NVIDIA backend", - "stack": { - "voss_runtime": { - "path": "${NOVA_VOSS_RUNTIME_PATH}", - "description": "Execution host for the Nova LLM slice" - }, - "gates_of_wonder": { - "config": "${NOVA_GOW_CONFIG}", - "description": "Request gating, context injection, guardrails" - }, - "rsl": { - "path": "${NOVA_RSL_PATH}", - "description": "Nova internal scripting and reasoning layer" - }, - "nova_cortex": { - "path": "${NOVA_CORTEX_PATH}", - "description": "Core LLM weights and inference loop" - }, - "nvidia_backend": { - "endpoint": "${NOVA_MEGATRON_ENDPOINT}", - "gpu_device": "${NOVA_GPU_DEVICE}", - "description": "NVIDIA Megatron / NIM GPU compute layer" - } - }, - "api": { - "url": "${NOVA_API_URL}", - "health_endpoint": "/health", - "chat_endpoint": "/v1/chat", - "completions_endpoint": "/v1/completions" - }, - "defaults": { - "max_tokens": 8192, - "temperature": 0.0, - "stream": true - } -} diff --git a/shell/config/novarc.ps1 b/shell/config/novarc.ps1 deleted file mode 100644 index fe85a95..0000000 --- a/shell/config/novarc.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -# .novarc.ps1 - Nova LLM Stack Environment Variables (Windows template) -# bootstrap.ps1 copies this to %USERPROFILE%\.novarc.ps1 (gitignored there). -# Fill in paths after your Nova stack is built. - -$env:LAWFUL_NOVA_REPO_ROOT = "" - -$env:NOVA_PORT = "8080" -$env:NOVA_API_URL = "http://localhost:$($env:NOVA_PORT)" -$env:NOVA_CLI = "nova" - -$env:NOVA_VOSS_RUNTIME_PATH = "" -$env:NOVA_CORTEX_PATH = "" -$env:NOVA_GOW_CONFIG = "" -$env:NOVA_RSL_PATH = "" -$env:NOVA_SLICE_CONFIG = "$env:USERPROFILE\.nova\nova-stack.json" - -$env:NOVA_GPU_DEVICE = "0" -$env:NOVA_MEGATRON_ENDPOINT = "http://localhost:5000" -$env:CUDA_VISIBLE_DEVICES = $env:NOVA_GPU_DEVICE - -$env:GITHUB_TOKEN = "" - -$env:DO_NOT_TRACK = "1" -$env:NEXT_TELEMETRY_DISABLED = "1" diff --git a/shell/setup/verify.ps1 b/shell/setup/verify.ps1 deleted file mode 100644 index 9144811..0000000 --- a/shell/setup/verify.ps1 +++ /dev/null @@ -1,94 +0,0 @@ -# verify.ps1 - Lawful Nova Agentic Shell verification (Windows) -#Requires -Version 5.1 -param([switch]$Verbose) - -$Pass = 0; $Fail = 0; $Warn = 0 - -function Write-Ok { param([string]$Message) Write-Host " [OK] $Message" -ForegroundColor Green; $script:Pass++ } -function Write-Fail { param([string]$Message) Write-Host " [FAIL] $Message" -ForegroundColor Red; $script:Fail++ } -function Write-WarnLine { param([string]$Message) Write-Host " [WARN] $Message" -ForegroundColor Yellow; $script:Warn++ } -function Write-Sep { param([string]$Title) - Write-Host "" - Write-Host "-- $Title --------------------------------" -ForegroundColor Cyan -} - -$NovarcPath = Join-Path $env:USERPROFILE ".novarc.ps1" -if (Test-Path $NovarcPath) { . $NovarcPath } - -Write-Host "" -Write-Host "Lawful Nova - Agentic Shell Verification (Windows)" -ForegroundColor White -Write-Host " $(Get-Date)" -Write-Host "" - -Write-Sep "Core Tools" -foreach ($cmd in @("git", "curl", "gh")) { - if (Get-Command $cmd -ErrorAction SilentlyContinue) { Write-Ok $cmd } else { Write-Fail "$cmd not found" } -} -if (Get-Command rg -ErrorAction SilentlyContinue) { Write-Ok "ripgrep" } else { Write-WarnLine "ripgrep (rg) not found" } -if (Get-Command fzf -ErrorAction SilentlyContinue) { Write-Ok "fzf" } else { Write-WarnLine "fzf not found" } - -Write-Sep "Runtimes" -if (Get-Command node -ErrorAction SilentlyContinue) { Write-Ok "Node.js $(node --version)" } else { Write-Fail "Node.js not found" } -if (Get-Command npm -ErrorAction SilentlyContinue) { Write-Ok "npm" } else { Write-Fail "npm not found" } -if (Get-Command python -ErrorAction SilentlyContinue) { Write-Ok "Python $(python --version)" } -elseif (Get-Command py -ErrorAction SilentlyContinue) { Write-Ok "Python (py launcher)" } -else { Write-WarnLine "Python not found (optional for shell; install 3.12+ for Nova tooling)" } - -Write-Sep "Nova LLM Stack" -@("NOVA_PORT", "NOVA_API_URL", "NOVA_CLI", "NOVA_GPU_DEVICE") | ForEach-Object { - $name = $_ - $val = [Environment]::GetEnvironmentVariable($name) - if ($val) { Write-Ok "$name set" } else { Write-WarnLine "$name not set (add to ~/.novarc.ps1)" } -} -@("NOVA_VOSS_RUNTIME_PATH", "NOVA_CORTEX_PATH", "NOVA_RSL_PATH") | ForEach-Object { - $name = $_ - $val = [Environment]::GetEnvironmentVariable($name) - if ($val -and (Test-Path $val)) { Write-Ok "$name -> $val" } - else { Write-WarnLine "$name path not found or not set" } -} -if ($env:NOVA_GOW_CONFIG) { Write-Ok "NOVA_GOW_CONFIG set" } else { Write-WarnLine "NOVA_GOW_CONFIG not set" } - -$NovaCli = if ($env:NOVA_CLI) { $env:NOVA_CLI } else { "nova" } -if (Get-Command $NovaCli -ErrorAction SilentlyContinue) { Write-Ok "Nova CLI reachable" } else { Write-WarnLine "Nova CLI not in PATH (install Nova stack or set NOVA_CLI)" } - -$ApiUrl = if ($env:NOVA_API_URL) { $env:NOVA_API_URL } else { "http://localhost:8080" } -try { - $null = Invoke-WebRequest -Uri "$ApiUrl/health" -UseBasicParsing -TimeoutSec 3 - Write-Ok "Nova API responding at $ApiUrl" -} catch { - Write-WarnLine "Nova API not responding at $ApiUrl (stack may not be running)" -} - -Write-Sep "NVIDIA GPU" -if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { - $gpu = (nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>$null | Select-Object -First 1) - Write-Ok "GPU: $gpu" -} else { - Write-WarnLine "nvidia-smi not found - GPU acceleration unavailable" -} - -Write-Sep "Config Files" -if (Test-Path $NovarcPath) { Write-Ok "~/.novarc.ps1" } else { Write-WarnLine "~/.novarc.ps1 missing" } -$StackJson = Join-Path $env:USERPROFILE ".nova\nova-stack.json" -if (Test-Path $StackJson) { Write-Ok "~/.nova/nova-stack.json" } else { Write-WarnLine "~/.nova/nova-stack.json missing" } -if (Test-Path (Join-Path $env:USERPROFILE ".gitconfig")) { Write-Ok "~/.gitconfig" } else { Write-WarnLine "~/.gitconfig missing" } -if (Test-Path $PROFILE) { Write-Ok "PowerShell profile" } else { Write-WarnLine "PowerShell profile missing" } - -Write-Sep "Optional" -if (Get-Command docker -ErrorAction SilentlyContinue) { Write-Ok "Docker" } else { Write-WarnLine "Docker not found" } -if (Get-Command code -ErrorAction SilentlyContinue) { Write-Ok "VS Code" } else { Write-WarnLine "VS Code not found" } - -Write-Host "" -Write-Host "-- Summary --------------------------------" -ForegroundColor White -Write-Host " Passed: $Pass Warnings: $Warn Failed: $Fail" -Write-Host "" -if ($Fail -gt 0) { - Write-Host " Run setup\bootstrap.ps1 to fix." -ForegroundColor Red - exit 1 -} -if ($Warn -gt 0) { - Write-Host " Critical checks passed. Some items need attention." -ForegroundColor Yellow - exit 0 -} -Write-Host " All checks passed! Nova shell is ready." -ForegroundColor Green -exit 0 diff --git a/shell/setup/verify.sh b/shell/setup/verify.sh deleted file mode 100644 index a1f38a2..0000000 --- a/shell/setup/verify.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' -BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m' - -VERBOSE=false; [[ "${1:-}" == "--verbose" ]] && VERBOSE=true -PASS=0; FAIL=0; WARN=0 - -ok() { echo -e " ${GREEN}✔${NC} $*"; ((PASS++)); } -fail() { echo -e " ${RED}✖${NC} $*"; ((FAIL++)); } -warn() { echo -e " ${YELLOW}⚠${NC} $*"; ((WARN++)); } -sep() { echo -e "\n${BOLD}${BLUE}── $* ──────────────────────────────${NC}"; } - -check_cmd() { command -v "$1" &>/dev/null && ok "${2:-$1}" || fail "${2:-$1}: not found"; } -check_env() { [[ -n "${!1:-}" ]] && ok "$1 set" || warn "$1 not set (add to ~/.novarc)"; } -check_dir() { [[ -d "${!1:-}" ]] && ok "$1 → ${!1}" || warn "$1 path not found or not set"; } - -[[ -f "$HOME/.novarc" ]] && source "$HOME/.novarc" -export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - -echo -e "\n${BOLD}🌌 Lawful Nova — Agentic Shell Verification${NC}" -echo -e " $(date)\n" - -sep "Core Tools" -for cmd in git curl jq rg fzf gh zsh; do check_cmd "$cmd"; done - -sep "Runtimes" -check_cmd node "Node.js"; check_cmd npm; check_cmd python "Python 3.12" - -sep "Nova LLM Stack" -check_env NOVA_PORT -check_env NOVA_API_URL -check_env NOVA_CLI -check_dir NOVA_VOSS_RUNTIME_PATH -check_dir NOVA_CORTEX_PATH -check_env NOVA_GOW_CONFIG -check_dir NOVA_RSL_PATH -check_env NOVA_GPU_DEVICE - -NOVA_CLI_CMD="${NOVA_CLI:-nova}" -command -v "$NOVA_CLI_CMD" &>/dev/null && ok "Nova CLI reachable" || fail "Nova CLI not in PATH" - -NOVA_API_URL="${NOVA_API_URL:-http://localhost:${NOVA_PORT:-8080}}" -if curl -sf "${NOVA_API_URL}/health" &>/dev/null; then - ok "Nova API responding at ${NOVA_API_URL}" -else - warn "Nova API not responding at ${NOVA_API_URL} (stack may not be running)" -fi - -sep "NVIDIA GPU" -if command -v nvidia-smi &>/dev/null; then - GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1) - GPU_MEM=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader 2>/dev/null | head -1) - ok "GPU: $GPU_NAME | $GPU_MEM" -else - warn "nvidia-smi not found — GPU acceleration unavailable" -fi - -sep "Config Files" -[[ -f "$HOME/.novarc" ]] && ok "~/.novarc" || warn "~/.novarc missing" -[[ -f "$HOME/.nova/nova-stack.json" ]] && ok "~/.nova/nova-stack.json" || warn "~/.nova/nova-stack.json missing" -[[ -f "$HOME/.gitconfig" ]] && ok "~/.gitconfig" || warn "~/.gitconfig missing" - -sep "Optional" -check_cmd docker "Docker"; command -v code &>/dev/null && ok "VS Code" || warn "VS Code not in PATH" - -echo -e "\n${BOLD}── Summary ───────────────────────────────${NC}" -echo -e " ${GREEN}Passed:${NC} $PASS ${YELLOW}Warnings:${NC} $WARN ${RED}Failed:${NC} $FAIL\n" -[[ "$FAIL" -gt 0 ]] && echo -e " ${RED}Run ./setup/bootstrap.sh to fix.${NC}" && exit 1 -[[ "$WARN" -gt 0 ]] && echo -e " ${YELLOW}Critical checks passed. Some items need attention.${NC}" && exit 0 -echo -e " ${GREEN}All checks passed! 🌌 Nova shell is ready.${NC}" diff --git a/shell/skills/code-review.md b/skills/code-review.md similarity index 100% rename from shell/skills/code-review.md rename to skills/code-review.md diff --git a/shell/skills/git-workflow.md b/skills/git-workflow.md similarity index 100% rename from shell/skills/git-workflow.md rename to skills/git-workflow.md diff --git a/shell/skills/test-runner.md b/skills/test-runner.md similarity index 100% rename from shell/skills/test-runner.md rename to skills/test-runner.md diff --git a/tests/test_github_download_ready.py b/tests/test_github_download_ready.py new file mode 100644 index 0000000..382a0bf --- /dev/null +++ b/tests/test_github_download_ready.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def test_github_ten_minute_start_documents_node_and_desktop_download_path() -> None: + readme = read("README.md") + guide = read("GITHUB-10-MINUTE-START.md") + + assert "GITHUB-10-MINUTE-START.md" in readme + assert "quickstart.ps1" in readme + assert "quickstart.sh" in readme + assert "Download ZIP" in guide + assert "git clone" in guide + assert "quickstart.ps1" in guide + assert "quickstart.sh" in guide + assert "nova/node" in guide + assert "desktop" in guide + assert "/node/status" in guide + assert "http://127.0.0.1:8080" in guide + + +def test_quickstart_scripts_install_backend_desktop_and_start_node() -> None: + windows = read("quickstart.ps1") + unix = read("quickstart.sh") + + assert "pip install -e" in windows + assert "npm install" in windows + assert "python.exe -m nova.api" in windows + assert "desktop" in windows + assert "setup\\verify.ps1" in windows + + assert "pip install -e" in unix + assert "npm install" in unix + assert "python -m nova.api" in unix + assert "desktop" in unix + assert "setup/verify.sh" in unix + + +def test_github_smoke_workflow_proves_download_ready_path() -> None: + workflow = read(".github/workflows/download-smoke.yml") + + assert "actions/setup-python" in workflow + assert "actions/setup-node" in workflow + assert "python -m pip install -e .[dev]" in workflow + assert "python -m pytest tests -q" in workflow + assert "npm test" in workflow + assert "scripts/package_windows_program.py" in workflow + assert "scripts/package_macos_shell.py" in workflow diff --git a/tests/test_local_nova_shell.py b/tests/test_local_nova_shell.py new file mode 100644 index 0000000..b47227a --- /dev/null +++ b/tests/test_local_nova_shell.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_local_nova_cli_health() -> None: + result = subprocess.run( + [sys.executable, "-m", "nova.cli", "health", "--json"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(result.stdout) + assert payload["service"] == "nova_local_cli" + assert payload["direct_lawful_llm"]["status"] == "ok" + + +def test_local_nova_api_chat_exposes_chain_contract() -> None: + from fastapi.testclient import TestClient + from nova.api import app + + client = TestClient(app) + response = client.post( + "/v1/chat", + json={"prompt": "observe lawful nova", "tenant_id": "local", "capability": "observe"}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["decision"] == "EXECUTED" + assert payload["receipt_verified"] is True + assert payload["chain"]["identity"]["instance_id"] + assert payload["chain"]["trace"]["trace_id"] + assert payload["chain"]["authority_boundary"]["operator_authority"] == "external" + assert payload["chain"]["reproducibility"]["prompt_sha256"] + + +def test_openai_models_lists_nova_local_for_cursor() -> None: + from fastapi.testclient import TestClient + from nova.api import app + + client = TestClient(app) + response = client.get("/v1/models") + + assert response.status_code == 200 + payload = response.json() + assert payload["object"] == "list" + assert any(model["id"] == "nova-local" for model in payload["data"]) + + +def test_openai_chat_completions_wraps_lawful_nova_turn() -> None: + from fastapi.testclient import TestClient + from nova.api import app + + client = TestClient(app) + response = client.post( + "/v1/chat/completions", + json={ + "model": "nova-local", + "messages": [ + {"role": "system", "content": "You are Nova inside Cursor."}, + {"role": "user", "content": "observe cursor adapter"}, + ], + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["object"] == "chat.completion" + assert payload["model"] == "nova-local" + assert payload["choices"][0]["message"]["role"] == "assistant" + assert "Nova Cortex" in payload["choices"][0]["message"]["content"] + assert payload["nova"]["decision"] == "EXECUTED" + assert payload["nova"]["receipt_verified"] is True + + +def test_openai_chat_completions_streams_sse_chunks() -> None: + from fastapi.testclient import TestClient + from nova.api import app + + client = TestClient(app) + with client.stream( + "POST", + "/v1/chat/completions", + json={ + "model": "nova-local", + "stream": True, + "messages": [{"role": "user", "content": "observe streaming cursor adapter"}], + }, + ) as response: + body = "".join(response.iter_text()) + + assert response.status_code == 200 + assert "text/event-stream" in response.headers["content-type"] + assert '"object":"chat.completion.chunk"' in body + assert "data: [DONE]" in body + + +def test_openai_compat_routes_require_api_key_when_configured(monkeypatch) -> None: + from fastapi.testclient import TestClient + from nova.api import app + + monkeypatch.setenv("NOVA_API_KEY", "cursor-secret") + client = TestClient(app) + + models_response = client.get("/v1/models") + chat_response = client.post( + "/v1/chat/completions", + json={"model": "nova-local", "messages": [{"role": "user", "content": "observe auth"}]}, + ) + + assert models_response.status_code == 401 + assert chat_response.status_code == 401 + + +def test_openai_compat_routes_accept_cursor_bearer_key(monkeypatch) -> None: + from fastapi.testclient import TestClient + from nova.api import app + + monkeypatch.setenv("NOVA_API_KEY", "cursor-secret") + client = TestClient(app) + headers = {"Authorization": "Bearer cursor-secret"} + + models_response = client.get("/v1/models", headers=headers) + chat_response = client.post( + "/v1/chat/completions", + headers=headers, + json={"model": "nova-local", "messages": [{"role": "user", "content": "observe auth"}]}, + ) + + assert models_response.status_code == 200 + assert chat_response.status_code == 200 + assert chat_response.json()["nova"]["decision"] == "EXECUTED" + + +def test_openai_chat_completions_can_route_through_ollama_provider(monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + class FakeOllamaProvider: + provider_id = "ollama" + + def __init__(self, *, base_url: str, model: str) -> None: + self.base_url = base_url + self.model = model + + async def invoke(self, messages, *, model, max_tokens, temperature): + assert self.base_url == "http://127.0.0.1:11434" + assert model == "qwen2.5-coder:7b" + assert messages[-1]["role"] == "user" + return SimpleNamespace( + content="ollama-coded-response", + provider="ollama", + model=model, + input_tokens=12, + output_tokens=3, + ) + + monkeypatch.setenv("NOVA_PROVIDER", "ollama") + monkeypatch.setenv("NOVA_OLLAMA_MODEL", "qwen2.5-coder:7b") + monkeypatch.setattr(nova_api, "OllamaChatProvider", FakeOllamaProvider) + + client = TestClient(nova_api.app) + response = client.post( + "/v1/chat/completions", + json={"model": "nova-local", "messages": [{"role": "user", "content": "write code"}]}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["choices"][0]["message"]["content"] == "ollama-coded-response" + receipt_payload = json.loads(payload["nova"]["receipt"]["payload"]) + assert receipt_payload["provider"] == "ollama" + assert receipt_payload["model"] == "qwen2.5-coder:7b" + assert receipt_payload["reproducibility"]["deterministic_core"] is False + + +def test_parent_stack_launchers_prefer_lawful_nova_package() -> None: + repo_root = ROOT.parent + windows_path = repo_root / "scripts" / "start-nova-stack.ps1" + bash_path = repo_root / "scripts" / "start-nova-stack.sh" + if not windows_path.exists() or not bash_path.exists(): + windows_quickstart = (ROOT / "quickstart.ps1").read_text(encoding="utf-8") + bash_quickstart = (ROOT / "quickstart.sh").read_text(encoding="utf-8") + assert "python.exe -m nova.api" in windows_quickstart + assert "python -m nova.api" in bash_quickstart + assert "npm start" in windows_quickstart + assert "npm start" in bash_quickstart + return + + windows_launcher = windows_path.read_text(encoding="utf-8") + bash_launcher = bash_path.read_text(encoding="utf-8") + bash_common = (ROOT / "setup" / "lib" / "common.sh").read_text(encoding="utf-8") + + assert 'Join-Path $Root "lawful-nova-shell"' in windows_launcher + assert '$ShellRoot;$Root' in windows_launcher + assert '-WorkingDirectory $ShellRoot' in windows_launcher + assert 'export PYTHONPATH="${shell_root}:${repo}${PYTHONPATH:+:${PYTHONPATH}}"' in bash_common + assert '(cd "${shell_root}" && "${PY}" -m nova.api' in bash_launcher + + +def test_productization_gate_checks_chain_contract() -> None: + out = ROOT / ".runtime" / "test_nova_productization_report.json" + result = subprocess.run( + [sys.executable, "scripts/nova_productization_gate.py", "--json-out", str(out)], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(out.read_text(encoding="utf-8")) + assert payload["local_lawful_slice_ready"] is True + assert payload["checks"]["chain_contract"]["status"] == "ok" diff --git a/tests/test_node_agent_manifest.py b/tests/test_node_agent_manifest.py new file mode 100644 index 0000000..8ee67a8 --- /dev/null +++ b/tests/test_node_agent_manifest.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import pytest + + +def test_agent_registry_preserves_color_team_roles_and_capability_gate() -> None: + from nova.node.agent_manifest import CapabilityGate, default_agent_registry + + registry = default_agent_registry() + + coder = registry.get("agent-coder") + reviewer = registry.get("agent-reviewer") + security = registry.get("agent-security") + sentinel = registry.get("agent-sentinel") + + assert coder.role == "blue_team_builder" + assert coder.color == "blue" + assert coder.can_write_files is True + assert coder.can_modify_deps is True + assert "write_patches" in coder.capabilities + assert reviewer.role == "red_team_inspector" + assert security.role == "purple_team_security" + assert sentinel.role == "orange_team_governance_enforcer" + + gate = CapabilityGate(registry, policy_version="1.0") + receipt = gate.authorize( + "agent-coder", + "write_files", + {"trace_id": "trace-agent-1", "file_path": "src/app.py"}, + ) + + assert receipt.agent_id == "agent-coder" + assert receipt.action == "write_files" + assert receipt.trace_id == "trace-agent-1" + assert receipt.compute_output_hash({"ok": True}).startswith("sha256:") + + with pytest.raises(PermissionError, match="agent-reviewer cannot write files"): + gate.authorize("agent-reviewer", "write_files", {"trace_id": "trace-agent-2"}) + + +def test_agent_manifest_routes_expose_agents_and_planned_tools(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + client = TestClient(nova_api.app) + agents = client.get("/node/agents").json() + tool_manifest = client.get("/node/agent-tools").json() + + assert agents["agents"]["coder"]["id"] == "agent-coder" + assert agents["agents"]["wiring"]["role"] == "green_team_integrator" + assert agents["agents"]["sentinel"]["governance"]["can_block_actions"] is True + assert agents["orchestration"]["status"] == "manifest_only" + assert "Tools remain stateless Ring-2 invocations" in agents["orchestration"]["guardrail"] + + names = [tool["name"] for tool in tool_manifest["tools"]] + assert "coder_tool" in names + assert "rollback_tool" in names + coder_tool = next(tool for tool in tool_manifest["tools"] if tool["name"] == "coder_tool") + rollback_tool = next(tool for tool in tool_manifest["tools"] if tool["name"] == "rollback_tool") + assert coder_tool["owner_agent"] == "agent-coder" + assert coder_tool["implemented"] is True + assert rollback_tool["owner_agent"] == "agent-sentinel" + assert rollback_tool["implemented"] is False + + +def test_node_tools_endpoint_includes_owner_agent_metadata(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + client = TestClient(nova_api.app) + tools = client.get("/node/tools").json()["tools"] + + code_tool = next(tool for tool in tools if tool["name"] == "code") + wire_tool = next(tool for tool in tools if tool["name"] == "wire") + assert code_tool["owner_agent"] == "agent-coder" + assert code_tool["tool_manifest_name"] == "coder_tool" + assert wire_tool["owner_agent"] == "agent-wiring" + assert wire_tool["tool_manifest_name"] == "wiring_tool" diff --git a/tests/test_node_continuity.py b/tests/test_node_continuity.py new file mode 100644 index 0000000..0e2739f --- /dev/null +++ b/tests/test_node_continuity.py @@ -0,0 +1,16 @@ +from __future__ import annotations + + +def test_continuity_log_is_append_only(tmp_path, monkeypatch) -> None: + from nova.node.continuity import append_event, read_events + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + e1 = append_event("submit", {"task_id": "t-1"}) + e2 = append_event("result", {"task_id": "t-1"}) + events = read_events() + + assert e1 in events + assert e2 in events + assert events[0]["kind"] == "submit" + assert events[1]["kind"] == "result" diff --git a/tests/test_node_event_bus.py b/tests/test_node_event_bus.py new file mode 100644 index 0000000..6435a38 --- /dev/null +++ b/tests/test_node_event_bus.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import json + + +def test_event_bus_matches_wildcards_persists_and_bounds_history(tmp_path, monkeypatch) -> None: + from nova.node.event_bus import EventBus + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + seen = [] + bus = EventBus(channels=["tool.*", "governance.*"], max_events=2) + bus.subscribe("tool.*", seen.append) + + bus.emit("tool.invoked", "started", {"tool_name": "code"}) + bus.emit("governance.receipt_verified", "verified", {"trace_id": "trace-1"}) + bus.emit("tool.completed", "completed", {"tool_name": "code"}) + + assert [event.channel for event in seen] == ["tool.invoked", "tool.completed"] + assert [event.channel for event in bus.history()] == ["governance.receipt_verified", "tool.completed"] + assert [event.channel for event in bus.history("tool.")] == ["tool.completed"] + + lines = (tmp_path / "event-bus.jsonl").read_text(encoding="utf-8").splitlines() + persisted = [json.loads(line) for line in lines] + assert [event["channel"] for event in persisted] == [ + "tool.invoked", + "governance.receipt_verified", + "tool.completed", + ] + + +def test_node_event_and_feature_manifest_routes(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + from nova.node.event_bus import get_event_bus + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + get_event_bus().emit("node.online", "health_check", {"status": "ok"}) + + client = TestClient(nova_api.app) + events = client.get("/node/events").json() + manifest = client.get("/node/feature-manifest").json() + + assert events["events"][-1]["channel"] == "node.online" + assert "governance.*" in events["channels"] + assert manifest["manifest"]["modules"]["core"] == [ + "file_search", + "symbol_search", + "patch_manager", + "terminal", + "test_runner", + "git_panel", + ] + assert "on_tool_invoked" in manifest["manifest"]["runtime"]["hooks"] + assert "tool.*" in manifest["manifest"]["event_bus"]["channels"] + + +def test_status_lists_event_bus_surface(tmp_path, monkeypatch) -> None: + from nova.node.status import node_status + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + status = node_status() + + assert "/node/events" in status["endpoints"] + assert "/node/feature-manifest" in status["endpoints"] + assert status["governance_health"]["event_bus_events"] == 0 diff --git a/tests/test_node_evidence_layer.py b/tests/test_node_evidence_layer.py new file mode 100644 index 0000000..43a3c9b --- /dev/null +++ b/tests/test_node_evidence_layer.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +from pathlib import Path + + +class FakeProvider: + provider_id = "local" + model = "nova-local" + + def chat_completion(self, governed_request): + text = governed_request["messages"][-1]["content"] + return { + "completion": { + "id": f"completion-{text}", + "object": "chat.completion", + "created": 123, + "model": self.model, + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": f"node:{text}"}, + } + ], + }, + "receipt": {"provider": "local", "deterministic_core": True}, + } + + +def test_replay_continuity_policy_mesh_and_alert_routes(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + from nova.node.federation import receive_gossip_payload + from nova.node.policy import GovernanceRuntime + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setenv("NOVA_NODE_ID", "jonai-node-evidence") + monkeypatch.setenv("NOVA_NODE_RATE_PER_MINUTE", "50") + monkeypatch.setattr(nova_api, "build_provider", lambda config: FakeProvider()) + nova_api.app.state.node_provider_factory = lambda config: FakeProvider() + + client = TestClient(nova_api.app) + submitted = client.post( + "/node/submit", + json={ + "task_id": "task-replay", + "intent": "chat", + "caller_id": "operator", + "payload": {"messages": [{"role": "user", "content": "replay me"}]}, + }, + ).json() + + replayed = client.post(f"/node/replay/{submitted['trace_id']}").json() + assert replayed["trace_id"] == submitted["trace_id"] + assert replayed["original_output"] == {"output": "node:replay me", "completion_id": "completion-replay me"} + assert replayed["replayed_output"]["output"] == "node:replay me" + assert replayed["deterministic"] is True + assert replayed["diff"]["type"] == "none" + assert replayed["policy_version_original"] == replayed["policy_version_replayed"] + + continuity = client.get("/node/continuity?limit=10").json() + assert any(event["kind"] == "submit" for event in continuity["events"]) + assert any(event["trace_id"] == submitted["trace_id"] for event in continuity["events"]) + + policy = client.get("/node/policy").json() + assert "current_policy" in policy + assert policy["policy_hash"] + assert set(policy["diff"]) == {"added", "removed", "changed"} + + receive_gossip_payload( + { + "summary": { + "node_id": "peer-invalid", + "policy_hash": "sha256:peer", + "timestamp": 123, + }, + "signature": "bad", + } + ) + mesh = client.get("/node/mesh").json() + assert mesh["peers"][0]["node_id"] == "peer-invalid" + assert mesh["peers"][0]["trust_level"] == "invalid" + assert "peer-invalid" in mesh["divergent_peers"] + + runtime = GovernanceRuntime() + runtime.enforce_invariants({"caller_id": "operator", "intent": "chat"}) + alerts = client.get("/node/alerts").json() + assert any(alert["category"] == "federation" for alert in alerts["alerts"]) + assert "/node/alerts" in client.get("/node/status").json()["endpoints"] + + +def test_n0_badge_report_and_evidence_bundle(tmp_path, monkeypatch) -> None: + from nova.node.ceb import generate_evidence_bundle + from nova.node.conformance import generate_n0_badge, generate_n0_report + from nova.node.continuity import append_event, append_receipt + from nova.node.ledger import stable_hash + + policy_path = tmp_path / "policy.yaml" + policy_path.write_text( + "\n".join( + [ + "version: '1.0'", + "node_id: jonai-node-bundle", + "operator_id: jon-halstead", + "invariants:", + " boundedness:", + " max_tokens: 2048", + "safety:", + " banned_intents: []", + " rate_limits:", + " per_user_per_minute: 10", + " per_user_per_hour: 100", + ] + ), + encoding="utf-8", + ) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path / ".runtime" / "node")) + monkeypatch.setenv("NOVA_NODE_ID", "jonai-node-bundle") + + append_event("submit", {"trace_id": "trace-bundle", "decision": {"decision": "allowed"}}) + append_receipt( + { + "entry_type": "nodeExecutionReceipt", + "trace_id": "trace-bundle", + "receipt_hash": "sha256:receipt", + "receipt": {"trace_id": "trace-bundle", "policy_version": "1.0"}, + } + ) + + report = generate_n0_report(policy_path=str(policy_path)) + assert report["profile"] == "N0" + assert report["verdict"]["conformant"] is True + assert report["sections"]["identity_policy"][0]["pass"] is True + + badge = generate_n0_badge(report, policy_path=str(policy_path)) + assert badge["profile"] == "N0" + assert badge["status"] == "conformant" + assert badge["policy_hash"] == report["policy_hash"] + assert badge["conformance_hash"] == stable_hash(report) + + bundle = generate_evidence_bundle(tmp_path / "bundle", report=report) + assert Path(bundle["bundle_path"]).exists() + assert bundle["manifest"]["version"] == "CEB-1.0" + assert bundle["manifest"]["bundle_hash"].startswith("sha256:") + assert any(item["path"] == "conformance/n0_report.json" for item in bundle["manifest"]["files"]) + + +def test_conformance_and_bundle_routes(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path / ".runtime" / "node")) + + client = TestClient(nova_api.app) + + report = client.get("/node/conformance/n0").json() + assert report["profile"] == "N0" + badge = client.get("/node/conformance/n0/badge").json() + assert badge["profile"] == "N0" + assert badge["conformance_hash"] + bundle = client.post("/node/evidence-bundle").json() + assert bundle["manifest"]["version"] == "CEB-1.0" + assert Path(bundle["bundle_path"]).exists() diff --git a/tests/test_node_evidence_verification.py b/tests/test_node_evidence_verification.py new file mode 100644 index 0000000..b716b83 --- /dev/null +++ b/tests/test_node_evidence_verification.py @@ -0,0 +1,82 @@ +from __future__ import annotations + + +def test_verify_tool_trace_reconstructs_receipt_events_manifest_and_hashes(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + from nova.node.tools import coder_tool + from nova.node.tools import replay_coding + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setattr(coder_tool, "generate", lambda prompt, **kwargs: "def health():\n return 'ok'\n") + monkeypatch.setattr(replay_coding, "generate", lambda prompt, **kwargs: "def health():\n return 'ok'\n") + + client = TestClient(nova_api.app) + created = client.post( + "/node/tool", + json={ + "task_id": "task-evidence-001", + "intent": "code", + "caller_id": "cursor", + "file_path": "src/app.py", + "instruction": "Add health endpoint", + "current_code": "def old():\n return 'old'\n", + }, + ).json() + + trace_id = created["governance"]["trace_id"] + evidence = client.get(f"/node/verify/{trace_id}").json() + + assert evidence["trace_id"] == trace_id + assert evidence["receipt"]["trace_id"] == trace_id + assert evidence["verification"]["receipt_hash"]["valid"] is True + assert evidence["verification"]["input_hash"]["valid"] is True + assert evidence["verification"]["output_hash"]["valid"] is True + assert evidence["verification"]["original_code_hash"]["valid"] is True + assert evidence["verification"]["replay"]["available"] is True + assert evidence["verification"]["replay"]["deterministic"] is True + assert evidence["policy"]["version"] == "1.0" + assert evidence["policy"]["manifest"]["node"]["description"] == "Sovereign Node feature manifest for Nova Desktop" + assert [event["channel"] for event in evidence["trace"]["events"]] == [ + "tool.invoked", + "tool.completed", + "governance.receipt_verified", + ] + assert any(event["kind"] == "nodeToolReceipt" for event in evidence["trace"]["continuity"]) + assert evidence["cross_node"]["comparable"] is True + assert "receipt_hash" in evidence["cross_node"]["compare_keys"] + + +def test_compare_receipts_reports_matching_and_drift(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + from nova.node.tools.receipts import write_coding_receipt + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + write_coding_receipt( + trace_id="trace-compare-1", + task={ + "intent": "code", + "file_path": "src/app.py", + "instruction": "Add comment", + "current_code": "print('x')\n", + }, + governance={"decision": "allowed", "policy_version": "1.0", "receipts": ["continuity"]}, + result={"updated_code": "# ok\nprint('x')\n", "diff": "+# ok", "receipts": ["coder_tool"]}, + tool="code", + ) + + client = TestClient(nova_api.app) + receipt = client.get("/node/verify/trace-compare-1").json()["receipt"] + same = client.post("/node/compare-receipts", json={"left": receipt, "right": receipt}).json() + drift = client.post( + "/node/compare-receipts", + json={"left": receipt, "right": {**receipt, "output_hash": "sha256:drift"}}, + ).json() + + assert same["matching"] is True + assert same["matches"]["receipt_hash"] is True + assert drift["matching"] is False + assert drift["matches"]["output_hash"] is False + assert "output_hash" in drift["drift_keys"] diff --git a/tests/test_node_federation.py b/tests/test_node_federation.py new file mode 100644 index 0000000..a7f7789 --- /dev/null +++ b/tests/test_node_federation.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import json + + +RSA_PRIVATE = { + "kty": "RSA", + "kid": "test-rsa", + "n": "221", + "e": "5", + "d": "77", +} +RSA_PUBLIC = { + "kty": "RSA", + "kid": "test-rsa", + "n": "221", + "e": "5", +} + + +def test_federation_gossip_summary_contains_identity(tmp_path, monkeypatch) -> None: + from nova.node.federation import gossip_summary + + monkeypatch.setenv("NOVA_NODE_ID", "jonai-node-fed") + + summary = gossip_summary() + + assert summary["node_id"] == "jonai-node-fed" + assert summary["policy_hash"] + assert "governance" in summary["capabilities"] + + +def test_federation_loads_peers_from_manifest(tmp_path, monkeypatch) -> None: + from nova.node.federation import load_peers + + peers = [{"peer_id": "peer-1", "endpoint": "https://peer.example"}] + path = tmp_path / "peers.json" + path.write_text(json.dumps(peers), encoding="utf-8") + monkeypatch.setenv("NOVA_NODE_PEERS_PATH", str(path)) + + assert load_peers() == peers + + +def test_signed_gossip_verifies_and_records_trust(tmp_path, monkeypatch) -> None: + from nova.node.federation import receive_gossip_payload, signed_gossip_summary + + private_path = tmp_path / "private.json" + public_path = tmp_path / "public.json" + private_path.write_text(json.dumps(RSA_PRIVATE), encoding="utf-8") + public_path.write_text(json.dumps(RSA_PUBLIC), encoding="utf-8") + monkeypatch.setenv("NOVA_NODE_OPERATOR_PRIVATE_KEY", str(private_path)) + monkeypatch.setenv("NOVA_NODE_OPERATOR_PUBLIC_KEY", str(public_path)) + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + signed = signed_gossip_summary() + received = receive_gossip_payload(signed) + + assert signed["signature"] + assert received["ack"] is True + assert received["signature_valid"] is True + assert received["trust_level"] == "trusted" + + signed["summary"]["policy_hash"] = "tampered" + tampered = receive_gossip_payload(signed) + assert tampered["signature_valid"] is False + assert tampered["trust_level"] == "invalid" + + +def test_node_hello_returns_signed_identity(tmp_path, monkeypatch) -> None: + from nova.node.federation import node_hello + + private_path = tmp_path / "private.json" + private_path.write_text(json.dumps(RSA_PRIVATE), encoding="utf-8") + monkeypatch.setenv("NOVA_NODE_OPERATOR_PRIVATE_KEY", str(private_path)) + monkeypatch.setenv("NOVA_NODE_ID", "jonai-node-hello") + + hello = node_hello() + + assert hello["node_id"] == "jonai-node-hello" + assert hello["policy_hash"] + assert hello["capabilities"] + assert hello["signature"] + assert hello["trust_level"] == "self" diff --git a/tests/test_node_identity.py b/tests/test_node_identity.py new file mode 100644 index 0000000..5e18f10 --- /dev/null +++ b/tests/test_node_identity.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import json + + +def test_node_identity_has_id_operator_and_policy_hash(tmp_path, monkeypatch) -> None: + from nova.node.identity import NodeIdentity + + policy_path = tmp_path / "policy.yaml" + policy_path.write_text(json.dumps({"version": "1.0"}), encoding="utf-8") + monkeypatch.setenv("NOVA_NODE_ID", "jonai-node-test") + monkeypatch.setenv("NOVA_OPERATOR_ID", "jon-halstead") + monkeypatch.setenv("NOVA_OPERATOR_KEY_ID", "operator-key-test") + + ident = NodeIdentity.load(str(policy_path)) + + assert ident.node_id.startswith("jonai-node-") + assert ident.operator_id == "jon-halstead" + assert ident.operator_key_id == "operator-key-test" + assert len(ident.policy_hash) == 64 diff --git a/tests/test_node_n1_surface.py b/tests/test_node_n1_surface.py new file mode 100644 index 0000000..1930e03 --- /dev/null +++ b/tests/test_node_n1_surface.py @@ -0,0 +1,21 @@ +from __future__ import annotations + + +def test_status_lists_n1_upgrade_surface(tmp_path, monkeypatch) -> None: + from nova.node.status import node_status + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + status = node_status() + + for endpoint in [ + "/node/replay/{trace_id}", + "/node/continuity", + "/node/policy", + "/node/mesh", + "/node/alerts", + "/node/conformance/n0", + "/node/conformance/n0/badge", + "/node/evidence-bundle", + ]: + assert endpoint in status["endpoints"] diff --git a/tests/test_node_policy.py b/tests/test_node_policy.py new file mode 100644 index 0000000..1a34213 --- /dev/null +++ b/tests/test_node_policy.py @@ -0,0 +1,80 @@ +from __future__ import annotations + + +def test_node_policy_blocks_banned_intent(monkeypatch) -> None: + from nova.node.policy import GovernanceRuntime + + runtime = GovernanceRuntime() + runtime.policy["safety"]["banned_intents"] = ["blocked-intent"] + + decision = runtime.enforce_invariants({"intent": "blocked-intent"}) + + assert decision["decision"] == "blocked" + assert decision["reason"] == "banned-intent" + assert decision["policy_version"] == "1.0" + + +def test_node_policy_allows_bounded_payload(monkeypatch) -> None: + from nova.node.policy import GovernanceRuntime + + monkeypatch.setenv("NOVA_NODE_MAX_PAYLOAD_BYTES", "1000") + runtime = GovernanceRuntime() + + decision = runtime.enforce_invariants({"intent": "chat", "payload": {"prompt": "hello"}}) + + assert decision["decision"] == "allowed" + assert "continuity" in decision["receipts"] + assert decision["trace_id"].startswith("trace-") + + +def test_canonical_policy_manifest_loads_node_identity() -> None: + from pathlib import Path + from nova.node.identity import NodeIdentity + from nova.node.policy import GovernanceRuntime + + policy_path = Path(__file__).resolve().parents[1] / "policy.yaml" + + runtime = GovernanceRuntime(str(policy_path)) + identity = NodeIdentity.load(str(policy_path)) + + assert runtime.policy["version"] == "1.0" + assert runtime.policy["operator_id"] == "jon-halstead" + assert identity.node_id == "jonai-node-001" + assert identity.operator_id == "jon-halstead" + assert len(identity.policy_hash) == 64 + + +def test_rate_limit_blocks_repeated_calls(tmp_path, monkeypatch) -> None: + from nova.node.policy import GovernanceRuntime + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + runtime = GovernanceRuntime() + runtime.policy["safety"]["rate_limits"] = { + "per_user_per_minute": 1, + "per_user_per_hour": 2, + } + + first = runtime.enforce_invariants({"caller_id": "cursor", "intent": "chat"}) + second = runtime.enforce_invariants({"caller_id": "cursor", "intent": "chat"}) + + assert first["decision"] == "allowed" + assert second["decision"] == "blocked" + assert second["reason"] == "rate-limit-exceeded" + assert second["rate_limit"]["per_user_per_minute"] == 1 + + +def test_rate_limit_counters_are_exposed(tmp_path, monkeypatch) -> None: + from nova.node.policy import GovernanceRuntime, get_rate_limit_state + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + runtime = GovernanceRuntime() + runtime.policy["safety"]["rate_limits"] = { + "per_user_per_minute": 10, + "per_user_per_hour": 10, + } + runtime.enforce_invariants({"caller_id": "operator-1", "intent": "chat"}) + + state = get_rate_limit_state() + + assert state["limits"]["per_user_per_minute"] == 10 + assert state["callers"]["operator-1"]["minute_count"] == 1 diff --git a/tests/test_node_status.py b/tests/test_node_status.py new file mode 100644 index 0000000..6f5cdff --- /dev/null +++ b/tests/test_node_status.py @@ -0,0 +1,19 @@ +from __future__ import annotations + + +def test_node_status_exposes_receipts_and_ledger_endpoints(tmp_path, monkeypatch) -> None: + from nova.node.status import node_status + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setenv("NOVA_NODE_ID", "jonai-node-status") + + status = node_status() + + assert status["node_id"] == "jonai-node-status" + assert "/node/receipts" in status["endpoints"] + assert "/node/ledger" in status["endpoints"] + assert "/node/gossip" in status["endpoints"] + assert "/node/hello" in status["endpoints"] + assert "rate_limits" in status + assert "federation" in status + assert "governance_health" in status diff --git a/tests/test_node_tool_registry.py b/tests/test_node_tool_registry.py new file mode 100644 index 0000000..eb42223 --- /dev/null +++ b/tests/test_node_tool_registry.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import json + + +def test_coder_tool_returns_updated_code_diff_and_receipt(monkeypatch) -> None: + from nova.node.tools import coder_tool + + captured = {} + + def fake_generate(prompt, **kwargs): + captured.update(kwargs) + return "def health():\n return 'ok'\n" + + monkeypatch.setattr(coder_tool, "generate", fake_generate) + + result = coder_tool.run( + { + "intent": "code", + "file_path": "src/app.py", + "instruction": "Add health endpoint", + "current_code": "def old():\n return 'old'\n", + } + ) + + assert result["updated_code"] == "def health():\n return 'ok'\n" + assert result["file_path"] == "src/app.py" + assert "--- src/app.py (original)" in result["diff"] + assert "+++ src/app.py (updated)" in result["diff"] + assert "-def old():" in result["diff"] + assert "+def health():" in result["diff"] + assert captured["model"] == "qwen2.5-coder:3b" + assert captured["temperature"] == 0.15 + assert result["receipts"] == ["coder_tool"] + + +def test_wiring_tool_returns_glue_code_and_receipt(monkeypatch) -> None: + from nova.node.tools import wiring_tool + + monkeypatch.setattr(wiring_tool, "generate", lambda prompt, **kwargs: "app.include_router(router)\n") + + result = wiring_tool.run( + { + "intent": "wire", + "goal": "Connect coder_tool to /node/tool", + "components": ["nova/api.py", "nova/node/tools/coder_tool.py"], + "context": "FastAPI backend, governed node", + } + ) + + assert result["glue_code"] == "app.include_router(router)\n" + assert result["goal"] == "Connect coder_tool to /node/tool" + assert result["components"] == ["nova/api.py", "nova/node/tools/coder_tool.py"] + assert result["receipts"] == ["wiring_tool"] + + +def test_tool_registry_invokes_ring2_tools(monkeypatch) -> None: + from nova.node.tools import registry + + monkeypatch.setattr(registry, "coder_run", lambda task: {"updated_code": task["current_code"], "receipts": ["coder_tool"]}) + + result = registry.invoke_tool("code", {"intent": "code", "current_code": "print('x')"}) + + assert result["receipts"] == ["coder_tool"] + assert registry.TOOLS["code"]["profile"] == "N2" + assert "patch" in registry.TOOLS["code"]["capabilities"] + assert "wire" in registry.TOOLS + + +def test_local_model_uses_non_streaming_ollama_payload_and_qwen_default(monkeypatch) -> None: + from nova.node.tools import local_model + + calls = [] + + def fake_post_json(url, payload, timeout): + calls.append((url, payload, timeout)) + return {"response": "from ollama"} + + monkeypatch.setattr(local_model, "_post_json", fake_post_json) + + result = local_model.generate("hello") + + assert result == "from ollama" + assert calls == [ + ( + "http://localhost:11434/api/generate", + { + "model": "qwen2.5-coder:3b", + "prompt": "hello", + "stream": False, + "options": {"temperature": 0.2, "num_predict": 2048}, + }, + 60.0, + ) + ] + + +def test_local_model_falls_back_from_ollama_to_vllm(monkeypatch) -> None: + from nova.node.tools import local_model + + calls = [] + + def fake_post_json(url, payload, timeout): + calls.append((url, payload, timeout)) + if url.endswith("/api/generate"): + raise OSError("ollama offline") + return {"choices": [{"text": "from vllm"}]} + + monkeypatch.setenv("NOVA_NODE_OLLAMA_URL", "http://127.0.0.1:11434/api/generate") + monkeypatch.setenv("NOVA_NODE_VLLM_URL", "http://127.0.0.1:8000/v1/completions") + monkeypatch.setattr(local_model, "_post_json", fake_post_json) + + result = local_model.generate("hello", model="qwen2.5-coder:7b", temperature=0.1, max_tokens=128) + + assert result == "from vllm" + assert calls[0][0] == "http://127.0.0.1:11434/api/generate" + assert calls[0][1] == { + "model": "qwen2.5-coder:7b", + "prompt": "hello", + "stream": False, + "options": {"temperature": 0.1, "num_predict": 128}, + } + assert calls[1][0] == "http://127.0.0.1:8000/v1/completions" + assert calls[1][1] == { + "model": "qwen2.5-coder:7b", + "prompt": "hello", + "temperature": 0.1, + "max_tokens": 128, + } + + +def test_coding_receipts_are_replayable(tmp_path, monkeypatch) -> None: + from nova.node.tools.receipts import write_coding_receipt + from nova.node.tools import replay_coding + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setattr(replay_coding, "generate", lambda prompt, **kwargs: "# hello\nprint('hello')\n") + + trace_file = write_coding_receipt( + trace_id="trace-code-1", + task={ + "intent": "code", + "file_path": "src/app.py", + "instruction": "Add a comment", + "current_code": "print('hello')\n", + }, + governance={"decision": "allowed", "policy_version": "1.0", "receipts": ["continuity"]}, + result={ + "updated_code": "# hello\nprint('hello')\n", + "diff": "--- src/app.py (original)\n+++ src/app.py (updated)\n", + "receipts": ["coder_tool"], + }, + tool="code", + ) + + assert trace_file == "tool-receipts\\trace-code-1.json" or trace_file == "tool-receipts/trace-code-1.json" + replayed = replay_coding.replay_coding("trace-code-1") + + assert replayed["trace_id"] == "trace-code-1" + assert replayed["intent"] == "code" + assert replayed["policy_version"] == "1.0" + assert replayed["deterministic"] is True + assert "diff_against_original" in replayed + + +def test_coding_tools_conformance_profile_documents_n2_slice() -> None: + from pathlib import Path + + profile = Path(__file__).resolve().parents[1] / "nova" / "node" / "tools" / "conformance_coding.yaml" + text = profile.read_text(encoding="utf-8") + + assert 'profile: "N2-coding-tools"' in text + assert 'id: "CT-1"' in text + assert 'id: "CT-2"' in text + assert "Local-only inference" in text + assert 'id: "CT-5"' in text + + +def test_node_tool_endpoint_governs_invokes_and_receipts(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + from nova.node.tools import coder_tool + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setenv("NOVA_NODE_ID", "node-tool-001") + monkeypatch.setenv("NOVA_OPERATOR_KEY_ID", "operator-tool-key") + monkeypatch.setattr(coder_tool, "generate", lambda prompt, **kwargs: "def health():\n return 'ok'\n") + + client = TestClient(nova_api.app) + response = client.post( + "/node/tool", + json={ + "task_id": "task-code-001", + "intent": "code", + "caller_id": "cursor", + "file_path": "src/app.py", + "instruction": "Add health endpoint", + "current_code": "def old():\n return 'old'\n", + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["governance"]["decision"] == "allowed" + assert payload["result"]["updated_code"] == "def health():\n return 'ok'\n" + assert payload["trace_file"].endswith(".json") + + trace_entry = json.loads((tmp_path / payload["trace_file"]).read_text(encoding="utf-8")) + assert trace_entry["intent"] == "code" + assert trace_entry["tool"] == "code" + assert trace_entry["decision"] == "allowed" + assert trace_entry["receipts"][-1] == "coder_tool" + assert "diff" in trace_entry + + continuity_log = tmp_path / "continuity.jsonl" + entries = [json.loads(line) for line in continuity_log.read_text(encoding="utf-8").splitlines()] + tool_receipts = [entry for entry in entries if entry.get("entry_type") == "nodeToolReceipt"] + assert len(tool_receipts) == 1 + assert tool_receipts[0]["trace_id"] == payload["governance"]["trace_id"] + + event_log = tmp_path / "event-bus.jsonl" + event_entries = [json.loads(line) for line in event_log.read_text(encoding="utf-8").splitlines()] + channels = [entry["channel"] for entry in event_entries] + assert "tool.invoked" in channels + assert "tool.completed" in channels + assert "governance.receipt_verified" in channels + completed = next(entry for entry in event_entries if entry["channel"] == "tool.completed") + assert completed["payload"]["tool_name"] == "code" + assert completed["payload"]["trace_id"] == payload["governance"]["trace_id"] + + +def test_node_tool_endpoint_blocks_unknown_intent(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + client = TestClient(nova_api.app) + response = client.post("/node/tool", json={"intent": "orchestrate", "caller_id": "cursor"}) + + assert response.status_code == 400 + assert response.json()["error"]["reason"] == "unknown-tool-intent" + + +def test_node_tools_endpoint_exposes_stateless_registry(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + + client = TestClient(nova_api.app) + payload = client.get("/node/tools").json() + + names = [tool["name"] for tool in payload["tools"]] + assert "code" in names + assert "wire" in names + code_tool = next(tool for tool in payload["tools"] if tool["name"] == "code") + assert code_tool["profile"] == "N2" + assert code_tool["stateless"] is True + assert "patch" in code_tool["capabilities"] diff --git a/tests/test_node_v0.py b/tests/test_node_v0.py new file mode 100644 index 0000000..5b30de5 --- /dev/null +++ b/tests/test_node_v0.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import json + + +def test_node_v0_status_submit_result_and_continuity_log(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + class FakeProvider: + provider_id = "local" + model = "nova-local" + + def chat_completion(self, governed_request): + text = governed_request["messages"][-1]["content"] + return { + "completion": { + "id": "node-completion-1", + "object": "chat.completion", + "created": 123, + "model": self.model, + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": f"node:{text}"}, + } + ], + }, + "receipt": {"provider": "local", "deterministic_core": True}, + } + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setenv("NOVA_NODE_ID", "node-test-001") + monkeypatch.setenv("NOVA_OPERATOR_KEY_ID", "operator-test-key") + monkeypatch.delenv("NOVA_PROVIDER", raising=False) + monkeypatch.setattr(nova_api, "build_provider", lambda config: FakeProvider()) + + client = TestClient(nova_api.app) + status = client.get("/node/status").json() + + assert status["node_id"] == "node-test-001" + assert status["operator_key_id"] == "operator-test-key" + assert status["policy_version"] == "1.0" + assert status["conformance_profile"] == "N0" + assert status["receipt_count"] == 0 + assert "/node/submit" in status["endpoints"] + assert "/node/result/{trace_id}" in status["endpoints"] + + submit = client.post( + "/node/submit", + json={ + "task_id": "task-001", + "intent": "chat", + "caller_id": "cursor", + "payload": {"messages": [{"role": "user", "content": "hello node"}]}, + }, + ).json() + + assert submit["decision"] == "allowed" + assert submit["trace_id"] + assert submit["result"]["output"] == "node:hello node" + assert submit["receipt"]["task_id"] == "task-001" + assert submit["receipt"]["node_id"] == "node-test-001" + assert submit["receipt"]["operator_key_id"] == "operator-test-key" + assert submit["receipt"]["model_id"] == "nova-local" + assert submit["receipt"]["policy_version"] == "1.0" + assert submit["receipt"]["input_hash"].startswith("sha256:") + assert submit["receipt"]["output_hash"].startswith("sha256:") + assert submit["receipt"]["receipt_hash"].startswith("sha256:") + assert submit["receipts"] == [submit["receipt"]["receipt_hash"]] + + result = client.get(f"/node/result/{submit['trace_id']}").json() + assert result["trace_id"] == submit["trace_id"] + assert result["receipt"]["receipt_hash"] == submit["receipt"]["receipt_hash"] + assert result["result"]["output"] == "node:hello node" + + continuity_log = tmp_path / "continuity.jsonl" + entries = [json.loads(line) for line in continuity_log.read_text(encoding="utf-8").splitlines()] + receipt_entries = [entry for entry in entries if entry.get("entry_type") == "nodeExecutionReceipt"] + assert len(receipt_entries) == 1 + assert receipt_entries[0]["trace_id"] == submit["trace_id"] + assert [entry.get("kind") for entry in entries if entry.get("kind")] == ["submit", "result"] + + +def test_node_v0_vetoes_payloads_that_exceed_policy_limit(tmp_path, monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + monkeypatch.setenv("NOVA_NODE_RUNTIME_DIR", str(tmp_path)) + monkeypatch.setenv("NOVA_NODE_MAX_PAYLOAD_BYTES", "20") + monkeypatch.setattr(nova_api, "build_provider", lambda config: object()) + + client = TestClient(nova_api.app) + response = client.post( + "/node/submit", + json={ + "task_id": "task-large", + "intent": "chat", + "caller_id": "cursor", + "payload": {"prompt": "this payload is deliberately too large"}, + }, + ) + + assert response.status_code == 400 + payload = response.json() + assert payload["error"]["decision"] == "blocked" + assert payload["error"]["reason"] == "payload-too-large" diff --git a/tests/test_provider_registry.py b/tests/test_provider_registry.py new file mode 100644 index 0000000..aa42f64 --- /dev/null +++ b/tests/test_provider_registry.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +import json + + +def test_ollama_defaults_to_installed_qwen_3b(monkeypatch) -> None: + from nova.config import load_nova_config + from nova.providers.provider_ollama import OllamaProvider + + monkeypatch.delenv("NOVA_OLLAMA_MODEL", raising=False) + + assert load_nova_config()["ollama_model"] == "qwen2.5-coder:3b" + assert OllamaProvider().model == "qwen2.5-coder:3b" + + +def test_provider_registry_builds_local_ollama_and_external() -> None: + from nova.providers import build_provider + from nova.providers.provider_external import ExternalProvider + from nova.providers.provider_local import LocalDeterministicProvider + from nova.providers.provider_ollama import OllamaProvider + + assert isinstance(build_provider({}), LocalDeterministicProvider) + + ollama = build_provider({ + "provider": "ollama", + "ollama_url": "http://fake-ollama", + "ollama_model": "qwen2.5-coder:7b", + }) + assert isinstance(ollama, OllamaProvider) + assert ollama.base_url == "http://fake-ollama" + assert ollama.model == "qwen2.5-coder:7b" + + external = build_provider({ + "provider": "external", + "external_url": "https://api.example.test/v1", + "external_model": "gpt-test", + "external_api_key": "secret", + }) + assert isinstance(external, ExternalProvider) + + +def test_ollama_provider_returns_completion_and_receipt(monkeypatch) -> None: + from nova.providers.provider_ollama import OllamaProvider + + captured = {} + + def fake_post_json(url, payload, *, timeout, headers=None): + captured.update({"url": url, "payload": payload, "timeout": timeout, "headers": headers}) + return { + "model": "qwen2.5-coder:7b", + "message": {"content": "Hello from Ollama"}, + "prompt_eval_count": 4, + "eval_count": 3, + } + + monkeypatch.setattr("nova.providers.provider_ollama.post_json", fake_post_json) + provider = OllamaProvider(base_url="http://fake-ollama", model="qwen2.5-coder:7b", timeout=11) + governed_request = { + "messages": [{"role": "user", "content": "Hello"}], + "max_tokens": 16, + "temperature": 0, + "slice_id": "test-slice", + "slice_version": "1.0", + "continuity_hash": "abc123", + "governance_path": ["rsl.validate"], + } + + result = provider.chat_completion(governed_request) + + assert captured["url"] == "http://fake-ollama/api/chat" + assert captured["payload"]["stream"] is False + assert captured["payload"]["options"]["num_predict"] == 16 + completion = result["completion"] + assert completion["id"].startswith("ollama-") + assert completion["object"] == "chat.completion" + assert completion["model"] == "qwen2.5-coder:7b" + assert completion["choices"][0]["message"]["role"] == "assistant" + assert completion["choices"][0]["message"]["content"] == "Hello from Ollama" + receipt = result["receipt"] + assert receipt["provider"] == "ollama" + assert receipt["model"] == "qwen2.5-coder:7b" + assert receipt["governed_request"] == governed_request + assert receipt["normalized_completion"] == completion + assert receipt["deterministic_core"] is False + assert receipt["rsl_version"] == "1.0" + assert receipt["slice_id"] == "test-slice" + assert receipt["continuity_hash"] == "abc123" + assert receipt["evidence_chain"] + + +def test_ollama_streaming_provider_emits_cursor_chunks(monkeypatch) -> None: + from nova.providers.provider_ollama import OllamaProvider + + lines = [ + json.dumps({"message": {"content": "hel"}, "done": False}).encode("utf-8"), + json.dumps({"message": {"content": "lo"}, "done": True}).encode("utf-8"), + ] + + monkeypatch.setattr( + "nova.providers.provider_ollama.post_json_lines", + lambda url, payload, *, timeout: iter(lines), + ) + provider = OllamaProvider(base_url="http://fake-ollama", model="qwen2.5-coder:7b") + + chunks = list(provider.chat_completion_stream({"messages": [{"role": "user", "content": "hi"}]})) + + assert chunks[0]["object"] == "chat.completion.chunk" + assert chunks[0]["choices"][0]["delta"]["role"] == "assistant" + assert chunks[0]["choices"][0]["delta"]["content"] == "hel" + assert chunks[1]["choices"][0]["delta"]["content"] == "lo" + assert chunks[-1]["completion"]["object"] == "chat.completion" + assert chunks[-1]["completion"]["choices"][0]["message"]["content"] == "hello" + assert chunks[-1]["receipt"]["provider"] == "ollama" + + +def test_api_models_health_completions_metrics_and_streaming(monkeypatch) -> None: + from fastapi.testclient import TestClient + import nova.api as nova_api + + class FakeProvider: + provider_id = "ollama" + model = "qwen2.5-coder:7b" + + def chat_completion(self, governed_request): + text = governed_request["messages"][-1]["content"] + return { + "completion": { + "id": "fake-completion-1", + "object": "chat.completion", + "created": 123, + "model": self.model, + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": f"echo:{text}"}, + } + ], + }, + "receipt": {"provider": "ollama"}, + } + + def chat_completion_stream(self, governed_request): + yield { + "id": "stream-1", + "object": "chat.completion.chunk", + "created": 123, + "model": self.model, + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": "part"}, + "finish_reason": None, + } + ], + } + yield {"completion": self.chat_completion(governed_request)["completion"], "receipt": {"provider": "ollama"}} + + monkeypatch.setenv("NOVA_PROVIDER", "ollama") + monkeypatch.setenv("NOVA_OLLAMA_MODEL", "qwen2.5-coder:7b") + monkeypatch.setattr(nova_api, "build_provider", lambda config: FakeProvider()) + nova_api.app.state.nova_config = nova_api.load_nova_config() + client = TestClient(nova_api.app) + + health = client.get("/health").json() + assert health["status"] == "ok" + assert health["provider"] == "ollama" + assert health["model"] == "qwen2.5-coder:7b" + + models = client.get("/v1/models").json() + assert models["object"] == "list" + assert {m["id"] for m in models["data"]} == {"nova-local", "qwen2.5-coder:7b"} + + completion = client.post("/v1/completions", json={"model": "nova-local", "prompt": "plain prompt"}).json() + assert completion["object"] == "text_completion" + assert completion["choices"][0]["text"] == "echo:plain prompt" + + chat = client.post( + "/v1/chat/completions", + json={"model": "qwen2.5-coder:7b", "messages": [{"role": "user", "content": "chat prompt"}]}, + ).json() + assert chat["object"] == "chat.completion" + assert chat["choices"][0]["message"]["content"] == "echo:chat prompt" + + with client.stream( + "POST", + "/v1/chat/completions", + json={"model": "qwen2.5-coder:7b", "stream": True, "messages": [{"role": "user", "content": "stream"}]}, + ) as response: + body = "".join(response.iter_text()) + assert response.status_code == 200 + assert '"object":"chat.completion.chunk"' in body + assert '"completion"' in body + assert "data: [DONE]" in body + + metrics = client.get("/metrics").json() + assert metrics["requests"] >= 3 + assert metrics["stream_requests"] >= 1 + assert metrics["provider"] == "ollama" + assert metrics["model"] == "qwen2.5-coder:7b" diff --git a/tests/test_unix_shell_support.py b/tests/test_unix_shell_support.py new file mode 100644 index 0000000..85aecb9 --- /dev/null +++ b/tests/test_unix_shell_support.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import re +import zipfile +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def test_unix_nova_shim_is_tracked_and_executable() -> None: + shim = ROOT / "bin" / "nova" + + assert shim.exists() + assert shim.read_text(encoding="utf-8").startswith("#!/usr/bin/env bash") + result = subprocess.run( + ["git", "ls-files", "-s", "--", "bin/nova"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.startswith("100755 ") + + +def test_unix_bootstrap_supports_non_interactive_local_install() -> None: + bootstrap = read("setup/bootstrap.sh") + + assert "--non-interactive" in bootstrap + assert "-m venv" in bootstrap + assert "pip install -e" in bootstrap + assert re.search(r'set_nova_var\s+"NOVA_CLI"\s+"Nova CLI command"\s+"\$REPO_ROOT/bin/nova"', bootstrap) + assert "read -rp" in bootstrap + assert "if [[ \"$NON_INTERACTIVE\" == true ]]" in bootstrap + + +def test_unix_shell_profile_prefers_repo_local_nova_shim() -> None: + profile = read("config/.zshrc") + + assert "LAWFUL_NOVA_REPO_ROOT" in profile + assert 'export PATH="$LAWFUL_NOVA_REPO_ROOT/bin:$PATH"' in profile + assert 'NOVA="${NOVA_CLI:-$LAWFUL_NOVA_REPO_ROOT/bin/nova}"' in profile + + +def test_unix_verifier_can_discover_repo_local_python_and_nova() -> None: + verify = read("setup/verify.sh") + + assert "get_candidate_repo_roots" in verify + assert "get_repo_python" in verify + assert "get_repo_nova_cli" in verify + assert "Nova CLI repo shim reachable" in verify + assert "Python .venv" in verify + + +def test_shell_scripts_are_lf_only() -> None: + for path in [ + "bin/nova", + "setup/bootstrap.sh", + "setup/install_linux.sh", + "setup/install_macos.sh", + "setup/install_nova.sh", + "setup/verify.sh", + "config/.zshrc", + "config/.novarc", + ]: + data = (ROOT / path).read_bytes() + assert b"\r\n" not in data, path + + +def test_macos_quickstart_documents_source_zip_flow() -> None: + guide = read("MACOS.md") + + assert "setup/bootstrap.sh --non-interactive" in guide + assert "bin/nova health --json" in guide + assert "setup/verify.sh" in guide + assert "LAWFUL_NOVA_REPO_ROOT" in guide + + +def test_macos_package_script_preserves_unix_modes_and_excludes_generated_dirs(tmp_path) -> None: + from scripts.package_macos_shell import create_macos_shell_zip + + archive = tmp_path / "lawful-nova-shell-macos.zip" + create_macos_shell_zip(ROOT, archive) + + assert archive.exists() + with zipfile.ZipFile(archive) as zf: + names = set(zf.namelist()) + assert "lawful-nova-shell/bin/nova" in names + assert "lawful-nova-shell/GITHUB-10-MINUTE-START.md" in names + assert "lawful-nova-shell/quickstart.sh" in names + assert "lawful-nova-shell/setup/bootstrap.sh" in names + assert "lawful-nova-shell/MACOS.md" in names + assert not any("/node_modules/" in name for name in names) + assert not any(name.startswith("lawful-nova-shell/.runtime/") for name in names) + assert not any(name.startswith("lawful-nova-shell/desktop/") for name in names) + + nova_info = zf.getinfo("lawful-nova-shell/bin/nova") + bootstrap_info = zf.getinfo("lawful-nova-shell/setup/bootstrap.sh") + nova_mode = (nova_info.external_attr >> 16) & 0o777 + bootstrap_mode = (bootstrap_info.external_attr >> 16) & 0o777 + assert nova_mode == 0o755 + assert bootstrap_mode == 0o755 diff --git a/tests/test_windows_program_package.py b/tests/test_windows_program_package.py new file mode 100644 index 0000000..dd75c17 --- /dev/null +++ b/tests/test_windows_program_package.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_windows_program_quickstart_documents_desktop_and_node_backend() -> None: + guide = (ROOT / "WINDOWS-DESKTOP.md").read_text(encoding="utf-8") + + assert "desktop" in guide + assert "nova/node" in guide + assert "npm install" in guide + assert "npm start" in guide + assert "setup\\verify.ps1" in guide + assert "python -m nova.api" in guide + + +def test_windows_wrappers_execute_the_shell_source_with_flexible_python_discovery() -> None: + verify = (ROOT / "setup" / "verify.ps1").read_text(encoding="utf-8") + wrapper = (ROOT / "bin" / "nova.ps1").read_text(encoding="utf-8") + + assert '$env:PYTHONPATH = "$ShellRoot;$RepoRoot"' in verify + assert 'Join-Path $ShellRoot ".venv\\Scripts\\python.exe"' in verify + assert 'Join-Path $RepoRoot ".venv\\Scripts\\python.exe"' in verify + assert '$env:PYTHONPATH = "$ShellRoot;$RepoRoot"' in wrapper + assert 'Join-Path $ShellRoot ".venv\\Scripts\\python.exe"' in wrapper + assert 'Join-Path $RepoRoot ".venv\\Scripts\\python.exe"' in wrapper + + +def test_windows_program_zip_includes_desktop_and_governed_node_backend(tmp_path) -> None: + from scripts.package_windows_program import create_windows_program_zip + + archive = tmp_path / "nova-desktop-windows.zip" + create_windows_program_zip(ROOT, archive) + + assert archive.exists() + with zipfile.ZipFile(archive) as zf: + names = set(zf.namelist()) + + required = { + "nova-desktop-windows/WINDOWS-DESKTOP.md", + "nova-desktop-windows/GITHUB-10-MINUTE-START.md", + "nova-desktop-windows/quickstart.ps1", + "nova-desktop-windows/desktop/main.js", + "nova-desktop-windows/desktop/preload.js", + "nova-desktop-windows/desktop/package.json", + "nova-desktop-windows/desktop/package-lock.json", + "nova-desktop-windows/desktop/renderer/index.html", + "nova-desktop-windows/desktop/core/node-client.js", + "nova-desktop-windows/nova/api.py", + "nova-desktop-windows/nova/node/evidence.py", + "nova-desktop-windows/nova/node/tools/routes.py", + "nova-desktop-windows/setup/bootstrap.ps1", + "nova-desktop-windows/setup/verify.ps1", + "nova-desktop-windows/pyproject.toml", + "nova-desktop-windows/requirements.txt", + "nova-desktop-windows/policy.yaml", + } + assert required.issubset(names) + assert not any("/node_modules/" in name for name in names) + assert not any(name.startswith("nova-desktop-windows/.runtime/") for name in names) + assert not any(name.startswith("nova-desktop-windows/desktop/dist/") for name in names) + assert not any("/__pycache__/" in name for name in names) diff --git a/tools/fuzz/fuzz-harness.ts b/tools/fuzz/fuzz-harness.ts deleted file mode 100644 index 2bab61f..0000000 --- a/tools/fuzz/fuzz-harness.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** CRK-1 kernel fuzz harness — see docs/integrity/CRK-1-KERNEL-FUZZ-HARNESS.md */ - -import { AgentRuntime, governance, continuity } from "../../agent/index"; - -const runtime = new AgentRuntime(); - -function randomPrompt(): string { - const samples = [ - "add logging", - "rm -rf /tmp/test", - "refactor utils", - "", - "x".repeat(500), - ]; - return samples[Math.floor(Math.random() * samples.length)] ?? "noop"; -} - -export async function fuzzActions(iterations = 100): Promise<{ caught: number }> { - let caught = 0; - for (let i = 0; i < iterations; i++) { - try { - await runtime.generateCode({ prompt: randomPrompt() }); - } catch { - caught++; - } - } - return { caught }; -} - -export async function fuzzContinuity(): Promise<{ replayed: number }> { - const snap = await continuity.snapshot(); - await continuity.snapshot(); - return { replayed: snap ? 1 : 0 }; -} - -export async function fuzzLedger(): Promise<{ receipts: number }> { - const receipts = await governance.listReceipts(); - return { receipts: receipts.length }; -} - -async function main(): Promise { - const action = await fuzzActions(50); - const cont = await fuzzContinuity(); - const ledger = await fuzzLedger(); - const report = { action, cont, ledger, ts: Date.now() }; - console.log(JSON.stringify(report, null, 2)); -} - -if (require.main === module) { - void main(); -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index b160b76..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "lib": ["ES2021"], - "outDir": "./dist", - "rootDir": ".", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "moduleResolution": "node" - }, - "include": ["agent/**/*", "config/nova.config.ts", "crk2/**/*", "control-tower/**/*", "backend/**/*", "tools/**/*"], - "exclude": ["node_modules", "dist", "examples", "cockpit"] -} diff --git a/web/index.html b/web/index.html deleted file mode 100644 index dfff76d..0000000 --- a/web/index.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - Nova – Constitutional Coding Agent - - - - - - -
-
-

Nova – Constitutional Coding Agent

-

Lawful, traceable, reproducible agentic coding on CRK‑1.

- Get Started -
- -
-

Why Nova

-
    -
  • Writes code with governance receipts
  • -
  • Plans with invariants
  • -
  • Refactors with continuity
  • -
  • Never acts silently
  • -
-
- -
-

How It Works

-
-
-

CRK‑1 Kernel

-

Invariant Engine, Pattern Ledger, Continuity Substrate.

-
-
-

Governed Agent Loop

-

Plan → Validate → Act → Record → Replay.

-
-
-
- -
-

For Developers

-
npm install nova-sdk
-

Use the SDK to build tools, enforce invariants, and integrate with editors.

-
- -
-

Documentation

- -
- -
-

Getting Started

-
    -
  1. Install Nova SDK
  2. -
  3. Configure invariants in config/nova.config.ts
  4. -
  5. Run your first governed agent loop
  6. -
-
-
- - - - diff --git a/web/styles.css b/web/styles.css deleted file mode 100644 index 7aaf7e5..0000000 --- a/web/styles.css +++ /dev/null @@ -1,113 +0,0 @@ -:root { - --nova-blue: #3a7bff; - --governance-gold: #f2c94c; - --continuity-slate: #1c1f26; - --text: #e8eaed; - --muted: #9aa0a6; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: Inter, system-ui, sans-serif; - background: var(--continuity-slate); - color: var(--text); - line-height: 1.6; -} - -.site-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 2rem; - border-bottom: 1px solid #2a2f3a; -} - -.logo { - font-weight: 600; - font-size: 1.25rem; - color: var(--nova-blue); -} - -nav a { - color: var(--muted); - text-decoration: none; - margin-left: 1.5rem; -} - -nav a:hover { - color: var(--governance-gold); -} - -.hero { - text-align: center; - padding: 5rem 2rem; - background: linear-gradient(180deg, #242830 0%, var(--continuity-slate) 100%); -} - -.hero h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; -} - -.cta { - display: inline-block; - margin-top: 1.5rem; - padding: 0.75rem 1.5rem; - background: var(--nova-blue); - color: white; - text-decoration: none; - border-radius: 6px; - font-weight: 600; -} - -.section { - max-width: 900px; - margin: 0 auto; - padding: 3rem 2rem; -} - -.columns { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.5rem; -} - -article { - background: #242830; - padding: 1.5rem; - border-radius: 8px; - border: 1px solid #2a2f3a; -} - -article h3 { - color: var(--governance-gold); - margin-top: 0; -} - -pre { - background: #0d1117; - padding: 1rem; - border-radius: 6px; - overflow-x: auto; -} - -code { - font-family: "JetBrains Mono", monospace; -} - -.site-footer { - text-align: center; - padding: 2rem; - color: var(--muted); - border-top: 1px solid #2a2f3a; -} - -@media (max-width: 640px) { - .columns { - grid-template-columns: 1fr; - } -}