Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .backlog/LOT-020/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>.zip` (with `dcs-serve.exe`) becomes downloadable | ⬜ |
| 02 | Publish the first release so `dcs-bridge-<version>.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

Expand Down
19 changes: 15 additions & 4 deletions .backlog/LOT-020/tickets/02-first-release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 02 — Publish the first release (so `dcs-serve.exe` is downloadable)

Status: ⬜ ready
Status: 🧑 waiting-human
Type: chore

## Context
Expand Down Expand Up @@ -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<version>` 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-<version>.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).
Expand Down
55 changes: 55 additions & 0 deletions .backlog/LOT-020/tickets/03-document-sanitisation-prerequisite.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion .backlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>.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-<version>.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

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dist/
build/
build_pyi/
test-mission/
site/
*.egg-info/
.mypy_cache/
.ruff_cache/
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
111 changes: 111 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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 <token>` 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: <host>:<port>`) 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.
34 changes: 34 additions & 0 deletions docs/guide/prerequisites.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions docs/guide/prerequisites.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading