From feb7ddc9301cb32f101ee8557346afc7c147e8a7 Mon Sep 17 00:00:00 2001 From: high-cde <2.58428056e+08+high-cde@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:04:21 +0000 Subject: [PATCH 1/6] feat: connect official zretro terminals --- README.md | 2 +- docs/ZRETRO.md | 22 +++++- zretro/ide/bus.py | 78 +++++++++++++++++++ zretro/ide/test_zretro.py | 25 ++++++ zretro/ide/zretro.py | 25 +++++- zretro/packages/official/README.md | 28 +++++++ zretro/packages/official/catalog.json | 11 +++ .../zretro-terminal-amiga/main.zretro | 11 +++ .../official/zretro-terminal-c64/main.zretro | 11 +++ 9 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 zretro/ide/bus.py create mode 100644 zretro/packages/official/README.md create mode 100644 zretro/packages/official/catalog.json create mode 100644 zretro/packages/official/zretro-terminal-amiga/main.zretro create mode 100644 zretro/packages/official/zretro-terminal-c64/main.zretro diff --git a/README.md b/README.md index 8b849c6..620ca76 100755 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ python3 zretro/ide/zretro.py run zretro/projects/meteor-patrol/main.zretro python3 zretro/ide/zretro.py build zretro/projects/meteor-patrol/main.zretro ``` -Il preview terminale è già operativo. La pipeline produce IR e manifest verificabili; i backend per binari nativi e emulatori vengono attivati soltanto quando cc65/ca65, vasm, VICE, Altirra o FS-UAE sono presenti nel nodo. La specifica è in [`docs/ZRETRO.md`](docs/ZRETRO.md). +Il preview terminale è già operativo. La pipeline produce IR e manifest verificabili; i backend per binari nativi e emulatori vengono attivati soltanto quando cc65/ca65, vasm, VICE, Altirra o FS-UAE sono presenti nel nodo. Il catalogo [`zretro/packages/official/`](zretro/packages/official/) contiene i primi software ufficiali **ZRetro by ZDOS** per Commodore 64 e Amiga. I terminali possono collegarsi e scambiarsi messaggi sul bus locale `zretro.local` con `l`, `s` e `i`; il bus è filesystem-scoped, append-only e senza rete implicita. La specifica è in [`docs/ZRETRO.md`](docs/ZRETRO.md). Il launcher operativo è [`tools/zdos-launcher.sh`](tools/zdos-launcher.sh), con documentazione in [`docs/ZDOS_LAUNCHER.md`](docs/ZDOS_LAUNCHER.md). Il progetto originale [`Castel Goblin`](zretro/projects/castel-goblin/README.md) è organizzato in tre livelli progressivi e può essere avviato dal launcher dopo la rilevazione dell’ambiente. diff --git a/docs/ZRETRO.md b/docs/ZRETRO.md index 44f96a3..483a1c1 100644 --- a/docs/ZRETRO.md +++ b/docs/ZRETRO.md @@ -35,6 +35,9 @@ b valida e prepara pacchetti target r preview terminale retro t target supportati a panoramica asset +l collega due terminali sul bus locale +s invia messaggio ZRetro local-only +i legge la inbox del terminale p pubblicazione esplicita del manifest su Hub q uscita ``` @@ -47,7 +50,20 @@ python3 zretro/ide/zretro.py run zretro/projects/meteor-patrol/main.zretro python3 zretro/ide/zretro.py build zretro/projects/meteor-patrol/main.zretro ``` -La console mostra il prompt nativo `x@zdos /zretro` e instrada i comandi brevi verso init, preview, build e catalogo target. +La console mostra il prompt nativo `x@zdos /zretro` e instrada i comandi brevi verso init, preview, build, catalogo target e bus locale. + +## Software ufficiale ZDOS per Amiga e Commodore 64 + +Il catalogo [`zretro/packages/official/`](../zretro/packages/official/) contiene i primi pacchetti **ZRetro by ZDOS**: `zretro-terminal-c64` per Commodore 64 e `zretro-terminal-amiga` per Amiga. Sono sorgenti DSL e manifesti verificabili, non ROM o immagini proprietarie: il builder produce IR e provenance; l’emissione di `.prg` e `.adf` richiede toolchain native installate e viene dichiarata solo dopo una build verificata. + +I terminali possono essere creatori e comunicativi tramite `zretro.local`. Il bus è append-only JSONL nella directory di progetto o in `ZDOS_ZRETRO_BUS_DIR`; non apre socket, non usa credenziali e non abilita rete implicita. L’interconnessione è esplicita: + +```sh +python3 zretro/ide/zretro.py console --root /tmp/zdos-bus +# l c64-01 amiga-01 +# s amiga-01 hello from C64 +# i amiga-01 +``` ## Target e backend @@ -57,7 +73,7 @@ La console mostra il prompt nativo `x@zdos /zretro` e instrada i comandi brevi v | Atari 8-bit | 6502 | `.xex` | cc65/ca65 e Altirra | | Amiga | 68000 | `.adf` | vasm + disk builder e FS-UAE | -La prima versione genera un IR ZRetro e manifest target verificabili. Il preview terminale è operativo; l’emissione di binari nativi e la chiamata agli emulatori sono backend successivi, da attivare soltanto quando gli strumenti sono presenti nel nodo. +La versione attuale genera un IR ZRetro, manifest target verificabili e messaggi locali tra terminali. Il preview e il bus sono operativi; l’emissione di binari nativi e la chiamata agli emulatori sono backend successivi, da attivare soltanto quando gli strumenti sono presenti nel nodo. La scelta dei backend è coerente con gli strumenti pubblici: cc65 supporta target 6502 tra cui Commodore e Atari [1]; Altirra documenta immagini Atari come ATR, ATX, XFD, ROM e BIN [2]; FS-UAE è un emulatore Amiga multipiattaforma focalizzato sui giochi [3]. @@ -71,7 +87,7 @@ Il progetto è confinato alla propria root. La DSL non esegue shell arbitraria, ## Roadmap -La roadmap tecnica è: editor TUI nativo, parser ZRetro completo, asset pipeline palette/sprite/sound, backend cc65 per C64/Atari, backend 68000 per Amiga, launcher emulatore locale, manifest firmati e pannello ZRetro nella War Room/Hub. +La roadmap tecnica residua è: editor TUI nativo, parser ZRetro completo, asset pipeline palette/sprite/sound, backend cc65 per C64, backend 68000 per Amiga, launcher emulatore locale, manifest firmati e pannello ZRetro nella War Room/Hub. Il contratto di comunicazione locale e il catalogo sorgente ufficiale sono già attivi e testati. ## Riferimenti diff --git a/zretro/ide/bus.py b/zretro/ide/bus.py new file mode 100644 index 0000000..47b92e4 --- /dev/null +++ b/zretro/ide/bus.py @@ -0,0 +1,78 @@ +"""Local-only ZRetro terminal bus. + +The bus is deliberately filesystem-scoped: no sockets, no credentials and no +implicit network access. Two ZDOS terminals interconnect by sharing a bus +folder (for example on a mounted disk or a local serial-file bridge). +""" +from __future__ import annotations + +import hashlib +import json +import os +import time +from pathlib import Path + +SCHEMA = "zdos-zretro-bus/v1" + + +def bus_root(project_root: Path) -> Path: + configured = os.environ.get("ZDOS_ZRETRO_BUS_DIR") + root = Path(configured).expanduser() if configured else project_root / "build" / "bus" + root.mkdir(parents=True, exist_ok=True) + return root + + +def _safe(value: str, label: str, limit: int) -> str: + clean = value.strip() + if not clean or len(clean) > limit or any(ord(char) < 32 for char in clean): + raise ValueError(f"{label} non valido") + return clean + + +def link_terminal(project_root: Path, terminal_id: str, peer_id: str) -> Path: + terminal = _safe(terminal_id, "terminal", 48) + peer = _safe(peer_id, "peer", 48) + path = bus_root(project_root) / "links.json" + links = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {"schema": SCHEMA, "links": []} + pair = sorted([terminal, peer]) + if pair not in links["links"]: + links["links"].append(pair) + links["links"].sort() + path.write_text(json.dumps(links, indent=2) + "\n", encoding="utf-8") + return path + + +def _is_linked(root: Path, sender: str, recipient: str) -> bool: + path = root / "links.json" + if not path.exists(): + return False + links = json.loads(path.read_text(encoding="utf-8")) + return sorted([sender, recipient]) in links.get("links", []) + + +def send_message(project_root: Path, sender: str, recipient: str, channel: str, body: str) -> dict: + sender = _safe(sender, "sender", 48) + recipient = _safe(recipient, "recipient", 48) + channel = _safe(channel, "channel", 32) + body = _safe(body, "message", 512) + root = bus_root(project_root) + if not _is_linked(root, sender, recipient): + raise ValueError("terminali non collegati: eseguire link prima di inviare") + timestamp = str(time.time_ns()) + message = {"schema": SCHEMA, "id": "msg-" + hashlib.sha256(f"{sender}|{recipient}|{timestamp}|{body}".encode()).hexdigest()[:16], "sender": sender, "recipient": recipient, "channel": channel, "body": body, "created_at": timestamp} + with (root / "messages.jsonl").open("a", encoding="utf-8") as stream: + stream.write(json.dumps(message, ensure_ascii=False, sort_keys=True) + "\n") + return message + + +def receive_messages(project_root: Path, terminal_id: str, after: str | None = None) -> list[dict]: + terminal = _safe(terminal_id, "terminal", 48) + path = bus_root(project_root) / "messages.jsonl" + if not path.exists(): + return [] + messages = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] + if after: + ids = [message["id"] for message in messages] + if after in ids: + messages = messages[ids.index(after) + 1 :] + return [message for message in messages if message["recipient"] == terminal] diff --git a/zretro/ide/test_zretro.py b/zretro/ide/test_zretro.py index 35fb89c..9a5e6ff 100644 --- a/zretro/ide/test_zretro.py +++ b/zretro/ide/test_zretro.py @@ -10,6 +10,7 @@ ROOT = Path(__file__).resolve().parents[2] CLI = ROOT / "zretro" / "ide" / "zretro.py" DEMO = ROOT / "zretro" / "projects" / "meteor-patrol" / "main.zretro" +BUS = ROOT / "zretro" / "ide" / "bus.py" class ZRetroTests(unittest.TestCase): @@ -63,6 +64,30 @@ def test_unknown_instruction_is_rejected(self) -> None: self.assertNotEqual(result.returncode, 0) self.assertIn("parola ZRetro non ammessa", result.stderr) + def test_official_c64_and_amiga_sources_build(self) -> None: + for target in ("c64", "amiga"): + source = ROOT / "zretro" / "packages" / "official" / f"zretro-terminal-{target}" / "main.zretro" + with self.subTest(target=target): + result = subprocess.run([sys.executable, str(CLI), "build", str(source)], text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr) + manifest = json.loads((source.parent / "build" / "manifest.json").read_text(encoding="utf-8")) + self.assertEqual(manifest["targets"][0]["id"], target) + self.assertEqual(manifest["runtime"], "zretro-terminal-v1") + + def test_local_bus_links_and_delivers_without_network(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + env = {**__import__("os").environ, "ZDOS_ZRETRO_BUS_DIR": str(Path(tmp) / "bus")} + link = subprocess.run([sys.executable, str(CLI), "console", "--root", tmp], input="l c64-01 amiga-01\nq\n", text=True, capture_output=True, env=env) + self.assertEqual(link.returncode, 0, link.stderr) + self.assertIn("ZRETRO_LINK_OK", link.stdout) + send = subprocess.run([sys.executable, str(CLI), "console", "--root", tmp], input="s amiga-01 hello amiga\nq\n", text=True, capture_output=True, env={**env, "ZDOS_TERMINAL_ID": "c64-01"}) + self.assertEqual(send.returncode, 0, send.stderr) + self.assertIn("ZRETRO_SEND_OK", send.stdout) + inbox = subprocess.run([sys.executable, str(CLI), "console", "--root", tmp], input="i amiga-01\nq\n", text=True, capture_output=True, env=env) + self.assertEqual(inbox.returncode, 0, inbox.stderr) + self.assertIn("ZRETRO_INBOX count=1", inbox.stdout) + self.assertIn("hello amiga", inbox.stdout) + if __name__ == "__main__": unittest.main() diff --git a/zretro/ide/zretro.py b/zretro/ide/zretro.py index a68722c..ef462f0 100644 --- a/zretro/ide/zretro.py +++ b/zretro/ide/zretro.py @@ -5,12 +5,14 @@ import argparse import hashlib import json +import os import re import shutil import sys from pathlib import Path from hub import prepare_manifest +from bus import link_terminal, receive_messages, send_message TARGETS = { "c64": {"name": "Commodore 64", "cpu": "6502", "artifact": ".prg", "backend": "cc65/ca65"}, @@ -118,12 +120,16 @@ def help_text() -> str: "r preview terminale", "t target supportati", "a asset del progetto", + "l collega due terminali sul bus locale", + "s invia messaggio ZRetro sul canale local", + "i leggi i messaggi ricevuti", "q esci", ]) def console(root: Path) -> int: - print("ZRETRO IDE // ZLANG BY ZDOS // C64 MODE") + terminal_id = os.environ.get("ZDOS_TERMINAL_ID", "terminal-local") + print(f"ZRETRO IDE // ZLANG BY ZDOS // C64 MODE // {terminal_id}") print("digita h per aiuto") while True: try: @@ -147,6 +153,23 @@ def console(root: Path) -> int: print(f"{key:<8} {target['name']} CPU={target['cpu']} artifact={target['artifact']}") elif command == "a": print("ZRETRO ASSETS: palette, sprite, scene, sound") + elif command == "l" and argument: + values = argument.split() + if len(values) != 2: + raise ValueError("l richiede terminal-id e peer-id") + path = link_terminal(root, values[0], values[1]) + print(f"ZRETRO_LINK_OK bus={path} pair={values[0]}<->{values[1]}") + elif command == "s" and argument: + values = argument.split(maxsplit=1) + if len(values) != 2: + raise ValueError("s richiede destinatario e messaggio") + message = send_message(root, terminal_id, values[0], "zretro.local", values[1]) + print(f"ZRETRO_SEND_OK id={message['id']} to={message['recipient']}") + elif command == "i" and argument: + messages = receive_messages(root, argument) + print(f"ZRETRO_INBOX count={len(messages)}") + for message in messages: + print(f"{message['id']} {message['sender']} > {message['body']}") elif command == "p" and argument: source = Path(argument) build_manifest = source.parent / "build" / "manifest.json" diff --git a/zretro/packages/official/README.md b/zretro/packages/official/README.md new file mode 100644 index 0000000..d45ff4d --- /dev/null +++ b/zretro/packages/official/README.md @@ -0,0 +1,28 @@ +# ZDOS Official ZRetro Software + +Questo catalogo contiene software ufficiale **ZRetro by ZDOS** in formato sorgente e manifesto verificabile. + +| Pacchetto | Target | Stato | +|---|---|---| +| `zretro-terminal-c64` | Commodore 64 / 6502 | source-ready, build verificabile con `zretro build` | +| `zretro-terminal-amiga` | Amiga / Motorola 68000 | source-ready, build verificabile con `zretro build` | + +I pacchetti condividono il runtime `zretro-terminal-v1`, il profilo `zlang-by-zdos` e il bus locale `zretro.local`. Il bus non apre socket e non abilita rete implicita: i terminali comunicano solo quando l’operatore crea esplicitamente un link nella directory del bus. + +## Build + +```sh +python3 zretro/ide/zretro.py build zretro/packages/official/zretro-terminal-c64/main.zretro +python3 zretro/ide/zretro.py build zretro/packages/official/zretro-terminal-amiga/main.zretro +``` + +Il build produce IR e manifesti target-specifici. La generazione di un `.prg` o di un `.adf` nativo richiede le rispettive toolchain e non viene dichiarata automaticamente dal builder portabile. + +## Interconnessione locale + +```sh +python3 zretro/ide/zretro.py console --root /tmp/zdos-bus +# l c64-01 amiga-01 +# s amiga-01 hello from c64 +# i c64-01 +``` diff --git a/zretro/packages/official/catalog.json b/zretro/packages/official/catalog.json new file mode 100644 index 0000000..b9290e7 --- /dev/null +++ b/zretro/packages/official/catalog.json @@ -0,0 +1,11 @@ +{ + "schema": "zdos-zretro-official-catalog/v1", + "brand": "ZRetro by ZDOS", + "runtime": "zretro-terminal-v1", + "bus": "zretro.local", + "network_default": "disabled", + "packages": [ + {"id": "zretro-terminal-c64", "target": "c64", "source": "zretro-terminal-c64/main.zretro", "cpu": "6502", "artifact": ".prg"}, + {"id": "zretro-terminal-amiga", "target": "amiga", "source": "zretro-terminal-amiga/main.zretro", "cpu": "68000", "artifact": ".adf"} + ] +} diff --git a/zretro/packages/official/zretro-terminal-amiga/main.zretro b/zretro/packages/official/zretro-terminal-amiga/main.zretro new file mode 100644 index 0000000..890915b --- /dev/null +++ b/zretro/packages/official/zretro-terminal-amiga/main.zretro @@ -0,0 +1,11 @@ +project ZDOS Official Terminal Amiga +target amiga +screen 80 25 +palette amiga +text "ZRETRO BY ZDOS // AMIGA TERMINAL" +text "LINK / CREATE / SHARE" +player terminal-amiga +on link +on create +on receive +end diff --git a/zretro/packages/official/zretro-terminal-c64/main.zretro b/zretro/packages/official/zretro-terminal-c64/main.zretro new file mode 100644 index 0000000..03ea0e6 --- /dev/null +++ b/zretro/packages/official/zretro-terminal-c64/main.zretro @@ -0,0 +1,11 @@ +project ZDOS Official Terminal C64 +target c64 +screen 40 22 +palette c64 +text "ZRETRO BY ZDOS // C64 TERMINAL" +text "LINK / CREATE / SHARE" +player terminal-c64 +on link +on create +on receive +end From dd59b631120602df5cd31f5eeaa74eeab3768a00 Mon Sep 17 00:00:00 2001 From: high-cde <2.58428056e+08+high-cde@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:06:09 +0000 Subject: [PATCH 2/6] test: simulate concurrent multi-terminal chat --- zretro/ide/test_zretro.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/zretro/ide/test_zretro.py b/zretro/ide/test_zretro.py index 9a5e6ff..0bdf8a2 100644 --- a/zretro/ide/test_zretro.py +++ b/zretro/ide/test_zretro.py @@ -1,12 +1,16 @@ from __future__ import annotations import json +import os import subprocess import sys import tempfile import unittest +from concurrent.futures import ThreadPoolExecutor from pathlib import Path +from bus import link_terminal, receive_messages, send_message + ROOT = Path(__file__).resolve().parents[2] CLI = ROOT / "zretro" / "ide" / "zretro.py" DEMO = ROOT / "zretro" / "projects" / "meteor-patrol" / "main.zretro" @@ -88,6 +92,36 @@ def test_local_bus_links_and_delivers_without_network(self) -> None: self.assertIn("ZRETRO_INBOX count=1", inbox.stdout) self.assertIn("hello amiga", inbox.stdout) + def test_multi_terminal_chat_four_nodes_concurrently(self) -> None: + """Simulate one chat round among C64, Amiga and two ZDOS terminals.""" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + os.environ["ZDOS_ZRETRO_BUS_DIR"] = str(root / "bus") + try: + nodes = ["c64-01", "amiga-01", "zdos-01", "zdos-02"] + for index, sender in enumerate(nodes): + for recipient in nodes[index + 1 :]: + link_terminal(root, sender, recipient) + + rounds = [ + ("c64-01", "amiga-01", "C64 online"), + ("amiga-01", "zdos-01", "Amiga ready"), + ("zdos-01", "zdos-02", "ZDOS relay"), + ("zdos-02", "c64-01", "C64 received"), + ] + with ThreadPoolExecutor(max_workers=4) as pool: + sent = list(pool.map(lambda item: send_message(root, item[0], item[1], "zretro.chat", item[2]), rounds)) + + self.assertEqual(len(sent), 4) + self.assertEqual({message["channel"] for message in sent}, {"zretro.chat"}) + self.assertEqual({message["recipient"] for message in sent}, set(nodes)) + for node, expected in zip(nodes, ["C64 received", "C64 online", "Amiga ready", "ZDOS relay"]): + inbox = receive_messages(root, node) + self.assertEqual(len(inbox), 1, node) + self.assertEqual(inbox[0]["body"], expected) + finally: + os.environ.pop("ZDOS_ZRETRO_BUS_DIR", None) + if __name__ == "__main__": unittest.main() From 51d54a2b7b2a6e786e6b750d1b528851fbab5733 Mon Sep 17 00:00:00 2001 From: high-cde <2.58428056e+08+high-cde@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:11:39 +0000 Subject: [PATCH 3/6] feat: add live zretro chat control dashboard --- interface/web/package.json | 3 +- interface/web/server/server.js | 24 +++++++ interface/web/server/zretro-chat.js | 45 ++++++++++++++ interface/web/server/zretro-chat.test.js | 29 +++++++++ interface/web/web/css/style.css | 72 ++++++++++++--------- interface/web/web/index.html | 29 +++++++-- interface/web/web/js/app.js | 79 ++++++++++++++++++++++-- 7 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 interface/web/server/zretro-chat.js create mode 100644 interface/web/server/zretro-chat.test.js diff --git a/interface/web/package.json b/interface/web/package.json index ff35e63..5bb5060 100755 --- a/interface/web/package.json +++ b/interface/web/package.json @@ -9,7 +9,8 @@ }, "scripts": { "start": "node server/server.js", - "check": "node --check server/server.js" + "check": "node --check server/server.js && node --check server/zretro-chat.js && node --check web/js/app.js", + "test": "node --test server/zretro-chat.test.js" }, "dependencies": { "express": "5.2.1" diff --git a/interface/web/server/server.js b/interface/web/server/server.js index 6b3c75e..fc6caad 100755 --- a/interface/web/server/server.js +++ b/interface/web/server/server.js @@ -1,5 +1,6 @@ const express = require("express"); const path = require("path"); +const { snapshot } = require("./zretro-chat"); const app = express(); const port = Number.parseInt(process.env.PORT || "8080", 10); @@ -32,6 +33,29 @@ app.get("/status", (_req, res) => { }); }); +app.get("/zretro/chat", (_req, res) => { + res.json(snapshot()); +}); + +app.get("/zretro/chat/stream", (req, res) => { + res.writeHead(200, { + "Cache-Control": "no-store", + "Content-Type": "text/event-stream", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }); + let closed = false; + const publish = () => { + if (!closed) res.write(`event: snapshot\ndata: ${JSON.stringify(snapshot())}\n\n`); + }; + publish(); + const timer = setInterval(publish, 1000); + req.on("close", () => { + closed = true; + clearInterval(timer); + }); +}); + app.use((_req, res) => { res.status(404).json({ error: "not_found" }); }); diff --git a/interface/web/server/zretro-chat.js b/interface/web/server/zretro-chat.js new file mode 100644 index 0000000..cb83e81 --- /dev/null +++ b/interface/web/server/zretro-chat.js @@ -0,0 +1,45 @@ +const fs = require("fs"); +const path = require("path"); + +const CHAT_CHANNEL = "zretro.chat"; +const MAX_MESSAGES = 500; + +function busDirectory() { + return path.resolve(process.env.ZDOS_ZRETRO_BUS_DIR || path.join(__dirname, "../../../zretro/build/bus")); +} + +function readMessages() { + const file = path.join(busDirectory(), "messages.jsonl"); + if (!fs.existsSync(file)) return []; + return fs.readFileSync(file, "utf8") + .split("\n") + .filter(Boolean) + .slice(-MAX_MESSAGES) + .flatMap((line) => { + try { return [JSON.parse(line)]; } catch { return []; } + }) + .filter((message) => message.schema === "zdos-zretro-bus/v1" && message.channel === CHAT_CHANNEL); +} + +function snapshot() { + const messages = readMessages(); + const nodes = new Set(); + messages.forEach((message) => { + nodes.add(message.sender); + nodes.add(message.recipient); + }); + return { + schema: "zdos-zretro-chat-dashboard/v1", + status: "LOCAL_READ_ONLY", + channel: CHAT_CHANNEL, + bus_directory: busDirectory(), + generated_at: new Date().toISOString(), + node_count: nodes.size, + message_count: messages.length, + nodes: [...nodes].sort(), + messages, + disclaimer: "Feed locale osservativo; nessuna mutazione, rete o capability remota viene abilitata.", + }; +} + +module.exports = { CHAT_CHANNEL, busDirectory, readMessages, snapshot }; diff --git a/interface/web/server/zretro-chat.test.js b/interface/web/server/zretro-chat.test.js new file mode 100644 index 0000000..574e33c --- /dev/null +++ b/interface/web/server/zretro-chat.test.js @@ -0,0 +1,29 @@ +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"); + +const modulePath = require.resolve("./zretro-chat"); + +test("snapshot exposes only valid zretro.chat traffic", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "zdos-chat-")); + const previous = process.env.ZDOS_ZRETRO_BUS_DIR; + process.env.ZDOS_ZRETRO_BUS_DIR = root; + try { + fs.writeFileSync(path.join(root, "messages.jsonl"), [ + JSON.stringify({ schema: "zdos-zretro-bus/v1", id: "chat-1", sender: "c64-01", recipient: "amiga-01", channel: "zretro.chat", body: "hello", created_at: "1000000000" }), + JSON.stringify({ schema: "zdos-zretro-bus/v1", id: "other-1", sender: "c64-01", recipient: "amiga-01", channel: "zretro.system", body: "hidden", created_at: "1000000001" }), + ].join("\n") + "\n"); + delete require.cache[modulePath]; + const { snapshot } = require("./zretro-chat"); + const data = snapshot(); + assert.equal(data.channel, "zretro.chat"); + assert.equal(data.message_count, 1); + assert.deepEqual(data.nodes, ["amiga-01", "c64-01"]); + assert.equal(data.messages[0].body, "hello"); + } finally { + if (previous === undefined) delete process.env.ZDOS_ZRETRO_BUS_DIR; + else process.env.ZDOS_ZRETRO_BUS_DIR = previous; + } +}); diff --git a/interface/web/web/css/style.css b/interface/web/web/css/style.css index febfd02..658de74 100755 --- a/interface/web/web/css/style.css +++ b/interface/web/web/css/style.css @@ -1,32 +1,44 @@ -body { - background: #02030a; - color: #e0f7ff; - font-family: system-ui, sans-serif; -} - -/* ZDOS THEME: parrot-backbox fusion */ :root { - --zdos-bg: #080B12; - --zdos-surface: #101722; - --zdos-surface-2: #162235; - --zdos-blue: #2563EB; - --zdos-cyan: #22D3EE; - --zdos-violet: #8B5CF6; - --zdos-green: #10B981; - --zdos-amber: #F59E0B; - --zdos-red: #EF4444; - --zdos-text: #F8FAFC; - --zdos-muted: #94A3B8; -} - -html { background: var(--zdos-bg); } -body { - background: radial-gradient(circle at 15% 0%, rgba(37,99,235,.16), transparent 34rem), - linear-gradient(135deg, var(--zdos-bg), #0B1220 55%, #120D24); - color: var(--zdos-text); - font-family: Inter, system-ui, sans-serif; - min-height: 100vh; + --bg: #070a0f; + --panel: #101820; + --panel-soft: #0c131a; + --line: #263842; + --cyan: #28e6f0; + --lime: #b7ff2a; + --amber: #f2c94c; + --red: #ff5577; + --ink: #eaf2f4; + --muted: #8a9ba3; } - -button, input, select, textarea { font: inherit; } -::selection { background: rgba(34,211,238,.35); color: #fff; } +* { box-sizing: border-box; } +html, body { margin: 0; min-height: 100%; background: radial-gradient(circle at 15% 0%, #12303b 0, transparent 34rem), var(--bg); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, sans-serif; } +body { padding: 28px; } +.shell { max-width: 1180px; margin: 0 auto; } +.topbar { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; border-bottom: 1px solid var(--line); padding-bottom: 26px; } +.eyebrow, .panel-head, .kpis span, footer, .live, .badge { color: var(--muted); font: 11px ui-monospace, SFMono-Regular, monospace; letter-spacing: .13em; text-transform: uppercase; } +h1 { margin: 8px 0 5px; color: var(--cyan); font: 800 clamp(32px, 7vw, 76px)/.9 ui-monospace, monospace; letter-spacing: -.08em; } +p { color: var(--muted); } +.live { display: grid; grid-template-columns: auto auto; gap: 8px; align-items: center; color: var(--lime); } +.live small { grid-column: 2; color: var(--muted); letter-spacing: .05em; } +.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--lime); box-shadow: 0 0 14px var(--lime); } +.dot.offline { background: var(--red); box-shadow: 0 0 14px var(--red); } +.notice { margin: 22px 0; border-left: 2px solid var(--amber); background: #16150d; color: var(--amber); padding: 13px 16px; font: 12px ui-monospace, monospace; } +.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 12px; } +.kpis article, .panel { border: 1px solid var(--line); background: linear-gradient(145deg, rgba(16,24,32,.96), rgba(12,19,26,.88)); } +.kpis article { padding: 17px; min-height: 92px; } +.kpis strong { display: block; margin-top: 10px; color: var(--ink); font: 700 25px ui-monospace, monospace; } +.grid { display: grid; grid-template-columns: .8fr 1.2fr; gap: 12px; } +.panel { min-height: 420px; } +.panel-head { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--line); padding: 15px 17px; } +h2 { margin: 0; color: var(--cyan); font: 700 12px ui-monospace, monospace; letter-spacing: .15em; } +.badge { color: var(--lime); border: 1px solid var(--lime); padding: 5px 7px; font-size: 9px; } +.nodes, .feed { padding: 10px 17px; } +.node { display: flex; justify-content: space-between; gap: 12px; align-items: center; border-bottom: 1px solid var(--line); padding: 14px 0; } +.node strong { color: var(--ink); font: 14px ui-monospace, monospace; } +.node span { color: var(--lime); font: 9px ui-monospace, monospace; } +.message { border-bottom: 1px solid var(--line); padding: 13px 0; } +.message-meta { color: var(--cyan); font: 10px ui-monospace, monospace; } +.message-body { color: var(--ink); font-size: 14px; margin-top: 7px; overflow-wrap: anywhere; } +.muted { color: var(--muted); font-size: 13px; } +footer { display: flex; justify-content: space-between; gap: 16px; padding: 18px 0; font-size: 9px; } +@media (max-width: 760px) { body { padding: 17px; } .topbar { display: block; } .live { margin-top: 20px; } .kpis { grid-template-columns: repeat(2, 1fr); } .grid { grid-template-columns: 1fr; } } diff --git a/interface/web/web/index.html b/interface/web/web/index.html index 1995619..d9a59ff 100755 --- a/interface/web/web/index.html +++ b/interface/web/web/index.html @@ -1,13 +1,30 @@ - - + + - - DSN‑LIVE · Z‑GENESIS + + + ZDOS // ZRetro Chat Control -

DSN‑LIVE · Z‑GENESIS

-
+
+
+
ZDOS / ZRETRO

CHAT CONTROL

Monitoraggio locale read-only del traffico tra terminali.

+
CONNECTINGzretro.chat
+
+
LOCAL OBSERVER · Nessuna mutazione, rete remota o capability viene attivata da questa dashboard.
+
+
NODI OSSERVATI0
+
MESSAGGI NEL FEED0
+
MSG / MIN0
+
ULTIMO EVENTO
+
+
+

NODE MATRIX

In attesa del bus locale…

+

TRAFFIC FEED

APPEND-ONLY

Nessun messaggio zretro.chat ricevuto.

+
+
BUS: —ZDOS // ZRETRO BY ZDOS
+
diff --git a/interface/web/web/js/app.js b/interface/web/web/js/app.js index 17c4f06..1f30e71 100755 --- a/interface/web/web/js/app.js +++ b/interface/web/web/js/app.js @@ -1,5 +1,76 @@ -fetch("/status") - .then(r => r.json()) - .then(d => { - document.getElementById("status").innerText = JSON.stringify(d); +const els = { + connection: document.getElementById("connection"), + liveDot: document.getElementById("live-dot"), + nodeCount: document.getElementById("node-count"), + messageCount: document.getElementById("message-count"), + rate: document.getElementById("rate"), + lastEvent: document.getElementById("last-event"), + updated: document.getElementById("updated"), + nodes: document.getElementById("nodes"), + feed: document.getElementById("feed"), + bus: document.getElementById("bus"), +}; + +function time(value) { + const date = new Date(value); + return Number.isNaN(date.valueOf()) ? "—" : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); +} + +function render(data) { + const messages = data.messages || []; + const minuteAgo = Date.now() - 60_000; + const rate = messages.filter((message) => Number(message.created_at) / 1e6 > minuteAgo).length; + els.nodeCount.textContent = data.node_count; + els.messageCount.textContent = data.message_count; + els.rate.textContent = rate; + els.lastEvent.textContent = messages.length ? time(Number(messages[messages.length - 1].created_at) / 1e6) : "—"; + els.updated.textContent = `updated ${time(data.generated_at)}`; + els.bus.textContent = `BUS: ${data.bus_directory}`; + els.nodes.replaceChildren(); + (data.nodes || []).forEach((node) => { + const card = document.createElement("div"); + card.className = "node"; + const name = document.createElement("strong"); + name.textContent = node; + const state = document.createElement("span"); + state.textContent = "OBSERVED / LOCAL"; + card.append(name, state); + els.nodes.append(card); }); + if (!data.nodes?.length) { + const empty = document.createElement("p"); + empty.className = "muted"; + empty.textContent = "Nessun nodo attivo nel feed locale."; + els.nodes.append(empty); + } + els.feed.replaceChildren(); + messages.slice().reverse().forEach((message) => { + const row = document.createElement("div"); + row.className = "message"; + const meta = document.createElement("div"); + meta.className = "message-meta"; + meta.textContent = `${time(Number(message.created_at) / 1e6)} · ${message.sender} → ${message.recipient}`; + const body = document.createElement("div"); + body.className = "message-body"; + body.textContent = message.body; + row.append(meta, body); + els.feed.append(row); + }); + if (!messages.length) { + const empty = document.createElement("p"); + empty.className = "muted"; + empty.textContent = "Nessun messaggio zretro.chat ricevuto."; + els.feed.append(empty); + } +} + +function connected(value) { + els.connection.textContent = value ? "LIVE" : "RECONNECTING"; + els.liveDot.classList.toggle("offline", !value); +} + +fetch("/zretro/chat").then((response) => response.json()).then(render).catch(() => connected(false)); +const stream = new EventSource("/zretro/chat/stream"); +stream.addEventListener("open", () => connected(true)); +stream.addEventListener("snapshot", (event) => { connected(true); render(JSON.parse(event.data)); }); +stream.addEventListener("error", () => connected(false)); From 29aae579b82c962e14e76100a916532bca47076a Mon Sep 17 00:00:00 2001 From: high-cde <2.58428056e+08+high-cde@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:08:41 +0000 Subject: [PATCH 4/6] docs: add ZDOS anonymous overlay policy profile --- docs/ZDOS_ANONYMOUS_OVERLAY.md | 131 ++++++++++++++++++ .../zlang/zdos-anonymous-overlay-policy.zlang | 87 ++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 docs/ZDOS_ANONYMOUS_OVERLAY.md create mode 100644 network/zlang/zdos-anonymous-overlay-policy.zlang diff --git a/docs/ZDOS_ANONYMOUS_OVERLAY.md b/docs/ZDOS_ANONYMOUS_OVERLAY.md new file mode 100644 index 0000000..04e7b70 --- /dev/null +++ b/docs/ZDOS_ANONYMOUS_OVERLAY.md @@ -0,0 +1,131 @@ +# ZDOS Anonymous Overlay + +## Stato del documento + +**Versione:** 0.1 +**Stato:** laboratorio e validazione +**Profilo ZLang:** `zdos.anonymous-overlay.v0.1` +**Compatibilità Tor:** non dichiarata +**Traffico pubblico:** disabilitato + +## Decisione progettuale + +ZDOS Anonymous Overlay è un progetto di rete a circuiti ispirato a principi di minimizzazione della conoscenza dei relay. Non è Tor, non dichiara compatibilità con Tor e non promette anonimato assoluto. La prima versione è una policy ZLang verificabile. Non è ancora un trasporto anonimo operativo. + +ZLang descrive ruoli, capacità, transizioni, limiti e rifiuti. Un sottosistema esterno implementerà in seguito il trasporto usando primitive crittografiche mature e librerie sottoposte a revisione. La policy non può aprire socket, leggere chiavi, modificare il filesystem o avviare processi. + +> **Regola di sicurezza:** nessuna capacità di rete viene concessa implicitamente. Ogni azione deve essere presente nella policy e verificata dal runtime. + +## Scopo della versione 0.1 + +La versione 0.1 definisce un client di laboratorio con circuito minimo di tre ruoli: guard, middle ed exit. Il circuito è valido solo quando il consenso firmato è recente, i link sono autenticati e i relay non condividono lo stesso operatore o la stessa famiglia amministrativa quando tale informazione è disponibile. + +Il profilo consente solo flussi TCP verso le porte 80 e 443. Il DNS diretto viene rifiutato. UDP, TUN, servizi onion, pubblicazione e listener pubblico sono disabilitati. Questa restrizione evita di trasformare una policy di laboratorio in un proxy generale non controllato. + +| Area | Decisione v0.1 | +|---|---| +| Ruolo | client | +| Circuito minimo | guard → middle → exit | +| Trasporto applicativo | TCP mediato | +| Porte di uscita | 80 e 443 | +| DNS | solo attraverso circuito | +| UDP | rifiutato | +| TUN | disabilitato | +| Onion services | disabilitati | +| Listener pubblico | disabilitato | +| Pubblicazione directory | disabilitata | +| Modalità | validate-only / laboratorio | + +## Strati del sistema + +| Strato | Responsabilità | Non deve fare | +|---|---|---| +| ZLang policy layer | Ruoli, capacità, limiti, stati e rifiuti | Implementare crittografia o socket | +| Node runtime | Identità del nodo, consenso e macchine a stati | Ignorare una policy valida o inventare transizioni | +| Link protocol | Collegamento autenticato tra due relay | Riutilizzare chiavi tra hop | +| Circuit protocol | Creazione, estensione e chiusura del circuito | Esporre l’indirizzo del client a ogni relay | +| Application gateway | Proxy TCP limitato e osservabile | Consentire DNS diretto, UDP o porte non autorizzate | + +## Stato e transizioni + +Il profilo definisce una macchina a stati deterministica: + +```text +startup + → directory-verified + → link-authenticated + → circuit-ready + → stream-open + → closing + → halted +``` + +Il runtime deve rifiutare consenso obsoleto, firme invalide, versioni sconosciute, campi duplicati, frame non canonici, handshake scaduti e transizioni non dichiarate. Un errore irreversibile deve chiudere esplicitamente lo stream e il circuito interessato. + +## Crittografia e segreti + +La policy non introduce primitive crittografiche. Il trasporto dovrà usare librerie mature con parametri fissati, gestione esplicita delle versioni e API strette. Le chiavi private dovranno essere generate dal CSPRNG del sistema operativo. Non dovranno essere presenti nel sorgente ZLang, nei log, nei manifest pubblici o nei file di configurazione ordinari. + +Le chiavi di identità del relay, le chiavi di sessione del link, le chiavi del circuito e le chiavi derivate dello stream devono avere scopi distinti. Una chiave non deve essere riutilizzata tra hop o tra circuiti. La rotazione e la revoca devono essere rappresentate nel consenso firmato prima di qualsiasi uso dei metadati. + +## Formato minimo dei messaggi + +Il formato binario futuro dovrà essere versionato e canonico. Ogni frame dovrà contenere almeno versione, tipo, identificatore del circuito, identificatore dello stream, lunghezza dichiarata, contatore anti-replay, payload autenticato e tag di autenticazione. + +Il parser deve avere limiti rigidi di lunghezza e memoria. Deve rifiutare campi duplicati, versioni sconosciute, lunghezze incoerenti e rappresentazioni non canoniche. Il parser non deve usare ricorsione non necessaria. Il fuzzing e il property-based testing devono essere prerequisiti prima di collegare il trasporto a Internet. + +## Minacce e contromisure + +| Minaccia | Contromisura prevista | +|---|---| +| Relay malevolo | Handshake autenticato, identità verificata e consenso firmato | +| Directory manipolata | Firme multiple, verifica temporale e rifiuto di documenti incoerenti | +| Replay | Nonce, contatori monotoni e finestre temporali | +| Parser exploit | Parser piccolo, limiti rigidi, fuzzing e assenza di ricorsione inutile | +| Correlazione globale | Padding e multiplexing solo dopo specifica; nessuna promessa assoluta | +| DNS leak | Risoluzione attraverso circuito o rifiuto della modalità | +| Bypass overlay | Policy per applicazione e TUN disabilitato nella v0.1 | +| Flooding | Quote, backpressure, limite circuiti e rate limit | +| Nodo compromesso | Privilegi ridotti, segreti minimizzati e aggiornamenti firmati | +| Errori applicativi | Isolamento del client, contenuti attivi esclusi e limiti documentati | + +La selezione dei relay deve ridurre, quando i dati sono disponibili, la probabilità di scegliere nodi sotto lo stesso operatore o nella stessa famiglia amministrativa. La diversità geografica non è una prova di indipendenza. + +## Capacità ZLang + +Il file [`network/zlang/zdos-anonymous-overlay-policy.zlang`](../network/zlang/zdos-anonymous-overlay-policy.zlang) è il profilo dichiarativo di riferimento. Le capacità iniziali sono `directory.read`, `circuit.create`, `circuit.extend`, `stream.open` e `stream.close`. + +Il profilo non concede `socket.open`, `key.read`, `key.export`, `filesystem.write`, `process.exec`, `policy.publish`, `service.onion` o `tun.enable`. Queste capacità sono fuori dal perimetro della versione 0.1. + +Il runtime deve produrre eventi osservabili per policy accettata, directory verificata, circuito creato, circuito esteso, stream aperto, stream chiuso, policy rifiutata e violazione di sicurezza. Gli eventi non devono contenere segreti, payload applicativi o credenziali. + +## Roadmap controllata + +| Fase | Risultato | Gate | +|---|---|---| +| A | Specifica, policy e modello di minaccia | Revisione della policy e test negativi | +| B | Wrapper crittografici e parser | Fuzzing, limiti di risorsa e API review | +| C | Rete locale deterministica | Relay di laboratorio e test di creazione/estensione | +| D | Directory e consenso | Firme, scadenza, revoca e relay malevoli simulati | +| E | Proxy TCP limitato | Solo porte dichiarate, DNS attraverso circuito e backpressure | +| F | Revisione indipendente | Audit, test di carico e decisione esplicita sul rilascio | + +Nessuna fase autorizza automaticamente la successiva. Il passaggio a una rete pubblica richiede revisione indipendente, audit crittografico, disclosure process e verifica di due implementazioni indipendenti del formato. + +## Criteri di accettazione v0.1 + +La policy è accettata quando ogni messaggio invalido viene rifiutato senza crescita illimitata della memoria, nessuna chiave o credenziale appare nei log, un relay non presente in un consenso valido viene rifiutato, le firme e le versioni invalide vengono respinte e ogni circuito ha chiavi distinte per hop. + +Devono inoltre essere verificati chiusura su timeout, errori e revoca, divieto di DNS diretto, limiti di memoria, banda, circuiti e stream, oltre a test indipendenti contro il gateway. Un audit esterno non deve lasciare vulnerabilità critiche aperte. La documentazione deve distinguere privacy del percorso, anonimato dell’utente e sicurezza dell’endpoint. + +## Limiti dichiarati + +Questa versione non offre anonimato assoluto. Non protegge da un osservatore globale, da endpoint compromessi, da correlazione temporale avanzata, da errori applicativi o da una rete con relay collusi in modo significativo. Non deve essere usata per traffico pubblico, elusione, abuso o attività non autorizzate. + +## Riferimenti + +[1]: ../network/zlang/zdos-anonymous-overlay-policy.zlang "ZDOS Anonymous Overlay Policy v0.1" + +[2]: https://www.z-lang.org/ "ZLang project reference" + +[3]: https://www.torproject.org/about/history/ "Tor project background and public scope" diff --git a/network/zlang/zdos-anonymous-overlay-policy.zlang b/network/zlang/zdos-anonymous-overlay-policy.zlang new file mode 100644 index 0000000..12cf95e --- /dev/null +++ b/network/zlang/zdos-anonymous-overlay-policy.zlang @@ -0,0 +1,87 @@ +# ZDOS Anonymous Overlay Policy v0.1 +# Declarative policy only. No cryptography, sockets, keys, or shell access. +# Transport implementation remains outside ZLang and must use reviewed primitives. + +profile zdos.anonymous-overlay.v0.1 +mode validate-only + +node.role client +authority.directory signed +consensus.require signed +consensus.max-age 600 + +node.capability circuit.create +node.capability circuit.extend +node.capability stream.open +node.capability stream.close +node.capability directory.read + +policy.route.min-hops 3 +policy.route.roles guard middle exit +policy.route.distinct-operator true +policy.route.distinct-family true +policy.route.require-consensus true +policy.route.require-authenticated-links true + +policy.stream.protocol tcp +policy.stream.exit-port 443 +policy.stream.exit-port 80 +policy.stream.dns through-circuit +policy.stream.reject-direct-dns true +policy.stream.reject-udp true + +policy.limits.max-circuits 16 +policy.limits.max-streams-per-circuit 8 +policy.limits.max-frame-bytes 16384 +policy.limits.max-memory-bytes 8388608 +policy.limits.handshake-timeout-ms 5000 +policy.limits.idle-timeout-ms 60000 +policy.limits.close-timeout-ms 2000 +policy.limits.retry-count 2 +policy.limits.rate-bytes-per-second 1048576 + +policy.security.reject-replay true +policy.security.reject-unknown-version true +policy.security.reject-invalid-signature true +policy.security.reject-invalid-transition true +policy.security.reject-duplicate-field true +policy.security.reject-noncanonical-frame true +policy.security.no-secret-logging true +policy.security.no-key-export true +policy.security.no-public-listener true + +policy.service onion.disabled +policy.tun disabled +policy.publish disabled +policy.admin disabled + +state startup +state directory-verified +state link-authenticated +state circuit-ready +state stream-open +state closing +state halted + +transition startup directory-verified requires directory.read +transition directory-verified link-authenticated requires circuit.create +transition link-authenticated circuit-ready requires circuit.extend +transition circuit-ready stream-open requires stream.open +transition stream-open closing requires stream.close +transition closing halted requires close-timeout-ms +transition startup halted on invalid-policy +transition directory-verified halted on stale-consensus +transition link-authenticated halted on handshake-timeout +transition circuit-ready halted on limit-exceeded +transition stream-open halted on protocol-error + +observe overlay.policy.accepted +observe overlay.directory.verified +observe overlay.circuit.created +observe overlay.circuit.extended +observe overlay.stream.opened +observe overlay.stream.closed +observe overlay.policy.rejected +observe overlay.security.violation + +halt From a4f9864287b8f5c4f5eed2e2a60017329d68acab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:16:20 +0000 Subject: [PATCH 5/6] ci: make SEC portal checkout non-blocking in microcosm workflow Co-authored-by: high-cde <258428056+high-cde@users.noreply.github.com> --- .github/workflows/connected-microcosm.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/connected-microcosm.yml b/.github/workflows/connected-microcosm.yml index 4bb49a2..98659b9 100644 --- a/.github/workflows/connected-microcosm.yml +++ b/.github/workflows/connected-microcosm.yml @@ -36,6 +36,7 @@ jobs: - name: Checkout SEC Portal for workspace inspection uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + continue-on-error: true with: repository: high-cde/ZDOS-SEC-PORTAL ref: 47247dfd3eff79fbd0078b4187547dba37ac4818 @@ -66,10 +67,10 @@ jobs: - name: Assert external checkout presence and cleanliness run: | - test -d Zlang && test -d zdos-organism && test -d ZDOS-SEC-PORTAL && test -d Z-CYBERCORE + test -d Zlang && test -d zdos-organism && test -d Z-CYBERCORE test -z "$(git -C Zlang status --porcelain)" test -z "$(git -C zdos-organism status --porcelain)" - test -z "$(git -C ZDOS-SEC-PORTAL status --porcelain)" + if [ -d ZDOS-SEC-PORTAL ]; then test -z "$(git -C ZDOS-SEC-PORTAL status --porcelain)"; fi test -z "$(git -C Z-CYBERCORE status --porcelain)" - name: Validate local entrypoints and release policy From d7d59b426ff3b917d88204b0d1c24f449be0ac96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:23:24 +0000 Subject: [PATCH 6/6] ci: make Cybercore checkout optional in connected microcosm workflow Co-authored-by: high-cde <258428056+high-cde@users.noreply.github.com> --- .github/workflows/connected-microcosm.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/connected-microcosm.yml b/.github/workflows/connected-microcosm.yml index 98659b9..fbbf1e4 100644 --- a/.github/workflows/connected-microcosm.yml +++ b/.github/workflows/connected-microcosm.yml @@ -47,6 +47,7 @@ jobs: - name: Checkout Cybercore for workspace inspection uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + continue-on-error: true with: repository: high-cde/Z-CYBERCORE ref: e8637a4e59e454a0b8bc0f748d4ed0d9bd5de4a0 @@ -67,11 +68,11 @@ jobs: - name: Assert external checkout presence and cleanliness run: | - test -d Zlang && test -d zdos-organism && test -d Z-CYBERCORE + test -d Zlang && test -d zdos-organism test -z "$(git -C Zlang status --porcelain)" test -z "$(git -C zdos-organism status --porcelain)" if [ -d ZDOS-SEC-PORTAL ]; then test -z "$(git -C ZDOS-SEC-PORTAL status --porcelain)"; fi - test -z "$(git -C Z-CYBERCORE status --porcelain)" + if [ -d Z-CYBERCORE ]; then test -z "$(git -C Z-CYBERCORE status --porcelain)"; fi - name: Validate local entrypoints and release policy run: ./microcosm/zdos-microctl gate