diff --git a/.backlog/LOT-020/PRD.md b/.backlog/LOT-020/PRD.md index 4f4d719..64d74c5 100644 --- a/.backlog/LOT-020/PRD.md +++ b/.backlog/LOT-020/PRD.md @@ -49,7 +49,10 @@ leftovers, not deliverables. Consider `.gitignore`ing `build_pyi/`. | # | Ticket | Status | |---|--------|--------| | 01 | Commit the `__main__` entry-point fix + guard it against regression (smoke-test the built exe in CI) | ✅ | -| 02 | Publish the first release so `dcs-bridge-.zip` (with `dcs-serve.exe`) becomes downloadable | ⬜ | +| 02 | Publish the first release so `dcs-bridge-.zip` (with `dcs-serve.exe`) becomes downloadable | 🧑 | +| 03 | Document the mandatory `MissionScripting.lua` sanitisation lift (found while writing the release notes — undocumented, and nothing connects without it) | ✅ | + +Target version: **1.0.0** (decided by David — first public release, so not a PATCH bump). ## Out of scope diff --git a/.backlog/LOT-020/tickets/02-first-release.md b/.backlog/LOT-020/tickets/02-first-release.md index a91a6d8..e327401 100644 --- a/.backlog/LOT-020/tickets/02-first-release.md +++ b/.backlog/LOT-020/tickets/02-first-release.md @@ -1,6 +1,6 @@ # 02 — Publish the first release (so `dcs-serve.exe` is downloadable) -Status: ⬜ ready +Status: 🧑 waiting-human Type: chore ## Context @@ -35,9 +35,20 @@ project started (no release was ever cut), so the notes must be curated, not cop ## Tasks -- [ ] Author `RELEASE_NOTES.md` (it does not exist — see the finding above). -- [ ] Confirm the version source of truth (`pyproject.toml`) matches the tag to push. -- [ ] Push a `published-v` tag and watch the workflow. +- [x] Author `RELEASE_NOTES.md` (it did not exist — see the finding above). Written for a + server operator / mission maker, not a project developer: what the download + contains, how to start, what the bridge does, and how to migrate a pre-release + source checkout (Bearer replacing `X-API-Key`, WS tickets, the reworked MCP tool + set). Validated by David. +- [x] Document the sanitisation prerequisite — see ticket 03, opened because writing the + "Requirements" section exposed that it was missing. +- [x] Confirm the version source of truth: `pyproject.toml` → `1.0.0`, CHANGELOG + `[Unreleased]` → `[1.0.0] — 2026-07-26`. Tag to push: `published-v1.0.0`. +- [ ] **David pushes the tag** (irreversible, triggers the publish workflow): + `git tag published-v1.0.0 && git push origin published-v1.0.0`, once this release + PR is merged into `develop`. +- [ ] Watch the workflow — note it now runs the ticket-01 smoke tests on the built exes + before publishing, so a broken build fails instead of shipping. - [ ] Check the release carries `dcs-bridge-.zip` and that the zip contains `dcs-serve.exe`, `dcs-client.exe` and `dcs-bridge.lua` (VMCT's kit job looks up `dcs-serve.exe` **and** `dcs-bridge.lua` by basename). diff --git a/.backlog/LOT-020/tickets/03-document-sanitisation-prerequisite.md b/.backlog/LOT-020/tickets/03-document-sanitisation-prerequisite.md new file mode 100644 index 0000000..a19e883 --- /dev/null +++ b/.backlog/LOT-020/tickets/03-document-sanitisation-prerequisite.md @@ -0,0 +1,55 @@ +# 03 — Document the mandatory `MissionScripting.lua` sanitisation lift + +Status: ✅ done +Type: docs + +## Why this surfaced + +Found while writing `RELEASE_NOTES.md` for ticket 02, checking that every stated +prerequisite was real. `docs/guide/prerequisites.{en,fr}.md` documented **only** how to +inject `dcs-bridge.lua` (three methods, VMCT recommended) and never mentioned the DCS +script sanitisation. + +But `src/lua/dcs-bridge.lua:45` does: + +```lua +local socket = require("socket") +``` + +and stock DCS strips `require` (along with `os`, `io`, `lfs`) in `MissionScripting.lua` +before any mission script runs. So a helper following the documentation on an untouched +DCS installation gets nothing — the bridge dies on its first line and never connects. + +This mattered for ticket 02 specifically: the whole point of publishing the release is +that helpers can run the map-capture kit. Shipping it with the one mandatory DCS-side +step undocumented would have made it the first failure everybody hit. + +## What was unclear, and how it was resolved + +The exact procedure was not written from memory. `DCS-SimpleTextToSpeech.lua` — a +community script VEAF already ships inside `veaf-mission-creation-tools` — documents the +ecosystem-standard instruction, which the new section follows: with DCS closed, remove +everything below the line starting `local function sanitizeModule(name)`, and **reapply +after every DCS update**. + +## Change + +New `## Lift the script sanitisation (mandatory)` section in +`docs/guide/prerequisites.en.md` and `prerequisites.fr.md`, placed **before** the +injection methods (it applies whichever one is chosen) with the existing methods regrouped +under an `## Injecting the script` heading. It covers: + +- why it is needed (`require("socket")`) and the symptom when it is missing; +- that VMCT automates *injection*, not the sandbox — it does not remove this step; +- the procedure, with DCS closed; +- a `warning` admonition on what lifting the sandbox actually allows; +- a `note` admonition that a DCS update silently reverts it (look for the `require` + error in `DCS.log`); +- that other common scripts need the same change, so it may already be done. + +Matches the admonition style already used in `docs/guide/configuration.{en,fr}.md`. + +## Definition of Done + +- A reader following the prerequisites from a stock DCS install reaches a working bridge. +- EN and FR in sync. diff --git a/.backlog/README.md b/.backlog/README.md index 8bb756e..eca5bb0 100644 --- a/.backlog/README.md +++ b/.backlog/README.md @@ -25,7 +25,7 @@ for the conventions that skills read at runtime. | [LOT-017](LOT-017/PRD.md) — MCP tools report misleading errors for non-200 responses (a `401` auth failure is shown as "DCS not ready") | ⬜ ready | | [LOT-018](LOT-018/PRD.md) — Implement the capability-aware bridge (ADR-0005): semantic-action façade over DCS/MIST/CTLD/VMCT, capability detection, role-based security | ✅ done | | [LOT-019](LOT-019/PRD.md) — Doc-only PRs blocked by the required `python-quality` check (paths filter never fires it) | ✅ done | -| [LOT-020](LOT-020/PRD.md) — Packaged `dcs-serve.exe` exits instantly doing nothing (`dcs-serve.spec` freezes `serve/app.py` as the script, but that module has no `__main__` guard, so `main()` is never called — the Poetry console-script masked it); fix applied locally, needs commit + a CI smoke test that the built exe actually listens. Then publish the **first** release, so `dcs-bridge-.zip` (with `dcs-serve.exe`) becomes downloadable — VMCT's new map-capture-kit job depends on it | 🔄 in-progress | +| [LOT-020](LOT-020/PRD.md) — Packaged `dcs-serve.exe` exits instantly doing nothing (`dcs-serve.spec` freezes `serve/app.py` as the script, but that module has no `__main__` guard, so `main()` is never called — the Poetry console-script masked it); fix applied locally, needs commit + a CI smoke test that the built exe actually listens. Then publish the **first** release (**v1.0.0**), so `dcs-bridge-.zip` (with `dcs-serve.exe`) becomes downloadable — VMCT's new map-capture-kit job depends on it. Also documents the mandatory `MissionScripting.lua` sanitisation lift, which was missing from the prerequisites | 🧑 waiting-human | ## Archived lots diff --git a/.gitignore b/.gitignore index ddc4ec6..15df819 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist/ build/ build_pyi/ test-mission/ +site/ *.egg-info/ .mypy_cache/ .ruff_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0f6e1..117026c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +## [1.0.0] — 2026-07-26 + +First public release. + ### Added +- docs(guide): document the mandatory `MissionScripting.lua` sanitisation lift in the prerequisites (EN + FR) — `dcs-bridge.lua` obtains its socket with `require("socket")`, which stock DCS strips before any mission script runs, so an untouched installation never connects whichever injection method is used (VMCT automates injection, not the sandbox). Covers the procedure (DCS closed, remove everything below `local function sanitizeModule(name)`), what it allows, and the fact that a DCS update silently reverts it (LOT-020) - test(packaging): `test/test_packaging_entrypoints.py` parses both `.spec` files, resolves the module each freezes as its script, and asserts it carries an `if __name__ == "__main__": main()` block — a cheap regression guard for the defect above, runnable without PyInstaller (LOT-020) - ci(release): smoke-test the built executables before publishing — `dcs-serve.exe` must accept a TCP connection on `127.0.0.1:7777` and `8080` within 30 s (server logs dumped on failure), and `dcs-client.exe --help` must list its subcommands. A packaging regression now fails the release instead of shipping an exe that does nothing. The step refuses to run if either port is already bound (a stale listener would otherwise yield a meaningless pass) and tears the server down with `taskkill /T`, since a one-file PyInstaller exe re-launches itself as a child process that owns the sockets and survives a kill of the returned PID (LOT-020) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..5e1243d --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,111 @@ +# dcs-bridge v1.0.0 + +First public release. **dcs-bridge** connects a running DCS World mission to the +outside world: a small Lua script inside DCS talks to a local server, which exposes the +mission over a REST + WebSocket API — and to a terminal UI, a live map in your browser, +and AI agents. + +📖 [Full documentation](https://veaf.github.io/VEAF-dcs-bridge/) + +## What's in the download + +`dcs-bridge-1.0.0.zip` contains three files: + +| File | What it is | +|---|---| +| `dcs-serve.exe` | The bridge server. Talks to DCS, serves the API. No Python needed. | +| `dcs-client.exe` | The clients — terminal UI, browser map, or AI-agent server. | +| `dcs-bridge.lua` | The script to load inside your DCS mission. | + +## Getting started + +1. Load `dcs-bridge.lua` in your mission (a `DO SCRIPT FILE` trigger at mission start). +2. Run `dcs-serve.exe`. On first start it writes `dcs-serve.yaml` next to itself with a + freshly generated access token. +3. Run `dcs-client.exe web` for the live map, `dcs-client.exe tui` for the terminal, or + `dcs-client.exe mcp` to expose the mission to an AI agent. + +The server listens on `127.0.0.1:7777` for DCS and `0.0.0.0:8080` for the API. + +## Highlights + +### Ask for what you want, not how to do it + +Rather than making callers write Lua, the bridge exposes an **action catalogue**: named +verbs (`spawn`, `smoke`, `remove`, `run_keyphrase`) with declared parameters. You call +`POST /api/action` with a verb, and the bridge picks how to carry it out. + +### It adapts to the mission it finds + +At connection time the Lua script announces which frameworks the mission has loaded — +MIST, CTLD, VEAF/VMCT — and the bridge routes each action to the best available one +(VMCT → CTLD → MIST → plain DCS), falling back gracefully when something is absent. +Spawning a FARP uses CTLD's scene manager where CTLD exists and a native DCS static +object where it doesn't, without the caller knowing or caring. + +`GET /api/capabilities` reports what was detected, and `GET /api/catalog` lists only the +actions actually available in *this* mission — so a client never offers something that +cannot work. + +### Access is graded, not all-or-nothing + +Tokens carry a role — `observer`, `pilot`, `operator`, `administrator`, `superuser`, +aligned on the VEAF levels — and every action declares the minimum role it needs. +Watching units is not the same permission as spawning a battalion, and running arbitrary +Lua (`POST /api/exec`) is restricted to `superuser`. Tokens live in `dcs-tokens.yaml`. + +Credentials never appear in a URL: the REST API uses `Authorization: Bearer`, and the +WebSocket is opened with a single-use ticket that expires in about ten seconds. The +browser map holds no token at all — the local web server keeps it and fetches tickets on +the page's behalf. + +### Three ways in + +- **Terminal UI** — live unit table over WebSocket, plus a Lua REPL. +- **Browser map** — Leaflet map with coalition-coloured markers, updating live, and a + panel of the actions this mission supports. Leaflet ships with the download; no + internet access required. +- **AI agents (MCP)** — a small, fixed tool set that stays constant no matter how large + the catalogue grows: agents *discover* actions (`list_catalog`, `search_catalog`, + `describe_action`) and invoke them through one generic `run_action`. + +## Requirements + +- Windows for the packaged executables. +- DCS World, with `dcs-bridge.lua` loaded into the mission (VMCT v6 does this for you). +- **One change to your DCS installation**: the script sanitisation in + `MissionScripting.lua` must be lifted, because the bridge needs `require("socket")` to + open its connection. Without it nothing connects, whichever way you inject the script. + It also has to be redone after every DCS update. The + [Prerequisites](https://veaf.github.io/VEAF-dcs-bridge/guide/prerequisites/) page walks + through it and explains what you are allowing — if you already run SRS's + text-to-speech script, it is likely done. +- Nothing else — Python is bundled inside the executables. + +## Upgrading from a source checkout + +If you have been running dcs-bridge from git before this release, three things changed +in the run-up to 1.0.0 and will break an old client: + +- **`X-API-Key` is gone.** Authenticate with `Authorization: Bearer ` instead. + The `api_key` query parameter is gone too — no credential in a URL, so none in a log. +- **The WebSocket takes a ticket, not a key.** `GET /ws/stream?ticket=…`, where the + ticket comes from `POST /api/ws-ticket`. +- **The MCP tool set changed.** The per-action `spawn_unit` and `get_mission_info` tools + were retired in favour of catalogue discovery plus a single generic `run_action`. + +Your existing `api_key` keeps working as a `superuser` token during the transition, so +the server starts and answers while you migrate. + +The default DCS-side TCP port is **7777**. Older documentation said `9999`; if you +copied a config from it, the connection failed silently. That is reconciled everywhere +now, and the Lua script logs its target (`target: :`) when it retries, so a +mismatch is visible in `DCS.log` instead of manifesting as silence. + +## Known limitations + +- Per-mission custom VEAF aliases are not catalogued — only the VMCT default shortcut + list is discoverable. +- Framework detection requires an **exact** version match against the version this build + targets; a locally patched MIST or VEAF script is reported as absent, with the reason + logged. Looser matching is planned. diff --git a/docs/guide/prerequisites.en.md b/docs/guide/prerequisites.en.md index 430699e..2dfe709 100644 --- a/docs/guide/prerequisites.en.md +++ b/docs/guide/prerequisites.en.md @@ -4,6 +4,40 @@ dcs-bridge requires **DCS World** installed on the mission server (Open Beta or Stable). +## Lift the script sanitisation (mandatory) + +`dcs-bridge.lua` talks to `dcs-serve` over a TCP socket, which it obtains with +`require("socket")`. By default DCS **sanitises** mission scripting: before any mission +script runs, `MissionScripting.lua` strips `require` along with the `os`, `io` and `lfs` +modules. On an untouched DCS installation the bridge therefore fails on its first line +and nothing ever connects. + +This is a one-time change to your DCS installation, and it is needed **whichever +injection method you choose below** — including VMCT, which automates injecting the +script but does not lift the sandbox. + +**With DCS closed**, open `DCS World/Scripts/MissionScripting.lua` and remove the +sanitisation: delete or comment out everything below the line that starts with + +```lua +local function sanitizeModule(name) +``` + +!!! warning "Understand what you are allowing" + Lifting the sanitisation lets **any** mission script running on this machine read and + write files and start programs. Only do this on a server whose missions you control, + and never in order to open a `.miz` from an untrusted source. + +!!! note "Reapply after every DCS update" + A DCS update restores the original `MissionScripting.lua`, and the bridge silently + stops connecting — look for the `require` error in `DCS.log`. Redo this change after + each update. + +Several widely used DCS scripts need the same modification (SRS's +DCS-SimpleTextToSpeech, for one), so it may already be in place on your server. + +## Injecting the script + The Lua script `dcs-bridge.lua` must be injected into each mission. Two methods are available: ### Method 1 — MissionScripting.lua (persistent) diff --git a/docs/guide/prerequisites.fr.md b/docs/guide/prerequisites.fr.md index ca9f55c..69aa346 100644 --- a/docs/guide/prerequisites.fr.md +++ b/docs/guide/prerequisites.fr.md @@ -4,6 +4,41 @@ dcs-bridge nécessite **DCS World** installé sur le serveur de mission (version Open Beta ou Stable). +## Lever la sanitisation des scripts (obligatoire) + +`dcs-bridge.lua` dialogue avec `dcs-serve` via une socket TCP, qu'il obtient avec +`require("socket")`. Or DCS **sanitise** le scripting de mission par défaut : avant +l'exécution du moindre script de mission, `MissionScripting.lua` supprime `require` ainsi +que les modules `os`, `io` et `lfs`. Sur une installation DCS intacte, le bridge échoue +donc dès sa première ligne et rien ne se connecte jamais. + +Cette modification de votre installation DCS n'est à faire qu'une fois, et elle est +nécessaire **quelle que soit la méthode d'injection choisie ci-dessous** — y compris +VMCT, qui automatise l'injection du script mais ne lève pas le bac à sable. + +**DCS étant fermé**, ouvrez `DCS World/Scripts/MissionScripting.lua` et retirez la +sanitisation : supprimez ou commentez tout ce qui se trouve sous la ligne commençant par + +```lua +local function sanitizeModule(name) +``` + +!!! warning "Mesurez ce que vous autorisez" + Lever la sanitisation permet à **n'importe quel** script de mission exécuté sur cette + machine de lire et écrire des fichiers et de lancer des programmes. Ne le faites que + sur un serveur dont vous maîtrisez les missions, et jamais pour ouvrir un `.miz` de + provenance douteuse. + +!!! note "À réappliquer après chaque mise à jour de DCS" + Une mise à jour de DCS restaure le `MissionScripting.lua` d'origine, et le bridge + cesse silencieusement de se connecter — cherchez l'erreur sur `require` dans + `DCS.log`. Refaites cette modification après chaque mise à jour. + +Plusieurs scripts DCS répandus exigent la même modification (DCS-SimpleTextToSpeech de +SRS, par exemple) : elle est peut-être déjà en place sur votre serveur. + +## Injection du script + Le script Lua `dcs-bridge.lua` doit être injecté dans chaque mission. Deux méthodes sont disponibles : ### Méthode 1 — MissionScripting.lua (persistant) diff --git a/pyproject.toml b/pyproject.toml index 85fd226..ee1571e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "dcs-bridge" -version = "0.8.3" +version = "1.0.0" description = "Generic bridge between DCS World and external consumers (TUI, WebUI, AI agents)" authors = [{ name = "VEAF" }] license = "MIT"