diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7b8d64c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +github: [Gsbreddy] +# TODO: enable additional funding ecosystems as they are set up. +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name +# community_bridge: # Replace with a single Community Bridge project-name +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name +# custom: # Replace with up to 4 custom sponsorship URLs diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..947f209 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Security disclosure + url: https://github.com/flightdeckdev/flightdeck/security/advisories/new + about: Report a vulnerability privately. + - name: Discussion / Q&A + url: https://github.com/flightdeckdev/flightdeck/discussions + about: Ask questions or share use cases. + - name: Documentation + url: https://github.com/flightdeckdev/flightdeck#documentation + about: Read CLI, HTTP API, policy, and operator docs. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..886fbc7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,57 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automated" + groups: + python-minor-patch: + update-types: + - "minor" + - "patch" + + - package-ecosystem: "pip" + directory: "/docs" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "automated" + - "docs" + + - package-ecosystem: "npm" + directory: "/web" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automated" + groups: + npm-minor-patch: + update-types: + - "minor" + - "patch" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automated" + groups: + actions-minor-patch: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10924b1..f254c1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,12 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["3.12"] + os: [ubuntu-latest, windows-latest] + python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout @@ -24,6 +25,11 @@ jobs: cache: npm cache-dependency-path: web/package-lock.json + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Set up uv uses: astral-sh/setup-uv@v5 with: @@ -34,6 +40,8 @@ jobs: run: uv sync --frozen --extra dev - name: Build web UI + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + shell: bash run: | cd web npm ci @@ -42,12 +50,16 @@ jobs: git diff --exit-code src/flightdeck/server/static/ - name: Playwright E2E (served UI) + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + shell: bash run: | cd web npx playwright install chromium npm run test:e2e - name: Playwright E2E (approval workspace) + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + shell: bash env: FD_E2E_FORCE_APPROVAL: "1" run: | @@ -62,6 +74,8 @@ jobs: run: uv run python -m pytest --cov=flightdeck --cov-fail-under=80 --cov-report=term - name: JSON Schemas drift check + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + shell: bash run: | uv run python scripts/generate_schemas.py git diff --exit-code schemas/ @@ -90,6 +104,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Set up uv uses: astral-sh/setup-uv@v5 with: @@ -104,79 +123,3 @@ jobs: - name: Integration tests run: uv run python -m pytest tests/test_integrations.py tests/test_integrations_langchain.py - - test-windows: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.12"] - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: npm - cache-dependency-path: web/package-lock.json - - - name: Set up uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - python-version: ${{ matrix.python-version }} - - - name: Sync dependencies - run: uv sync --frozen --extra dev - - - name: Build web UI - shell: bash - run: | - cd web - npm ci - npm run build - cd .. - git diff --exit-code src/flightdeck/server/static/ - - - name: Playwright E2E (served UI) - shell: bash - run: | - cd web - npx playwright install chromium - npm run test:e2e - - - name: Playwright E2E (approval workspace) - shell: bash - env: - FD_E2E_FORCE_APPROVAL: "1" - run: | - cd web - npx playwright install chromium - npx playwright test e2e/actions-approval.spec.ts - - - name: Lint - run: uv run python -m ruff check src tests - - - name: Test - run: uv run python -m pytest --cov=flightdeck --cov-fail-under=80 --cov-report=term - - - name: JSON Schemas drift check - run: | - uv run python scripts/generate_schemas.py - git diff --exit-code schemas/ - - - name: Quickstart smoke (cross-platform) - run: uv run flightdeck-quickstart-verify - - - name: Example CI ledger gate - env: - FD_PROJECT: ${{ github.workspace }} - WORKSPACE: ${{ runner.temp }}/fd-ledger-gate-${{ github.run_id }}-${{ github.run_attempt }} - QUICKSTART_ROOT: ${{ github.workspace }}/examples/quickstart - run: uv run python examples/ci/ledger_gate.py - - - name: CLI smoke - run: uv run flightdeck --help diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..62b2e33 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,37 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 8 * * 1' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['python', 'javascript-typescript'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..80b6453 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,72 @@ +name: docker-publish + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + push_latest: + description: 'Tag the resulting image as :latest (uncheck for backport / hotfix releases that should not move :latest backwards).' + type: boolean + default: true + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/flightdeckdev/flightdeck + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ github.event_name == 'push' || inputs.push_latest }} + flavor: | + latest=false + + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + context: examples/deploy + file: examples/deploy/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: true + sbom: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-name: ghcr.io/flightdeckdev/flightdeck + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..122f691 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,55 @@ +# Builds MkDocs site from docs/ and deploys to GitHub Pages. +# +# One-time repo setup (maintainer): Settings → Pages → Build and deployment → +# Source: GitHub Actions. The first successful run publishes the site. +name: Deploy documentation to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: docs/requirements.txt + + - name: Install MkDocs + run: pip install -r docs/requirements.txt + + - name: Build site + run: mkdocs build --strict + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..9998d91 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,57 @@ +# Generate a CycloneDX Software Bill of Materials (SBOM) for each release tag, +# upload it as a build artifact, and attach it to the GitHub Release. +# https://github.com/CycloneDX/cyclonedx-python + +name: SBOM (CycloneDX) + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + workflow_dispatch: + +jobs: + sbom: + name: Generate and publish SBOM + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install cyclonedx-bom + # Pinned so the SBOM tool itself is reproducible across releases. Bump in lockstep + # with a deliberate test run; do not auto-upgrade. (Reviewer MAJOR-4) + run: python -m pip install --upgrade pip "cyclonedx-bom==6.1.4" + + - name: Compile pinned requirements from pyproject.toml + run: uv pip compile pyproject.toml -o requirements.txt + + - name: Generate CycloneDX SBOM + run: cyclonedx-py requirements requirements.txt -o bom.json --schema-version 1.5 + + - name: Upload SBOM as build artifact + uses: actions/upload-artifact@v4 + with: + name: flightdeck-sbom-${{ github.ref_name }} + path: bom.json + retention-days: 90 + + - name: Attach SBOM to GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: bom.json diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..aa38d08 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,49 @@ +# OpenSSF Scorecard — supply-chain health checks for public repos. +# Results are published to the GitHub Security tab and (when publish_results is true) +# to the OpenSSF scorecard.dev public dataset. +# https://github.com/ossf/scorecard-action + +name: Scorecard supply-chain security + +on: + branch_protection_rule: + schedule: + - cron: '0 6 * * 2' + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + contents: read + actions: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Run Scorecard analysis + uses: ossf/scorecard-action@v2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload SARIF as artifact + uses: actions/upload-artifact@v4 + with: + name: scorecard-sarif + path: results.sarif + retention-days: 14 + + - name: Upload SARIF to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..7eede1b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,51 @@ +name: stale + +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Mark and close stale issues / PRs + uses: actions/stale@v9 + with: + days-before-issue-stale: 60 + days-before-issue-close: 14 + days-before-pr-stale: 45 + days-before-pr-close: 14 + exempt-issue-labels: 'pinned,security,roadmap,keep-open' + exempt-pr-labels: 'pinned,security,roadmap,keep-open' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + stale-issue-message: >- + Hi, thanks for opening this issue! It has been quiet for 60 days, + so we are marking it as stale. If it is still relevant, please + leave a comment or share more context and we will take another + look. Otherwise it will be closed in 14 days. For questions or + use-case discussions, the GitHub Discussions board is a great + place: https://github.com/flightdeckdev/flightdeck/discussions — + and please report security issues privately via + https://github.com/flightdeckdev/flightdeck/security/advisories/new. + close-issue-message: >- + Closing this issue due to inactivity. Please feel free to reopen + or start a thread on Discussions + (https://github.com/flightdeckdev/flightdeck/discussions) if it + is still relevant. Thanks for helping make FlightDeck better! + stale-pr-message: >- + Hi, thanks for the contribution! This pull request has been + inactive for 45 days, so we are marking it as stale. If you plan + to continue, please push an update or leave a comment within the + next 14 days. Need a hand or want to discuss the approach? Open + a thread on + https://github.com/flightdeckdev/flightdeck/discussions. + close-pr-message: >- + Closing this pull request due to inactivity. Please reopen when + you are ready to pick it back up — we are happy to help review. + operations-per-run: 100 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..b52f5b4 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,74 @@ +# Trivy vulnerability scans: filesystem (source tree) and container image +# (example deploy image). Findings are surfaced in the GitHub Security tab +# via SARIF; exit-code is 0 today so we report but do not gate yet. +# https://github.com/aquasecurity/trivy-action + +name: Trivy security scans + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: '0 7 * * 3' + workflow_dispatch: + +jobs: + trivy-fs: + name: Trivy filesystem scan + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@0.35.0 + with: + scan-type: fs + format: sarif + output: trivy-fs.sarif + severity: CRITICAL,HIGH + exit-code: '0' + + - name: Upload Trivy filesystem SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-fs.sarif + category: trivy-fs + + trivy-image: + name: Trivy container image scan + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build example deploy image + run: docker build -t flightdeck-local:scan -f examples/deploy/Dockerfile examples/deploy + + - name: Run Trivy image scan + uses: aquasecurity/trivy-action@0.35.0 + with: + scan-type: image + image-ref: flightdeck-local:scan + format: sarif + output: trivy-image.sarif + severity: CRITICAL,HIGH + exit-code: '0' + + - name: Upload Trivy image SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-image.sarif + category: trivy-image diff --git a/.gitignore b/.gitignore index 0a70fe5..2539546 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ web/node_modules/ .tools/ build/ dist/ + +# MkDocs (local `mkdocs build`; GitHub Pages uploads `site/` from CI only) +/site/ *.egg-info/ .coverage htmlcov/ diff --git a/AGENTS.md b/AGENTS.md index 968143b..bd92124 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,3 +119,28 @@ If it does not, it waits. Public docs explain implemented behavior and near-term roadmap. Internal product strategy, legal notes, and fundraising/customer discovery material do not belong in this repo. + +## Cursor Cloud specific instructions + +### Services + +| Service | How to run | Notes | +|---------|-----------|-------| +| Python CLI + tests | `uv run python -m pytest`, `uv run flightdeck --help` | All tests use in-process SQLite; no external DB needed | +| HTTP server | `uv run flightdeck serve` (from a dir with `flightdeck.yaml`) | Serves REST API on `:8765` + committed static web UI | +| Web UI dev server | `cd web && npm run dev` | Proxies `/v1` to `flightdeck serve` on `127.0.0.1:8765` | + +### Running commands + +- **Lint:** `uv run python -m ruff check src tests` +- **Tests:** `uv run python -m pytest --cov=flightdeck --cov-fail-under=80` +- **Smoke:** `uv run flightdeck-quickstart-verify` +- **Web build:** `cd web && npm run build` (then `git diff --exit-code src/flightdeck/server/static/` from repo root) +- **Playwright e2e:** `cd web && FLIGHTDECK_E2E_PYTHON=/workspace/.venv/bin/python npm run test:e2e` + +### Non-obvious caveats + +- Playwright e2e tests require `FLIGHTDECK_E2E_PYTHON=/workspace/.venv/bin/python` because the `e2e-server.mjs` script defaults to system `python3` which doesn't have `flightdeck` installed. The CI path uses `uv run` (triggered by `GITHUB_ACTIONS` env var). +- `flightdeck serve` requires a workspace directory containing `flightdeck.yaml` (created by `flightdeck init`). Create a temporary workspace before starting the server. +- PostgreSQL tests are auto-skipped unless `FLIGHTDECK_TEST_POSTGRES_URL` is set and `psycopg` is installed. +- The web UI static bundle is **committed** to `src/flightdeck/server/static/`. After any `web/` source change, rebuild and commit the output. diff --git a/CHANGELOG.md b/CHANGELOG.md index b6204d0..03ce390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,39 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0** ### Added -- **Web UI (`flightdeck serve`):** **`/#/settings`** for appearance (Light / Dark / System, **`flightdeck-theme`**); collapsible sidebar (**`flightdeck-sidebar-collapsed`**); **offline system font stack** (no remote font CSS); sidebar + favicon use **bundled** **`/assets/flightdeck-icon-*.png`** with stable **`GET /flightdeck-icon.png`** fallback; **`html[data-theme="dark"]`** tokens and Playwright **`web/e2e/`** (`smoke` icon checks, `theme.spec.ts`, `sidebar.spec.ts`). +- **Identity passthrough → audit actor:** HTTP mutating routes (`POST /v1/promote*`, `POST /v1/rollback`) now read **`X-FlightDeck-Actor`** (first) and **`X-Forwarded-User`** (second) before falling back to the body `actor` field. Lets a reverse-proxy / SSO layer stamp the audit ledger authoritatively. Trust model documented in **`SECURITY.md`** and **`docs/http-api.md`**. +- **`flightdeck workspace info [--json]`** — one-screen snapshot of workspace path, database backend, schema version, ledger counters (releases / promoted / actions / run events), configuration (policy, pricing catalog, approval mode), and webhook count. JSON mode for CI / chatops pipelines. +- **`flightdeck version [--json]`** — explicit version subcommand alongside the existing `--version` flag; `--json` emits `{"name":"flightdeck-ai","version":"1.3.0"}` for scripts and dashboards. +- **Request-context middleware (`flightdeck serve`):** every response now carries **`X-Request-Id`** (per-request UUID, echoes the caller's header if provided) and **`X-FlightDeck-Server-Version`** (package version). Enables end-to-end log correlation alongside webhook **`delivery_id`**. +- **Webhooks — SSRF defence:** `POST /v1/webhooks` validates the URL on creation; rejects non-http(s) schemes (no `file://`, `gopher://`, etc.), link-local IPv4/IPv6 (covers AWS IMDS `169.254.169.254`, ECS `169.254.170.2`, and IPv6 `fe80::/10`), and known cloud-metadata hostnames. Private RFC1918 addresses and loopback are allowed (self-hosted receivers). 21 tests. +- **Web UI UX fixes:** Diff result auto-scrolls into view after Compute diff (verdict was below the fold at 1440 × 900); typed-confirm error message now shows exactly what to type; pricing hints/warnings moved to collapsible `
` accordions (no longer prominent alert boxes); Runs Release ID input carries a datalist affordance hint. +- **Community / supply-chain:** `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), `GOVERNANCE.md` (BDFL model + steering-committee transition criteria), `CITATION.cff` (CFF 1.2.0), `FUNDING.yml`, `.github/ISSUE_TEMPLATE/config.yml`. OpenSSF Scorecard, CycloneDX SBOM (attached to GitHub Releases), Trivy filesystem + container scans, multi-arch container image to GHCR (`ghcr.io/flightdeckdev/flightdeck`) with SLSA provenance + SBOM attestations, stale-issues bot. +- **CI matrix:** Python 3.11 / 3.12 / 3.13 × Linux / Windows (was single Python version). CodeQL SAST for Python and JavaScript/TypeScript. Dependabot for pip, npm (`web/`), and GitHub Actions. +- **Webhooks (v1.3.0):** HMAC-signed outbound webhooks for **`promote.succeeded`**, + **`rollback.succeeded`**, and **`promote.blocked`**. New routes **`POST /v1/webhooks`**, + **`GET /v1/webhooks`**, **`DELETE /v1/webhooks/{id}`** (same Bearer / loopback gate as + promote). New CLI **`flightdeck webhook add | list | remove | test`**. Signing uses + GitHub-convention **`X-FlightDeck-Signature: sha256=`** over the raw body with a + per-webhook secret; delivery is best-effort (5 s timeout, 3 attempts, backoff 1 s / 2 s + / 4 s) and never breaks the originating promote / rollback. Schema migration **v5** adds + the **`webhooks`** table (SQLite + PostgreSQL). No new runtime dependencies. +- **`flightdeck demo`** — runs the packaged **examples/quickstart** workflow (init → pricing → policy → register → ingest → diff → promote → history) in a **temp workspace**, with no **`sed`** or repo paths; wheels ship fixtures under **`flightdeck/_bundled_quickstart`** via Hatch **`force-include`**. **`FLIGHTDECK_QUICKSTART_ROOT`** overrides fixture resolution for CI or forks. +- **Web UI (`flightdeck serve`):** **Theme** (Light / Dark / System icon radios, **`flightdeck-theme`**) in the **sidebar Settings** popover; legacy **`/#/settings`** redirects to **`#/`**; collapsible sidebar (**`flightdeck-sidebar-collapsed`**); **offline system font stack** (no remote font CSS); sidebar + favicon use **bundled** **`/assets/flightdeck-icon-*.png`** with stable **`GET /flightdeck-icon.png`** fallback; **`html[data-theme="dark"]`** tokens and Playwright **`web/e2e/`** (`smoke` icon checks, `theme.spec.ts`, `sidebar.spec.ts`). +- **Documentation (GitHub Pages):** workflow **`.github/workflows/pages.yml`** builds **`docs/`** with MkDocs Material on pushes to **`main`**; published URL on **`[project.urls] Documentation`**, README, **`DEVELOPMENT.md`**, and **`CONTRIBUTING.md`**. The static site includes a floating **Ask AI** shortcut (Perplexity with docs + repo context); no FlightDeck servers are involved. - **`flightdeck pricing check`** — reports **`flightdeck-bundled-*`** snapshot age vs **`--max-age-days`** (default **90**); **`--fail`** for CI. **`release diff`** / **`POST /v1/diff`** append **`pricing.warnings`** when bundled snapshots exceed the same age threshold. - **`flightdeck.integrations.telemetry.configure_otel_tracing()`** — optional OTLP HTTP **`TracerProvider`** wiring when the **`telemetry`** extra is installed (see **`docs/sdk-integrations.md`**). - **SDK:** **`flightdeck.sdk.http_common`** shared serializers and retry policy; parity tests keep sync/async clients aligned. **`pytest-cov`** no longer omits **`sdk/client.py`**. +### Fixed + +- **`pymdown-extensions` bump 10.16 → 10.21.3:** patches CVE-2026-46338 (MEDIUM — sibling-prefix path traversal in `pymdownx.snippets`) and CVE-2025-68142 (LOW — ReDoS in figure caption extension) in the GitHub Pages build dependency (`docs/requirements.txt`). Not a runtime dependency. + ### Changed +- **README:** `pip install flightdeck-ai && flightdeck demo` promoted to top of landing; install section split into user vs contributor; demo video and screenshot gallery added; stale "~31%" cost-regression example removed; `docs/index.md` completely rewritten for first-time visitors. - **`[project.optional-dependencies] dev`:** **`ruff`** is **`>=0.15,<0.16`** (was an exact patch pin) so **`pip install`** / shared venvs can resolve alongside other tools; **`uv sync --frozen`** still follows **`uv.lock`**. **`docs/troubleshooting.md`** notes checking **`uv.lock`** for the resolved **`0.15.x`** wheel. - **Docs / positioning:** README local-first and ICP copy; bundled pricing cadence, vendor pricing URLs in YAML comments, and **`docs/pricing-catalog.md`** / **ROADMAP** / **RELEASE_NOTES** staleness commitments. +- **Web UI (`flightdeck serve`):** Sidebar **Settings** popover shows a **Settings** heading and **Theme** as sun / moon / monitor icon radios (replacing text-only appearance controls in that surface). ## 1.2.0 - 2026-05-03 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..7987f05 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,26 @@ +cff-version: 1.2.0 +message: "If you use FlightDeck in research or publications, please cite it." +title: "FlightDeck: AI Release Governance for Production Agents" +authors: + - family-names: "Sai Bharath" + given-names: "Gottam" + website: "https://github.com/Gsbreddy" +version: "1.2.0" +date-released: "2026-05-03" +repository-code: "https://github.com/flightdeckdev/flightdeck" +license: "Apache-2.0" +keywords: + - ai + - agents + - llmops + - governance + - deployment + - finops + - release-engineering +abstract: >- + FlightDeck is an open-source AI release governance platform for production + agents. It provides policy-driven deployment gates, evaluation harnesses, + cost and latency budgets, and progressive rollout controls for LLM-backed + systems. Operators use FlightDeck to ship agent changes safely via a CLI, + HTTP API, and Kubernetes-native integrations while enforcing reproducible + release engineering practices across model providers and runtime stacks. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..af37d47 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,43 @@ +# Code of Conduct + +## Our pledge + +FlightDeck is a welcoming, harassment-free space for everyone. We pledge to make participation in our community a respectful experience for all contributors, reviewers, users, and maintainers, regardless of experience level, background, or identity. + +## Standards + +Examples of behavior we encourage: + +- Being respectful of differing viewpoints and experiences +- Giving and gracefully accepting constructive feedback +- Focusing on what is best for the project and the broader community +- Showing empathy toward other community members + +Examples of behavior we will not tolerate: + +- Personal attacks, insults, or derogatory comments +- Public or private harassment, including sustained disruption of issues, pull requests, or discussions +- Publishing others' private information (such as a physical or electronic address) without explicit permission +- Sexualized language or imagery, and unwelcome sexual attention +- Any other conduct that a reasonable person would consider inappropriate in a professional setting + +## Scope + +This Code of Conduct applies to all FlightDeck spaces — the GitHub repository (issues, pull requests, discussions, security advisories), the documentation site, and any official communication channels — and also applies when an individual is representing the project in public. + +## Enforcement + +Instances of unacceptable behavior may be reported privately to the maintainers by: + +- Opening a **[private security advisory](https://github.com/flightdeckdev/flightdeck/security/advisories/new)** and selecting the "Conduct" category in the report body, or +- Direct message to the maintainer on GitHub: **[@Gsbreddy](https://github.com/Gsbreddy)**. + +All complaints will be reviewed and investigated, and will result in a response appropriate to the circumstances. The maintainers are obligated to maintain the confidentiality of the reporter. + +Possible responses include a private warning, a request to edit or remove a comment, a temporary ban from project spaces, or a permanent ban. Decisions will be communicated to the reporter and to the subject of the report. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/), available under the Creative Commons Attribution 4.0 license. The Contributor Covenant project also maintains [translations](https://www.contributor-covenant.org/translations) and a more detailed [FAQ](https://www.contributor-covenant.org/faq). + +For answers to common questions about this Code of Conduct, see the Contributor Covenant FAQ. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24081f1..57f04e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,6 +55,10 @@ If you change **`pyproject.toml`** dependencies, run **`uv lock`** and commit ** Details, Windows notes, and doctrine: **`AGENTS.md`** (Verification), **`DEVELOPMENT.md`**, **`web/README.md`**. +## GitHub Pages (maintainers) + +The **Deploy documentation to GitHub Pages** workflow publishes **`docs/`** on pushes to **`main`**. In the GitHub repo, use **Settings → Pages → Build and deployment → Source: GitHub Actions** so the workflow can attach the **`github-pages`** environment. The live URL is linked from the root **`README.md`** and **`pyproject.toml`** **`Documentation`** URL. + ## Private files and pushing to GitHub Do not commit credentials, customer data, internal strategy docs, or local ledger data. The repo ignores **`.flightdeck/`**, **`.env*`**, optional **`private/`** / **`secrets/`**, and common key/credential patterns—see **`.gitignore`** and **[SECURITY.md](SECURITY.md)**. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f1d2c5c..eb4c58d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -73,9 +73,12 @@ python -m ruff check src tests python -m pytest flightdeck --help flightdeck doctor +flightdeck demo flightdeck-quickstart-verify ``` +**Fast path for contributors:** **`flightdeck demo`** runs the same core ledger steps as below in a **temp workspace** (fixtures from **`examples/quickstart`**, or **`flightdeck/_bundled_quickstart`** inside an installed wheel). **`flightdeck-quickstart-verify`** adds **`release verify`** + **`doctor`**. + Match **CI**’s CLI smoke: **`flightdeck --help`** must run successfully after changes to the CLI surface. Full command flags and exit codes: [README.md](https://github.com/flightdeckdev/flightdeck/blob/main/README.md). Cross-platform quickstart parity: **`flightdeck-quickstart-verify`** / **`python -m flightdeck.quickstart_smoke`** (also run in CI). HTTP API reference: **[docs/http-api.md](docs/http-api.md)**. Python SDK: **[docs/sdk.md](docs/sdk.md)**. @@ -151,6 +154,14 @@ If **PyPI** rejects **attestations** for your project, set **`attestations: fals ## Local Demo +**One command** (uses bundled **`examples/quickstart`** fixtures; no **`sed`**): + +```bash +flightdeck demo +``` + +**Manual** (same story as **`flightdeck demo`**, in your cwd): + ```bash flightdeck init flightdeck pricing import examples/quickstart/pricing-baseline.yaml @@ -282,3 +293,14 @@ Use **`uv run python -m pytest`** from the repo root so imports like **`from tes | `VITE_FLIGHTDECK_LOCAL_API_TOKEN` | Web dev server | Build-time variable for the React UI dev server (Vite). Copy `web/.env.example` → `web/.env.local` to set it when testing mutations through `npm run dev` against a token-protected server. | | `VITE_DEV_PROXY_TARGET` | Web dev server | Overrides the Vite proxy target for `/v1` (default: `http://127.0.0.1:8765`). | | `TMPDIR` / `TEMP` / `TMP` | Tests / OS | Standard temp directory environment variables. Set any of these to a repo-local `.tmp/` path if the OS default is restricted or permissions cause pytest failures. | + +## Documentation site (MkDocs) + +The Markdown under **`docs/`** is also built as a static site for **[GitHub Pages](https://flightdeckdev.github.io/flightdeck/)** (workflow **`.github/workflows/pages.yml`**). To preview locally: + +```bash +pip install -r docs/requirements.txt +mkdocs serve +``` + +Then open the URL MkDocs prints (usually **http://127.0.0.1:8000/**). The build output directory **`site/`** is gitignored; CI uploads it as the Pages artifact. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..d3c390d --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,70 @@ +# Governance + +This document describes how decisions are made in the FlightDeck project, who can make them, and how the project plans to evolve its governance over time. + +For the day-to-day engineering doctrine (mission, non-goals, public contracts, product doctrine, verification, doc rules), see [`AGENTS.md`](AGENTS.md). This file complements it; it does not override it. + +## Project model + +FlightDeck is currently a **single-maintainer project**. The maintainer is: + +- **Gottam Sai Bharath** ([@Gsbreddy](https://github.com/Gsbreddy)) + +The project follows a *benevolent dictator* model: the maintainer has final say on all merges, releases, roadmap direction, and infrastructure choices. This is an honest reflection of the project's stage, not an aspiration. Concentrating decision authority lets a young project move quickly and stay coherent; the next section describes how that responsibility is meant to dissolve as the project grows. + +## Decision-making + +- **Day-to-day decisions** (bug fixes, doc tweaks, minor refactors, new tests, dependency bumps) are made by the maintainer at merge time. +- **Non-trivial changes** — anything that adds, removes, or alters a public contract (CLI synopsis or exit codes, `release.yaml` shape, `RunEvent` schema, HTTP `/v1/*` routes, policy YAML shape, audit ledger semantics) — must be proposed first in an **issue or draft PR**. The proposal must explain which doctrine items in [`AGENTS.md`](AGENTS.md) it strengthens (release artifact integrity, runtime evidence, safety ledger accuracy, policy-gated promotion, audit history, or developer onboarding). Changes that do not strengthen at least one of those items wait. +- **Out-of-scope changes** match the **Non-goals** list in [`AGENTS.md`](AGENTS.md). Re-opening any of those (prompt IDEs, in-product agent orchestration frameworks, dashboards before CLI is proven, default gateway/proxy, compliance-scanner product, fine-tuning ops, broad plugin systems) requires a written argument with concrete user evidence and explicit acknowledgement of the trust-boundary cost. + +## Public contracts + +The list of public contracts and the stability bar for each is the **Public contracts** section of [`AGENTS.md`](AGENTS.md). Any change to a public contract is a release-notes-worthy event and must be reflected in [`RELEASE_NOTES.md`](RELEASE_NOTES.md) and [`CHANGELOG.md`](CHANGELOG.md). + +## Releases + +- The project follows [Semantic Versioning](https://semver.org/) from **v1.0.0** onward. +- **Patch and minor** releases ship on a rolling basis whenever there is meaningful, tested work to ship. +- **Major** releases require an explicit signal in [`ROADMAP.md`](ROADMAP.md) plus a corresponding [`RELEASE_NOTES.md`](RELEASE_NOTES.md) entry describing the breaking change and any migration steps. +- Release publication is automated via [`.github/workflows/release-pypi.yml`](.github/workflows/release-pypi.yml) and tag pushes; see [`VERSIONING.md`](VERSIONING.md) and [`CONTRIBUTING.md`](CONTRIBUTING.md). + +## Security + +Vulnerabilities must be reported privately per [`SECURITY.md`](SECURITY.md), not in public issues or pull requests. The maintainer will acknowledge reports within five business days and aim to ship a fix within the timelines in that document. + +## Code of Conduct + +All participation in FlightDeck spaces is governed by [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). Enforcement is handled by the maintainer; reports go to the contact listed in that file. + +## Becoming a maintainer + +Maintainership is by **invitation**, not by request. Signals that increase the likelihood of an invitation: + +- A sustained track record (months, not weeks) of high-quality contributions across code, tests, and documentation +- Demonstrated alignment with the doctrine in [`AGENTS.md`](AGENTS.md), especially the non-goals and the product-doctrine list +- Sound review judgment on others' pull requests, including the ability to say "this waits" +- Reliability: responsive to review, finishes what they start, communicates blockers early + +There is no fixed number of maintainers, and no required cadence of contribution to remain one. A maintainer who is inactive for an extended period and unreachable will be moved to an emeritus role. + +## Path to broader governance + +The single-maintainer model is a starting state, not the goal. The project will transition to a small **steering committee** when *all* of the following are true: + +- At least **three active maintainers** hold merge rights and have demonstrated independent judgment on non-trivial changes +- No single contributor is responsible for more than **70%** of merged commits over the prior 6 months +- A short **trademark policy** is published (clarifying use of the "FlightDeck" name and marks) +- A **contribution license model** is documented (today the implicit model is the Apache-2.0 inbound = outbound; the project may add a DCO or CLA at that time) + +At that point this file will be amended to describe how the steering committee makes decisions, how disputes are escalated, and how new committee members are added or rotated off. + +## License and contributions + +The project is licensed under the **Apache License, Version 2.0** (see [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE)). Contributions are accepted under the same license (inbound = outbound). + +**DCO sign-off** (`git commit -s`) is welcomed but **not currently required**. The maintainer may make sign-off mandatory as part of the steering-committee transition above; if that happens, it will be announced in [`CONTRIBUTING.md`](CONTRIBUTING.md) with a grace period. + +--- + +_If you want to discuss governance proposals, open an issue with the label `governance` or start a thread in [GitHub Discussions](https://github.com/flightdeckdev/flightdeck/discussions)._ diff --git a/README.md b/README.md index 8501379..3c5cbd4 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ # FlightDeck +[![PyPI version](https://img.shields.io/pypi/v/flightdeck-ai)](https://pypi.org/project/flightdeck-ai/) [![Python versions](https://img.shields.io/pypi/pyversions/flightdeck-ai)](https://pypi.org/project/flightdeck-ai/) [![CI status](https://github.com/flightdeckdev/flightdeck/actions/workflows/ci.yml/badge.svg)](https://github.com/flightdeckdev/flightdeck/actions/workflows/ci.yml) [![Docs](https://img.shields.io/badge/docs-flightdeckdev.github.io-blue)](https://flightdeckdev.github.io/flightdeck/) [![License](https://img.shields.io/github/license/flightdeckdev/flightdeck)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/flightdeckdev/flightdeck?style=social)](https://github.com/flightdeckdev/flightdeck) [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + **Ship AI agents safely with release diffs, runtime evidence, and policy gates.** Local-first **CLI + SQLite**. Optional **`flightdeck serve`** exposes a small web UI and **`/v1`** JSON API—data stays on your machine unless you change that. -**Core loop:** register releases → ingest run evidence → diff baseline vs candidate → promote or rollback under policy (optional human approval). +> **Try it now — runs in 30 seconds, no account needed:** +> ```bash +> pip install flightdeck-ai +> flightdeck demo # end-to-end release diff in a temp workspace +> ``` + +**How it works:** you tag an agent build as a release, collect run evidence (cost, latency, errors), diff a baseline against a candidate, and then promote or rollback only when a policy gate says the numbers are acceptable — with an optional human approval step before the ledger moves. ## Product snapshot ![FlightDeck — AI release governance (illustrative)](docs/images/flightdeck-overview.png) -*Illustrative composite, not a screenshot of the shipped UI.* Policy is threshold-based on rollups from ingested runs—not built-in PII scanners. [Theming notes](docs/web-ui.md#theming-and-brand-alignment) +*Marketing composite. See the [demo video](#demo) and [actual screenshots](artifacts/flightdeck-demo-share/) for the shipped UI.* Policy is threshold-based on rollups from ingested runs—not built-in PII scanners. [Theming notes](docs/web-ui.md#theming-and-brand-alignment) | | FlightDeck | Tracing SaaS | Git/CI alone | |--|--------------|----------------|--------------| @@ -18,6 +26,16 @@ Local-first **CLI + SQLite**. Optional **`flightdeck serve`** exposes a small we | Versioned release artifact | Yes | No | DIY | | Cost/latency diff + policy gate | Yes | Different lens | DIY | +## Demo + + + FlightDeck UI demo — click to play + + +*Click the image to play the demo video · [Download WebM](docs/screenshots/flightdeck-demo.webm)* + ## In ~20 seconds 1. **Register** immutable agent releases (`release.yaml` + bundle checksum). @@ -38,7 +56,7 @@ Small prompt or model changes can move **cost**, **latency**, and **error rate** ## Example outcome -You ship a candidate whose **prompt or model** shifts slightly; under your tariffs the diff shows **cost per run** rising while policy caps spend. **`flightdeck release promote`** (or the HTTP promote path) **stays blocked** until you change the model, adjust policy with intent, or gather more evidence—not because CI is slow, but because the **ledger** says no. The **~31%** style story in [examples/quickstart/](examples/quickstart/) uses **two custom pricing YAMLs**; **`flightdeck init`** alone seeds a **bundled** snapshot so your first cost-aware diff is not empty. +You ship a candidate whose **prompt or model** shifts slightly; under your tariffs the diff shows **cost per run** rising while policy caps spend. **`flightdeck release promote`** (or the HTTP promote path) **stays blocked** until you change the model, adjust policy with intent, or gather more evidence — not because CI is slow, but because the **ledger** says no. The [examples/quickstart/](examples/quickstart/) scenario uses **two custom pricing YAMLs** (baseline and candidate tariffs) to show a concrete cost delta across releases; **`flightdeck init`** alone seeds a **bundled** snapshot so your first cost-aware diff is not empty. ## How it fits your stack @@ -67,11 +85,42 @@ flowchart LR --- +## Fast start + +After **`pip install flightdeck-ai`** (or **`uv tool install flightdeck-ai`**): + +```bash +flightdeck demo +``` + +**`flightdeck demo`** runs the full quickstart ledger flow in a disposable temp workspace—no **`sed`**, no fixture paths—using **`examples/quickstart`** from your checkout or packaged **`flightdeck/_bundled_quickstart`** from PyPI. + +**Web UI** (needs a workspace in the current directory): + +```bash +flightdeck init +flightdeck serve +``` + +Open **http://127.0.0.1:8765/**. Same end-to-end checks CI uses: **`flightdeck-quickstart-verify`** (contributors: **`uv run flightdeck-quickstart-verify`**). + +--- + ## Install and smoke-test +**User install (recommended):** + +```bash +pip install flightdeck-ai +flightdeck demo +``` + +**Contributor / development install (uv):** + ```bash uv sync --extra dev uv run flightdeck --help +uv run flightdeck demo uv run flightdeck-quickstart-verify ``` @@ -115,6 +164,8 @@ Bundled pricing from `init` is a **convenience snapshot**—`flightdeck pricing ## Documentation +**Published site (GitHub Pages):** [flightdeckdev.github.io/flightdeck](https://flightdeckdev.github.io/flightdeck/) — built from `docs/` on each push to `main` (`.github/workflows/pages.yml`). Enable **Pages → GitHub Actions** in the repository settings if the site is not live yet. + | Area | Links | |------|--------| | CLI | [docs/cli.md](docs/cli.md) | @@ -132,12 +183,37 @@ Bundled pricing from `init` is a **convenience snapshot**—`flightdeck pricing --- +## Webhooks + +FlightDeck ships **HMAC-signed outbound webhooks** for `promote.succeeded`, +`rollback.succeeded`, and `promote.blocked`. Point them at any HTTPS endpoint +(Slack incoming webhook, Discord, PagerDuty Events v2, Linear inbound webhook, your +own relay) — FlightDeck owns the signing, not the integrations. + +```bash +flightdeck webhook add \ + --url https://hooks.slack.com/services/T000/B000/XXXX \ + --event promote.succeeded --event rollback.succeeded \ + --description "prod release alerts" +# Save the printed secret — it will not be shown again. + +flightdeck webhook list +flightdeck webhook test wh_ +``` + +Receivers verify each delivery by recomputing `HMAC-SHA256(secret, raw_body)` and +comparing against `X-FlightDeck-Signature: sha256=` (GitHub convention). Full +details: **[docs/cli.md](docs/cli.md)** · **[docs/http-api.md](docs/http-api.md)**. + +--- + ## Contributing (quick CI match) ```bash uv sync --frozen --extra dev uv run python -m ruff check src tests uv run python -m pytest +uv run flightdeck demo uv run flightdeck-quickstart-verify uv run flightdeck --help ``` @@ -146,6 +222,18 @@ Full gates (web static, schemas, e2e): [DEVELOPMENT.md](DEVELOPMENT.md) --- +## Screenshots + +| Overview — releases + promotion ledger | Diff — policy PASS verdict | +|---|---| +| ![Overview](docs/screenshots/overview.png) | ![Diff result](docs/screenshots/diff-result.png) | + +| Runs — forensics with datalist search | Dark mode | +|---|---| +| ![Runs](docs/screenshots/runs.png) | ![Dark mode](docs/screenshots/dark-mode.png) | + +--- + ## License Apache-2.0 — [LICENSE](LICENSE) · [NOTICE](NOTICE) diff --git a/SECURITY.md b/SECURITY.md index 2f6104f..fdecfab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -50,6 +50,33 @@ When **`FLIGHTDECK_LOCAL_API_TOKEN`** is set, **read-only `GET /v1/*`** routes ( **`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. +### Identity passthrough headers — when to trust them + +Mutating routes accept two HTTP headers — **`X-FlightDeck-Actor`** and **`X-Forwarded-User`** — and prefer them over the body `actor` field when stamping the audit ledger (precedence and exact semantics: **[docs/http-api.md](docs/http-api.md#identity-passthrough-audit-actor)**). This unlocks SSO-stamped audit rows behind an existing auth layer (oauth2-proxy, Pomerium, Authelia, Cloudflare Access, nginx `auth_request`) **without** FlightDeck owning identity. + +**Threat model:** both headers are **trivially forgeable** by any client that can reach the FlightDeck process directly. They are safe to trust **only** when: + +1. **All inbound traffic flows through a trusted reverse proxy** (no path that bypasses it — no `--host 0.0.0.0` shortcut, no port-forward, no co-located client). +2. **That proxy strips any incoming `X-Forwarded-User` and `X-FlightDeck-Actor` headers from client requests** before injecting its own authenticated value. Stripping is the single most important step; without it, a client can spoof the identity by setting the header themselves. Examples: + - **nginx:** `proxy_set_header X-Forwarded-User $remote_user;` after upstream auth. + - **Caddy:** `header_up X-Forwarded-User {http.auth.user.id}` with the relevant `forward_auth` handler. + - **oauth2-proxy:** sets `X-Forwarded-User` after a successful OIDC handshake; ensure the upstream is bound to loopback so direct ingress is impossible. +3. **The Bearer token (`FLIGHTDECK_LOCAL_API_TOKEN`) is also set** so a leaked / mis-routed request cannot reach mutating routes without it. + +Without those three controls, treat the headers as advisory only and rely on the body `actor` plus the Bearer-gate for audit attribution. A future release will add scoped tokens with an embedded identity claim so callers can self-attest without depending on a proxy layer. + +### Outbound webhooks — SSRF defence + +`POST /v1/webhooks` registers a URL that receives every promote / rollback / policy-blocked payload. The server validates the URL on create and **rejects**: + +- schemes other than **`http`** or **`https`** (no `file://`, `gopher://`, `ftp://`, `javascript:`, `data:`) +- link-local IP literals (covers AWS IMDS `169.254.169.254`, ECS `169.254.170.2`, and IPv6 `fe80::/10`) +- known cloud-metadata hostnames (`metadata.google.internal`, `metadata`, `instance-data`, `instance-data.ec2.internal`) + +Loopback and RFC1918 private addresses are intentionally **allowed** — FlightDeck is local-first, and self-hosted Slack / Discord receivers commonly live on private networks. Use HTTPS in production; HTTP is permitted for local relays and testing. + +Webhook payloads carry **promote / rollback metadata** including the actor, reason, environment, and release id. Treat any registered webhook URL as receiving the same audit-grade information the ledger holds. + **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)**. diff --git a/docs/cli.md b/docs/cli.md index 5591085..e531c98 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -15,10 +15,12 @@ serve` see [http-api.md](http-api.md). | `--version` | Print the installed version and exit | | `--help` | Print help for any command or subcommand | -All commands require a `flightdeck.yaml` in the working directory (or the default path +Most commands require `flightdeck.yaml` in the working directory (or the default path `./flightdeck.yaml`). Run `flightdeck init` to create one. **`flightdeck init`** writes the config, then loads it to migrate the ledger and (by default) import bundled pricing. +**`flightdeck demo`** is an exception: it creates a **temporary** workspace and does not read `./flightdeck.yaml` from your shell cwd. + ## Actor resolution Several commands that write to the audit ledger (`release promote`, `release rollback`, @@ -76,6 +78,27 @@ set **`database_url`** to a `postgresql://…` (or `postgres://…`) DSN and ins --- +## `flightdeck demo` + +Run the **examples/quickstart** workflow end-to-end in a **disposable temp directory**: **`init`** → custom **`pricing import`** (both YAMLs) → **`policy set`** → **`release register`** (both bundles) → substitute **`release_id`** placeholders in JSONL → **`runs ingest`** → **`release diff`** → **`release promote`** (baseline under policy) → **`release history`**. + +Does **not** require **`flightdeck.yaml`** in the current directory. Fixtures resolve in order: **`--quickstart-root`**, **`FLIGHTDECK_QUICKSTART_ROOT`**, **`examples/quickstart`** relative to a git checkout, then **`flightdeck/_bundled_quickstart`** packaged in the wheel (PyPI installs). + +```bash +flightdeck demo [--quickstart-root DIR] [--verify / --no-verify] [--doctor / --no-doctor] [--keep-workspace] +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `--quickstart-root` | (see above) | Directory containing `policy.yaml`, pricing YAMLs, `*-events.jsonl`, and `baseline-release` / `candidate-release` | +| `--verify` | off | Also run **`release verify`** on the baseline bundle (parity with **`flightdeck-quickstart-verify`**) | +| `--doctor` | off | Also run **`flightdeck doctor`** | +| `--keep-workspace` | off | Keep the temp workspace and print its path | + +On success, prints a short confirmation. Exit **0** on success, **1** on failure (same as subprocess failures from underlying CLI steps). + +--- + ## `flightdeck doctor` Run read-only health checks on the workspace ledger (SQLite file or PostgreSQL when @@ -412,6 +435,37 @@ flightdeck pricing show --provider PROVIDER --version VERSION Both flags are required. If the table does not exist, exits 1 with an error message. +### `flightdeck pricing check` + +Check the age of **`flightdeck-bundled-*`** pricing tables in the ledger. Prints one line +per bundled snapshot with its anchor date and approximate age. Non-bundled tables are +ignored. + +```bash +flightdeck pricing check [--max-age-days N] [--fail] +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `--max-age-days` | `90` | Threshold in days. Tables older than this print `STALE` to stderr (and count toward `--fail`). Tables at or under the limit print `OK`. | +| `--fail` | off | Exit 1 if any bundled table exceeds `--max-age-days`. Useful as a CI gate. | + +**Example output:** +``` +OK flightdeck-bundled-2026-05 (~11 days old; max 90) +``` + +If no `flightdeck-bundled-*` tables are in the ledger (e.g. after `flightdeck init --no-bundled-pricing`), +exits 0 and prints `No flightdeck-bundled-* pricing tables in the ledger.` + +Use in CI to surface stale bundled snapshots before they silently affect cost estimates: +```bash +flightdeck pricing check --max-age-days 90 --fail +``` + +See [pricing-catalog.md](pricing-catalog.md) for the bundled snapshot lifecycle and when +to replace with `flightdeck pricing import`. + --- ## `flightdeck policy` @@ -568,3 +622,19 @@ flightdeck doctor The `flightdeck-quickstart-verify` command (or `python -m flightdeck.quickstart_smoke`) runs this entire workflow end-to-end using the bundled example fixtures in `examples/quickstart/`. + +## `flightdeck webhook ...` + +Manage HMAC-signed outbound webhooks. Each subscription stores a per-webhook secret and +fires for one or more of `promote.succeeded`, `rollback.succeeded`, `promote.blocked`. + +- `flightdeck webhook add --url URL --event EVENT [--event EVENT ...] [--description TEXT]` + Creates a subscription and prints the freshly generated secret **once** — save it, + it will not be shown again. +- `flightdeck webhook list` — tabular view of all subscriptions; secrets are redacted. +- `flightdeck webhook remove WEBHOOK_ID [--yes]` — delete (confirms unless `--yes`). +- `flightdeck webhook test WEBHOOK_ID` — POST a synthetic `test.ping` payload using the + same signing path; prints the HTTP status and the first 200 chars of the response body. + +The HTTP equivalents live under `/v1/webhooks` (see +[http-api.md](http-api.md#webhooks-post-v1webhooks-get-v1webhooks-delete-v1webhooksid)). diff --git a/docs/http-api.md b/docs/http-api.md index d6704eb..734eb89 100644 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -31,6 +31,7 @@ Two access tiers: | `POST /v1/promote` | loopback only | `Authorization: Bearer ` required | | `POST /v1/promote/request`, `POST /v1/promote/confirm` | loopback only | `Authorization: Bearer ` required | | `POST /v1/rollback` | loopback only | `Authorization: Bearer ` required | +| `POST /v1/webhooks`, `GET /v1/webhooks`, `DELETE /v1/webhooks/{id}` | loopback only | `Authorization: Bearer ` required | `POST /v1/events` uses the **same** loopback / Bearer gate as promote and rollback (`require_ledger_write_access` in `server/mutation_access.py`). **`GET /v1/*`** uses @@ -47,6 +48,22 @@ flightdeck serve See [SECURITY.md](../SECURITY.md) for the full trust model. +### Identity passthrough (audit `actor`) + +Mutating routes (`/v1/promote`, `/v1/promote/request`, `/v1/promote/confirm`, +`/v1/rollback`) prefer two request headers over the body `actor` field when +populating the audit ledger: + +| Header | Source | Precedence | +|--------|--------|-----------:| +| `X-FlightDeck-Actor` | Explicit caller-set (CI wrappers, GitHub Actions, scripts) | 1 | +| `X-Forwarded-User` | Reverse-proxy / SSO injection (oauth2-proxy, Pomerium, Authelia, Cloudflare Access, nginx `auth_request`) | 2 | +| body `actor` | Last-resort fallback (Pydantic default is `"http"`) | 3 | + +The header form lets an upstream auth layer authoritatively stamp the audit +row without trusting caller-controlled JSON. Both headers are stripped of +surrounding whitespace; whitespace-only values are ignored. + ## Base URL All paths below are relative to the server base URL, e.g. `http://127.0.0.1:8765`. @@ -691,6 +708,32 @@ Validation errors (Pydantic) return an array under `detail`: --- +## Webhooks (`POST /v1/webhooks`, `GET /v1/webhooks`, `DELETE /v1/webhooks/{id}`) + +Outbound HMAC-signed notifications for `promote.succeeded`, `rollback.succeeded`, and +`promote.blocked`. Same Bearer / loopback gate as `POST /v1/promote`. + +- **`POST /v1/webhooks`** — body `{url, events: [str], description?}`. Response is the full + webhook record including the freshly generated `secret` (this is the **only** time the + cleartext secret is returned — store it). +- **`GET /v1/webhooks`** — list. Secrets are redacted to `secret_preview` (e.g. `abc123…wxyz`). +- **`DELETE /v1/webhooks/{webhook_id}`** — delete. Returns `404` if not found. + +When an event fires, FlightDeck POSTs the JSON payload to every enabled subscribed webhook +(5 s timeout, 3 attempts with exponential backoff: 1 s, 2 s, 4 s) with these headers: + +| Header | Value | +|--------|-------| +| `X-FlightDeck-Signature` | `sha256=` — `HMAC-SHA256(secret, raw_body)` (GitHub convention) | +| `X-FlightDeck-Event` | event name (e.g. `promote.succeeded`) | +| `X-FlightDeck-Delivery` | per-delivery UUID for receiver-side deduplication | + +Verify on the receiver by re-computing `HMAC-SHA256` over the raw request body with the +shared secret and comparing against the header (constant-time compare). Delivery failures +are logged but never break the originating promote / rollback. + +--- + ## Interactive docs (Swagger UI) When the server is running, visit `http://127.0.0.1:8765/docs` for auto-generated diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..dc7a7dd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,82 @@ +# FlightDeck + +**Ship AI agents safely** — release diffs, runtime evidence, and policy gates. + +FlightDeck is a local-first CLI + SQLite + optional web UI that versions your agent builds, collects runtime evidence (cost, latency, errors), diffs a baseline against a candidate, and blocks promote until policy passes. Apache-2.0. + +--- + +## Try it — 30 seconds, no account needed + +```bash +pip install flightdeck-ai +flightdeck demo +``` + +`flightdeck demo` runs the full register → ingest → diff → promote loop in a temporary workspace. Nothing leaves your machine. + +--- + +## How it works + +1. **Register** an immutable release snapshot (`release.yaml` + checksum). +2. **Ingest** runtime evidence from your agent (cost, latency, errors, confidence). +3. **Diff** baseline vs candidate — cost per run, latency delta, error rate, pricing catalog. +4. **Promote** only when policy passes; optional human approval before the ledger moves. + +The same contract works from the CLI, the HTTP API (`POST /v1/promote`), and the bundled web UI (`flightdeck serve`). + +--- + +## Quick reference + +| Topic | Doc | +|--------|-----| +| Commands, flags, exit codes | [CLI reference](cli.md) | +| `flightdeck serve` JSON API | [HTTP API](http-api.md) | +| Diff, promote, rollback, SQLite | [Operations & policy](operations-and-policy.md) | +| `release.yaml`, workspace config | [Release artifact](release-artifact.md) | +| Optional pricing catalog YAML | [Pricing catalog](pricing-catalog.md) | +| `flightdeck` Python client | [Python SDK](sdk.md) | +| Experimental adoption hooks | [SDK integrations](sdk-integrations.md) | +| Shipped web UI vs roadmap | [Web UI](web-ui.md) · [UI roadmap](ui-roadmap.md) | +| Common failures | [Troubleshooting](troubleshooting.md) | + +Full examples and CI templates: [github.com/flightdeckdev/flightdeck/examples](https://github.com/flightdeckdev/flightdeck/tree/main/examples) + +--- + +## Who should use this? + +- **ML / platform engineering** teams shipping LLM agents to production who want a policy-gated promote path — not just a dashboard. +- **Regulated or compliance-sensitive** teams (fintech, healthcare) where data residency and audit trails matter. Local-first by default; self-host `flightdeck serve` to keep data on-prem. +- Engineers who want to answer "is this candidate safe to ship?" with **numbers and policy**, not gut feel. + +--- + +## Install options + +**User (recommended):** + +```bash +pip install flightdeck-ai +``` + +Optional extras: `flightdeck-ai[openai]`, `flightdeck-ai[anthropic]`, `flightdeck-ai[postgres]`, `flightdeck-ai[telemetry]` + +**Contributor (uv):** + +```bash +git clone https://github.com/flightdeckdev/flightdeck +cd flightdeck +uv sync --extra dev +uv run flightdeck --help +``` + +See [DEVELOPMENT.md](https://github.com/flightdeckdev/flightdeck/blob/main/DEVELOPMENT.md) for web bundle, schema generation, and CI parity. + +--- + +## Ask AI + +Use the floating **Ask AI** button (bottom-right) to open Perplexity with this docs site and the GitHub repo as context. No FlightDeck servers are involved. diff --git a/docs/javascripts/ask-ai.js b/docs/javascripts/ask-ai.js new file mode 100644 index 0000000..5031275 --- /dev/null +++ b/docs/javascripts/ask-ai.js @@ -0,0 +1,31 @@ +/* Floating “Ask AI” — opens Perplexity with repo + docs context (no FlightDeck servers). */ +(function () { + var url = + "https://www.perplexity.ai/search?q=" + + encodeURIComponent( + "FlightDeck: AI agent release governance (CLI, SQLite ledger, policy gates, diff, promote). Official docs site https://flightdeckdev.github.io/flightdeck/ and source https://github.com/flightdeckdev/flightdeck — answer using those when possible." + ); + + function addButton() { + if (document.getElementById("fd-floating-ask-ai")) { + return; + } + var a = document.createElement("a"); + a.id = "fd-floating-ask-ai"; + a.href = url; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + a.textContent = "Ask AI"; + a.setAttribute( + "aria-label", + "Ask AI about FlightDeck in Perplexity (new tab)" + ); + document.body.appendChild(a); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", addButton); + } else { + addButton(); + } +})(); diff --git a/docs/operations-and-policy.md b/docs/operations-and-policy.md index 0e53a88..cec11e0 100644 --- a/docs/operations-and-policy.md +++ b/docs/operations-and-policy.md @@ -521,7 +521,7 @@ endpoints (`GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`) and intern ## SQLite storage schema -The operations layer reads and writes seven tables (via `src/flightdeck/storage.py`): +The operations layer reads and writes eight tables (via `src/flightdeck/storage.py`): | Table | Purpose | |-------|---------| @@ -532,6 +532,7 @@ The operations layer reads and writes seven tables (via `src/flightdeck/storage. | `active_policy` | Single-row table holding the active `Policy` JSON | | `promoted_releases` | Current promoted pointer per `(agent_id, environment)` | | `release_actions` | Append-only audit ledger; `audit_seq` is monotonically increasing | +| `promotion_requests` | Pending / completed / cancelled approval requests (added in migration v4); used when `promotion_requires_approval: true` | `Storage.migrate()` runs forward-only numbered migrations. `flightdeck doctor` verifies that migrations are applied through `LATEST_SCHEMA_MIGRATION_VERSION` and that @@ -593,6 +594,7 @@ Migrations are numbered and forward-only; they are never reversed. | 1 | Initial schema (all base tables via `CREATE TABLE IF NOT EXISTS`) | | 2 | `CREATE INDEX … ON run_events(release_id, timestamp)` — speeds up diff/query | | 3 | `ALTER TABLE release_actions ADD COLUMN audit_seq INTEGER`; backfill existing rows; add unique index | +| 4 | `CREATE TABLE IF NOT EXISTS promotion_requests` — adds the approval request/confirm workflow (columns: `request_id`, `status`, `release_id`, `agent_id`, `environment`, `window`, `reason`, `actor`, `baseline_release_id`, `policy_result_json`, `created_at`, `resolved_at`, `completed_action_id`) | New migrations must increment `LATEST_SCHEMA_MIGRATION_VERSION` in `storage.py` and add a corresponding check in `test_schemas.py` (or `test_doctor.py`). diff --git a/docs/pricing-catalog.md b/docs/pricing-catalog.md index ce150c3..dc00b0c 100644 --- a/docs/pricing-catalog.md +++ b/docs/pricing-catalog.md @@ -24,7 +24,7 @@ your own YAML (and optionally **`--replace`** with **`--reason`**). Bundled table YAML in the wheel includes **comment links** to each provider’s official list-pricing page so you can spot-check rates between FlightDeck releases. -**Staleness guardrails:** list prices change often. Run **`flightdeck pricing check`** to see whether any **`flightdeck-bundled-*`** table in the ledger is older than **`--max-age-days`** (default **90**); pass **`--fail`** for CI. **`flightdeck release diff`** and **`POST /v1/diff`** add **`pricing.warnings`** when baseline or candidate **`pricing_version`** is a stale bundled snapshot so economics do not look authoritative after the snapshot has aged out. +**Staleness guardrails:** list prices change often. Run **`flightdeck pricing check`** to see whether any **`flightdeck-bundled-*`** table in the ledger is older than **`--max-age-days`** (default **90**); pass **`--fail`** for CI. **`flightdeck release diff`** and **`POST /v1/diff`** add entries to **`pricing.warnings`** when baseline or candidate **`pricing_version`** is a stale bundled snapshot so economics do not look authoritative after the snapshot has aged out. See [cli.md § flightdeck pricing check](cli.md#flightdeck-pricing-check) for the full option reference. **Maintainer cadence:** the bundled snapshot is **updated on each minor release** when vendor public list pricing changes materially (see **[ROADMAP.md](../ROADMAP.md)**). Operators in production should still treat **`flightdeck pricing import`** as the source of truth. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..f8cccf6 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +# Pinned for reproducible GitHub Pages builds (not part of the Python package). +# Transitive pin for pymdown-extensions guards against the strict-mode build +# failing on a silent transitive bump (reviewer MAJOR-6). +mkdocs==1.6.1 +mkdocs-material==9.6.14 +pymdown-extensions==10.21.3 diff --git a/docs/screenshots/actions.png b/docs/screenshots/actions.png new file mode 100644 index 0000000..6c91034 Binary files /dev/null and b/docs/screenshots/actions.png differ diff --git a/docs/screenshots/dark-mode.png b/docs/screenshots/dark-mode.png new file mode 100644 index 0000000..75d4e50 Binary files /dev/null and b/docs/screenshots/dark-mode.png differ diff --git a/docs/screenshots/diff-result.png b/docs/screenshots/diff-result.png new file mode 100644 index 0000000..038c9d5 Binary files /dev/null and b/docs/screenshots/diff-result.png differ diff --git a/docs/screenshots/flightdeck-demo.webm b/docs/screenshots/flightdeck-demo.webm new file mode 100644 index 0000000..1d114ca Binary files /dev/null and b/docs/screenshots/flightdeck-demo.webm differ diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png new file mode 100644 index 0000000..dbd85d0 Binary files /dev/null and b/docs/screenshots/overview.png differ diff --git a/docs/screenshots/runs.png b/docs/screenshots/runs.png new file mode 100644 index 0000000..89f4f12 Binary files /dev/null and b/docs/screenshots/runs.png differ diff --git a/docs/sdk-integrations.md b/docs/sdk-integrations.md index 7c0a329..da27799 100644 --- a/docs/sdk-integrations.md +++ b/docs/sdk-integrations.md @@ -52,6 +52,100 @@ batch processor. Set **`OTEL_EXPORTER_OTLP_ENDPOINT`** (for example FlightDeck does not auto-instrument **`httpx`** or the Python SDK; create spans in your app or attach upstream auto-instrumentation if you need request-level traces. +## Module reference + +Each submodule under `flightdeck.integrations` has a single responsibility: map +third-party SDK output into a `RunEvent`. Import only the submodule you need. + +### `flightdeck.integrations.common` (no extras required) + +Available as `from flightdeck.integrations import make_run_end_event, temporal_labels`. + +#### `make_run_end_event(**kwargs) -> RunEvent` + +Convenience constructor for a `type=run_end` `RunEvent`. All named parameters map +directly to fields on the v1 wire shape: + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `agent_id` | yes | Stable agent ID | +| `release_id` | yes | Release ID from `flightdeck release register` | +| `run_id` | yes | Unique identifier; duplicates are skipped at ingest | +| `tenant_id` | yes | Tenant scoping dimension | +| `task_id` | yes | Task type dimension | +| `environment` | yes | Deployment environment | +| `provider` | yes | LLM provider (e.g. `"openai"`) | +| `model` | yes | Model name (e.g. `"gpt-4o"`) | +| `input_tokens` | yes | Prompt token count | +| `output_tokens` | yes | Completion token count | +| `cached_input_tokens` | no | Cached-prompt token count (default `0`) | +| `latency_ms` | no | End-to-end latency in milliseconds | +| `success` | no | Whether the run succeeded (default `True`) | +| `error_type` | no | Optional error class string | +| `trace_id`, `session_id`, `span_id` | no | Tracing identifiers (stored in `request.*`) | +| `labels` | no | Arbitrary string labels dict | +| `timestamp` | no | Event timestamp (defaults to `datetime.now(UTC)`) | +| `workspace_id` | no | Workspace identifier (default `"ws_local"`) | + +#### `temporal_labels(*, workflow_id, workflow_run_id=None) -> dict[str, str]` + +Returns a `labels` dict with `temporal.workflow_id` (and optionally `temporal.run_id`) +for tagging run events emitted from Temporal workflows. Pass the result as the `labels=` +argument to `make_run_end_event`. + +### `flightdeck.integrations.openai_chat` (no extra needed; `openai` extra for the SDK itself) + +#### `run_event_from_openai_chat_completion(response, *, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from an `openai.types.chat.ChatCompletion` response object. +Extracts `model`, `input_tokens`, `output_tokens`, and `cached_input_tokens` from +`response.usage`. Extra `kwargs` are passed to `make_run_end_event` (e.g. `latency_ms`, +`trace_id`). See `examples/integration/adoption/openai_chat/emit_run.py`. + +### `flightdeck.integrations.anthropic_messages` (no extra needed; `anthropic` extra for the SDK itself) + +#### `run_event_from_anthropic_message(message, *, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from an `anthropic.types.Message` object. Extracts `model`, +`input_tokens`, `output_tokens`, and `cache_read_input_tokens` from `message.usage`. +See `examples/integration/adoption/anthropic_messages/emit_run.py`. + +### `flightdeck.integrations.openai_agents` (`integrations-openai-agents` extra) + +#### `run_event_from_openai_agents_result(result, *, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from an OpenAI Agents SDK `RunResult` (or compatible object). +Aggregates token usage across all items in `result.raw_responses`. See +`examples/integration/adoption/openai_agents/emit_run.py`. + +### `flightdeck.integrations.langchain_callback` (`integrations-langchain` extra) + +#### `FlightDeckLangChainCallbackHandler` + +A `BaseCallbackHandler` subclass. Pass an instance to LangChain chains or agents as +`callbacks=[handler]`. On `on_llm_end`, extracts token usage from the LLM result and +appends a `RunEvent` to `handler.events` (a list). After the chain completes, call +`client.ingest_run_events(handler.events)`. Constructor parameters: + +| Parameter | Description | +|-----------|-------------| +| `agent_id` | Stable agent ID | +| `release_id` | Release ID | +| `run_id` | Unique run identifier (used for all events this handler captures) | +| `tenant_id`, `task_id`, `environment` | Standard scoping dimensions | + +See `examples/integration/adoption/langchain/emit_run.py`. + +### `flightdeck.integrations.crewai_bridge` (no extra; install `crewai` in your app env) + +#### `run_event_from_crew_token_totals(input_tokens, output_tokens, *, model, provider, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from manually collected CrewAI token totals (no direct dependency +on CrewAI's internal classes). Collect totals from your crew's result callbacks and pass +them here. See `examples/integration/adoption/crewai/emit_totals.py`. + +--- + ## Trust boundaries Anyone who can reach **`POST /v1/events`** can append ledger rows. Keep **`flightdeck serve`** @@ -61,6 +155,88 @@ on loopback or a private network unless you add your own controls. See **[SECURI Copy-paste scripts: **[examples/integration/adoption/](../examples/integration/adoption/README.md)**. +## Outbound webhooks — Slack, Discord, PagerDuty, Linear, … + +FlightDeck fires HMAC-signed POSTs to any URL you register for the events +`promote.succeeded`, `rollback.succeeded`, and `promote.blocked`. The +payload shape is **generic JSON** (envelope: `event`, `delivery_id`, +`created_at`, `data`) — most chat / on-call tools want a vendor-specific +shape, so the canonical pattern is a **3-line adapter** in front of the +webhook URL. + +### Slack + +Slack [incoming webhooks](https://api.slack.com/messaging/webhooks) accept +a `{"text": "..."}` body. Use [webhook.site](https://webhook.site/) or +[Pipedream](https://pipedream.com/) (free tier) as the adapter, or a tiny +Cloudflare Worker / AWS Lambda: + +```js +// Cloudflare Worker — receives FlightDeck, forwards to Slack +export default { + async fetch(req, env) { + const evt = await req.json(); + const text = `:rocket: *${evt.event}* — release ${evt.data.release_id} ` + + `(${evt.data.environment}) by ${evt.data.actor}\n` + + `_${evt.data.reason}_`; + await fetch(env.SLACK_WEBHOOK_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text }), + }); + return new Response("ok"); + }, +}; +``` + +Then register the Worker URL with FlightDeck: + +```bash +flightdeck webhook add \ + --url https://flightdeck-slack.YOUR-SUBDOMAIN.workers.dev \ + --event promote.succeeded \ + --event rollback.succeeded \ + --event promote.blocked \ + --description "Slack #releases" +``` + +The Worker should also **verify the `X-FlightDeck-Signature` header** +against the per-webhook secret (`hmac.sha256(secret, raw_body)`) — same +shape as GitHub webhooks. See `src/flightdeck/webhooks.py::sign_payload` +for the canonical signing function. + +### Discord + +Discord webhook URLs accept `{"content": "..."}`. Same adapter pattern as +Slack; swap the body to `{ content: text }` and set +`https://discord.com/api/webhooks/...` as the destination. + +### PagerDuty + +For incidents on `rollback.succeeded` or `promote.blocked`, the adapter +posts to the [PagerDuty Events API v2](https://developer.pagerduty.com/api-reference/YXBpOjI3NDgyNjU-pager-duty-v2-events-api) +with `event_action: "trigger"`, `severity` mapped from the event name, +and `payload.summary` derived from `data.reason` + `data.release_id`. + +### Linear / Jira / Asana + +For project-tracker auto-comments, the adapter looks up the ticket id in +`data.reason` (e.g. `"hot-fix for issue #1234"`), then posts a comment via +the tracker's REST API. + +### Verifying signatures (any language) + +```python +import hmac, hashlib + +def verify(secret: str, raw_body: bytes, signature_header: str) -> bool: + expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, signature_header) +``` + +Always use `hmac.compare_digest` (or your language's equivalent) — a +string `==` comparison is timing-attack vulnerable. + ## Policy boundary (contributors) Contributor rules in **`AGENTS.md`** distinguish **in-product agent frameworks** (non-goals) from diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..06c73d6 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,19 @@ +/* Floating Perplexity shortcut (paired with docs/javascripts/ask-ai.js) */ +#fd-floating-ask-ai { + position: fixed; + bottom: 1.25rem; + right: 1.25rem; + z-index: 200; + padding: 0.45rem 1rem; + border-radius: 999px; + background: var(--md-primary-fg-color); + color: var(--md-primary-bg-color) !important; + font-weight: 600; + font-size: 0.8rem; + box-shadow: var(--md-shadow-z2); + text-decoration: none !important; +} + +#fd-floating-ask-ai:hover { + opacity: 0.92; +} diff --git a/docs/web-ui.md b/docs/web-ui.md index 5dd4df4..f69d27f 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -20,7 +20,7 @@ The **[README product overview](../README.md#product-overview)** image is a **ma | Art direction | Application in this repo | |---------------|---------------------------| -| Dark navy / near-black shell | **`html[data-theme="dark"]`** in `web/src/index.css` mirrors semantic tokens; **Appearance** control in the sidebar defaults to **Light** (stored under **`localStorage`** key **`flightdeck-theme`**). | +| Dark navy / near-black shell | **`html[data-theme="dark"]`** in `web/src/index.css` mirrors semantic tokens; **Theme** (sun / moon / monitor icons) in the sidebar **Settings** popover defaults to **Light** (stored under **`localStorage`** key **`flightdeck-theme`**). | | Cyan → purple gradient | CSS variables (for example `--fd-accent-gradient`) for **active nav**, **primary buttons**, and **focus-visible** accents—used sparingly so trust/safety UI stays calm. | | High-contrast titles | Tune `--fd-type-*` and weights under dark mode; avoid shrinking body text for density. | | “Neon” feel | Reserve for **interactive** states, not large background fills. | @@ -30,7 +30,7 @@ The **[README product overview](../README.md#product-overview)** image is a **ma 1. **Token foundation** — Extend `:root` with any missing semantics (`--fd-surface-elevated`, gradient stops, optional `--fd-bg-subtle`). Replace scattered literals in `web/src/index.css` (for example warning callout backgrounds) with variables so dark mode does not require hunting hex values. 2. **`[data-theme="dark"]` block** — Mirror every semantic token used by `.fd-shell`, sidebar, cards, tables, `Badge`, drawers, and `JsonPanel`; set `color-scheme: dark` on `html` when active. Validate **WCAG AA** for body text and links. -3. **Preference UI** — **`/#/settings`** (and room for more prefs later): **Light** / **Dark** / **System**; listen to `prefers-color-scheme` when System is selected. Persist `localStorage` key **`flightdeck-theme`** (`light` \| `dark` \| `system`). +3. **Preference UI** — **Sidebar Settings** popover (and room for more prefs later): **Theme** as sun / moon / monitor icon radios for **Light** / **Dark** / **System**; listen to `prefers-color-scheme` when System is selected. Persist `localStorage` key **`flightdeck-theme`** (`light` \| `dark` \| `system`). Legacy **`/#/settings`** redirects to **`#/`**. 4. **Brand accents** — Apply the gradient token to **active** `.fd-nav__link--active` (left rail) and primary submit-style buttons; keep destructive actions on existing red semantics. 5. **Light theme polish** — Even before dark ships: align spacing rhythm and card shadows with the same tokens so both themes stay maintainable. 6. **Verification** — From `web/`: **`npm ci`**, **`npm run build`**, commit **`src/flightdeck/server/static/`**; **`npm run test:e2e`** (includes **`e2e/theme.spec.ts`**: default light, dark persistence, system / `prefers-color-scheme`, overview smoke in dark). Manually smoke **Diff** and **Actions** in both themes (policy panels, JSON drawer, rollback affordances). @@ -54,11 +54,11 @@ The app uses **HashRouter** (`react-router-dom`) so all navigation stays within | Hash path | Component | HTTP calls | Notes | |-----------|-----------|-----------|-------| -| `#/` | `OverviewPage` | `GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`, `GET /v1/metrics` (parallel where applicable) | Ledger metrics (read-only); short per-counter hints; skeleton on first load; **auto-refresh** every 30s when the tab is visible + on timeline **`generation`** bump; links to Diff/Runs | -| `#/diff` | `DiffPage` | `POST /v1/diff` | Sections: policy gate (incl. `evaluated_at`), evidence window, pricing/catalog/hints (incl. provider/version skew callout when sides differ), per-1k prices when present, cost/quality rollups; raw JSON panel | +| `#/` | `OverviewPage` | `GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`, `GET /v1/metrics` | `ReleaseLifecycleStrip` + optional `?release=` hero; promoted table first; releases table with filter row and copy/diff shortcuts; collapsible ledger metrics; **auto-refresh** every 30 s while tab is visible + on timeline **`generation`** bump | +| `#/diff` | `DiffPage` | `POST /v1/diff` | URL params prefill form (`baseline`, `candidate`, `window`, `environment`); result rendered through `DiffVerdictStack` → `DiffReleaseTwin` → `DiffPolicyPanel` → `DiffChangeImpact` (with collapsible `DiffPricingExpand`) → `DiffDecisionCard` + **Continue to promote** link → raw JSON panel | | `#/runs` | `RunsPage` | `GET /v1/releases` (for datalist), `GET /v1/runs`, `GET /v1/runs/export` | Forensics: filters, table (trace/status, trace band rows or **Group by trace_id**), **View** drawer (focus trap, session/span ids), typed **run-query error** card with **Retry**, empty/offset/truncation hints, NDJSON download | -| `#/settings` | `SettingsPage` | *(none)* | **Color theme** (Light / Dark / System) via `ThemeToggle`; more preferences later. | -| `#/actions` | `ActionsPage` | `GET /v1/workspace`, `GET /v1/promotion-requests` (when `promotion_requires_approval`), `POST /v1/promote` **or** `POST /v1/promote/request` + `POST /v1/promote/confirm`, `POST /v1/rollback` | Workspace skeleton then strip; approval path: numbered steps, pending **Refresh list** / **Use for confirm**; **Rollback** danger-styled; see **ActionsPage** below | +| `#/settings` | *(redirect)* | — | Redirects to **`#/`**; **Theme** (Light / Dark / System) lives in the **sidebar Settings** dialog (`SidebarSettingsMenu` + icon `ThemeToggle`). | +| `#/actions` | `ActionsPage` | `GET /v1/workspace`, `GET /v1/promotion-requests` (when `promotion_requires_approval`), `POST /v1/promote` **or** `POST /v1/promote/request` + `POST /v1/promote/confirm`, `POST /v1/rollback` | URL params prefill form (`release_id`, `environment`, `window`); workspace skeleton then strip; approval path: numbered steps, pending **Refresh list** / **Use for confirm**; **Rollback** danger-styled; see **ActionsPage** below | | `#/*` (any other) | — | Redirects to `#/` | | `App.tsx` declares the route tree. `AppShell` is the layout wrapper rendered for all routes. @@ -81,17 +81,29 @@ ThemePreferenceProvider (`App.tsx`) ├── aside.fd-sidebar (brand, collapse chevron, primary nav, footer nav → Settings) └── div.fd-shell__content ├── SecurityStatusBar - └── main#main-content → OverviewPage | DiffPage | RunsPage | ActionsPage | SettingsPage + └── main#main-content + ├── OverviewPage + │ ├── ReleaseLifecycleStrip + │ └── focused release hero (when ?release= is set) + ├── DiffPage + │ ├── DiffVerdictStack + │ ├── DiffReleaseTwin + │ ├── DiffPolicyPanel + │ ├── DiffChangeImpact → DiffPricingExpand + │ ├── DiffDecisionCard + │ └── JsonPanel + ├── RunsPage + └── ActionsPage ``` --- ## `AppShell` (`web/src/components/AppShell.tsx`) -Renders a fixed-width **left sidebar** (`aside.fd-sidebar`) with brand (gradient **FlightDeck** wordmark, mark in a **raised tile**), a **collapse** control (SVG chevrons, `localStorage` **`flightdeck-sidebar-collapsed`**), a **primary** nav (inline SVG icons + labels; icon-only when collapsed), and a **footer** nav pinned to the bottom of the rail with **Settings** → `#/settings`. Then a **`fd-shell__content`** column with `SecurityStatusBar` and +Renders a fixed-width **left sidebar** (`aside.fd-sidebar`) with brand (gradient **FlightDeck** wordmark, mark in a **raised tile**), a **collapse** control (SVG chevrons, `localStorage` **`flightdeck-sidebar-collapsed`**), a **primary** nav (inline SVG icons + labels; icon-only when collapsed), and a **footer** **Settings** control that opens a compact **Settings** popover (portal, **Theme** row with sun / moon / monitor icon radios). Legacy `#/settings` redirects to `#/`. Then a **`fd-shell__content`** column with `SecurityStatusBar` and `
` wrapping an `` for the active page. On narrow viewports the sidebar stacks above the content with a horizontal nav row; a **collapsed** rail is expanded back to full labels in that breakpoint. Wraps the subtree in `TimelineRefreshProvider` -so any descendant can access the refresh context. `ThemePreferenceProvider` (from `App.tsx`) wraps the router so `ThemeToggle` on **Settings** can read and update **`flightdeck-theme`**; `main.tsx` applies the effective theme before the first paint to avoid a flash of the wrong scheme. +so any descendant can access the refresh context. `ThemePreferenceProvider` (from `App.tsx`) wraps the router so `ThemeToggle` in the popover can read and update **`flightdeck-theme`**; `main.tsx` applies the effective theme before the first paint to avoid a flash of the wrong scheme. A **Skip to main content** link (class `fd-skip-link`) appears first in the shell; it uses `preventDefault` + `focus()` on `#main-content` so **HashRouter** hash URLs (`#/…`) are not @@ -172,20 +184,50 @@ fail. This is a configuration hint only — the server enforces the actual gate. ## `OverviewPage` (`web/src/pages/OverviewPage.tsx`) -Read-only dashboard. Renders a **Ledger metrics** card from `fetchMetrics()` plus three tables from `loadTimeline()` output: +Read-only dashboard. Layout: -| Block | Source | Content | -|-------|--------|---------| -| Ledger metrics | `GET /v1/metrics` | Releases, pricing tables, run events, promoted pointers, and actions totals (plus `actions_by_action` breakdown), `schema_version`, `generated_at` | -| Releases | `GET /v1/releases` | Release ID, Agent, Version, Environment, Checksum, Created | -| Promoted | `GET /v1/promoted` | Agent, Environment, Active release | -| Recent actions | `GET /v1/actions` | When, Action, Policy (PASS/FAIL badge), Release, Environment, Reason | +1. **`ReleaseLifecycleStrip`** — horizontal workflow guide showing the four stages + (Register → Ingest → Diff & policy → Promote & rollback) as linked steps. Each step + links to the relevant page; the Promote step is static (no link) in read-only builds. + Includes a note that deep links prefill forms but do not auto-submit. + +2. **Focused release hero** — when `?release=` is present in the URL, a hero + section appears above the tables. It shows agent, version, environment, abbreviated + release ID (with **Copy ID** button), checksum, and the current promoted baseline for + that agent/environment pair (or a note that no pointer exists). Action buttons link to + Diff, Runs, and Promote with the release, environment, and a default `7d` window + pre-filled. A **Clear focus** button removes the `?release=` param. If the ID does not + match any registered release, a warning is shown instead. + +3. **Promoted releases table** — lists current `(agent_id, environment)` → `release_id` + pointers. Each row has a **View** link to `#/?release=` to focus that release. + +4. **Releases table** — lists all registered releases with Agent, Version, Environment, ID, + Checksum, and Created columns. A **Status** badge shows **Live** (the release matches the + current promoted pointer for that agent/environment) or **Registered**. A filter row + (agent substring, environment substring, and Live / Not live / All dropdown) reduces the + table without re-fetching. **Copy** buttons (via `CopyTextButton`) copy the release ID. + Each row has a **Diff** shortcut (links to `#/diff` with baseline = promoted pointer, + candidate = this release, environment and `7d` window pre-filled) and a **Focus** link. + +5. **Recent actions table** — promote/rollback audit rows: When, Action, Policy badge, + Release, Environment, Reason. + +6. **Ledger metrics** — collapsible panel (collapsed by default, toggle via button). Shows + raw counters from `GET /v1/metrics`: releases, pricing tables, run events, promoted + pointers, actions totals + breakdown, `schema_version`, `generated_at`. Long IDs are abbreviated with `shortId(id, keepStart, keepEnd)` and shown in full on hover via the HTML `title` attribute. +**URL params for OverviewPage:** + +| Param | Effect | +|-------|--------| +| `?release=` | Activates the focused release hero. The releases table filter and tables remain visible below. | + **Refresh:** while the document tab is visible, the page **auto-polls** metrics and the -timeline on an interval and uses **silent** fetches after the first load. The `generation` +timeline every 30 s and uses **silent** fetches after the first load. The `generation` counter from `TimelineRefreshContext` triggers an immediate refresh after mutations from `ActionsPage`. @@ -193,14 +235,18 @@ counter from `TimelineRefreshContext` triggers an immediate refresh after mutati ## `DiffPage` (`web/src/pages/DiffPage.tsx`) -Form-based interface for `POST /v1/diff`. Fields mirror the request body: +Form-based interface for `POST /v1/diff`. The page reads initial field values from URL +search params and writes them back on each submission, enabling **deep links** that +pre-fill the form: -| Field | Default | Maps to | -|-------|---------|---------| -| Baseline release ID | (empty) | `baseline_release_id` | -| Candidate release ID | (empty) | `candidate_release_id` | -| Window | `7d` | `window` | -| Environment | `local` | `environment` (sent as `null` when empty) | +| URL param | Form field | Default | +|-----------|-----------|---------| +| `baseline` | Baseline release ID | (empty) | +| `candidate` | Candidate release ID | (empty) | +| `window` | Time window | `7d` | +| `environment` | Environment | `local` | + +Example: `#/diff?baseline=rel_abc&candidate=rel_xyz&window=7d&environment=production` `tenant_id` and `task_id` are **not exposed** in the UI form. To run a diff narrowed to a specific tenant or task, use the CLI (`flightdeck release diff --tenant --task `) @@ -209,25 +255,25 @@ or call `POST /v1/diff` directly with the `tenant_id` and `task_id` fields. See [operations-and-policy.md § compute_diff vs. promote_release filter scope](operations-and-policy.md#compute_diff-vs-promote_release--rollback_release-filter-scope) for details on what those filters affect. -On submit, the raw diff response is parsed and rendered as: - -- **Summary card:** policy badge (PASS / FAIL), failure reasons list, sample counts and - confidence label (including `confidence_reason` when present). -- **Pricing table warnings:** when `pricing.warnings` is a non-empty string array, a - `fd-alert--warn` list is shown above the pricing/model-change banner (diagnostic only). -- **Catalog / hints:** when `pricing.catalog` or `pricing.hints` is present, the UI surfaces - catalog enabled state, lines, and hint strings (see [pricing-catalog.md](pricing-catalog.md)). -- **Pricing change warning:** when the diff response includes a `pricing` block with - `pricing_or_model_changed: true`, a `fd-alert--warn` banner is shown in the summary - card. It names the baseline and candidate provider/version/model so the user knows the - cost delta includes pricing assumption changes, not just usage changes. When the response - also includes a `pricing.prices` block with all four per-1k token rates present, the - banner additionally shows a **Per-1k token prices** line (baseline → candidate, input and - output separately) so the user can separate tariff moves from token volume changes in the - cost delta. Rates are rendered to six decimal places via `toFixed(6)`. -- **Metric cards:** cost/run (USD), latency avg (ms), error rate — each showing baseline, - candidate, and delta. -- **Raw diff JSON** panel (collapsed by default via `JsonPanel`). +On submit, the response is parsed via helpers in `diffPayload.tsx` and rendered through a +sequence of dedicated components: + +1. **`DiffVerdictStack`** — full-width strip at the top. Shows a **Blocked** banner with the + first policy reason when policy fails, then a **verdict strip** (green PASS / red FAIL + with a short narrative). If the diff response contains no `policy` block, a warning is + shown instead. +2. **`DiffReleaseTwin`** — side-by-side baseline vs candidate IDs, environment, window, and + resolved `provider/version model` lines from each side's pricing block. +3. **`DiffPolicyPanel`** — card showing the policy PASS/FAIL badge, `evaluated_at` + timestamp, and full reasons list. +4. **`DiffChangeImpact`** — card with three sub-sections: + - **Sample coverage** — baseline/candidate run counts and confidence label (with `confidence_reason` when present). + - **Cost and quality rollups** — `DiffMetric` cards for cost/run (USD), latency avg (ms), error rate, each with baseline → candidate and delta. + - **`DiffPricingExpand`** — collapsible pricing & model section (collapsed on each new diff result). Shows baseline vs candidate `provider/version model` inline. Expands to reveal: provider/version skew warning, `pricing.warnings` list, `pricing.hints` list, pricing catalog detail (when enabled), and per-1k token prices (input/output, baseline → candidate) when all four rates are present and pricing changed. +5. **`DiffDecisionCard`** — summarizes the gate outcome in plain English and, when policy + passes and the candidate release ID is known, shows a **Continue to promote** link to + `#/actions` with `release_id`, `environment`, and `window` pre-filled. +6. **Raw diff JSON** panel (`JsonPanel`, collapsed by default). The **Compute diff** button is disabled while the request is in flight (`busy` state). Errors from the API are shown as an inline `fd-alert--error` element. @@ -235,6 +281,23 @@ Errors from the API are shown as an inline `fd-alert--error` element. Note: `POST /v1/diff` is a **read-only computation** and does not require a mutation token. See [http-api.md](http-api.md) for the full response schema. +### Diff component subtree + +``` +DiffPage +├── DiffVerdictStack (full-width verdict/block strip) +├── DiffReleaseTwin (baseline vs candidate identity, env, pricing line) +├── DiffPolicyPanel (policy badge + reasons) +├── DiffChangeImpact (samples, metric rollups, expandable pricing) +│ └── DiffPricingExpand (collapsed; shows per-1k prices, warnings, catalog) +├── DiffDecisionCard (verdict copy + "Continue to promote" link) +└── JsonPanel (raw diff JSON, collapsed by default) +``` + +Shared data extraction: `web/src/components/diff/diffPayload.tsx` exports typed helpers +(`pickPolicy`, `pickPricing`, `pricingLine`, `DiffMetric`) that isolate JSON traversal from +rendering. + --- ## `ActionsPage` (`web/src/pages/ActionsPage.tsx`) @@ -276,6 +339,27 @@ After a successful **promote** or **rollback** (or **confirm**): --- +## `urlSearch.ts` (`web/src/urlSearch.ts`) + +Helpers for hash-router deep-linking. Both `DiffPage`, `OverviewPage`, `RunsPage`, and +`ActionsPage` use these to read from and write to `URLSearchParams`: + +| Export | Description | +|--------|-------------| +| `pickTrimmedSearch(searchParams, key)` | Returns `searchParams.get(key)?.trim() ?? ""`. Never returns `null`. | +| `searchParamsFromRecord(rec)` | Builds a `?key=value` string from a `Record`, omitting entries with empty values. Returns `""` when all values are empty. | + +**Deep-link examples:** + +| Page | URL | Effect | +|------|-----|--------| +| Overview | `#/?release=rel_abc123` | Activates focused release hero | +| Diff | `#/diff?baseline=rel_a&candidate=rel_b&window=7d&environment=production` | Pre-fills the diff form | +| Runs | `#/runs?release_id=rel_abc&window=24h&environment=staging` | Pre-fills release and filters | +| Actions | `#/actions?release_id=rel_abc&environment=production&window=7d` | Pre-fills promote/rollback form | + +--- + ## `api.ts` (`web/src/api.ts`) Typed client helpers shared across pages. @@ -371,6 +455,29 @@ Calls `GET /v1/promotion-requests` with optional query parameters. Used by `Acti ## Shared components +### `CopyTextButton` (`web/src/components/CopyTextButton.tsx`) + +Inline button that copies a string to the clipboard. Uses `navigator.clipboard.writeText` +with an `execCommand` fallback for headless or insecure contexts (so Playwright E2E tests +also work). Status cycles through `idle → "Copied" → idle` (2 s) or `idle → "Failed" → +idle` (2.5 s). Props: + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `label` | string | — | Accessible label prefix (e.g. `"Release ID"`) | +| `value` | string | — | String to copy | +| `buttonText` | string | `"Copy"` | Visible button text when idle | +| `className` | string | `"fd-btn fd-btn--ghost fd-copy-btn"` | CSS class | +| `testId` | string | — | Optional `data-testid` for E2E | + +### `ReleaseLifecycleStrip` (`web/src/components/ReleaseLifecycleStrip.tsx`) + +Horizontal `