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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
run: uv run python -m ruff check src tests

- name: Test
run: uv run python -m pytest
run: uv run python -m pytest --cov=flightdeck --cov-fail-under=80 --cov-report=term

- name: JSON Schemas drift check
run: |
Expand Down Expand Up @@ -161,7 +161,7 @@ jobs:
run: uv run python -m ruff check src tests

- name: Test
run: uv run python -m pytest
run: uv run python -m pytest --cov=flightdeck --cov-fail-under=80 --cov-report=term

- name: JSON Schemas drift check
run: |
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Fallback (activated **venv** or global tools): the same steps with **`python -m

On **Windows**, use `py -3` in place of `python` if that is how your environment is set up. If pytest temp dirs fail with permissions, see **`DEVELOPMENT.md`** / **`tests/conftest.py`**.

**CI bar** (mirrors **`.github/workflows/ci.yml`** on the **`.python-version`** interpreter): see the workflow for the exact sequence; includes **`uv sync --frozen --extra dev`**, **`web/`** **`npm ci`** + **`npm run build`** + **`git diff --exit-code`** on **`static/`**, Playwright **`npm run test:e2e`**, **ruff**, **pytest**, schema drift check, **`flightdeck-quickstart-verify`**, **`flightdeck --help`**. When you change **`pyproject.toml`** optional extras (including **`flightdeck.integrations`** extras), run **`uv lock`** and commit **`uv.lock`**. The workflow may include a separate **integrations** job that **`uv sync`**s **`dev`** plus selected integration extras and runs targeted tests.
**CI bar** (mirrors **`.github/workflows/ci.yml`** on the **`.python-version`** interpreter): see the workflow for the exact sequence; includes **`uv sync --frozen --extra dev`**, **`web/`** **`npm ci`** + **`npm run build`** + **`git diff --exit-code`** on **`static/`**, Playwright **`npm run test:e2e`**, **ruff**, **`pytest --cov=flightdeck --cov-fail-under=80`**, schema drift check, **`flightdeck-quickstart-verify`**, **`flightdeck --help`**. When you change **`pyproject.toml`** optional extras (including **`flightdeck.integrations`** extras), run **`uv lock`** and commit **`uv.lock`**. The workflow may include a separate **integrations** job that **`uv sync`**s **`dev`** plus selected integration extras and runs targeted tests.

Use a repo-local temp directory if the OS temp directory is restricted.

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0**
### Breaking

- **`POST /v1/events`:** uses the same **`FLIGHTDECK_LOCAL_API_TOKEN`** / loopback policy as promotion and rollback. Remote unauthenticated ingest is no longer accepted; set the env var and send **`Authorization: Bearer`** (Python SDK **`api_token=`**, or **`--api-token`** / env in **[examples/integration/emit_sample_events.node.mjs](examples/integration/emit_sample_events.node.mjs)**).
- **`GET /v1/*`:** when **`FLIGHTDECK_LOCAL_API_TOKEN`** is set, read APIs require **`Authorization: Bearer`** (same header as writes); previously only mutations were Bearer-gated.
- **Python:** **`requires-python`** is **`>=3.11,<4`** (replaces **`>=3.14,<3.15`**). **`[tool.ruff] target-version`** is **`py311`**. CI follows **`.python-version`** (currently **3.12**).

### Changed
Expand All @@ -17,6 +18,9 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0**

### Added

- **`GET /health`:** **`read_auth`** (`open` vs `bearer`) describes whether **`GET /v1/*`** requires **`Authorization: Bearer`** when **`FLIGHTDECK_LOCAL_API_TOKEN`** is set (aligned with writes).
- **SQLite:** bounded retries on **`database is locked` / busy** for ledger **`execute`** paths; **`flightdeck serve --sqlite-lock-timeout`** / **`--retry-sqlite-lock`** (and env **`FLIGHTDECK_SQLITE_*`**) plus **`docs/operations-and-policy.md`** concurrency notes.
- **CI / dev:** **`pytest-cov`** with **`--cov-fail-under=80`** on **`src/flightdeck`** (**`integrations/*`**, **`quickstart_smoke`**, and **`sdk/client.py`** omitted from the denominator — see **`[tool.coverage.run]`** in **`pyproject.toml`**).
- **Experimental `flightdeck.integrations`:** optional extras **`integrations-langchain`**, **`integrations-temporal`**, **`integrations-openai-agents`**, and meta **`integrations-ci`** (CI job); thin mappers from OpenAI chat completions, Anthropic messages, OpenAI Agents–style results, LangChain callbacks, CrewAI-style manual totals, and Temporal-oriented **`labels`**. Docs: **`docs/sdk-integrations.md`**; examples: **`examples/integration/adoption/`**. Contributor policy updates in **`AGENTS.md`** / **`CLAUDE.md`**.

### Changed
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ uv run flightdeck-quickstart-verify
uv run flightdeck --help
```

CI runs **`pytest --cov=flightdeck --cov-fail-under=80`** (see **`AGENTS.md`** / **`pyproject.toml`**).

If you changed **Pydantic / wire models** affecting **`schemas/`**: **`uv run python scripts/generate_schemas.py`**, then **`git diff --exit-code schemas/`** must be clean—commit **`schemas/`** updates with the PR.

If you changed **`web/`** (React UI): from **`web/`**, run **`npm ci`** then **`npm run build`**, then from the repo root **`git diff --exit-code src/flightdeck/server/static/`** must be clean—commit all updates under that path (CI fails otherwise). When behavior changes, run **`npm run test:e2e`** from **`web/`**.
Expand Down
4 changes: 2 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Local driver test helper (Docker optional): **`scripts/run_postgres_tests.ps1`**

| Extra | Packages installed | When to use |
|-------|--------------------|-------------|
| `dev` | `pytest`, `ruff` | Development and CI; required to run tests and lint |
| `dev` | `pytest`, `pytest-cov`, `ruff` | Development and CI; required to run tests and lint |
| `openai` | `openai>=1.0` | If you want to use the OpenAI Python client alongside the SDK in your own agent code (not required by FlightDeck core) |
| `anthropic` | `anthropic>=0.20` | Same, for the Anthropic Python client |
| `telemetry` | `opentelemetry-api`, `-sdk`, `-exporter-otlp` | Forward-looking OTLP integration; FlightDeck core does **not** import OpenTelemetry at runtime |
Expand Down Expand Up @@ -270,7 +270,7 @@ virtual environment's Python executable directly:
.venv/bin/python -m pytest
```

Use **`uv run python -m pytest`** from the repo root so imports like **`from tests.test_spine import …`** resolve the same way as in CI.
Use **`uv run python -m pytest`** from the repo root so imports like **`from tests.test_spine import …`** resolve the same way as in CI. CI adds **`--cov=flightdeck --cov-fail-under=80`** (see **`[tool.coverage.run]`** in **`pyproject.toml`** for **`omit`** — integrations shims, the quickstart entrypoint, and the HTTP SDK client are excluded so the gate tracks core ledger/diff/policy code).

## Environment variables

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Shipped locally:
### Local HTTP API

With **`flightdeck serve`** (default bind **127.0.0.1**), the app exposes **`GET /health`**, **`GET /v1/workspace`**
(read-only workspace flags for scripts and the bundled UI), **`GET /v1/metrics`**, **`GET /v1/releases`**, **`GET /v1/promoted`**, **`GET /v1/actions`**, **`GET /v1/promotion-requests`**, **`GET /v1/runs`**, **`POST /v1/events`**, **`POST /v1/diff`**, **`POST /v1/promote`**, **`POST /v1/promote/request`**, **`POST /v1/promote/confirm`**, and **`POST /v1/rollback`**. **`POST /v1/promote`**, **`POST /v1/promote/request`**, **`POST /v1/promote/confirm`**, **`POST /v1/rollback`**, and **`POST /v1/events`** accept requests only from loopback clients unless **`FLIGHTDECK_LOCAL_API_TOKEN`** is set, in which case callers must send **`Authorization: Bearer <token>`** (same behavior as the **`web/`** dev UI via **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`**). **`POST /v1/diff`** stays read-only and does not use that gate. See **[docs/http-api.md](docs/http-api.md)** and **[SECURITY.md](SECURITY.md)**.
(read-only workspace flags for scripts and the bundled UI), **`GET /v1/metrics`**, **`GET /v1/releases`**, **`GET /v1/promoted`**, **`GET /v1/actions`**, **`GET /v1/promotion-requests`**, **`GET /v1/runs`**, **`POST /v1/events`**, **`POST /v1/diff`**, **`POST /v1/promote`**, **`POST /v1/promote/request`**, **`POST /v1/promote/confirm`**, and **`POST /v1/rollback`**. **`POST /v1/promote`**, **`POST /v1/promote/request`**, **`POST /v1/promote/confirm`**, **`POST /v1/rollback`**, and **`POST /v1/events`** accept requests only from loopback clients unless **`FLIGHTDECK_LOCAL_API_TOKEN`** is set, in which case callers must send **`Authorization: Bearer <token>`**; when that token is set, the same Bearer header is required for **`GET /v1/*`** read APIs (bundled UI via **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`**). **`POST /v1/diff`** stays unauthenticated. See **[docs/http-api.md](docs/http-api.md)** and **[SECURITY.md](SECURITY.md)**.

## Quickstart

Expand Down
8 changes: 6 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for a pre-push checklist aligned with

The bundled server is intended for **local development and demos**. **`POST /v1/promote`**, **`POST /v1/promote/request`**, **`POST /v1/promote/confirm`**, **`POST /v1/rollback`**, and **`POST /v1/events`** (run event ingest) share one **ledger-write** access model in server code: with no token configured, only **loopback** clients (`127.0.0.1`, `::1`, `localhost`, and the Starlette test client) may call them. If you set **`FLIGHTDECK_LOCAL_API_TOKEN`**, every such request must include **`Authorization: Bearer <that value>`**; use a strong random value and treat it like a local secret. Remote emitters (agents, sidecars) must use the Bearer path when the server listens beyond loopback.

**Human approval** (`promotion_requires_approval: true` in `flightdeck.yaml`) adds a **second actor step** before a promote is applied: **`POST /v1/promote/request`** creates a pending row; **`POST /v1/promote/confirm`** completes it. **Policy still runs on confirm** — approval is not a bypass; a request that fails policy remains blocked with the same HTTP **409** outcome as a direct promote. **`GET /v1/workspace`**, **`GET /v1/promotion-requests`**, and other read-only **`GET /v1/*`** routes stay on the read tier (no Bearer required unless you add external controls).
**Human approval** (`promotion_requires_approval: true` in `flightdeck.yaml`) adds a **second actor step** before a promote is applied: **`POST /v1/promote/request`** creates a pending row; **`POST /v1/promote/confirm`** completes it. **Policy still runs on confirm** — approval is not a bypass; a request that fails policy remains blocked with the same HTTP **409** outcome as a direct promote.

**`POST /v1/diff`** is intentionally unauthenticated (read-only computation on stored evidence). When `flightdeck serve` binds to `127.0.0.1` (the default), callers are constrained by network topology; if you use **`--host 0.0.0.0`**, protect read routes at the network layer if exposure is a concern.
When **`FLIGHTDECK_LOCAL_API_TOKEN`** is set, **read-only `GET /v1/*`** routes (workspace, metrics, runs, audit slices, etc.) require the **same** **`Authorization: Bearer`** header as ledger writes, so port-forwards and shared networks do not expose the audit trail without credentials. With no token configured, those reads stay open—use network controls if you bind beyond loopback.

**`POST /v1/diff`** is intentionally unauthenticated (read-only computation on stored evidence). When `flightdeck serve` binds to `127.0.0.1` (the default), callers are constrained by network topology; if you use **`--host 0.0.0.0`**, treat **`POST /v1/diff`** exposure explicitly.

**SQLite:** one hot writer per workspace file is the safe default; parallel servers on the same path risk **`database is locked`**. The server retries for a bounded time (see **`flightdeck serve --help`** and **`docs/http-api.md`**). Prefer **separate workspace directories** per concurrent process in CI, or **`database_url`** (PostgreSQL) for multi-writer deployments.

For **Compose healthchecks**, **SQLite backup** scheduling, and an **operator checklist** (logs, restarts, one writer per workspace file), see **[examples/deploy/README.md](examples/deploy/README.md)**.
6 changes: 4 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,22 @@ Failed checks print to stderr and exit 1. Passing exits 0.
Start the local FlightDeck HTTP service.

```bash
flightdeck serve [--host HOST] [--port PORT] [--reload]
flightdeck serve [--host HOST] [--port PORT] [--reload] [--sqlite-lock-timeout SECONDS] [--retry-sqlite-lock / --no-retry-sqlite-lock]
```

| Option | Default | Description |
|--------|---------|-------------|
| `--host` | `127.0.0.1` | Bind address. Non-loopback addresses print a security warning |
| `--port` | `8765` | Bind port |
| `--reload` | off | Hot-reload on source changes (development only) |
| `--sqlite-lock-timeout` | `30` | Seconds to retry SQLite `database is locked` / busy errors on ledger statements (`0` disables timed retries) |
| `--retry-sqlite-lock` | on | Retry locked/busy SQLite executes until the timeout elapses |

The server exposes `/v1/*` JSON routes. See [http-api.md](http-api.md) for full route
documentation.

**Authentication:** set `FLIGHTDECK_LOCAL_API_TOKEN` to require a Bearer token for
mutation routes (`POST /v1/promote`, `POST /v1/rollback`). See
ledger writes and for **`GET /v1/*`** read APIs. See
[http-api.md § Authentication](http-api.md).

```bash
Expand Down
26 changes: 15 additions & 11 deletions docs/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ default and is intended for **local development and CI**, not public exposure.
flightdeck serve # default: 127.0.0.1:8765
flightdeck serve --port 9000 # custom port
flightdeck serve --host 0.0.0.0 # non-loopback (prints warning; see Security)
flightdeck serve --sqlite-lock-timeout 45 --retry-sqlite-lock # SQLite busy/locked retries (default 30s on)
```

The server requires a `flightdeck.yaml` in the working directory. Run `flightdeck init`
Expand All @@ -22,19 +23,20 @@ Two access tiers:
| Route | No token configured | `FLIGHTDECK_LOCAL_API_TOKEN` set |
|-------|--------------------|---------------------------------|
| `GET /health` | open | open |
| `GET /v1/*` (reads, including `GET /v1/workspace`, `GET /v1/metrics`, `GET /v1/runs`, `GET /v1/runs/export`, `GET /v1/promotion-requests`) | open | open |
| `GET /v1/*` (reads: workspace, metrics, releases, promoted, actions, promotion-requests, runs, runs/export) | open | `Authorization: Bearer <token>` required |
| `POST /v1/events` | loopback only | `Authorization: Bearer <token>` required |
| `POST /v1/diff` | open | open |
| `POST /v1/promote` | loopback only | `Authorization: Bearer <token>` required |
| `POST /v1/promote/request`, `POST /v1/promote/confirm` | loopback only | `Authorization: Bearer <token>` required |
| `POST /v1/rollback` | loopback only | `Authorization: Bearer <token>` required |

`POST /v1/events` uses the **same** loopback / Bearer gate as promote and rollback
(`require_ledger_write_access` in `server/mutation_access.py`). Remote agents must set
`FLIGHTDECK_LOCAL_API_TOKEN` on the server and send matching `Authorization: Bearer` headers
(including the Python SDK’s `api_token=`). When no token is configured, only loopback
callers (`127.0.0.1`, `::1`, `localhost`) may append run events, so binding `--host 0.0.0.0`
does not leave ingest open to arbitrary clients on the network.
(`require_ledger_write_access` in `server/mutation_access.py`). **`GET /v1/*`** uses
`require_protected_read_access`: with a token set, send the same **`Authorization: Bearer`**
header (Python SDK **`api_token=`**). Remote agents must set `FLIGHTDECK_LOCAL_API_TOKEN` on
the server and send matching Bearer headers when using non-loopback hosts. When no token is
configured, only loopback callers (`127.0.0.1`, `::1`, `localhost`) may append run events, so
binding `--host 0.0.0.0` does not leave ingest open to arbitrary clients on the network.

```bash
export FLIGHTDECK_LOCAL_API_TOKEN="$(openssl rand -hex 32)"
Expand All @@ -56,15 +58,17 @@ Health probe. Always returns HTTP 200 while the server is up.
**Response**

```json
{"status": "ok", "mutation_auth": "loopback"}
{"status": "ok", "mutation_auth": "loopback", "read_auth": "open"}
```

`mutation_auth` is always present on current servers:
`mutation_auth` and `read_auth` are always present on current servers:

- `"loopback"` — `FLIGHTDECK_LOCAL_API_TOKEN` is not set; ledger writes (including **`POST /v1/events`**) are allowed only from loopback clients (no Bearer gate).
- `"bearer"` — `FLIGHTDECK_LOCAL_API_TOKEN` is set; ledger writes require `Authorization: Bearer <that value>` from any client host.
- **`mutation_auth`:** `"loopback"` — no API token; ledger writes (including **`POST /v1/events`**) are allowed only from loopback clients. `"bearer"` — token set; writes require `Authorization: Bearer <that value>` from any host.
- **`read_auth`:** `"open"` — no API token; **`GET /v1/*`** need no Bearer. `"bearer"` — token set; read APIs require the same Bearer header as writes.

This field never includes secret material.
Neither field includes secret material.

**SQLite contention:** parallel writers against the **same** workspace SQLite file can see `database is locked`. The server retries locked/busy statements for a bounded time (CLI **`--sqlite-lock-timeout`** / **`--no-retry-sqlite-lock`**, env **`FLIGHTDECK_SQLITE_LOCK_TIMEOUT_S`**, **`FLIGHTDECK_SQLITE_RETRY_ON_LOCK`**, **`FLIGHTDECK_SQLITE_BUSY_TIMEOUT_MS`** for `PRAGMA busy_timeout`). CI and multi-process setups should still use **one workspace path per concurrent server** or switch to **`database_url`** (PostgreSQL) for multi-writer throughput — see **[operations-and-policy.md](operations-and-policy.md#sqlite-concurrency-and-postgresql)**.

---

Expand Down
27 changes: 25 additions & 2 deletions docs/operations-and-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,33 @@ scenarios), it re-runs the same load-and-migrate sequence and stores the results
it also means the working directory at **first request time** determines which
`flightdeck.yaml` is loaded, not the directory at process start.

`_require_mutation_access` (called by `POST /v1/promote` and `POST /v1/rollback`) reads
`require_ledger_write_access` (ledger writes, including **`POST /v1/events`**) and
`require_protected_read_access` (**`GET /v1/*`** when a token is configured) read
`request.app.state.local_api_token` set during lifespan or lazy init. The test client host
`"testclient"` is included in `_LOCAL_CLIENT_HOSTS` alongside loopback addresses so that
integration tests can call mutation routes without a Bearer token.
integration tests can call **write** routes without a Bearer token when no server token is
set; when **`FLIGHTDECK_LOCAL_API_TOKEN`** is set, writes and reads require **Bearer** even
from the test client.

---

## SQLite concurrency and PostgreSQL

The default ledger is **SQLite** under **`.flightdeck/`**. SQLite allows **one writer at a
time** per database file. Running **multiple `flightdeck serve` processes** (or CI jobs)
against the **same** workspace directory can surface **`database is locked`** /
**`OperationalError`**. Mitigations:

- **One server per workspace path** in automation; give each parallel job its own **`$WORKSPACE`** / temp dir (`flightdeck init` there).
- **Bounded retries:** the server retries locked/busy SQLite executes for up to
**`--sqlite-lock-timeout`** seconds (see **`flightdeck serve --help`** and env vars in
**`docs/http-api.md`**). This improves correctness under brief contention; it does not make
concurrent multi-writer access safe on one file.
- **PostgreSQL:** set **`database_url`** in **`flightdeck.yaml`** and install **`psycopg`**
(**`--extra postgres`**). Migrations are the same **`Storage.migrate()`** DDL as SQLite.
There is **no built-in SQLite → Postgres row copy**; for a controlled migration, export
evidence (**`runs export`**, **`GET /v1/runs/export`**) and re-ingest, or use your own ETL
plus a fresh Postgres workspace.

---

Expand Down
Loading
Loading