Inspiration

MIT is the most-used open-source license. It asks one thing in return: keep the copyright notice with the code. Modern web apps bundle hundreds of packages into the JavaScript every visitor downloads, and bundlers strip the notices. Nobody's pipeline checks. With the EU Cyber Resilience Act making software bills of materials a legal duty, "what exactly do we ship, and under what terms" is becoming a release question, not a legal afterthought.

What it does

Clearance measures exactly which packages are in a build (from its sourcemaps) and checks each against what its license asks to ship. On Excalidraw (100k+ stars), it found:

  • 160 packages shipped. Notices for 119 don't ship anywhere, including Babel, CodeMirror, Radix and Sentry.
  • 16 Apache packages ship only a pointer to their license. That's debatable, so it's never counted as a violation.
  • 19 need a human decision: no license file, no declared license, or a dual license someone has to pick.

Then a GitLab Duo flow does the paralegal work, and a person approves every step:

  1. It audits what's unresolved, and waits for approval of the scope.
  2. It researches each package's license at the exact version that ships, from pinned sources, and drafts the attributions. The person approves, corrects ("use Apache-2.0 for dompurify") or rejects them.
  3. It records the decisions, makes the build generate and link third-party-notices.txt, and opens a merge request. The person approves, then merges when the pipeline is green.

The pipeline decides whether it worked, deterministically: coverage of the real build, a policy that every decision names the person who made it and its pinned evidence, and a policy that the agent can't edit the checker. Release and deploy are manual jobs, so a person clicks those too.

The result: 6 → 160 of 160 shipped packages covered. The original build's cleared job failed with "not cleared: 119 missing, 19 need a person's decision". After one review session, with three approvals and three decisions made by a person, release cleared-20 reads "License notices: 160 of 160 shipped packages covered": 0 missing, 0 pointer-only, 0 needing a decision. The live site serves its notices at https://clearance-excalidraw-3iqnep7gyq-uc.a.run.app/third-party-notices.txt, and as a readable, searchable page at https://clearance-excalidraw-3iqnep7gyq-uc.a.run.app/third-party-notices.html, linked from the app's main menu ("Third-party notices").

How I built it

  • GitLab Duo Agent Platform: a custom flow with three HumanInputComponent approval gates (Approve / Modify / Reject, with Modify routing feedback back to the agent), small and large models per step, and run_command so the agent measures the real build instead of guessing.
  • GitLab CI/CD: the nine DevSecOps stages, JUnit per package, manual review/release/deploy jobs, the security and dependency scanning templates, and releases carrying the coverage report and notices.
  • Deterministic core: bundle.mjs (what ships), notice-coverage.mjs (the judge), notices.mjs (the generator), policy.mjs (the decision record's rules). Plain Node, no model.
  • Google Cloud: Cloud Run serving Excalidraw's own nginx image, scale to zero, deployed through Workload Identity Federation (no keys).

Challenges I ran into

  • Honest counting. My first measure called a notice "shipped" if the sentence "the above copyright notice…" appeared, which is true of any MIT text. The final rule requires the package's own copyright line. Apache headers that point to the license are reported apart, not as violations.
  • Assisted, done properly. It would have been easier to let the agent write the attributions. Instead every decision carries a person's name, a timestamp and pinned evidence, and the policy job refuses anything else.
  • The agent left gaps instead of filling them. It didn't pick a license for dompurify or pako, and didn't invent text. For react-remove-scroll-bar 2.3.8, upstream has no v2.3.8 tag and v2.3.7 has no LICENSE file; it reported that rather than guess. I decided all three with Modify, and it recorded them.
  • My judge had a gap. It ignored recorded decisions for compound licenses, so cleared stayed red at "2 need a person's decision". I fixed it by hand: an OR is settled by the picked license, and an AND is met only when every license is met. The stricter rule then caught a real bug: the generator shipped pako's own MIT-only LICENSE instead of the recorded MIT + Zlib text. The original build still measures the same with the new judge.
  • Green MR, red master. The first master pipeline failed build:image: .dockerignore left clearance/ out of the Docker context, and MR pipelines didn't build the image, so nothing caught it. I fixed the context, and MR pipelines now build the real image without pushing it.
  • Platform limits. Flows can't merge, and participants can't create project access tokens, so merges, releases and deploys run under my account. Agent sessions are visible only to project members; the MR and its notes are the public record. The two tooling fixes above were mine, not the agent's.

Accomplishments that I'm proud of

  • Measured, not guessed. 6 of 160 shipped packages had their notices shipping before; 160 of 160 after. The number comes from the build's own sourcemaps, so it counts what reaches the browser, not what's in package.json.
  • The human part is real. Three decisions needed judgement (a dual license, a compound license, a package with no license file at its version), and the person made each one in a gate. The other 147 texts were researched by the agent at the exact version that ships and approved as they stood.
  • Readable, not just compliant. The live app links a searchable notices page from its own menu, beside the plain text file.

What I learned

  • An agent is most useful where it says "I don't know" precisely: the three packages it refused to guess about were the three that needed a person.
  • A judge needs judging too. Mine missed compound licenses, and an empty build would have read as fully covered; both are now tests the pipeline enforces.

What's next

A GitLab CI component any project can include. Coverage for server-side images, not just browser bundles. CycloneDX SBOMs that carry the human decisions alongside the scanner's facts.

Built With

  • docker
  • excalidraw
  • gitlab
  • gitlab-ci
  • gitlab-duo-agent-platform
  • google-cloud-run
  • nginx
  • node.js
  • sourcemaps
  • vite
  • workload-identity-federation
Share this project:

Updates

Submission history