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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/license-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: License check

on:
pull_request:
branches:
- master
paths:
- "pyproject.toml"
- "uv.lock"
- ".licenses/**"
- "scripts/check-licenses.py"
- ".github/workflows/license-check.yml"
push:
branches:
- master

jobs:
licenses:
name: Audit dependency licenses
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install the latest version of uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0

- name: Sync dependencies
run: uv sync --frozen --group dev

- name: Check licenses
id: check
run: |
uv run --frozen scripts/check-licenses.py \
--markdown "$GITHUB_STEP_SUMMARY"

- name: Upload license report
if: always()
run: |
uv run --frozen scripts/check-licenses.py --json > license-report.json || true
shell: bash

- name: Save report artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: license-report
path: license-report.json
retention-days: 30
if-no-files-found: warn
151 changes: 151 additions & 0 deletions .licenses/policy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# License policy for nextcloud-mcp-server
#
# The project is distributed under AGPL-3.0-only and the maintainer (Astrolabe
# Cloud) intends to offer commercial dual-licensed builds in the future. Every
# direct or transitive Python dependency must therefore satisfy BOTH:
#
# 1. AGPL-3.0-only distribution (the public release), and
# 2. proprietary/commercial distribution under separate terms.
#
# The lists below classify licenses by their compatibility with that goal.
#
# Maintenance: when a CI run flags a new dependency, either
# - move its license into `allowed` if it is permissive,
# - move it into the per-package `exceptions` table with rationale, or
# - replace the dependency.
#
# Reference: https://www.gnu.org/licenses/license-list.html

# Permissive licences that are compatible with both AGPL-3 distribution and
# proprietary re-licensing. These need no per-package review.
[allowed]
licenses = [
# Permissive
"MIT",
"MIT License",
"MIT-CMU",
"BSD",
"BSD License",
"BSD-2-Clause",
"BSD-3-Clause",
"3-Clause BSD License",
"Apache-2.0",
"Apache 2.0",
"Apache License",
"Apache License 2.0",
"Apache Software License",
"ISC",
"ISC License (ISCL)",
"PSF",
"PSF-2.0",
"Python Software Foundation License",
"Python-2.0",
"Unlicense",
"Artistic License",
"Artistic License 2.0",
# Weak copyleft — compatible via dynamic linking (Python import).
# Distributing a modified version of the LGPL component itself still
# requires source release, but linking from proprietary code is allowed.
"LGPL-3.0-or-later",
"GNU Lesser General Public License v3 or later (LGPLv3+)",
# File-level copyleft — only the MPL'd files themselves carry obligations,
# combinations with proprietary code are explicitly allowed (MPL-2.0 §3.3).
"MPL-2.0",
"Mozilla Public License 2.0 (MPL 2.0)",
]

# Composite SPDX expressions that resolve to an allowed arm. The checker
# splits on " OR " / " AND " and accepts the expression if EVERY AND-arm
# resolves to an allowed license, OR ANY OR-arm does.
# (Handled in the script — listed here for documentation.)
[allowed.composite_examples]
explained = [
"Apache-2.0 OR BSD-3-Clause", # accept Apache-2.0 arm
"Apache-2.0 OR MIT", # accept either
"Apache-2.0 AND BSD-2-Clause", # both permissive — accept
"Apache-2.0 AND MIT", # both permissive — accept
"MIT AND Python-2.0", # both permissive — accept
"GPL-3.0-or-later OR Apache-2.0", # accept Apache-2.0 arm
"Apache Software License; BSD License",# pip-licenses style — accept
"Apache Software License; MIT License",
"MIT License; Mozilla Public License 2.0 (MPL 2.0)",
]

# Strong copyleft (GPL/AGPL) and proprietary licenses are NOT allowed by
# default. Each package below has been individually reviewed and accepted
# with a recorded rationale. Re-review whenever the version changes.
[exceptions]

[exceptions.PyMuPDF]
license = "Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License"
rationale = """
Dual-licensed AGPL-3 / Artifex Commercial. Compatible with our AGPL release
out of the box. For any future commercial/proprietary build, Astrolabe Cloud
must obtain an Artifex commercial license — track this requirement before
shipping a non-AGPL artifact.
"""
allowed_for = ["agpl"]
review_required_for = ["commercial"]

[exceptions.pymupdf4llm]
license = "Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License"
rationale = "Same Artifex dual-license terms as PyMuPDF — see PyMuPDF entry."
allowed_for = ["agpl"]
review_required_for = ["commercial"]

[exceptions."pymupdf-layout"]
license = "Other/Proprietary License"
rationale = """
Artifex proprietary commercial-only package (LICENSE/COPYING contains only
'Commercial license. See artifex.com for details.'). Currently declared in
pyproject.toml but NOT imported anywhere in nextcloud_mcp_server/ — candidate
for removal. If retained, requires an explicit Artifex commercial agreement
even for the AGPL build.
"""
allowed_for = []
review_required_for = ["agpl", "commercial"]
action = "remove-or-license"

[exceptions."icalendar-searcher"]
license = "AGPL-3.0-or-later"
rationale = """
Pure AGPL — fine for our AGPL distribution but blocks commercial dual-licensing.
Either replace with a permissive alternative or negotiate a separate license
with the upstream author before any commercial release.
"""
allowed_for = ["agpl"]
review_required_for = ["commercial"]

# Packages whose license metadata is wrong/missing in the wheel. The actual
# license file shipped inside the wheel is recorded here.
[overrides]

[overrides.fastembed]
declared = "Other/Proprietary License" # classifier is wrong
actual = "Apache-2.0"
source = ".dist-info/licenses/LICENSE — full Apache-2.0 text"

[overrides."matplotlib-inline"]
declared = "UNKNOWN"
actual = "BSD-3-Clause"
source = ".dist-info/licenses/LICENSE — BSD 3-Clause, IPython Development Team"

[overrides.py_rust_stemmers]
declared = "UNKNOWN"
actual = "MIT"
source = ".dist-info/licenses/LICENSE — MIT, Copyright (c) 2024 qdrant"

# Packages explicitly NOT allowed under any circumstance. Kept here so the
# checker emits a clear message instead of a generic 'unknown license'.
[denied]
licenses = [
# Strong copyleft without dual-licensing — would force AGPL on commercial builds
"GPL-2.0-only",
"GPL-3.0-only",
"GPL-3.0-or-later",
"GNU General Public License v3 (GPLv3)",
"GNU General Public License v2 (GPLv2)",
# SSPL is non-OSI and known-incompatible with our distribution model
"SSPL",
"Server Side Public License",
]
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ that you are permitted to contribute under the CLA. For larger organizations,
a separate Corporate CLA may be required — open an issue if this applies to
you.

## Adding a Dependency

The project distributes under AGPL-3.0 today and Astrolabe Cloud intends to
offer commercial dual-licensed builds in the future, so every direct or
transitive dependency must be licensed under terms that allow both. The
allowlist and per-package exceptions live in
[`.licenses/policy.toml`](./.licenses/policy.toml) and are enforced by the
**License check** workflow on every PR.

Before opening a PR that adds or upgrades a dependency:

```bash
uv run scripts/check-licenses.py
```

If the script flags a new package, either:

- the package is permissively licensed → add the license string to the
`[allowed]` block in `.licenses/policy.toml`, **or**
- the package is copyleft / dual-licensed and you have a justification → add
it under `[exceptions]` with `rationale`, `allowed_for`, and
`review_required_for` fields, **or**
- the metadata is wrong → add an `[overrides]` entry pointing at the actual
license file shipped in the wheel, **or**
- replace the dep with a permissively-licensed alternative.

## Version Management

This project uses [commitizen](https://commitizen-tools.github.io/commitizen/) for version management following PEP 440 (`major_version_zero = true`, 0.x.x for pre-1.0).
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ dev = [
"reportlab>=4.0.0",
"ty>=0.0.1a25",
"pytest-otel>=2.0.1",
"pip-licenses>=5.5.5",
]

[project.scripts]
Expand Down
Loading
Loading