cd /news/ai-agents/case-study-a-license-inventory-endpo… · home › topics › ai-agents › article
[ARTICLE · art-139304] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Case Study: A License Inventory Endpoint That Fails Closed on Unknown Obligations

A developer published a case study on building a license inventory endpoint that fails closed when a dependency's license is unknown or ambiguous, returning HTTP 422 rather than guessing. The approach freezes a classification table (permissive, weak copyleft, strong copyleft, unknown) and response rules before any coding agent is allowed to draft the handler, keeping classification in a pure function behind a standard-library HTTP server. The stated outcome is a failing test for unknown licenses rather than a more elaborate handler.

by read8 min views1 publishedSep 24, 2026

You should freeze license labels before a coding agent writes your release inventory endpoint, because fluent code can still invent obligations. This case study walks through one small service that reports third-party package licenses for a single repository snapshot. You will see the background, the goal, the implementation, the checks, and the lessons in that order. The useful outcome is a failing test for unknown licenses, not a longer handler that merely sounds complete.

Your release checklist asks whether every direct dependency has a known license obligation before you tag a build. A coding agent can scaffold a JSON endpoint quickly, yet it often guesses when a license string is missing or ambiguous. That guess becomes a product bug if your release gate treats the generated response as an authoritative decision. You need a small written contract that fails closed before any assistant is allowed to touch the handler.

You want one inventory endpoint that reads a frozen dependency snapshot and returns a stable JSON envelope. Each package must land in exactly one class, which is permissive, weak copyleft, strong copyleft, or unknown. Unknown must fail the release gate with HTTP 422, and it must never be rewritten as permissive. The handler may be drafted later, while the classification table and the fixtures have to come first.

You write the rules in a table so a reviewer can argue with the policy instead of arguing with generated branches. The table below is an engineering checklist for this case study, not legal advice and not a complete SPDX catalog. You should replace the sample rows with obligation labels that your own counsel has already approved.

Normalized license Class Release gate
MIT permissive allow
Apache-2.0 permissive allow
LGPL-2.1-only weak_copyleft allow_with_notice
GPL-3.0-only strong_copyleft block
empty or unrecognized unknown fail_closed

You also freeze three response rules in writing before any implementation code is allowed to exist. You keep those rules next to the table so a later draft cannot quietly drop a field. You review that short rules page before you accept any generated file into the working branch.

The snippets in this section are a proposed workflow you can copy and run, not a log of a production deployment. You keep the classifier in a pure function so the HTTP layer cannot hide a bad label. You then wrap that function in a tiny standard-library server that a free server option can host without extra framework weight.


CLASSES = {
    "MIT": ("permissive", "allow"),
    "Apache-2.0": ("permissive", "allow"),
    "LGPL-2.1-only": ("weak_copyleft", "allow_with_notice"),
    "GPL-3.0-only": ("strong_copyleft", "block"),
}

def classify(name, license_name):
    key = (license_name or "").strip()
    if key not in CLASSES:
        return {
            "name": name,
            "license": key,
            "class": "unknown",
            "gate": "fail_closed",
        }
    label, gate = CLASSES[key]
    return {
        "name": name,
        "license": key,
        "class": label,
        "gate": gate,
    }

def build_report(packages):
    items = [classify(item["name"], item.get("license")) for item in packages]
    blocked = any(item["gate"] in {"block", "fail_closed"} for item in items)
    return {"blocked": blocked, "items": items}

You add a handler that refuses to soften an unknown license into a misleading success status code. The status rule is part of the contract, so you do not leave it to the model's taste. You bind the server to port 8080 only for this exercise, and you change it if that port is already taken.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from inventory_rules import build_report

SNAPSHOT = [
    {"name": "left-pad", "license": "MIT"},
    {"name": "widget", "license": ""},
]

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/inventory":
            self.send_error(404)
            return
        report = build_report(SNAPSHOT)
        status = 422 if report["blocked"] else 200
        body = json.dumps(report).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

def main():
    ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

if __name__ == "__main__":
    main()

You should deliberately run a naive classifier that substitutes MIT for a blank license, because that is the bug agents tend to introduce. The function below is an anti-pattern for this case study, and you should not ship it. You keep that anti-pattern in the repo only long enough to watch the unknown-license fixture fail.

def naive_classify(name, license_name):
    key = (license_name or "MIT").strip() or "MIT"
    return {
        "name": name,
        "license": key,
        "class": "permissive",
        "gate": "allow",
    }

You then point the unknown-license fixture at naive_classify and expect an assertion error, not a green check. A green check on this anti-pattern means your test is weaker than the contract you already wrote. You fix the classifier until a blank input stays unknown and the recorded gate stays fail_closed.

You give the assistant the table, the three response rules, and the failing naive test, and you withhold permission to change assertions. You ask for a classifier that imports nothing beyond the Python standard library, so the free server run stays easy to reproduce. You reject any draft that adds a default license, a silent continue, or a catch that returns HTTP 200. You rerun the three contract tests yourself before you read the rest of the generated diff.

You write the assertions against the pure function so a generated handler cannot pass by changing the URL only. These tests describe the contract, and you should treat a failing unknown-license case as a release blocker. Run them locally first, then run the same file on whatever free server you actually have.

from inventory_rules import build_report

def test_known_permissive_stays_allowed():
    report = build_report([{"name": "left-pad", "license": "MIT"}])
    assert report["blocked"] is False
    assert report["items"][0]["gate"] == "allow"

def test_blank_license_fails_closed():
    report = build_report([{"name": "widget", "license": ""}])
    assert report["items"][0]["class"] == "unknown"
    assert report["items"][0]["gate"] == "fail_closed"
    assert report["blocked"] is True

def test_input_package_is_never_dropped():
    report = build_report([
        {"name": "left-pad", "license": "MIT"},
        {"name": "widget", "license": "NOT-A-LICENSE"},
    ])
    names = [item["name"] for item in report["items"]]
    assert names == ["left-pad", "widget"]
    assert report["blocked"] is True
python -m pytest -q test_inventory_rules.py
python server.py
curl -sS -D - http://127.0.0.1:8080/inventory

You accept the run only when the blank license returns fail_closed and the HTTP status is 422. You reject the run if the body omits widget or if the status becomes 200 while blocked is true. Those two failures are the exact bugs this small case study is designed to catch early.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode is the assistant you can point at this frozen table when you want a first draft of the handler. The outreach brief describes MonkeyCode as an open-source project that also offers free model access and a free server option. This walkthrough does not name models, quotas, hardware, or a duration, because those terms can change and were not verified here as stable facts. You should read the project page and confirm the current offer before you plan a release around it.

You use free model access only after the fixtures exist, and you paste the failing test output back as the review comment. You use a free server to run pytest and the curl check away from your laptop, so a teammate can see the same 422. If either free option is unavailable, the contract and the tests still stand on any Python 3 environment you control.

If you want the same split of labor, start from the MonkeyCode project page rather than from a generated handler. Confirm that free model access and the free server option still match the notes used in this article. Run the fixture set there only when those terms still fit the constraints of your release.

You should record pass or fail for each fixture, not a vanity metric about how fast the draft appeared. In this proposed case, success means three unit checks pass and the live curl shows blocked true for the sample snapshot. Failure means any unknown license is classified as permissive, or the endpoint returns 200 while a gate is fail_closed. Do not publish those checks as completed results until you have actually executed them on your machine.

You should skip this workflow if you need a lawyer's opinion rather than an engineering gate. The sample table is intentionally tiny, and it will mislead you if you treat it as a full license review. You should also skip it when your release cannot tolerate a hard 422 from the inventory route.

Skip it too if you need a capacity guarantee that the free server option does not claim. You should not adopt the assistant step if your team pastes generated handlers into production without rerunning fixtures. A free draft is optional labor, and it is never a substitute for the frozen table.

You learned that the expensive mistake is not a missing route, but a silent rewrite of an unknown license into an allowed class. You keep that mistake visible by freezing the table, the envelope, and the status code before generation starts. You can still let an assistant draft the handler, including through free model access, as long as the fixtures remain the reviewer. A free server is only a place to repeat those checks, and it is not evidence that the legal classification was correct.

── more in #ai-agents 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/case-study-a-license…] indexed:0 read:8min 2026-09-24 · —