keyfleet 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. keyfleet-0.1.0/.gitattributes +4 -0
  2. keyfleet-0.1.0/.github/workflows/ci.yml +28 -0
  3. keyfleet-0.1.0/.github/workflows/release.yml +38 -0
  4. keyfleet-0.1.0/.gitignore +16 -0
  5. keyfleet-0.1.0/.pre-commit-config.yaml +14 -0
  6. keyfleet-0.1.0/.python-version +1 -0
  7. keyfleet-0.1.0/AGENTS.md +100 -0
  8. keyfleet-0.1.0/CHANGELOG.md +61 -0
  9. keyfleet-0.1.0/CLAUDE.md +34 -0
  10. keyfleet-0.1.0/CONTRIBUTING.md +75 -0
  11. keyfleet-0.1.0/LICENSE +201 -0
  12. keyfleet-0.1.0/PKG-INFO +181 -0
  13. keyfleet-0.1.0/PLAN.md +58 -0
  14. keyfleet-0.1.0/README.md +155 -0
  15. keyfleet-0.1.0/SECURITY.md +29 -0
  16. keyfleet-0.1.0/docs/ASSUMPTIONS.md +24 -0
  17. keyfleet-0.1.0/docs/DECISIONS.md +94 -0
  18. keyfleet-0.1.0/docs/SERVICES.md +45 -0
  19. keyfleet-0.1.0/docs/demo.gif +0 -0
  20. keyfleet-0.1.0/keyfleet-BRIEF.md +197 -0
  21. keyfleet-0.1.0/keyfleet.example.yaml +99 -0
  22. keyfleet-0.1.0/pyproject.toml +61 -0
  23. keyfleet-0.1.0/schema/keyfleet.schema.json +535 -0
  24. keyfleet-0.1.0/scripts/demo_ledger.yaml +79 -0
  25. keyfleet-0.1.0/scripts/gen_demo_gif.py +171 -0
  26. keyfleet-0.1.0/scripts/gen_schema.py +26 -0
  27. keyfleet-0.1.0/scripts/gen_services_md.py +24 -0
  28. keyfleet-0.1.0/src/keyfleet/__init__.py +5 -0
  29. keyfleet-0.1.0/src/keyfleet/bundled.py +243 -0
  30. keyfleet-0.1.0/src/keyfleet/checks.py +263 -0
  31. keyfleet-0.1.0/src/keyfleet/cli.py +196 -0
  32. keyfleet-0.1.0/src/keyfleet/crypto.py +67 -0
  33. keyfleet-0.1.0/src/keyfleet/data/advisories.yaml +32 -0
  34. keyfleet-0.1.0/src/keyfleet/data/example.yaml +99 -0
  35. keyfleet-0.1.0/src/keyfleet/data/models.yaml +52 -0
  36. keyfleet-0.1.0/src/keyfleet/data/services.yaml +271 -0
  37. keyfleet-0.1.0/src/keyfleet/impact.py +86 -0
  38. keyfleet-0.1.0/src/keyfleet/model.py +418 -0
  39. keyfleet-0.1.0/src/keyfleet/report.py +430 -0
  40. keyfleet-0.1.0/tests/conftest.py +70 -0
  41. keyfleet-0.1.0/tests/fixtures/bad_ref.yaml +10 -0
  42. keyfleet-0.1.0/tests/fixtures/dup_ids.yaml +4 -0
  43. keyfleet-0.1.0/tests/fixtures/min_keys_gap.yaml +38 -0
  44. keyfleet-0.1.0/tests/fixtures/not_yaml.yaml +2 -0
  45. keyfleet-0.1.0/tests/fixtures/secret_codes.yaml +15 -0
  46. keyfleet-0.1.0/tests/fixtures/secret_seed.yaml +12 -0
  47. keyfleet-0.1.0/tests/fixtures/valid.yaml +71 -0
  48. keyfleet-0.1.0/tests/golden/check_min_keys_gap.json +50 -0
  49. keyfleet-0.1.0/tests/golden/check_min_keys_gap.txt +9 -0
  50. keyfleet-0.1.0/tests/golden/lost_k_main.md +8 -0
  51. keyfleet-0.1.0/tests/golden/report_valid.md +27 -0
  52. keyfleet-0.1.0/tests/test_advisories.py +84 -0
  53. keyfleet-0.1.0/tests/test_bundled.py +113 -0
  54. keyfleet-0.1.0/tests/test_check_capacity.py +109 -0
  55. keyfleet-0.1.0/tests/test_check_lost_retired.py +60 -0
  56. keyfleet-0.1.0/tests/test_check_min_keys.py +70 -0
  57. keyfleet-0.1.0/tests/test_check_recovery_codes.py +58 -0
  58. keyfleet-0.1.0/tests/test_check_spare_unregistered.py +30 -0
  59. keyfleet-0.1.0/tests/test_check_unknown_service.py +54 -0
  60. keyfleet-0.1.0/tests/test_check_weak_factors.py +44 -0
  61. keyfleet-0.1.0/tests/test_checks.py +31 -0
  62. keyfleet-0.1.0/tests/test_cli.py +253 -0
  63. keyfleet-0.1.0/tests/test_crypto.py +102 -0
  64. keyfleet-0.1.0/tests/test_data_files.py +89 -0
  65. keyfleet-0.1.0/tests/test_golden.py +42 -0
  66. keyfleet-0.1.0/tests/test_impact.py +107 -0
  67. keyfleet-0.1.0/tests/test_model.py +177 -0
  68. keyfleet-0.1.0/tests/test_no_network.py +22 -0
  69. keyfleet-0.1.0/tests/test_schema_sync.py +19 -0
  70. keyfleet-0.1.0/uv.lock +452 -0
@@ -0,0 +1,4 @@
1
+ # Owner works on Windows and macOS: normalize to LF in the repo.
2
+ * text=auto eol=lf
3
+ *.png binary
4
+ *.gif binary
@@ -0,0 +1,28 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: test (${{ matrix.os }})
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest, macos-latest]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: astral-sh/setup-uv@v10.0.1
19
+ with:
20
+ python-version: "3.12"
21
+ # Exercise the age round-trip tests on at least one OS (they skip
22
+ # gracefully where the binary is absent).
23
+ - if: runner.os == 'Linux'
24
+ run: sudo apt-get update && sudo apt-get install -y age
25
+ - run: uv sync --all-extras --dev
26
+ - run: uv run ruff check .
27
+ - run: uv run ruff format --check .
28
+ - run: uv run pytest -q
@@ -0,0 +1,38 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI via trusted publishing (OIDC, no stored token) when a
4
+ # v* tag is pushed. PyPI-side publisher config: owner ThePrimeLayer,
5
+ # repo keyfleet, workflow release.yml, environment pypi.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - uses: astral-sh/setup-uv@v10.0.1
17
+ with:
18
+ python-version: "3.12"
19
+ - run: uv build
20
+ - uses: actions/upload-artifact@v7
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment:
29
+ name: pypi
30
+ url: https://pypi.org/project/keyfleet/
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v8
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ # keyfleet — never commit a real ledger (AGENTS.md §8.3).
2
+ # Only the fictional keyfleet.example.yaml is committed.
3
+ keyfleet.yaml
4
+ keyfleet.yml
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *.egg-info/
10
+ .venv/
11
+ build/
12
+ dist/
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ htmlcov/
@@ -0,0 +1,14 @@
1
+ # ruff and gitleaks are pinned to exact versions on purpose (AGENTS.md §8.5).
2
+ # The ruff rev must match the ruff pin in pyproject.toml [dependency-groups].
3
+ # Bump either only in a dedicated `chore(deps):` commit.
4
+ repos:
5
+ - repo: https://github.com/astral-sh/ruff-pre-commit
6
+ rev: v0.16.5
7
+ hooks:
8
+ - id: ruff-check
9
+ args: [--fix]
10
+ - id: ruff-format
11
+ - repo: https://github.com/gitleaks/gitleaks
12
+ rev: v8.30.1
13
+ hooks:
14
+ - id: gitleaks
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,100 @@
1
+ # AGENTS.md — keyfleet
2
+
3
+ Instructions for any coding agent working in this repository (Codex, Cursor, Copilot, Cline/Kilo, Claude Code via `CLAUDE.md`). Explicit instructions from the user override this file. This file is the single source of truth for **how to work here**; `keyfleet-BRIEF.md` is the source of truth for **what to build**. If they conflict on scope, the brief wins; on process, this file wins.
4
+
5
+ ## 1. What this project is
6
+
7
+ `keyfleet` is a local-first Python CLI that keeps a YAML ledger of hardware security keys ↔ accounts ↔ credential types and reports coverage gaps, lost-key impact, capacity, and advisories. It stores **no secrets** and makes **no network calls**. Full specification: `keyfleet-BRIEF.md` — read it completely before your first change, then consult it by section (§7 data model, §8 CLI, §9 logic, §15 open decisions).
8
+
9
+ ## 2. Session start (every session, every harness)
10
+
11
+ 1. If `PLAN.md` exists, read it first: milestones, open questions, and the session log tell you where the last agent stopped. If it does not exist, read the brief and create `PLAN.md` per brief §16.
12
+ 2. Run `git status` and `git log --oneline -15` to orient.
13
+ 3. Work in small steps. Before ending, append a session-log line to `PLAN.md` (format in §9).
14
+
15
+ ## 3. Commands
16
+
17
+ | Task | Command |
18
+ |---|---|
19
+ | Set up environment | `uv sync --all-extras --dev` |
20
+ | Run the CLI | `uv run keyfleet --help` |
21
+ | Tests (all / one file) | `uv run pytest -q` / `uv run pytest tests/test_checks.py -q` |
22
+ | Lint + format | `uv run ruff check . && uv run ruff format .` |
23
+ | All hooks | `uv run pre-commit run --all-files` |
24
+ | Build wheel | `uv build` |
25
+
26
+ Always prefix with `uv run`. Never `pip install` into a global/system Python. Commands are identical in PowerShell on Windows; write cross-platform code (`pathlib`, no shell one-liners in library code).
27
+
28
+ ## 4. Repository map
29
+
30
+ ```
31
+ src/keyfleet/ cli.py (typer) · model.py (pydantic) · checks.py, impact.py (pure functions) · report.py (rich/markdown/json) · crypto.py (optional age decryption) · data/{models,services,advisories}.yaml
32
+ schema/ keyfleet.schema.json (generated from the pydantic models; regenerate when model.py changes)
33
+ tests/ fixtures/*.yaml · one test module per check · data-integrity tests
34
+ docs/ ASSUMPTIONS.md (unverified facts) · DECISIONS.md (choices + rationale) · SERVICES.md (generated)
35
+ keyfleet.example.yaml fictional example ledger; the only ledger ever committed
36
+ PLAN.md milestones, open questions, session log
37
+ CHANGELOG.md Keep a Changelog format; add to "Unreleased" as you go
38
+ ```
39
+
40
+ ## 5. Code conventions
41
+
42
+ - Python 3.12+, full type hints, `pydantic` v2 models in `model.py`, `typer` CLI in `cli.py`, `rich` for terminal output, `yaml.safe_load` only.
43
+ - `checks.py` and `impact.py` are **pure**: they take the loaded ledger model and return findings; no file or terminal I/O inside them.
44
+ - Exit codes: `0` clean, `1` findings at FAIL level, `2` tool/usage error. Every finding carries an actionable message and, where possible, a link from `services.yaml`.
45
+ - Errors for bad ledgers must say *which file, which key/account id, which field* and how to fix it.
46
+ - Use `logging`, not `print`, outside the report layer. No global mutable state.
47
+ - Keep modules under ~400 lines; split by responsibility, not by size.
48
+ - Do not add a dependency without a line in `docs/DECISIONS.md` (what, why, alternative considered).
49
+
50
+ ## 6. Data files (`src/keyfleet/data/*.yaml`)
51
+
52
+ - Every entry has `source_url` and `verified: YYYY-MM-DD`. If a fact cannot be verified on the vendor's or service's own page, write `null` — never guess.
53
+ - `models.yaml`: capabilities, interfaces, discoverable-credential capacity per model/firmware.
54
+ - `services.yaml`: security-settings URL, max keys allowed, discoverable-credential support. Keep entries alphabetical by id.
55
+ - `advisories.yaml`: vendor advisories with `affects` rules (firmware ranges). Summaries in your own words; link the advisory.
56
+ - Data-integrity tests must pass after any edit (unique ids, valid URL syntax, ints or null for capacities). Regenerate `docs/SERVICES.md` when `services.yaml` changes.
57
+
58
+ ## 7. Testing
59
+
60
+ - `pytest`; fixtures under `tests/fixtures/`; one module per check with positive and negative cases; golden files for `check`/`report` output.
61
+ - Tests never touch the network and never depend on the `age` binary (skip that test when it is absent).
62
+ - Must pass on Linux, macOS, and Windows (CI runs all three): use `tmp_path`, `pathlib`, UTF-8 explicitly.
63
+ - Add or update tests in the same commit as the behavior change. Target ≥85% coverage on `checks.py` and `impact.py`.
64
+
65
+ ## 8. Security and privacy invariants (non-negotiable)
66
+
67
+ 1. **No network calls in runtime code.** A test greps `src/` for `httpx`, `requests`, `urllib`, `socket`, `aiohttp` and fails on any import.
68
+ 2. **No secrets in the ledger.** Validation rejects fields or values that look like recovery codes, TOTP seeds, PINs, or OTP secrets, with a clear message. Tests cover this.
69
+ 3. **Never commit a real ledger.** `keyfleet.yaml` and `*.age` decrypted output are gitignored; only `keyfleet.example.yaml` (fictional) is committed.
70
+ 4. **No telemetry, ever.**
71
+ 5. **Supply chain:** `.pre-commit-config.yaml` pins `ruff` and `gitleaks` to exact versions; do not change pins except in a dedicated `chore(deps):` commit.
72
+ 6. If a request would violate any of the above, stop and say so instead of complying.
73
+
74
+ ## 9. Workflow and git
75
+
76
+ - Conventional commits: `feat:`, `fix:`, `test:`, `docs:`, `chore:`, `refactor:`. One logical change per commit; run `ruff` and `pytest` before committing.
77
+ - Small changes go directly on `main`. Anything that changes the ledger schema, a data-file format, or the CLI surface goes on a `feat/<topic>` branch with a short PR description (even if the owner merges it alone).
78
+ - Never force-push `main` or rewrite published history.
79
+ - `CHANGELOG.md`: add a bullet under **Unreleased** in the same commit as a user-visible change.
80
+ - `docs/ASSUMPTIONS.md`: record every external fact you relied on but could not verify (vendor capacities, service limits, API details) with a date and a link.
81
+ - `docs/DECISIONS.md`: one short entry per non-obvious choice (dependency, schema shape, threshold): context → decision → alternatives.
82
+ - **No churn.** Do not reformat, rename, or reorganize files that the task does not require. Do not rewrite `PLAN.md` milestones or the brief; propose changes in chat and edit only after agreement.
83
+ - Session log (append to the end of `PLAN.md`):
84
+ `- 2026-08-29 · <harness> · <what changed, 1 line> · next: <1 line> · open: <question or "none">`
85
+
86
+ ## 10. Decisions that require the owner
87
+
88
+ From brief §15: license (Apache-2.0 recommended), default policy numbers (`T0: 3, T1: 2, T2: 1`), and the package/repo name if `keyfleet` is taken on PyPI/GitHub. Ask these **once, in a single batched message**, propose a default for each, and continue with the recommended default on anything non-blocking. Record the answers in `docs/DECISIONS.md`. Do not ask questions the brief or this file already answers.
89
+
90
+ ## 11. Definition of done for v0.1.0
91
+
92
+ Everything in brief §4 shipped; CI green on all three OSes; the README quick start executed literally from a clean clone; `CHANGELOG.md` entry; git tag `v0.1.0`; `uv build` succeeds. When a milestone or the release is complete, report: what shipped, what is assumed (link `docs/ASSUMPTIONS.md`), what is next.
93
+
94
+ ## 12. Harness notes
95
+
96
+ - **Claude Code** reads `CLAUDE.md`, which imports this file. Put shared rules here, not there.
97
+ - **Codex** reads this file directly (keep it under the 32 KiB cap; no `AGENTS.override.md` is used in this repo).
98
+ - **Cursor** reads `AGENTS.md` natively; add `.cursor/rules/*.mdc` only for glob-scoped extras, never for rules that belong here.
99
+ - **GitHub Copilot** reads `AGENTS.md`; no separate `copilot-instructions.md` is maintained.
100
+ - **Other harnesses** that do not discover `AGENTS.md`: point their rules file at this one (a one-line "Read and follow AGENTS.md" is enough). Do not fork the content.
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable, user-visible changes to keyfleet are documented in this file.
4
+
5
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ·
6
+ versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-08-30
11
+
12
+ First release: the ledger, the checker, the incident tooling, and the
13
+ source-cited services dataset.
14
+
15
+ ### Added
16
+
17
+ - YAML ledger schema (keys ↔ accounts ↔ registrations, policy, advisories)
18
+ with strict validation, referential-integrity checks, and errors that name
19
+ the file, the offending key/account id, and the field.
20
+ - Secret rejection: a ledger containing anything that looks like a recovery
21
+ code, TOTP seed, PIN, or OTP secret (by field name or value shape) is
22
+ refused with an explanation — keyfleet stores pointers, never secrets.
23
+ - `keyfleet validate [LEDGER]` — exit 0 valid, 1 invalid, 2 missing file.
24
+ - Published JSON Schema at `schema/keyfleet.schema.json` for editor
25
+ completion, generated from the models.
26
+ - Fictional example ledger `keyfleet.example.yaml`.
27
+ - `keyfleet check [LEDGER] [--json]` with the min-keys coverage rule: FAIL for
28
+ every account holding fewer active/spare keys than `policy.min_keys[tier]`
29
+ (defaults T0:3, T1:2, T2:1); lost/retired keys never count. Exit 1 when any
30
+ FAIL finding exists.
31
+ - Lost/retired hygiene check: FAIL for every lost or retired key that is still
32
+ registered on any account, pointing at `keyfleet lost KEY` for the
33
+ de-registration checklist.
34
+ - Weak-factor check: WARN for every factor a tier's policy warns against
35
+ (default: sms and email on T0 accounts).
36
+ - Unregistered-spare check: WARN for spare keys registered on no account.
37
+ - Recovery-code pointer check: INFO for accounts in tiers listed in
38
+ `policy.require_recovery_codes_for` (default T0+T1) that have no stored
39
+ recovery-code pointer.
40
+ - Encrypted ledgers: every command transparently reads `keyfleet.yaml.age`
41
+ (or any `*.age` path) via the `age` CLI, decrypting to memory only; set
42
+ `KEYFLEET_AGE_IDENTITY` to an identity file for non-interactive use.
43
+ - `keyfleet init` — writes the fictional example ledger into the current
44
+ directory and makes sure `.gitignore` covers `keyfleet.yaml`.
45
+ - `keyfleet advisories` — matches every key against the bundled advisory list
46
+ (plus any ledger-local advisories) by vendor and firmware range; keys
47
+ without `firmware:` are prompted to set it.
48
+ - `keyfleet services [--search NAME]` — prints the bundled service table.
49
+ - `keyfleet report [--md|--json]` — coverage matrix (accounts x keys with
50
+ registration types), per-tier summary, and key utilization including
51
+ discoverable-credential usage against known capacities.
52
+ - `keyfleet lost KEY_ID [--md]` — lost-key impact analysis and an ordered
53
+ de-registration checklist (tier first, becomes-inaccessible first) with the
54
+ registration nicknames to delete and each service's security-settings URL.
55
+ - Bundled reference data, every fact verified on the vendor's own page (or
56
+ `null`, never guessed): `services.yaml` with 32 services (settings URLs,
57
+ documented key limits, passkey support), `models.yaml` with
58
+ discoverable-credential capacities per firmware (YubiKey 5 / Security Key
59
+ series 25→100 at 5.7, Nitrokey 3 family), and `advisories.yaml` seeded with
60
+ YSA-2024-02, YSA-2024-03, and YSA-2025-02. `docs/SERVICES.md` is generated
61
+ from the services table.
@@ -0,0 +1,34 @@
1
+ # keyfleet — Claude Code
2
+
3
+ @AGENTS.md
4
+
5
+ Shared rules live in `AGENTS.md` (imported above). This file adds only what is specific to working with Claude Code. Keep it under 60 lines; if a rule applies to every harness, move it to `AGENTS.md`.
6
+
7
+ ## Orientation
8
+
9
+ - Start from `PLAN.md`. If it is missing, read `keyfleet-BRIEF.md` in full and create `PLAN.md` per the brief's §16 before touching code.
10
+ - The brief is deliberately **not** imported here: imports load on every launch, and the brief is only needed when a task touches something it specifies (schema, CLI surface, a check's logic, data files). Open the relevant section at that point.
11
+
12
+ ## Working style
13
+
14
+ - When a decision needs the owner, present it as a short numbered fork with a recommended default, batch all such questions into one message, and keep working on unblocked tasks. No "shall I continue?" check-ins; end the turn when the requested slice is done or you are blocked.
15
+ - Push back plainly when the brief, `PLAN.md`, or a request is wrong or would breach an invariant in `AGENTS.md` §8 — before doing it, not after.
16
+ - Use plan mode (or a ≤10-line plan in chat) for anything that changes the ledger schema, a data-file format, or the CLI surface. Skip the plan for single-check or single-test changes.
17
+ - Do not rewrite `PLAN.md` milestones or the brief on your own; propose in chat, edit after agreement.
18
+
19
+ ## Tooling
20
+
21
+ - `uv run …` for every Python invocation; never call `pip`, `python -m pip`, or a global interpreter.
22
+ - The owner works on both Windows (PowerShell) and macOS: prefer commands that are identical on both; use `pathlib` in code and in test helpers.
23
+ - When you need a fact about a key vendor, a service's security-key settings, or an advisory, read the vendor's/service's own page and record the URL and date in the data file (`source_url`, `verified`) or in `docs/ASSUMPTIONS.md`. Never fill a data entry from memory.
24
+
25
+ ## Finishing a task or milestone
26
+
27
+ 1. `uv run ruff check . && uv run ruff format . && uv run pytest -q`
28
+ 2. Update `CHANGELOG.md` (Unreleased) for user-visible changes and append the session-log line to `PLAN.md`.
29
+ 3. Summarize: shipped / assumed / next.
30
+
31
+ ## Don'ts
32
+
33
+ - Don't add dependencies silently, touch the `gitleaks` or `ruff` pins outside a `chore(deps):` commit, add network calls, or commit `keyfleet.yaml`.
34
+ - Don't reformat or reorganize files the task doesn't require.
@@ -0,0 +1,75 @@
1
+ # Contributing to keyfleet
2
+
3
+ Thanks for helping! The most valuable contribution by far is a **service
4
+ entry** — it takes ten minutes and makes `keyfleet lost` and `keyfleet check`
5
+ smarter for everyone.
6
+
7
+ ## Add a service (the ten-minute PR)
8
+
9
+ 1. Open the service's **own** documentation/help page about security keys or
10
+ passkeys. Facts must come from a page on the vendor's domain — that page
11
+ becomes `source_url`. No blog posts, no memory, no guessing.
12
+ 2. Add an entry to [`src/keyfleet/data/services.yaml`](src/keyfleet/data/services.yaml),
13
+ **alphabetical by id**:
14
+
15
+ ```yaml
16
+ example:
17
+ name: "Example"
18
+ security_settings_url: https://example.com/settings/security # only if documented/linked; else null
19
+ max_keys: null # integer only if the page states a limit
20
+ fido2_discoverable: true # passkeys supported? true / false / null (page doesn't say)
21
+ notes: "One short sentence from the page, in your own words."
22
+ source_url: https://help.example.com/security-keys
23
+ verified: 2026-08-30 # the day YOU read the page
24
+ ```
25
+
26
+ `null` never means "probably not" — it means *the page does not say*.
27
+ A service with **no** security-key support at all is still a great entry:
28
+ set `fido2_discoverable: false` and say so in `notes` (see `steam`).
29
+ 3. Regenerate the table and run the data tests:
30
+
31
+ ```bash
32
+ uv run python scripts/gen_services_md.py
33
+ ```
34
+
35
+ ```bash
36
+ uv run pytest tests/test_data_files.py -q
37
+ ```
38
+
39
+ 4. Commit as `data(services): add example`, one service per PR, and paste the
40
+ sentence from the vendor page that supports each non-null fact into the PR
41
+ description.
42
+
43
+ Corrections use the same rules — update `verified` to the day you re-checked.
44
+ `models.yaml` (key capabilities/capacities) and `advisories.yaml` (vendor
45
+ advisories with firmware ranges) follow the same source-cited pattern; see the
46
+ header comments in each file.
47
+
48
+ ## Code contributions
49
+
50
+ ```bash
51
+ uv sync --all-extras --dev
52
+ ```
53
+
54
+ ```bash
55
+ uv run pytest -q
56
+ ```
57
+
58
+ ```bash
59
+ uv run pre-commit run --all-files
60
+ ```
61
+
62
+ - Conventional commits (`feat:`, `fix:`, `test:`, `docs:`, `data:`, `chore:`);
63
+ one logical change per commit; tests land in the same commit as the change.
64
+ - `checks.py` and `impact.py` stay pure (no I/O); rendering lives in
65
+ `report.py`; every finding must tell the user what to *do*.
66
+ - Two hard rules, enforced by tests, not negotiable: **no network calls in
67
+ runtime code** and **no storing secrets** (the validator must keep refusing
68
+ anything that looks like one).
69
+ - After changing `model.py`, regenerate the JSON schema:
70
+ `uv run python scripts/gen_schema.py`. After changing CLI output on
71
+ purpose, regenerate goldens: `KEYFLEET_UPDATE_GOLDENS=1 uv run pytest -q`
72
+ and review the diff.
73
+
74
+ Agent-assisted contributions are welcome; the repo's ground rules for that
75
+ live in [AGENTS.md](AGENTS.md).
keyfleet-0.1.0/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.