Add the licence policy, and enforce permissive-only dependencies
Continuum is intended to be sold, so every dependency has to permit distribution in closed-source commercial software. Nothing recorded that before: our own crates and npm packages carried no licence field at all, which made `cargo deny` report them as `unlicensed` errors and left the actual policy nowhere. docs/LICENSES.md is the policy and lives here because it is the repo both other tiers already depend on. It covers the allow/deny list, the 2026-09-17 audit of all six repos (no copyleft reaches any binary), and the three categories `cargo deny` structurally cannot see: external binaries we spawn or ship, ML model weights, and self-hosted server software whose licence is not its client library's. Two findings in there are worth more than the tooling: Redis 7.4+ is RSALv2/SSPL and 8.0+ adds AGPLv3, and Ultralytics YOLO and Obico's print-failure model are both AGPL-3.0 - the obvious choice in each case is the one we cannot ship. Crates declare `LicenseRef-Proprietary` with `publish = false`; that string is deliberately not valid SPDX so it must never enter an allow list. npm packages use `"license": "UNLICENSED"`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
# Licence policy for a closed-source commercial product.
|
||||||
|
# See docs/LICENSES.md in continuum-common for the rationale and the
|
||||||
|
# non-cargo dependencies (external binaries, ML models) this file cannot see.
|
||||||
|
#
|
||||||
|
# Run: cargo deny check licenses
|
||||||
|
|
||||||
|
[licenses]
|
||||||
|
# Permissive only. Anything copyleft reaching a shipped binary would make
|
||||||
|
# the product undistributable without releasing our source.
|
||||||
|
allow = [
|
||||||
|
"MIT",
|
||||||
|
"Apache-2.0",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"ISC",
|
||||||
|
"Zlib",
|
||||||
|
"Unicode-3.0",
|
||||||
|
"Unlicense",
|
||||||
|
"MPL-2.0",
|
||||||
|
"CC0-1.0",
|
||||||
|
"BlueOak-1.0.0",
|
||||||
|
]
|
||||||
|
confidence-threshold = 0.9
|
||||||
|
|
||||||
|
# Our own crates declare `LicenseRef-Proprietary`, which is deliberately not
|
||||||
|
# an SPDX identifier and must never appear in the allow list above - a
|
||||||
|
# third-party crate claiming it would be a real finding. They are recognised
|
||||||
|
# as first-party by `publish = false` in their manifests.
|
||||||
|
private = { ignore = true }
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
deny = []
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
yanked = "deny"
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Licence policy
|
||||||
|
|
||||||
|
Continuum is a closed-source commercial product. We intend to sell it, so
|
||||||
|
every dependency must permit distribution in proprietary software without
|
||||||
|
publishing our own source. This document is the policy; `deny.toml` in each
|
||||||
|
repo is the machine-enforced half of it.
|
||||||
|
|
||||||
|
## The rule
|
||||||
|
|
||||||
|
**Permissive only.** A dependency is allowed if it is one of:
|
||||||
|
|
||||||
|
| Licence | Notes |
|
||||||
|
| --- | --- |
|
||||||
|
| MIT, ISC, 0BSD, BSD-2-Clause, BSD-3-Clause | The common case |
|
||||||
|
| Apache-2.0 (incl. `WITH LLVM-exception`) | Carries a patent grant, which is a plus |
|
||||||
|
| Unlicense, CC0-1.0 | Public-domain dedications |
|
||||||
|
| Zlib, BlueOak-1.0.0 | Permissive, OSI/Blue Oak approved |
|
||||||
|
| Unicode-3.0 | Unicode data tables |
|
||||||
|
| MPL-2.0 | File-level copyleft only; fine as long as we do not edit the dependency's own files |
|
||||||
|
|
||||||
|
**Forbidden, no exceptions:** GPL, LGPL (when statically linked), AGPL, SSPL,
|
||||||
|
RSAL, Commons Clause, "source available", CC-BY-NC, and any licence with a
|
||||||
|
field-of-use or non-commercial restriction.
|
||||||
|
|
||||||
|
Two rules that are easy to get wrong:
|
||||||
|
|
||||||
|
- **Dual-licensed dependencies are fine** - we elect the permissive option.
|
||||||
|
Record the election here so it is not re-litigated (see `node-forge` below).
|
||||||
|
- **LGPL is conditionally fine** but only via *dynamic* linking with the
|
||||||
|
library replaceable by the user. Rust static linking does not qualify.
|
||||||
|
Assume LGPL is forbidden unless someone writes down why a specific case
|
||||||
|
is not.
|
||||||
|
|
||||||
|
## How it is enforced
|
||||||
|
|
||||||
|
Rust, per repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo deny check licenses # proxy, ai-worker
|
||||||
|
cargo deny --manifest-path packages/rust-core/Cargo.toml check licenses # common, schemas
|
||||||
|
cargo deny --manifest-path apps/desktop/src-tauri/Cargo.toml check licenses # app
|
||||||
|
```
|
||||||
|
|
||||||
|
Our own crates declare `license = "LicenseRef-Proprietary"` with
|
||||||
|
`publish = false`. `LicenseRef-Proprietary` is deliberately *not* a real SPDX
|
||||||
|
identifier and must never be added to a `deny.toml` allow list - a
|
||||||
|
third-party crate claiming it would be a genuine finding, and `publish =
|
||||||
|
false` is what marks ours as first-party. Our npm packages use
|
||||||
|
`"license": "UNLICENSED"` with `"private": true`, which is npm's equivalent
|
||||||
|
convention.
|
||||||
|
|
||||||
|
JavaScript has no `cargo deny` equivalent wired up yet; the audit below was
|
||||||
|
done by walking installed `node_modules` manifests.
|
||||||
|
|
||||||
|
## What the tooling cannot see
|
||||||
|
|
||||||
|
`cargo deny` only reads the cargo graph. Three categories sit outside it and
|
||||||
|
have to be checked by hand:
|
||||||
|
|
||||||
|
1. **External binaries we spawn or ship.** Invoking a GPL program as a
|
||||||
|
separate process is not itself a derivative work, but *shipping* it in our
|
||||||
|
installer means distributing it under its own terms.
|
||||||
|
2. **ML model weights.** A model's licence is independent of the runtime that
|
||||||
|
loads it. This is the single biggest trap in this project (see
|
||||||
|
`continuum-ai-worker` below).
|
||||||
|
3. **Managed/self-hosted services.** The client library's licence is not the
|
||||||
|
server's licence. `ioredis` being MIT says nothing about Redis.
|
||||||
|
|
||||||
|
## Audit, 2026-09-17 (Redis resolved 2026-10-03)
|
||||||
|
|
||||||
|
All six repos audited. **No copyleft code is currently compiled into any
|
||||||
|
Continuum binary.** Every Rust crate across `continuum-proxy`,
|
||||||
|
`continuum-ai-worker`, `continuum-app/apps/desktop`, `continuum-common` and
|
||||||
|
`continuum-schemas` passes the policy above. The JS graphs are MIT/Apache/ISC/
|
||||||
|
BSD/BlueOak throughout.
|
||||||
|
|
||||||
|
Fixed in this pass: all six of our own crates and all nine of our npm
|
||||||
|
packages were missing a licence field (`cargo deny` reported them as
|
||||||
|
`unlicensed` errors), and no repo had a `deny.toml`.
|
||||||
|
|
||||||
|
### Findings carried forward
|
||||||
|
|
||||||
|
**Redis - RESOLVED 2026-10-03, we use Valkey.** Redis 7.4+ is RSALv2/SSPLv1
|
||||||
|
and Redis 8.0+ added AGPLv3 as a third option. All three are non-permissive,
|
||||||
|
and SSPL §13 specifically targets offering the software's functionality as a
|
||||||
|
service - which is what a cloud control plane does. The *clients* were never
|
||||||
|
the problem (`ioredis` and the `redis` crate are both MIT); the server we
|
||||||
|
deploy was.
|
||||||
|
|
||||||
|
We now deploy **Valkey** (BSD-3-Clause, Linux Foundation, forked from Redis
|
||||||
|
7.2.4). What changed: `REDIS_URL` -> `VALKEY_URL` and
|
||||||
|
`CONTINUUM_REDIS_URL` -> `CONTINUUM_VALKEY_URL`, the exported `redis` client
|
||||||
|
in `continuum-backend/src/queue/` renamed to `valkey`, `Config.redis_url` ->
|
||||||
|
`Config.valkey_url` in `continuum-ai-worker`, `/health` now reports a
|
||||||
|
`valkey` field, and `continuum-backend/docker-compose.yml` pins
|
||||||
|
`valkey/valkey:8-alpine`. Both tiers still typecheck/compile unchanged
|
||||||
|
otherwise, because Valkey speaks the same protocol.
|
||||||
|
|
||||||
|
Two things deliberately did **not** change, so nobody "fixes" them later:
|
||||||
|
the `redis://` URL scheme (it names the RESP wire protocol, and no client
|
||||||
|
library understands a `valkey://` scheme), and the client libraries
|
||||||
|
themselves (`ioredis`, the `redis` crate) - they are RESP clients, and their
|
||||||
|
names refer to the protocol's origin, not to the server we run.
|
||||||
|
|
||||||
|
**AI model weights - blocks `PLAN.md` step 4.** `src/inference/mod.rs`
|
||||||
|
currently stubs a `"spaghetti"` detection. The two obvious off-the-shelf
|
||||||
|
sources are both **AGPL-3.0**: Ultralytics YOLO (v5/v8/v11) and Obico / The
|
||||||
|
Spaghetti Detective's print-failure model. Neither can ship in a commercial
|
||||||
|
product without a paid enterprise licence. The `ort` crate itself is MIT and
|
||||||
|
fine. Permissive alternatives to evaluate when step 4 comes up: YOLOX
|
||||||
|
(Apache-2.0), RT-DETR (Apache-2.0), RF-DETR (Apache-2.0), or training our own
|
||||||
|
weights on a permissively-licensed architecture. **Decide the architecture
|
||||||
|
before collecting training data** - retraining because of a licence is the
|
||||||
|
expensive version of this mistake.
|
||||||
|
|
||||||
|
**go2rtc and ffmpeg - currently fine, keep it that way.**
|
||||||
|
`continuum-proxy/src/go2rtc.rs` spawns go2rtc (MIT) from `PATH` and expects
|
||||||
|
the operator to have installed it, so we distribute nothing. If that ever
|
||||||
|
changes: go2rtc shells out to ffmpeg, and ffmpeg is LGPL-2.1+ *only* when
|
||||||
|
built without `--enable-gpl`. Most distro and prebuilt ffmpeg binaries are
|
||||||
|
GPL builds (they bundle x264). Shipping one would be a GPL distribution.
|
||||||
|
|
||||||
|
**`node-forge` - dual-licensed, we elect BSD-3-Clause.** It appears in
|
||||||
|
`continuum-app` as `(BSD-3-Clause OR GPL-2.0)`. The election is ours to make
|
||||||
|
and is recorded here so the GPL half does not trigger a false alarm later.
|
||||||
|
|
||||||
|
**`caniuse-lite` - CC-BY-4.0, attribution required if redistributed.** A
|
||||||
|
build-time browserslist dependency in `continuum-app`; its data is not
|
||||||
|
bundled into the Nuxt output, so no attribution obligation today. It would
|
||||||
|
acquire one if anything started shipping the dataset itself.
|
||||||
|
|
||||||
|
**Infrastructure already clear.** PostgreSQL and pgvector (PostgreSQL
|
||||||
|
licence), PgBouncer (ISC), pgCat (MIT), Supabase (Apache-2.0), ONNX Runtime
|
||||||
|
(MIT), Cloudflare R2 (a service, governed by contract not licence) all
|
||||||
|
permit commercial use.
|
||||||
|
|
||||||
|
### Not findings
|
||||||
|
|
||||||
|
A `node_modules` walk turns up packages named `baz`, `browser_field`,
|
||||||
|
`invalid_main`, `false_main` and `nw-pre-gyp-module-test` with no licence
|
||||||
|
field. These are test fixtures *inside* other packages, not dependencies.
|
||||||
|
Ignore them.
|
||||||
|
|
||||||
|
## When adding a dependency
|
||||||
|
|
||||||
|
1. Check the licence against the table above **before** writing code against
|
||||||
|
it. Replacing a library after it is load-bearing is the costly path.
|
||||||
|
2. Run the repo's `cargo deny check licenses`.
|
||||||
|
3. If it is an external binary, a model, or a service, none of the tooling
|
||||||
|
will catch it - add it to the audit section above by hand.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "continuum-common",
|
"name": "continuum-common",
|
||||||
|
"license": "UNLICENSED",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ name = "continuum-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Shared error handling, tracing setup, and G-code helpers for Continuum Rust services"
|
description = "Shared error handling, tracing setup, and G-code helpers for Continuum Rust services"
|
||||||
|
# Proprietary: deliberately not an SPDX id, so cargo-deny treats it as
|
||||||
|
# first-party. See docs/LICENSES.md in continuum-common.
|
||||||
|
license = "LicenseRef-Proprietary"
|
||||||
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ name = "continuum-schema-instance"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Builds a JSON instance from a JSON Schema - fills const/enum/default, only builds required properties, everything else is left out"
|
description = "Builds a JSON instance from a JSON Schema - fills const/enum/default, only builds required properties, everything else is left out"
|
||||||
|
# Proprietary: deliberately not an SPDX id, so cargo-deny treats it as
|
||||||
|
# first-party. See docs/LICENSES.md in continuum-common.
|
||||||
|
license = "LicenseRef-Proprietary"
|
||||||
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@continuum/ts-core",
|
"name": "@continuum/ts-core",
|
||||||
|
"license": "UNLICENSED",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Reference in New Issue
Block a user