Open Source License Policy as Code
Turn an open source license policy from a wiki page into a versioned JSON document your build can execute: policy structure, matching rules, default outcomes, deprecated identifiers, policy hashing and review workflow.
Task: Express an existing open source licence policy as a versioned JSON document, validate it, and make its decisions reproducible. · Uses @wardenkit/open-source-licence-policy · Product overview · Documentation
Why a prose policy cannot be enforced
Most engineering organizations already have an open source licence policy. It usually lives in a wiki page, a slide, or a spreadsheet column that says "approved". That form has three failure modes: it cannot be executed by a build, it cannot be diffed in review, and it cannot explain why a specific dependency was accepted eight months ago.
Policy as code fixes all three by making the policy a file in your repository. The file states the decisions, the build executes them, and every result records a hash of the exact policy that produced it. Nothing about your decisions changes; only their form and their reproducibility do.
Which licences your organization allows, denies or sends to review is your decision, made with your own advisers. Every identifier on this page is an illustration of a customer-authored policy, not a WardenKit recommendation, and WardenKit gives no legal advice or compatibility opinion.
The shape of an executable policy
A policy document has two halves: the fallbacks that decide anything you did not name explicitly, and the rules that name specific licence identities. Getting the fallbacks right matters more than the length of the rule list, because the fallbacks decide the long tail.
{
"policySchemaVersion": 1,
"policyId": "acme-platform-licence-policy",
"policyVersion": "2026.09.1",
"defaultOutcome": "review-required",
"complexExpressionTreatment": "review-required",
"deprecatedIdTreatment": "review-required",
"ciFailureOutcomes": ["policy-denied", "invalid-unsupported"],
"rules": [
{ "ruleId": "allow-mit", "match": { "type": "exact-license-id", "licenseId": "MIT" }, "outcome": "policy-allowed" },
{ "ruleId": "allow-apache", "match": { "type": "exact-license-id", "licenseId": "Apache-2.0" }, "outcome": "policy-allowed" },
{ "ruleId": "allow-bsd-3", "match": { "type": "exact-license-id", "licenseId": "BSD-3-Clause" }, "outcome": "policy-allowed" },
{ "ruleId": "allow-dual-apache-mit", "match": { "type": "exact-normalized-expression", "expression": "Apache-2.0 OR MIT" }, "outcome": "policy-allowed" },
{ "ruleId": "allow-first-party", "match": { "type": "exact-license-ref", "licenseRef": "LicenseRef-acme-internal" }, "outcome": "policy-allowed" },
{ "ruleId": "deny-agpl-3-only", "match": { "type": "exact-license-id", "licenseId": "AGPL-3.0-only" }, "outcome": "policy-denied" }
]
}| Field | What it decides |
|---|---|
defaultOutcome | The outcome for a component no rule matched. policy-denied gives you an allowlist; review-required gives you a triage queue. |
complexExpressionTreatment | The outcome for a multi-operand expression such as "Apache-2.0 OR MIT" that no exact-normalized-expression rule matched. |
deprecatedIdTreatment | review-required, policy-denied, or treat-as-listed to let your rules decide a deprecated identifier as normal. |
ciFailureOutcomes | Which outcomes are CI-blocking. It must always include policy-denied; a policy that omits it is rejected as invalid. |
rules | Explicit decisions, matched by exact-license-id, exact-license-ref or exact-normalized-expression. |
Start strict on the fallbacks and widen deliberately. A policy whose defaultOutcome is review-required will never silently accept a licence you have not considered, which is the property that makes the policy trustworthy as evidence.
Writing rules that actually match
Rules match normalized values, not the raw strings in your metadata. Three rule types cover the whole surface, and each has one pitfall worth knowing before you write forty rules.
| Rule type | Matches | Pitfall |
|---|---|---|
exact-license-id | One listed SPDX identifier, for example Apache-2.0. | It does not match a compound expression that merely contains that identifier. |
exact-license-ref | One custom LicenseRef- identity. | References normalize to lower case: write LicenseRef-acme-internal, not LicenseRef-Acme-Internal. |
exact-normalized-expression | A whole expression in canonical normalized form. | "MIT OR Apache-2.0" is not canonical; the canonical form is "Apache-2.0 OR MIT". |
The canonical-form requirement is enforced rather than merely documented: a rule expression that is not canonical is rejected at validation time with POLICY_EXPRESSION_NOT_CANONICAL, so a rule that could never match cannot reach production. Check any expression before you commit a rule for it.
node -e "import('@wardenkit/open-source-licence-policy').then(m =>
console.log(m.createEvaluator().normalizeExpression('MIT or apache-2.0')))"
# { valid: true, normalizedExpression: 'Apache-2.0 OR MIT', shape: 'complex', diagnostics: [] }Duplicate rule identifiers, duplicate matches and directly conflicting rules are refused as POLICY_DUPLICATE_RULE, POLICY_DUPLICATE_MATCH and POLICY_CONFLICTING_RULES. A policy that contradicts itself is a defect, not a tie to be broken silently at evaluation time.
Validate and hash the policy before you rely on it
Validate the policy as its own step, separately from evaluating components against it. An invalid policy is a pipeline defect and should never be reported as a clean licence result.
npx wardenkit-licence-policy validate-policy --policy licence-policy.json --json
# {"valid":true,"policyHash":"b52e45c0…","diagnostics":[]}The policy hash is the part that makes the policy auditable. It is stable for identical policy content, it is recorded in the provenance block of every evaluation result, and it changes the moment anyone edits a rule. Six months later, the hash in an archived result tells you exactly which policy decided that release.
import { readFile } from "node:fs/promises";
import { validatePolicy } from "@wardenkit/open-source-licence-policy";
const policy = JSON.parse(await readFile("licence-policy.json", "utf8"));
const check = validatePolicy(policy);
if (!check.valid) {
for (const diagnostic of check.diagnostics) {
console.error(`${diagnostic.code}: ${diagnostic.message} (${diagnostic.path})`);
}
process.exit(2);
}
console.log(policy.policyId, policy.policyVersion, check.policyHash);Reviewing and changing the policy
Once the policy is a file, changing it becomes a pull request, which is the point. A workable review workflow needs only four habits.
- Commit the policy beside the code it governs, and bump policyVersion in the same change that edits a rule.
- Require review on the policy path from whoever owns the decision — legal, engineering leadership, or both.
- Keep the evaluation result as a build artefact, so the policy hash is retained with the release it approved.
- Move a licence out of review-required and into an explicit rule as soon as a decision is made, so the queue reflects genuinely undecided cases.
What the tool never does is decide for you. Evaluation is deterministic and local: identical component records, identical policy, identical product version and identical reference version produce byte-identical output, with no telemetry and nothing transmitted. The judgment stays in your policy file, where it can be reviewed.
Next: validate the expressions your metadata actually contains, then wire the check into CI so the policy is enforced on every change.
Run this locally
- Try the same workflow locally — Open-Source Licence Policy SDK quickstart
- Learn what the SDK does, its boundaries and its plans — Open-Source Licence Policy SDK
Related guides
- Validate SPDX License Expressions in Node.js — Call a local SPDX expression validator from your own Node.js code and act on its diagnostics.
- Run License Policy Checks in CI — Fail a build when a dependency's licence is denied by your policy, and keep the JSON evidence for the release.
- SPDX License Expression Reference: AND, OR, WITH and LicenseRef — Look up how a specific SPDX expression parses, normalizes and fails, with a runnable example for each case.
Deterministic tooling, professional judgment
WardenKit performs deterministic technical validation, normalization and evaluation of the policy you author, over the records you supply. It does not provide legal advice, determine legal permissions or obligations, opine on licence compatibility, or certify compliance. Which licences your organization allows, denies or reviews remains your decision.
Product overview and pricing · All guides · Docs · Support
Corrections and questions: support@wardenkit.com.