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/README.md b/README.md index 23605fe..876145e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,33 @@ Self-bootstrapping agentic coding environment for **macOS**, **Linux**, and **Windows**, powered by the **Nova LLM** stack — Voss Runtime · Gates of Wonder · RSL · Nova Cortex · NVIDIA backend. -This repo does **not** build Nova; it validates paths to your already-built Nova slice and wires your dev shell (zsh / PowerShell), `AGENTS.md`, skills, and devcontainer. +This repo ships the **source** for the local Lawful Nova shell and validates paths to either the bundled local lawful slice or your own built Nova stack. It wires your dev shell (zsh / PowerShell), `AGENTS.md`, skills, and devcontainer. + +No generated `.exe`, installer, model weights, checkpoints, `.venv`, or runtime databases are committed. Build artifacts stay local. Clone → Run one command → Code with Nova. +## Windows-native status + +Lawful Nova is Windows-native first. Docker is optional and is only needed later +for Linux parity, isolated CI, container deployment, or GPU/container proof +lanes. + +The repo includes: + +- `bin/nova.ps1` and `bin/nova.cmd` local CLI shims +- `nova/` source package for the local lawful runtime, CLI, and API +- `scripts/nova_productization_gate.py` for source-level readiness proof +- `setup/verify.ps1` for Windows environment checks + +Core local checks: + +```powershell +python -m pip install -r requirements-dev.txt +python -m pytest tests -q +python scripts\nova_productization_gate.py +``` + [![macOS](https://img.shields.io/badge/macOS-13%2B-black?logo=apple)](https://apple.com) [![Linux](https://img.shields.io/badge/Linux-Ubuntu%2022.04%2B-orange?logo=linux)](https://ubuntu.com) [![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?logo=windows)](https://microsoft.com/windows) 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..caaa197 --- /dev/null +++ b/bin/nova.ps1 @@ -0,0 +1,20 @@ +# Repo-local Nova CLI shim for the Lawful Nova slice. +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Args +) + +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$ShellRoot = Split-Path -Parent $ScriptRoot +$ProjectRoot = Split-Path -Parent $ShellRoot + +$Python = Join-Path $ProjectRoot ".venv\Scripts\python.exe" +if (-not (Test-Path $Python)) { + $Python = Join-Path $ShellRoot ".venv\Scripts\python.exe" +} +if (-not (Test-Path $Python)) { + $Python = "python" +} + +& $Python -m nova.cli @Args +exit $LASTEXITCODE diff --git a/config/nova/nova-stack.json b/config/nova/nova-stack.json index 1cfa217..bb03b6b 100644 --- a/config/nova/nova-stack.json +++ b/config/nova/nova-stack.json @@ -1,7 +1,7 @@ { "name": "Lawful Nova LLM Slice", "version": "1.0.0", - "description": "Voss Runtime + Gates of Wonder + RSL + Nova Cortex + NVIDIA backend", + "description": "Local Lawful Nova slice: Voss receipt runtime + Gates of Wonder presentation + RSL policy + Nova Cortex, with optional NVIDIA backend", "stack": { "voss_runtime": { "path": "${NOVA_VOSS_RUNTIME_PATH}", @@ -22,6 +22,7 @@ "nvidia_backend": { "endpoint": "${NOVA_MEGATRON_ENDPOINT}", "gpu_device": "${NOVA_GPU_DEVICE}", + "required_for_local_lawful_slice": false, "description": "NVIDIA Megatron / NIM GPU compute layer" } }, diff --git a/config/novarc.ps1 b/config/novarc.ps1 index fe85a95..50511d3 100644 --- a/config/novarc.ps1 +++ b/config/novarc.ps1 @@ -6,12 +6,23 @@ $env:LAWFUL_NOVA_REPO_ROOT = "" $env:NOVA_PORT = "8080" $env:NOVA_API_URL = "http://localhost:$($env:NOVA_PORT)" -$env:NOVA_CLI = "nova" +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" +} -$env:NOVA_VOSS_RUNTIME_PATH = "" -$env:NOVA_CORTEX_PATH = "" -$env:NOVA_GOW_CONFIG = "" -$env:NOVA_RSL_PATH = "" +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" 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..1277464 --- /dev/null +++ b/nova/api.py @@ -0,0 +1,64 @@ +"""HTTP compatibility surface for the local Lawful Nova slice.""" + +from __future__ import annotations + +import os +import json +from typing import Any + +from fastapi import FastAPI +from pydantic import BaseModel, Field + +from nova.lawful_llm import LawfulLLM + + +class ChatRequest(BaseModel): + prompt: str = Field(min_length=1) + tenant_id: str = "local" + capability: str = "observe" + + +app = FastAPI(title="Local Lawful Nova API", version="0.1.0") + + +@app.get("/health") +def health() -> dict[str, str]: + return {"status": "ok", "service": "nova_local_api"} + + +@app.post("/v1/chat") +def chat(request: ChatRequest) -> dict[str, Any]: + llm = LawfulLLM(operator_session_id="nova-local-api", signing_secret="local-api-secret") + turn = llm.ask( + request.prompt, + tenant_id=request.tenant_id, + capability=request.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 _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/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/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/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/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..aefbcb6 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +-e .[dev] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d6e1198 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +-e . diff --git a/scripts/nova_productization_gate.py b/scripts/nova_productization_gate.py new file mode 100644 index 0000000..d6c975d --- /dev/null +++ b/scripts/nova_productization_gate.py @@ -0,0 +1,116 @@ +"""Productization readiness gate for the local Lawful Nova slice.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from nova.cli import collect_health +from nova.lawful_llm import LawfulLLM + + +def _python_runtime() -> 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/setup/bootstrap.ps1 b/setup/bootstrap.ps1 index 883030f..13d428e 100644 --- a/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/setup/verify.ps1 b/setup/verify.ps1 index 9144811..e325491 100644 --- a/setup/verify.ps1 +++ b/setup/verify.ps1 @@ -7,6 +7,7 @@ $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-InfoLine { param([string]$Message) Write-Host " [INFO] $Message" -ForegroundColor DarkCyan } function Write-Sep { param([string]$Title) Write-Host "" Write-Host "-- $Title --------------------------------" -ForegroundColor Cyan @@ -15,6 +16,35 @@ function Write-Sep { param([string]$Title) $NovarcPath = Join-Path $env:USERPROFILE ".novarc.ps1" if (Test-Path $NovarcPath) { . $NovarcPath } +function Get-CandidateRepoRoots { + $roots = @() + if ($env:LAWFUL_NOVA_REPO_ROOT) { $roots += $env:LAWFUL_NOVA_REPO_ROOT } + $roots += (Resolve-Path (Join-Path $PSScriptRoot "..") -ErrorAction SilentlyContinue | ForEach-Object { $_.Path }) + $roots += (Resolve-Path (Join-Path $PSScriptRoot "..\..") -ErrorAction SilentlyContinue | ForEach-Object { $_.Path }) + $roots += (Get-Location).Path + $roots | Where-Object { $_ } | Select-Object -Unique +} + +function Get-RepoPython { + foreach ($root in Get-CandidateRepoRoots) { + foreach ($rel in @(".venv\Scripts\python.exe", "venv\Scripts\python.exe")) { + $candidate = Join-Path $root $rel + if (Test-Path $candidate) { return $candidate } + } + } + return $null +} + +function Get-RepoNovaCli { + foreach ($root in Get-CandidateRepoRoots) { + foreach ($rel in @("lawful-nova-shell\bin\nova.ps1", "bin\nova.ps1")) { + $candidate = Join-Path $root $rel + if (Test-Path $candidate) { return $candidate } + } + } + return $null +} + Write-Host "" Write-Host "Lawful Nova - Agentic Shell Verification (Windows)" -ForegroundColor White Write-Host " $(Get-Date)" @@ -32,6 +62,7 @@ if (Get-Command node -ErrorAction SilentlyContinue) { Write-Ok "Node.js $(node - 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)" } +elseif ($repoPython = Get-RepoPython) { Write-Ok "Python .venv $repoPython" } else { Write-WarnLine "Python not found (optional for shell; install 3.12+ for Nova tooling)" } Write-Sep "Nova LLM Stack" @@ -49,7 +80,9 @@ Write-Sep "Nova LLM Stack" 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)" } +if (Get-Command $NovaCli -ErrorAction SilentlyContinue) { Write-Ok "Nova CLI reachable" } +elseif ($repoNovaCli = Get-RepoNovaCli) { Write-Ok "Nova CLI repo shim reachable -> $repoNovaCli" } +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 { @@ -75,7 +108,7 @@ if (Test-Path (Join-Path $env:USERPROFILE ".gitconfig")) { Write-Ok "~/.gitconfi 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 docker -ErrorAction SilentlyContinue) { Write-Ok "Docker" } else { Write-InfoLine "Docker not found - optional for native Windows agent" } if (Get-Command code -ErrorAction SilentlyContinue) { Write-Ok "VS Code" } else { Write-WarnLine "VS Code not found" } Write-Host "" diff --git a/tests/test_local_nova_shell.py b/tests/test_local_nova_shell.py new file mode 100644 index 0000000..cc5eb89 --- /dev/null +++ b/tests/test_local_nova_shell.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +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_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"