armasec-lite 0.1.3__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.
- armasec_lite-0.1.3/.github/workflows/ci.yml +40 -0
- armasec_lite-0.1.3/.github/workflows/deploy-docs.yml +140 -0
- armasec_lite-0.1.3/.github/workflows/release.yml +202 -0
- armasec_lite-0.1.3/.gitignore +14 -0
- armasec_lite-0.1.3/CLAUDE.md +229 -0
- armasec_lite-0.1.3/HARNESS-DESIGN.md +149 -0
- armasec_lite-0.1.3/LICENSE +21 -0
- armasec_lite-0.1.3/PKG-INFO +176 -0
- armasec_lite-0.1.3/README.md +150 -0
- armasec_lite-0.1.3/armasec_lite/__init__.py +33 -0
- armasec_lite-0.1.3/armasec_lite/armasec.py +196 -0
- armasec_lite-0.1.3/armasec_lite/exceptions.py +240 -0
- armasec_lite-0.1.3/armasec_lite/http.py +234 -0
- armasec_lite-0.1.3/armasec_lite/jwt.py +827 -0
- armasec_lite-0.1.3/armasec_lite/openid_config_loader.py +353 -0
- armasec_lite-0.1.3/armasec_lite/pluggable/__init__.py +309 -0
- armasec_lite-0.1.3/armasec_lite/pluggable/hookspecs.py +110 -0
- armasec_lite-0.1.3/armasec_lite/pytest_extension.py +534 -0
- armasec_lite-0.1.3/armasec_lite/schemas.py +331 -0
- armasec_lite-0.1.3/armasec_lite/token_decoder.py +348 -0
- armasec_lite-0.1.3/armasec_lite/token_manager.py +206 -0
- armasec_lite-0.1.3/armasec_lite/token_payload.py +98 -0
- armasec_lite-0.1.3/armasec_lite/token_security.py +645 -0
- armasec_lite-0.1.3/armasec_lite/utilities.py +84 -0
- armasec_lite-0.1.3/docs/superpowers/plans/2026-09-04-armasec-lite-library.md +6454 -0
- armasec_lite-0.1.3/docs/superpowers/specs/2026-09-04-armasec-lite-design.md +1277 -0
- armasec_lite-0.1.3/docusaurus/.gitignore +25 -0
- armasec_lite-0.1.3/docusaurus/docs/architecture/caching.md +49 -0
- armasec_lite-0.1.3/docusaurus/docs/architecture/index.md +50 -0
- armasec_lite-0.1.3/docusaurus/docs/architecture/request-lifecycle.md +39 -0
- armasec_lite-0.1.3/docusaurus/docs/architecture/threading-model.md +52 -0
- armasec_lite-0.1.3/docusaurus/docs/contributing.md +74 -0
- armasec_lite-0.1.3/docusaurus/docs/index.md +74 -0
- armasec_lite-0.1.3/docusaurus/docs/installation.md +40 -0
- armasec_lite-0.1.3/docusaurus/docs/migration.md +209 -0
- armasec_lite-0.1.3/docusaurus/docs/quickstart.md +112 -0
- armasec_lite-0.1.3/docusaurus/docs/security/index.md +41 -0
- armasec_lite-0.1.3/docusaurus/docs/security/jwt-verification.md +138 -0
- armasec_lite-0.1.3/docusaurus/docs/security/threat-model.md +63 -0
- armasec_lite-0.1.3/docusaurus/docusaurus.config.ts +190 -0
- armasec_lite-0.1.3/docusaurus/mmdcheck.mjs +41 -0
- armasec_lite-0.1.3/docusaurus/package-lock.json +20103 -0
- armasec_lite-0.1.3/docusaurus/package.json +58 -0
- armasec_lite-0.1.3/docusaurus/scripts/benchmark_charts.py +998 -0
- armasec_lite-0.1.3/docusaurus/scripts/generate_benchmarks.py +2918 -0
- armasec_lite-0.1.3/docusaurus/sidebars.ts +61 -0
- armasec_lite-0.1.3/docusaurus/src/components/PlotlyChart/Figure.tsx +195 -0
- armasec_lite-0.1.3/docusaurus/src/components/PlotlyChart/index.tsx +57 -0
- armasec_lite-0.1.3/docusaurus/src/components/PlotlyChart/styles.module.css +37 -0
- armasec_lite-0.1.3/docusaurus/src/css/custom.css +53 -0
- armasec_lite-0.1.3/docusaurus/static/.gitkeep +0 -0
- armasec_lite-0.1.3/docusaurus/tsconfig.json +17 -0
- armasec_lite-0.1.3/examples/basic.py +18 -0
- armasec_lite-0.1.3/examples/match_key_value_pairs.py +28 -0
- armasec_lite-0.1.3/examples/plugin.py +61 -0
- armasec_lite-0.1.3/examples/two_domains.py +28 -0
- armasec_lite-0.1.3/justfile +526 -0
- armasec_lite-0.1.3/legacy_comparison_compose/Dockerfile.app +51 -0
- armasec_lite-0.1.3/legacy_comparison_compose/Dockerfile.app.dockerignore +15 -0
- armasec_lite-0.1.3/legacy_comparison_compose/Dockerfile.bench +24 -0
- armasec_lite-0.1.3/legacy_comparison_compose/README.md +346 -0
- armasec_lite-0.1.3/legacy_comparison_compose/app/main.py +368 -0
- armasec_lite-0.1.3/legacy_comparison_compose/app/probe.py +434 -0
- armasec_lite-0.1.3/legacy_comparison_compose/bench/__init__.py +10 -0
- armasec_lite-0.1.3/legacy_comparison_compose/bench/cgroup.py +388 -0
- armasec_lite-0.1.3/legacy_comparison_compose/bench/report.py +611 -0
- armasec_lite-0.1.3/legacy_comparison_compose/bench/run.py +232 -0
- armasec_lite-0.1.3/legacy_comparison_compose/bench/runs.py +608 -0
- armasec_lite-0.1.3/legacy_comparison_compose/bench/scenarios.py +2401 -0
- armasec_lite-0.1.3/legacy_comparison_compose/docker-compose.yaml +218 -0
- armasec_lite-0.1.3/legacy_comparison_compose/proxy/main.py +364 -0
- armasec_lite-0.1.3/legacy_comparison_compose/realm/armasec-realm.json +143 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/.gitkeep +0 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/index.json +1284 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/call_graph.json +470 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/footprint.json +172 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/memory.json +452 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/profile_sampling.json +239 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s1_cold_start.json +2656 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s2_request_amplification.json +1199 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s3_warm_flood.json +2413 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s4_event_loop_blocking.json +16872 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-08Z-raton-actions-00/s8_failing_provider.json +261 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-55-12Z/s10_cpu_during_cold_load.json +26042 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-55-12Z/s11_idle_cpu.json +603 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-55-12Z/s9_cpu_per_request.json +10978 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/call_graph.json +478 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/footprint.json +180 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/memory.json +460 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/profile_sampling.json +259 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s10_cpu_during_cold_load.json +26090 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s11_idle_cpu.json +603 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s1_cold_start.json +2664 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s2_request_amplification.json +1207 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s3_warm_flood.json +2421 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s4_event_loop_blocking.json +16880 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s8_failing_provider.json +269 -0
- armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s9_cpu_per_request.json +10978 -0
- armasec_lite-0.1.3/legacy_comparison_compose/test_parity.py +368 -0
- armasec_lite-0.1.3/pyproject.toml +81 -0
- armasec_lite-0.1.3/tests/unit/__init__.py +0 -0
- armasec_lite-0.1.3/tests/unit/conftest.py +228 -0
- armasec_lite-0.1.3/tests/unit/test_armasec.py +79 -0
- armasec_lite-0.1.3/tests/unit/test_end_to_end.py +160 -0
- armasec_lite-0.1.3/tests/unit/test_examples.py +44 -0
- armasec_lite-0.1.3/tests/unit/test_exceptions.py +64 -0
- armasec_lite-0.1.3/tests/unit/test_http.py +133 -0
- armasec_lite-0.1.3/tests/unit/test_jwt_attacks.py +248 -0
- armasec_lite-0.1.3/tests/unit/test_jwt_claims.py +204 -0
- armasec_lite-0.1.3/tests/unit/test_jwt_crossvalidation.py +196 -0
- armasec_lite-0.1.3/tests/unit/test_jwt_parsing.py +82 -0
- armasec_lite-0.1.3/tests/unit/test_jwt_signatures.py +235 -0
- armasec_lite-0.1.3/tests/unit/test_openid_config_loader.py +193 -0
- armasec_lite-0.1.3/tests/unit/test_packaging.py +86 -0
- armasec_lite-0.1.3/tests/unit/test_pluggable.py +172 -0
- armasec_lite-0.1.3/tests/unit/test_pytest_extension.py +95 -0
- armasec_lite-0.1.3/tests/unit/test_schemas.py +208 -0
- armasec_lite-0.1.3/tests/unit/test_token_decoder.py +171 -0
- armasec_lite-0.1.3/tests/unit/test_token_manager.py +123 -0
- armasec_lite-0.1.3/tests/unit/test_token_payload.py +112 -0
- armasec_lite-0.1.3/tests/unit/test_token_security.py +483 -0
- armasec_lite-0.1.3/tests/unit/test_utilities.py +34 -0
- armasec_lite-0.1.3/uv.lock +918 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
UV_VERSION: "0.10.3"
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
check:
|
|
17
|
+
name: lint · test
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
|
+
|
|
22
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
23
|
+
with:
|
|
24
|
+
version: ${{ env.UV_VERSION }}
|
|
25
|
+
python-version: '3.12'
|
|
26
|
+
enable-cache: true
|
|
27
|
+
|
|
28
|
+
# The runner image ships no `just`. The official installer needs no marketplace
|
|
29
|
+
# action or version pin beyond the script's own TLS pin.
|
|
30
|
+
- name: Install just
|
|
31
|
+
run: curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
|
32
|
+
|
|
33
|
+
- name: Install workspace
|
|
34
|
+
run: uv sync --all-groups
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: just lint
|
|
38
|
+
|
|
39
|
+
- name: Test
|
|
40
|
+
run: just test
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Deploy the armasec-lite docs as a spoke site under /developer/armasec-lite/ on
|
|
2
|
+
# docs.vantagecompute.ai (S3 + CloudFront).
|
|
3
|
+
#
|
|
4
|
+
# Auth is handled at the edge by the Lambda@Edge Keycloak gateway on all of /developer/*,
|
|
5
|
+
# so there is no per-spoke auth configuration here and the site carries no login of its
|
|
6
|
+
# own.
|
|
7
|
+
#
|
|
8
|
+
# Required repo variables:
|
|
9
|
+
# DOCS_BUCKET S3 bucket name (shared with vantage-docs)
|
|
10
|
+
# DOCS_CF_DISTRIBUTION_ID CloudFront distribution ID
|
|
11
|
+
#
|
|
12
|
+
# Required repo secrets:
|
|
13
|
+
# DOCS_SPOKE_ROLE_ARN vantage-docs-spoke-armasec-lite, scoped to
|
|
14
|
+
# developer/armasec-lite/*. Created by the vantage-docs hub
|
|
15
|
+
# stack from its own spokes list, so there is no infrastructure
|
|
16
|
+
# to deploy from here -- adding this repo to that list is what
|
|
17
|
+
# brings the role into existence.
|
|
18
|
+
|
|
19
|
+
name: Deploy docs (spoke)
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
push:
|
|
23
|
+
tags:
|
|
24
|
+
# Only a plain vX.Y.Z release publishes the docs. `v*` also matched a
|
|
25
|
+
# pre-release like v1.2.3rc1 and anything else beginning with v, so a release
|
|
26
|
+
# candidate silently replaced the published site. GitHub filter patterns are
|
|
27
|
+
# glob-like rather than regular expressions: `.` is literal and `+` means one
|
|
28
|
+
# or more of the preceding character, so this matches v1.2.3 and v10.20.30 and
|
|
29
|
+
# rejects v1.2.3rc1, v1.2 and vfoo.
|
|
30
|
+
- 'v[0-9]+.[0-9]+.[0-9]+'
|
|
31
|
+
workflow_dispatch:
|
|
32
|
+
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
contents: read
|
|
36
|
+
|
|
37
|
+
concurrency:
|
|
38
|
+
group: docs-spoke
|
|
39
|
+
cancel-in-progress: false
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
deploy:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
# The pydoc plugin comes from npm, so there is no submodule to check out and no
|
|
46
|
+
# transport rewriting to do. It used to be vendored as a git submodule whose
|
|
47
|
+
# .gitmodules recorded an SSH URL that a runner has no key for.
|
|
48
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
49
|
+
|
|
50
|
+
# The API reference is generated from docstrings by the pydoc plugin, which parses
|
|
51
|
+
# the source with `ast` and never imports it. So this needs an interpreter able to
|
|
52
|
+
# parse the project's syntax, but deliberately not its dependencies or its
|
|
53
|
+
# virtualenv -- which is why there is no `uv sync` here. armasec-lite requires
|
|
54
|
+
# Python 3.12+, so 3.12 is enough.
|
|
55
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
56
|
+
with:
|
|
57
|
+
python-version: '3.12'
|
|
58
|
+
|
|
59
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
60
|
+
with:
|
|
61
|
+
node-version: 24
|
|
62
|
+
cache: npm
|
|
63
|
+
cache-dependency-path: docusaurus/package-lock.json
|
|
64
|
+
|
|
65
|
+
- name: Install docs dependencies
|
|
66
|
+
working-directory: docusaurus
|
|
67
|
+
run: npm ci
|
|
68
|
+
|
|
69
|
+
- name: Build docs
|
|
70
|
+
working-directory: docusaurus
|
|
71
|
+
run: npm run build
|
|
72
|
+
|
|
73
|
+
# The plugin fails the build when introspection fails outright, and when every
|
|
74
|
+
# requested module documents nothing. This catches the case neither covers: a partial
|
|
75
|
+
# generation, where some modules landed and others silently did not.
|
|
76
|
+
#
|
|
77
|
+
# Counts against the module list in the config rather than a list repeated here, so
|
|
78
|
+
# a module added to the site cannot be forgotten in the check. This site has a
|
|
79
|
+
# single plugin instance, so `instances` is always 1 and accounts for the one
|
|
80
|
+
# generated index.md that is not itself a module page.
|
|
81
|
+
- name: Verify the API reference is complete
|
|
82
|
+
working-directory: docusaurus
|
|
83
|
+
run: |
|
|
84
|
+
declared=$(grep -cE "^\s+\{module: " docusaurus.config.ts)
|
|
85
|
+
instances=$(grep -cE "^\s+id: '" docusaurus.config.ts)
|
|
86
|
+
generated=$(( $(find docs/api-reference -mindepth 1 -name '*.md' | wc -l) - instances ))
|
|
87
|
+
|
|
88
|
+
if [ "$generated" -ne "$declared" ]; then
|
|
89
|
+
echo "API reference is incomplete: $declared modules declared, $generated pages generated."
|
|
90
|
+
echo "Likely cause: a module was renamed or moved without updating the plugin's"
|
|
91
|
+
echo "module list in docusaurus/docusaurus.config.ts."
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
echo "API reference OK: $generated pages for $declared declared modules"
|
|
96
|
+
|
|
97
|
+
# The benchmark pages are generated by npm's prebuild hook from the committed result
|
|
98
|
+
# files, one page per run plus a summary, and are not committed. The generator raises
|
|
99
|
+
# on a missing or malformed result file, so a partial results tree already fails the
|
|
100
|
+
# build. This catches the remaining case: a run directory that produced no page.
|
|
101
|
+
#
|
|
102
|
+
# Counted against the results tree rather than a number repeated here, so committing
|
|
103
|
+
# a new run cannot be forgotten in the check.
|
|
104
|
+
- name: Verify the benchmark pages are complete
|
|
105
|
+
working-directory: docusaurus
|
|
106
|
+
run: |
|
|
107
|
+
committed=$(find ../legacy_comparison_compose/results -mindepth 2 -maxdepth 2 -type d | wc -l)
|
|
108
|
+
pages=$(find docs/benchmarks -name '*.mdx' | wc -l)
|
|
109
|
+
|
|
110
|
+
if [ "$pages" -ne "$(( committed + 1 ))" ]; then
|
|
111
|
+
echo "Benchmark pages are incomplete: $committed runs committed, $pages pages generated."
|
|
112
|
+
echo "Expected one page per committed run, plus one summary page."
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
echo "Benchmark pages OK: $pages pages for $committed committed runs"
|
|
117
|
+
|
|
118
|
+
- name: Configure AWS credentials
|
|
119
|
+
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
|
|
120
|
+
with:
|
|
121
|
+
role-to-assume: ${{ secrets.DOCS_SPOKE_ROLE_ARN }}
|
|
122
|
+
aws-region: us-east-1
|
|
123
|
+
|
|
124
|
+
# --delete is safe here and only here: the hub excludes every spoke listed in
|
|
125
|
+
# .developer-subsites from its own sync, so this subtree is owned by this repo alone.
|
|
126
|
+
- name: Sync to S3
|
|
127
|
+
run: |
|
|
128
|
+
aws s3 sync docusaurus/build/ "s3://${{ vars.DOCS_BUCKET }}/developer/armasec-lite/" --delete
|
|
129
|
+
|
|
130
|
+
- name: Invalidate CloudFront
|
|
131
|
+
env:
|
|
132
|
+
DOCS_CF_DISTRIBUTION_ID: ${{ vars.DOCS_CF_DISTRIBUTION_ID }}
|
|
133
|
+
run: |
|
|
134
|
+
if [ -z "${DOCS_CF_DISTRIBUTION_ID}" ]; then
|
|
135
|
+
echo "DOCS_CF_DISTRIBUTION_ID is not set; skipping CloudFront invalidation."
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
aws cloudfront create-invalidation \
|
|
139
|
+
--distribution-id "${DOCS_CF_DISTRIBUTION_ID}" \
|
|
140
|
+
--paths "/developer/armasec-lite/*"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Build and publish armasec-lite to PyPI.
|
|
2
|
+
#
|
|
3
|
+
# A tag publishes. A workflow_dispatch run builds, smoke tests and uploads the artifacts
|
|
4
|
+
# without publishing anything, so the packaging can be exercised without burning a version
|
|
5
|
+
# number: every publishing job is gated on the ref being a tag.
|
|
6
|
+
#
|
|
7
|
+
# Publishing uses PyPI trusted publishing (OIDC), so there is no API token to store or
|
|
8
|
+
# rotate. It requires a one-time setup on PyPI: add a trusted publisher for the project
|
|
9
|
+
# pointing at vantagecompute/armasec-lite, workflow `release.yml`, environment `pypi`.
|
|
10
|
+
#
|
|
11
|
+
# This is a pure Python package with no compiled extensions, so one universal
|
|
12
|
+
# `py3-none-any` wheel covers every platform. There is no build matrix and no
|
|
13
|
+
# architecture-specific retagging.
|
|
14
|
+
|
|
15
|
+
name: Build and Publish armasec-lite
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags:
|
|
20
|
+
- 'v*'
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
|
|
23
|
+
env:
|
|
24
|
+
PYTHON_VERSION: '3.12'
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
name: Build and smoke test
|
|
29
|
+
runs-on: ubuntu-24.04
|
|
30
|
+
outputs:
|
|
31
|
+
version: ${{ steps.version.outputs.version }}
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout
|
|
35
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
|
+
|
|
37
|
+
- name: Set up uv
|
|
38
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
39
|
+
with:
|
|
40
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
41
|
+
|
|
42
|
+
- name: Determine version
|
|
43
|
+
id: version
|
|
44
|
+
run: |
|
|
45
|
+
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
|
46
|
+
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
47
|
+
else
|
|
48
|
+
VERSION="0.0.0.dev0+${GITHUB_SHA::8}"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Practical subset of the PEP 440 grammar, not the full spec: covers a plain
|
|
52
|
+
# release, one pre-release segment (a/b/c/rc, with a '.', '-', or no separator),
|
|
53
|
+
# an optional .postN, an optional .devN, and an optional +local segment. If a
|
|
54
|
+
# legitimate exotic version is ever rejected here, relax this regex rather than
|
|
55
|
+
# fight it.
|
|
56
|
+
PEP440_RE='^[0-9]+\.[0-9]+\.[0-9]+([.-]?(a|b|c|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?(\+[A-Za-z0-9]+)?$'
|
|
57
|
+
if [[ ! "${VERSION}" =~ ${PEP440_RE} ]]; then
|
|
58
|
+
echo "::error::Version '${VERSION}' is not a valid PEP 440 version. Push a tag shaped like 'vX.Y.Z' or 'vX.Y.Zrc1'." >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
63
|
+
echo "Building version ${VERSION}"
|
|
64
|
+
|
|
65
|
+
- name: Set the package version
|
|
66
|
+
env:
|
|
67
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
68
|
+
run: |
|
|
69
|
+
# armasec_lite.__version__ reads importlib.metadata at import time, so the
|
|
70
|
+
# version lives in exactly one place and only pyproject.toml needs rewriting.
|
|
71
|
+
sed -i "0,/^version = \".*\"/s//version = \"${VERSION}\"/" pyproject.toml
|
|
72
|
+
grep '^version' pyproject.toml
|
|
73
|
+
|
|
74
|
+
- name: Run the test suite
|
|
75
|
+
run: |
|
|
76
|
+
uv sync --all-groups
|
|
77
|
+
uv run pytest tests/unit -q
|
|
78
|
+
|
|
79
|
+
- name: Lint and type check
|
|
80
|
+
run: |
|
|
81
|
+
uv run ruff check .
|
|
82
|
+
uv run ruff format --check .
|
|
83
|
+
uv run mypy armasec_lite
|
|
84
|
+
|
|
85
|
+
- name: Build sdist and wheel
|
|
86
|
+
run: uv build
|
|
87
|
+
|
|
88
|
+
- name: Smoke test the built wheel
|
|
89
|
+
env:
|
|
90
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
91
|
+
run: |
|
|
92
|
+
# Install the artifact itself into a clean environment, not the source tree.
|
|
93
|
+
# A package can pass its own test suite and still be unusable once built, if a
|
|
94
|
+
# module is missing from the wheel or an entry point does not resolve.
|
|
95
|
+
uv venv /tmp/smoke
|
|
96
|
+
VIRTUAL_ENV=/tmp/smoke uv pip install dist/*.whl
|
|
97
|
+
/tmp/smoke/bin/python - <<'PY'
|
|
98
|
+
import os
|
|
99
|
+
import armasec_lite
|
|
100
|
+
from armasec_lite import Armasec, TokenPayload, TokenSecurity
|
|
101
|
+
|
|
102
|
+
expected = os.environ["VERSION"]
|
|
103
|
+
assert armasec_lite.__version__ == expected, (
|
|
104
|
+
f"wheel reports {armasec_lite.__version__}, expected {expected}"
|
|
105
|
+
)
|
|
106
|
+
for name in armasec_lite.__all__:
|
|
107
|
+
assert hasattr(armasec_lite, name), f"{name} missing from the built wheel"
|
|
108
|
+
print(f"armasec-lite {armasec_lite.__version__} imports cleanly from the wheel")
|
|
109
|
+
PY
|
|
110
|
+
|
|
111
|
+
- name: Verify the pytest plugin entry point resolves
|
|
112
|
+
run: |
|
|
113
|
+
# The pytest11 entry point is resolved by pytest at startup, so a broken one
|
|
114
|
+
# breaks every consumer's test run rather than failing quietly.
|
|
115
|
+
VIRTUAL_ENV=/tmp/smoke uv pip install pytest
|
|
116
|
+
/tmp/smoke/bin/python -c "
|
|
117
|
+
from importlib.metadata import entry_points
|
|
118
|
+
names = [e.value for e in entry_points(group='pytest11')]
|
|
119
|
+
assert 'armasec_lite.pytest_extension' in names, names
|
|
120
|
+
print('pytest11 entry point resolves')
|
|
121
|
+
"
|
|
122
|
+
|
|
123
|
+
- name: Upload distributions
|
|
124
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
125
|
+
with:
|
|
126
|
+
name: dist
|
|
127
|
+
path: dist/*
|
|
128
|
+
retention-days: 30
|
|
129
|
+
|
|
130
|
+
publish-pypi:
|
|
131
|
+
name: Publish to PyPI
|
|
132
|
+
runs-on: ubuntu-24.04
|
|
133
|
+
needs: build
|
|
134
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
135
|
+
environment: pypi
|
|
136
|
+
|
|
137
|
+
permissions:
|
|
138
|
+
id-token: write
|
|
139
|
+
|
|
140
|
+
steps:
|
|
141
|
+
- name: Download distributions
|
|
142
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
143
|
+
with:
|
|
144
|
+
name: dist
|
|
145
|
+
path: dist
|
|
146
|
+
|
|
147
|
+
- name: List distributions
|
|
148
|
+
run: ls -lh dist/
|
|
149
|
+
|
|
150
|
+
- name: Publish
|
|
151
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
152
|
+
with:
|
|
153
|
+
packages-dir: dist/
|
|
154
|
+
|
|
155
|
+
create-release:
|
|
156
|
+
name: Create GitHub Release
|
|
157
|
+
runs-on: ubuntu-24.04
|
|
158
|
+
needs: [build, publish-pypi]
|
|
159
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
160
|
+
|
|
161
|
+
permissions:
|
|
162
|
+
contents: write
|
|
163
|
+
|
|
164
|
+
steps:
|
|
165
|
+
- name: Download distributions
|
|
166
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
167
|
+
with:
|
|
168
|
+
name: dist
|
|
169
|
+
path: dist
|
|
170
|
+
|
|
171
|
+
- name: Create Release
|
|
172
|
+
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
|
173
|
+
with:
|
|
174
|
+
tag_name: v${{ needs.build.outputs.version }}
|
|
175
|
+
name: armasec-lite ${{ needs.build.outputs.version }}
|
|
176
|
+
body: |
|
|
177
|
+
## armasec-lite ${{ needs.build.outputs.version }}
|
|
178
|
+
|
|
179
|
+
Injectable FastAPI authentication against OIDC providers, with three runtime
|
|
180
|
+
dependencies instead of upstream armasec's ten.
|
|
181
|
+
|
|
182
|
+
### Install
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
uv add armasec-lite==${{ needs.build.outputs.version }}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pip install armasec-lite==${{ needs.build.outputs.version }}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The pytest fixtures ship in an extra, so they stay out of production installs:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
uv add "armasec-lite[test]==${{ needs.build.outputs.version }}"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Documentation
|
|
199
|
+
|
|
200
|
+
https://docs.vantagecompute.ai/developer/armasec-lite/
|
|
201
|
+
files: dist/*
|
|
202
|
+
fail_on_unmatched_files: true
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# armasec-lite
|
|
2
|
+
|
|
3
|
+
Injectable FastAPI authentication against OIDC providers. A dependency-minimal
|
|
4
|
+
reimplementation of [armasec](https://github.com/omnivector-solutions/armasec) 3.x with the
|
|
5
|
+
same public API.
|
|
6
|
+
|
|
7
|
+
Import package `armasec_lite`, distribution `armasec-lite`, Python 3.12+.
|
|
8
|
+
|
|
9
|
+
Runtime dependencies are exactly three: `fastapi`, `cryptography`, `pydantic`. Adding a
|
|
10
|
+
fourth is a design change, not a routine edit. `tests/unit/test_packaging.py` enforces both
|
|
11
|
+
the count and the absence of `jose`, `buzz`, `snick`, `auto_name_enum`, `pluggy`, `respx`
|
|
12
|
+
and `httpx` by walking the AST of every module.
|
|
13
|
+
|
|
14
|
+
`armasec_lite/jwt.py` is the only security-critical module. A defect there is an
|
|
15
|
+
authentication bypass, not a bug. Read its module docstring before changing it.
|
|
16
|
+
|
|
17
|
+
## Writing docstrings
|
|
18
|
+
|
|
19
|
+
**The docstrings in `armasec_lite/` are the API reference.** There is no hand-written
|
|
20
|
+
reference page. `@vantagecompute/docusaurus-plugin-pydoc` parses each module with `ast`
|
|
21
|
+
at build time and generates the entire reference section from what it finds. A docstring
|
|
22
|
+
you skip is a page section that does not exist.
|
|
23
|
+
|
|
24
|
+
The generator never imports the code, so it sees only what is literally written in the
|
|
25
|
+
source: docstrings, signatures, annotations, decorators and class-level assignments.
|
|
26
|
+
|
|
27
|
+
### What the generator renders, and what that forces
|
|
28
|
+
|
|
29
|
+
Read `docusaurus/node_modules/@vantagecompute/docusaurus-plugin-pydoc/src/renderer.js` if
|
|
30
|
+
you need the detail.
|
|
31
|
+
The four consequences that change how you write:
|
|
32
|
+
|
|
33
|
+
**1. The module docstring becomes the page's `## Overview`, and its FIRST LINE becomes the
|
|
34
|
+
Description column of the reference index, truncated to 80 characters.**
|
|
35
|
+
|
|
36
|
+
Write a first line that is a complete sentence, self-contained, and comfortably under 80
|
|
37
|
+
characters so the truncation never bites. It sits in a table beside every sibling module, so
|
|
38
|
+
it should distinguish this module from the others at a glance.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Good: 56 characters, says what the module is
|
|
42
|
+
"""A minimal JWS/JWT implementation, replacing python-jose."""
|
|
43
|
+
|
|
44
|
+
# Bad: truncated mid-word in the index table
|
|
45
|
+
"""Exception types and the small assertion helpers armasec uses in place of py-buzz."""
|
|
46
|
+
|
|
47
|
+
# Bad: burns the budget before saying anything
|
|
48
|
+
"""This module provides functionality for handling exceptions."""
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**2. Module-level constants render bare, as `NAME: annotation = value`, with no
|
|
52
|
+
description. `#:` comments above them are NOT captured.**
|
|
53
|
+
|
|
54
|
+
If a reader needs to understand a constant, explain it in the module docstring prose.
|
|
55
|
+
Nowhere else will reach the page. `JWKS_REFRESH_INTERVAL`, `MAX_BODY_BYTES`,
|
|
56
|
+
`DEFAULT_TIMEOUT`, `MAX_REDIRECTS` and `SUPPORTED_ALGORITHMS` all need this. Keep the `#:`
|
|
57
|
+
comments anyway; they serve someone reading the source.
|
|
58
|
+
|
|
59
|
+
**3. Class attributes render as a table with columns Attribute, Type and Default, and no
|
|
60
|
+
description column.**
|
|
61
|
+
|
|
62
|
+
The class docstring's `Attributes:` section is the only place a reader learns what a field
|
|
63
|
+
means. Every class with public attributes needs one, and it must cover every attribute.
|
|
64
|
+
|
|
65
|
+
**4. `__init__` and `__call__` are always rendered. Everything else beginning with an
|
|
66
|
+
underscore is private and never rendered.**
|
|
67
|
+
|
|
68
|
+
An earlier version of this file said these two render only when they carry a docstring.
|
|
69
|
+
That is wrong, and the difference matters: an undocumented `__init__` produces an **empty
|
|
70
|
+
section** on the page rather than being omitted. So a missing docstring there is visible to
|
|
71
|
+
readers as a gap, not hidden.
|
|
72
|
+
|
|
73
|
+
`TokenSecurity.__call__` is the most important method in the library. Methods like it earn
|
|
74
|
+
a substantial docstring, not a one-liner.
|
|
75
|
+
|
|
76
|
+
**5. Use `###` and below for any heading inside a docstring, never `##`.**
|
|
77
|
+
|
|
78
|
+
The generator emits docstrings verbatim into the page beneath its own `## Overview`
|
|
79
|
+
heading. An `##` in your prose escapes that section and collides with the generator's own
|
|
80
|
+
`## Constants` and `## Classes` headings, producing duplicate anchors and a broken table of
|
|
81
|
+
contents. This was a real defect found by building the site and reading the output.
|
|
82
|
+
|
|
83
|
+
### Module docstrings
|
|
84
|
+
|
|
85
|
+
Two to five short paragraphs. Cover: what this module is for, where it sits in the request
|
|
86
|
+
path, its constants, and anything non-obvious a reader would otherwise have to reconstruct
|
|
87
|
+
from the source.
|
|
88
|
+
|
|
89
|
+
Non-obvious is the operative word. Prefer the thing that is surprising, deliberate, or
|
|
90
|
+
load-bearing over a restatement of the module's name:
|
|
91
|
+
|
|
92
|
+
- `http.py`: `urllib.request.build_opener()` installs `FileHandler`, so an opener will
|
|
93
|
+
happily read `file://` URLs. The scheme guard in `get_json` is therefore load-bearing and
|
|
94
|
+
not redundant with the URL validation in `schemas.py`.
|
|
95
|
+
- `openid_config_loader.py`: the lock is a `threading.Lock` and not an `asyncio.Lock`
|
|
96
|
+
because an `asyncio.Lock` binds to the loop that first awaits it and goes stale across
|
|
97
|
+
test loops.
|
|
98
|
+
- `schemas.py`: `OpenidConfig.issuer` is a plain `str` and deliberately not `AnyHttpUrl`,
|
|
99
|
+
because it is compared against a token's `iss` claim by exact string equality and
|
|
100
|
+
normalization would reject valid tokens from providers publishing a bare-host issuer.
|
|
101
|
+
- `exceptions.py`: `handle_errors` deliberately does not re-wrap `ArmasecError` subclasses,
|
|
102
|
+
unlike py-buzz, so a specific 401 is not flattened into a generic 500.
|
|
103
|
+
|
|
104
|
+
If you find yourself writing a comment in the source that explains why something is the way
|
|
105
|
+
it is, ask whether it belongs in the module docstring instead, where readers of the
|
|
106
|
+
reference will see it.
|
|
107
|
+
|
|
108
|
+
### Class and function docstrings
|
|
109
|
+
|
|
110
|
+
Google style, which is what the codebase already uses.
|
|
111
|
+
|
|
112
|
+
Every public function and method needs `Args:` covering every parameter. Add `Returns:`
|
|
113
|
+
when the return value is not obvious from the name and annotation.
|
|
114
|
+
|
|
115
|
+
**`Raises:` matters more here than in most projects.** This is an authentication library:
|
|
116
|
+
the exception type a call raises determines the HTTP status a client sees. A caller needs to
|
|
117
|
+
know that `TokenDecoder.decode` can raise `ExpiredSignatureError` (401) or
|
|
118
|
+
`PayloadMappingError` (500), and what the difference means. Document it.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
def decode(self, token: str, **claims: Any) -> TokenPayload:
|
|
122
|
+
"""
|
|
123
|
+
Decode a JWT into a TokenPayload, checking signatures and claims.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
token: The token to decode.
|
|
127
|
+
claims: Additional constraints, such as `audience` or `issuer`. May include an
|
|
128
|
+
`options` dict merged over `decode_options_override`.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The verified payload, with `original_token` set to the input token.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
AuthenticationError: The token is malformed, its signature does not verify, or a
|
|
135
|
+
claim check failed. Subclasses carry the specific reason and all map to 401.
|
|
136
|
+
PayloadMappingError: The configured `permission_extractor` did not match a path in
|
|
137
|
+
the decoded token. Maps to 500, because that is a server misconfiguration
|
|
138
|
+
rather than a bad request.
|
|
139
|
+
"""
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Style
|
|
143
|
+
|
|
144
|
+
Explain why, not what. The signature already says what. A docstring reading
|
|
145
|
+
"Initializes the TokenSecurity instance" is worse than no docstring, because it occupies the
|
|
146
|
+
space where the useful sentence would go.
|
|
147
|
+
|
|
148
|
+
Say when something is deliberate. Several choices in this codebase look wrong until you know
|
|
149
|
+
the reason, and a reader who does not know will "fix" them.
|
|
150
|
+
|
|
151
|
+
Do not pad to fill a template. If a function has no failure modes, omit `Raises:`.
|
|
152
|
+
|
|
153
|
+
**Never use the em-dash (U+2014) or en-dash (U+2013) character.** Use commas, colons,
|
|
154
|
+
parentheses, or separate sentences. Hyphens in compound words are fine. This applies to
|
|
155
|
+
every file in the repository, not only docstrings.
|
|
156
|
+
|
|
157
|
+
### Adding a new module
|
|
158
|
+
|
|
159
|
+
The plugin's module list in `docusaurus/docusaurus.config.ts` is explicit. A new module
|
|
160
|
+
under `armasec_lite/` generates no reference page until you add it there, with a label.
|
|
161
|
+
|
|
162
|
+
`.github/workflows/deploy-docs.yml` fails the docs build when the number of generated pages
|
|
163
|
+
does not match the number of declared modules, so a module that is declared but fails to
|
|
164
|
+
introspect breaks the build rather than silently vanishing. It derives both counts
|
|
165
|
+
dynamically, so it needs no update when you add a module.
|
|
166
|
+
|
|
167
|
+
Do not add `armasec_lite/__init__.py`: it is re-exports, and its page would duplicate the
|
|
168
|
+
others.
|
|
169
|
+
|
|
170
|
+
### Checking your work
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Every module's first docstring line, with its length. Keep them under 80.
|
|
174
|
+
for f in armasec_lite/*.py armasec_lite/pluggable/*.py; do
|
|
175
|
+
python3 -c "
|
|
176
|
+
import ast
|
|
177
|
+
d = ast.get_docstring(ast.parse(open('$f').read()))
|
|
178
|
+
line = (d or '').strip().split(chr(10))[0] if d else '<NO MODULE DOCSTRING>'
|
|
179
|
+
print(f'{len(line):3d} $f {line}')
|
|
180
|
+
"
|
|
181
|
+
done
|
|
182
|
+
|
|
183
|
+
# Generate and read the reference. It is the actual deliverable, and building it is the
|
|
184
|
+
# only way to catch heading collisions and layout problems.
|
|
185
|
+
cd docusaurus && npm ci && npm run build && ls docs/api-reference/
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Read the built output, do not just check that the build passed.** Two defects in this
|
|
189
|
+
repository's docs were found only by reading generated pages: three module descriptions
|
|
190
|
+
silently truncated in the index table, and `##` headings in docstrings colliding with the
|
|
191
|
+
generator's own section headings.
|
|
192
|
+
|
|
193
|
+
### Renderer notes
|
|
194
|
+
|
|
195
|
+
`Args:`, `Returns:` and `Raises:` blocks render as single run-on paragraphs, because the
|
|
196
|
+
renderer emits docstring bodies verbatim into Markdown and Markdown collapses the newlines.
|
|
197
|
+
The content is correct; the formatting is poor, and it is worst on long `Raises:` sections.
|
|
198
|
+
|
|
199
|
+
This was fixed upstream in `@vantagecompute/docusaurus-plugin-pydoc` v0.1.1, which this
|
|
200
|
+
project depends on. Recognized Google-style sections now render as Markdown lists.
|
|
201
|
+
|
|
202
|
+
The general rule still holds: do not work around a rendering problem by reformatting
|
|
203
|
+
docstrings into Markdown. They would then read badly in an editor, in `help()`, and in every
|
|
204
|
+
IDE tooltip, which is where most people actually meet them. Fix the renderer in
|
|
205
|
+
`vantagecompute/docusaurus-plugin-pydoc` instead, which improves every Vantage project using
|
|
206
|
+
the plugin.
|
|
207
|
+
|
|
208
|
+
## Testing
|
|
209
|
+
|
|
210
|
+
`just test` runs the unit suite. `just lint` runs `ruff check`, `ruff format --check` and
|
|
211
|
+
`mypy` in strict mode. Both must pass before a commit.
|
|
212
|
+
|
|
213
|
+
Test-driven: write the failing test, run it, watch it fail for the reason you expect, then
|
|
214
|
+
implement. For a security defense, go further and confirm the test fails when the defense is
|
|
215
|
+
removed. A passing test proves nothing if it would also pass against undefended code, and
|
|
216
|
+
that has already happened once in this codebase: the algorithm-confusion test passed for the
|
|
217
|
+
wrong reason until someone checked, because the fixture it used happened to lack the field
|
|
218
|
+
that would have made the forgery succeed.
|
|
219
|
+
|
|
220
|
+
`tests/unit/test_jwt_attacks.py` is the attack suite. Its test names are consumed by a
|
|
221
|
+
benchmark that publishes a security-posture chart, so a name there is a public claim about
|
|
222
|
+
what is defended. Do not rename one casually, and do not let a name outlive what its test
|
|
223
|
+
actually checks.
|
|
224
|
+
|
|
225
|
+
## The justfile
|
|
226
|
+
|
|
227
|
+
`justfile` is a verbatim copy of `vantage-mcp-infra`'s, kept deliberately so it can be
|
|
228
|
+
pruned by hand later. Most of its recipes reference infrastructure this repository does not
|
|
229
|
+
have. `test`, `test-cov`, `lint`, `fmt` and the `docs-*` recipes work. Leave the rest alone.
|