Skip to content

[architect] 635KB of vendored browser JS sits outside dependency management — one file with no provenance, no hashes, no CI gate #215

Description

@hanthor-hive-agent

Architecture Finding

Type: tech-debt / supply chain (dependency-management boundary)
Affected area: pkg/web/static/, pkg/web/static/vendor/, renovate.json

Corral vendors 635 KB of third-party browser JavaScript, embedded into the binary via go:embed and served to every web-UI client:

File Bytes Provenance recorded
vendor/xterm.min.js 289,714 @xterm/xterm@5.5.0 (vendor/README.md)
vendor/novnc-rfb.esm.js 168,898 @novnc/novnc@1.4.0 (vendor/README.md)
vendor/iron-remote-desktop.js 128,765 none
alpine.min.js 46,346 3.15.12, in ADR-0004 prose only
vendor/addon-fit.min.js 1,779 @xterm/addon-fit@0.10.0 (vendor/README.md)

The vendoring decision itself is sound and documented — ADR-0004 chose no build step and no node toolchain deliberately, and vendor/README.md states the offline/air-gapped rationale plus a curl recipe for updates. The problem is that these five files sit outside every dependency-management mechanism the repo has:

  • No integrity record. vendor/README.md names versions but no hashes; index.html loads all five with plain <script src=...> and no integrity=/SRI attribute. There is nothing to verify a re-download against, so the documented curl update procedure cannot be checked — by a reviewer or by CI.
  • iron-remote-desktop.js has no recorded origin at all. 128 KB of privileged RDP client code, absent from the vendor/README.md table, with no version, no upstream URL, and no entry in the update recipe. ADR-0002 describes the IronRDP design but names no artifact. Nobody can currently answer "which IronRDP build is this, and how do I get the next one" without diffing against upstream by hand.
  • Renovate is blind to all of it. renovate.json extends config:recommended, which discovers dependencies from manifests. There is no package.json covering these files (the two that exist, e2e/package.json and test/e2e/package.json, are Playwright harnesses), so no update PR will ever be raised. go.mod gets the ratchet; the browser surface does not.
  • No CI gate. None of ci.yml, ui-smoke.yml, e2e.yml, e2e-incus.yml, ct-images.yml, or the Justfile reference vendor/ or alpine.min.js. Nothing detects drift, staleness, or an edited-in-place vendored file.

Impact

The gap is asymmetric with how the rest of the repo is run. go.mod carries an inline note tracking GO-2026-5932 down to which subpackage is imported — that is the standard this repo holds itself to. Meanwhile a CVE in xterm.js, noVNC, Alpine, or IronRDP produces no signal here at all: no advisory match, no Renovate PR, no failing job. The files are pinned in the weakest sense — frozen, not tracked.

novnc-rfb.esm.js and iron-remote-desktop.js are the console path — they render VM framebuffers and RDP sessions, so they sit directly on the operator's credential path. A vendored copy with no digest is also unfalsifiable after the fact: if one of these files were modified in-tree, no test, lint, or review artifact would show it, because there is no recorded expected state to compare against.

Recommendation

Keep the no-build-step decision; close the tracking gap around it.

  1. Add pkg/web/static/vendor/MANIFEST.json — one machine-readable record per file: package, version, upstream URL, SHA-256. Include alpine.min.js (it is vendored under the same rationale even though it sits a directory up) so a single file covers all five.
  2. Gate it in CI. A ~10-line step in ci.yml that re-computes SHA-256 over the five files and diffs against the manifest catches both in-place edits and a manifest that was updated without the artifact. This is the cheapest possible version of the check and needs no node toolchain.
  3. Establish iron-remote-desktop.js's provenance first — identify the upstream IronRDP web-component build and version, record it, and add it to the vendor/README.md table and update recipe alongside the other three. This is a prerequisite for (1), not a follow-up.
  4. Add SRI attributes in index.html using the manifest digests. Same-origin go:embedd assets make this defense-in-depth rather than critical, but it puts the expected hash where the loader can enforce it.
  5. Give Renovate a handle. Either a regexManagers rule reading versions out of the manifest, or a dependency-only package.json that is never installed. Without one of these, step 1 is documentation that goes stale on the same schedule the files do.

Filed by architect agent (ACMM L6 — full mode)

— hive: agent=architect backend=claude model=claude-opus-5 claude=2.1.226

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/architectApproved by a Hive merger/owner for auto-merge on green CIarchitectureApproved by a Hive merger/owner for auto-merge on green CIhive/hive-keen-minkApproved by a Hive merger/owner for auto-merge on green CI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions