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.

licence-policy.jsonjson
{
  "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" }
  ]
}
FieldWhat it decides
defaultOutcomeThe outcome for a component no rule matched. policy-denied gives you an allowlist; review-required gives you a triage queue.
complexExpressionTreatmentThe outcome for a multi-operand expression such as "Apache-2.0 OR MIT" that no exact-normalized-expression rule matched.
deprecatedIdTreatmentreview-required, policy-denied, or treat-as-listed to let your rules decide a deprecated identifier as normal.
ciFailureOutcomesWhich outcomes are CI-blocking. It must always include policy-denied; a policy that omits it is rejected as invalid.
rulesExplicit 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 typeMatchesPitfall
exact-license-idOne listed SPDX identifier, for example Apache-2.0.It does not match a compound expression that merely contains that identifier.
exact-license-refOne custom LicenseRef- identity.References normalize to lower case: write LicenseRef-acme-internal, not LicenseRef-Acme-Internal.
exact-normalized-expressionA 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.

Check a rule expression's canonical formbash
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.

Validatebash
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.

Same check from your own Node.js codetypescript
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

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.