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.
- 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.
- 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.
- 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.
- 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.
- 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
Architecture Finding
Type: tech-debt / supply chain (dependency-management boundary)
Affected area:
pkg/web/static/,pkg/web/static/vendor/,renovate.jsonCorral vendors 635 KB of third-party browser JavaScript, embedded into the binary via
go:embedand served to every web-UI client:vendor/xterm.min.js@xterm/xterm@5.5.0(vendor/README.md)vendor/novnc-rfb.esm.js@novnc/novnc@1.4.0(vendor/README.md)vendor/iron-remote-desktop.jsalpine.min.js3.15.12, in ADR-0004 prose onlyvendor/addon-fit.min.js@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.mdstates the offline/air-gapped rationale plus acurlrecipe for updates. The problem is that these five files sit outside every dependency-management mechanism the repo has:vendor/README.mdnames versions but no hashes;index.htmlloads all five with plain<script src=...>and nointegrity=/SRI attribute. There is nothing to verify a re-download against, so the documentedcurlupdate procedure cannot be checked — by a reviewer or by CI.iron-remote-desktop.jshas no recorded origin at all. 128 KB of privileged RDP client code, absent from thevendor/README.mdtable, 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.jsonextendsconfig:recommended, which discovers dependencies from manifests. There is nopackage.jsoncovering these files (the two that exist,e2e/package.jsonandtest/e2e/package.json, are Playwright harnesses), so no update PR will ever be raised.go.modgets the ratchet; the browser surface does not.ci.yml,ui-smoke.yml,e2e.yml,e2e-incus.yml,ct-images.yml, or theJustfilereferencevendor/oralpine.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.modcarries an inline note trackingGO-2026-5932down 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.jsandiron-remote-desktop.jsare 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.
pkg/web/static/vendor/MANIFEST.json— one machine-readable record per file: package, version, upstream URL, SHA-256. Includealpine.min.js(it is vendored under the same rationale even though it sits a directory up) so a single file covers all five.ci.ymlthat 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.iron-remote-desktop.js's provenance first — identify the upstream IronRDP web-component build and version, record it, and add it to thevendor/README.mdtable and update recipe alongside the other three. This is a prerequisite for (1), not a follow-up.index.htmlusing the manifest digests. Same-origingo:embedd assets make this defense-in-depth rather than critical, but it puts the expected hash where the loader can enforce it.regexManagersrule reading versions out of the manifest, or a dependency-onlypackage.jsonthat 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