diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..44db90c --- /dev/null +++ b/deny.toml @@ -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" diff --git a/docs/LICENSES.md b/docs/LICENSES.md new file mode 100644 index 0000000..bb2ed51 --- /dev/null +++ b/docs/LICENSES.md @@ -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. diff --git a/package.json b/package.json index 5312d5f..80de208 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "continuum-common", + "license": "UNLICENSED", "private": true, "version": "0.1.0", "workspaces": [ diff --git a/packages/rust-core/Cargo.toml b/packages/rust-core/Cargo.toml index 7e6cd22..18fad30 100644 --- a/packages/rust-core/Cargo.toml +++ b/packages/rust-core/Cargo.toml @@ -3,6 +3,10 @@ name = "continuum-core" version = "0.1.0" edition = "2021" 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] thiserror = "1.0" diff --git a/packages/rust-schema-instance/Cargo.toml b/packages/rust-schema-instance/Cargo.toml index 5662cea..63b4419 100644 --- a/packages/rust-schema-instance/Cargo.toml +++ b/packages/rust-schema-instance/Cargo.toml @@ -3,6 +3,10 @@ name = "continuum-schema-instance" version = "0.1.0" edition = "2021" 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] serde_json = "1" diff --git a/packages/ts-core/package.json b/packages/ts-core/package.json index fa68f37..4de7026 100644 --- a/packages/ts-core/package.json +++ b/packages/ts-core/package.json @@ -1,5 +1,6 @@ { "name": "@continuum/ts-core", + "license": "UNLICENSED", "version": "0.1.0", "private": true, "type": "module",