Skip to content

Support bidirectional host-loopback access - #512

Merged
Pedro Henrique Penna (ppenna) merged 5 commits into
devfrom
agents/fix-issue-276-validate-implementation
Oct 13, 2026
Merged

Pedro Henrique Penna (ppenna) merged 5 commits into
devfrom
agents/fix-issue-276-validate-implementation

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • support network.ingress.hostLoopback: "allow" without a caller-supplied forwarding list: the workload reaches host loopback through its gateway, subject to the egress policy, and the host reaches every TCP and UDP port on which the workload listens on a non-loopback address, on host 127.0.0.1 or ::1 alone
  • build on the private-network ingress mechanism of Support private-network inbound access #275 instead of a separate publisher: the guest's nvx-ingress announcer now also runs for virtnet_ingress=host-loopback, and guest-loopback listeners stay private
  • lower hostLoopback: allow and hostLoopbackForwards through nvx-rs and the ACI Edge Sandboxes OpenVMM backend to --host-loopback allow and up to 64 explicit TCP or UDP forwards; forwards need host-loopback access allowed, an egress default of allow, and no deny rule over the guest network, each protocol's host ports and guest ports appear at most once, and the sandbox record keeps them
  • egress rules that deny a gateway port also deny it at the addresses at which OpenVMM presents host peers to the guest, and those addresses lie inside the guest's subnet of each family, so the guest answers host clients over IPv6 loopback from its own address under any egress policy

Fixes #276.

OpenVMM dependency

Changes

  • guest/common/init starts nvx-ingress for virtnet_ingress=allow and virtnet_ingress=host-loopback
  • nvx-rs: NetworkPolicy gains the host-loopback access and validated loopback forwards, and the fake OpenVMM accepts --host-loopback-forward
  • ACI Edge Sandboxes: the OpenVMM backend advertises host-loopback allow and forwards (SandboxSpec::with_host_loopback_forward, AciEdgeSandbox::provision_with) and persists the forwards in the sandbox record without changing its format version
  • the host-loopback-policy microVM scenario checks, under an egress default of deny, the token and announcer, host clients of published IPv4 and IPv6 TCP listeners, UDP exchanges over IPv4 and IPv6 loopback, an explicit forward, guest-loopback privacy, and the snapshot rejection
  • ACI tests: launch arguments and rejections against the fake OpenVMM, host_loopback_access_is_bidirectional end to end, and the agent guest test
  • documentation: doc/run.md, doc/usage.md, the design documents, the ACI README, and the OpenVMM roadmap index

Review follow-ups

  • 54b80b44: nvx-rs rejects a second forward of one protocol to the same guest port, which OpenVMM keys by guest port and could not bind
  • 87be52ce and 3f9e5f0d: host-to-guest UDP over ::1 got no reply under an active egress policy, because OpenVMM presented host peers at link-local addresses that the guest answered from its link-local address. consomme: present IPv6 host peers inside the static subnet nanvix/openvmm#135 presents them inside the guest's subnet instead, and the scenario now exchanges UDP over IPv6 loopback; it fails with the previous pin
  • 5cfba311: nvx-rs rejects host-loopback access for a /30 guest network, which holds only the gateway and the guest, so OpenVMM presents IPv4 host peers at the gateway there and IPv4 UDP replies to the host would not return through the published port; the ACI OpenVMM backend reports it as policy_validation

Validation

On 5cfba311e9b5f4a6c59a815899830154fc2446d1, on Windows: the nvx-rs and ACI Edge Sandboxes gates (cargo fmt --check, clippy for all features and for the library without default features, tests for all features and default features, docs with -D warnings, and the MSRV 1.89 checks).

On 3f9e5f0dcb10236a3d041b84ac634ccaf8631d1a, whose successor changes only Rust validation and documentation, on Windows:

  • python scripts\nvx.py verify
  • ruff check and ruff format --check for scripts and .github/specula; pyright for Linux and Windows: 0 errors
  • python -m unittest scripts/test_microvm_tests.py scripts/test_guest_ingress.py: 198 tests OK (14 skipped on Windows)
  • Windows/WHP with OpenVMM built from 7f225e2c: test-microvm host-loopback-policy, private-network-ingress, ipv6-egress-policy, directional-network-policy, network-snapshot, and endpoint-policy-snapshot passed, and test-aci-edge-sandboxes passed 18/18

On 54b80b44: the same nvx-rs and ACI Edge Sandboxes gates. Before the rebase onto the current dev, whose new commits change neither guest/, the microVM scenarios, nor nvx-rs: ShellCheck and shfmt for guest/common/init.

CI passed on 420f280e (https://github.com/microsoft/nvx/actions/runs/38425484006) and 54b80b44 (https://github.com/microsoft/nvx/actions/runs/38431878926), including the Linux/KVM, Linux/MSHV, and Windows/WHP microVM and platform jobs. The OpenVMM revisions' own gates are listed in nanvix/openvmm#134 and nanvix/openvmm#135.

Known limitations

  • On Windows, a host listener that binds every host address after a publication, without asking for exclusive use of the port, succeeds and gets the port's traffic except loopback, which stays with the guest, as it would with any host process listening on loopback alone. Linux refuses that binding. doc/run.md documents this.
  • A /30 IPv4 guest network has no free address for an IPv4 host peer, so OpenVMM presents it at the gateway, as it did before this PR, and the guest's UDP replies to host clients over IPv4 loopback do not return through the published port. nvx-rs and therefore ACI Edge Sandboxes reject host-loopback access for a /30; direct OpenVMM and nvx.py use accept it, and doc/run.md documents the limit.

Exact NVX head: 5cfba311e9b5f4a6c59a815899830154fc2446d1
Exact OpenVMM head: 7f225e2cd635bbd1e1839e587e659996433396c8

Copilot AI balanced review requested due to automatic review settings October 12, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Listener classification, interrupted socket dumps, forward-policy validation, and a false-positive privacy test need correction.

4 open findings
What changed in this PR

Adds bidirectional host-loopback support across the NVX guest, OpenVMM integration, ACI backend, tests, and documentation.

Changes:

  • Publishes eligible guest TCP/UDP listeners on host loopback.
  • Persists host-loopback policy and explicit forwards through ACI provisioning.
  • Expands workload tests and documentation for allow/deny behavior.
File Description
openvmm Pins the supporting OpenVMM revision.
guest/​common/​nvx-host-loopback-agent.c Discovers and publishes guest listeners.
guest/​common/​init Starts the publisher after network setup.
scripts/​nvx_tools/​build_constants.py Adds the agent to guest artifacts.
scripts/​nvx_tools/​microvm_tests.py Exercises bidirectional access and isolation.
scripts/​nvx_tools/​microvm_test_scripts/​host-loopback-policy.sh.in Implements guest-side policy probes.
scripts/​test_microvm_tests.py Updates scenario unit tests.
aci_edge_sandboxes/​src/​openvmm/​mod.rs Advertises and validates new capabilities.
aci_edge_sandboxes/​src/​openvmm/​network.rs Translates host-loopback policy to CLI arguments.
aci_edge_sandboxes/​src/​openvmm/​launch.rs Emits explicit forwarding arguments.
aci_edge_sandboxes/​src/​openvmm/​state.rs Persists explicit forwards.
aci_edge_sandboxes/​src/​bin/​aci-edge-sandboxes-fake-openvmm.rs Accepts forwarding arguments in the fake.
aci_edge_sandboxes/​model/​src/​validate.rs Enables generic host-loopback capability handling.
aci_edge_sandboxes/​tests/​openvmm_fake.rs Tests generated launch arguments.
aci_edge_sandboxes/​tests/​agent_guest.rs Adds real generic publication coverage.
aci_edge_sandboxes/​README.md Documents backend behavior.
doc/​run.md Documents runtime options and restrictions.
doc/​design/​machine-and-device-abi.md Describes the publication protocol and limits.
doc/​design/​current-limits.md Updates snapshot limitations.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread aci_edge_sandboxes/src/openvmm/mod.rs Outdated
Comment thread guest/common/nvx-host-loopback-agent.c Outdated
Comment thread guest/common/nvx-host-loopback-agent.c Outdated
Comment thread scripts/nvx_tools/microvm_test_scripts/host-loopback-policy.sh.in Outdated
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-276-validate-implementation branch from 07cb008 to c7f3a41 Compare October 12, 2026 18:33
Copilot AI balanced review requested due to automatic review settings October 12, 2026 18:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Per-socket IPv6-only listeners are misclassified, and automatic UDP publication lacks end-to-end coverage.

3 open findings
4 resolved since last review

🧠 Review effort: Balanced

Comment thread guest/common/nvx-host-loopback-agent.c Outdated
Comment thread guest/common/nvx-host-loopback-agent.c Outdated
Comment thread scripts/nvx_tools/microvm_test_scripts/host-loopback-policy.sh.in Outdated
Copilot AI balanced review requested due to automatic review settings October 12, 2026 18:51
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-276-validate-implementation branch from c7f3a41 to a9f286a Compare October 12, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Listener-family detection and unreliable withdrawals can create incorrect or stale host-loopback bindings.

5 open findings

🧠 Review effort: Balanced

Comment thread guest/common/nvx-host-loopback-agent.c Outdated
Comment thread guest/common/nvx-host-loopback-agent.c Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread aci_edge_sandboxes/tests/agent_guest.rs Outdated
Comment thread guest/common/nvx-host-loopback-agent.c Outdated
Comment thread scripts/nvx_tools/microvm_tests.py Outdated
Comment thread kernel/config-microvm Outdated
Copilot AI balanced review requested due to automatic review settings October 12, 2026 20:12
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-276-validate-implementation branch from 58b7386 to 7869796 Compare October 12, 2026 20:12
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-276-validate-implementation branch from 7869796 to 21c1525 Compare October 12, 2026 20:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pinned implementation cannot simultaneously publish a generic port and an explicit remap targeting the same guest listener.

6 open findings

🧠 Review effort: Balanced

Comment thread scripts/nvx_tools/microvm_tests.py Outdated
Comment thread scripts/nvx_tools/microvm_tests.py Outdated
Copilot AI balanced review requested due to automatic review settings October 12, 2026 20:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It coordinates low-level guest, kernel, networking, persistence, and external OpenVMM changes requiring final human review.

2 open findings
4 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 12, 2026 21:35
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-276-validate-implementation branch from 21c1525 to bb98b18 Compare October 12, 2026 21:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pinned OpenVMM revision no longer matches the cited source PR head, making its provenance and validation stale.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread doc/design/machine-and-device-abi.md Outdated
Copilot AI balanced review requested due to automatic review settings October 12, 2026 21:43
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-276-validate-implementation branch from bb98b18 to 904b644 Compare October 12, 2026 21:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The agent integration test releases its selected publication port too early, creating a race with unrelated host processes.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Release port reservation only after guest listener is ready

aci_edge_sandboxes/​tests/​agent_guest.rs:2570

The selected port is released before the sandbox boots, leaving a long window for another host process to claim it. In that case OpenVMM correctly leaves the existing owner in place, but this test times out or connects to the unrelated service. Keep a loopback reservation through boot and guest-listener readiness, then release it so the publisher can claim the port on its next announcement.

🧠 Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit b5dc210 into dev Oct 13, 2026
33 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/fix-issue-276-validate-implementation branch October 13, 2026 19:36
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 15, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or an
  HTML href or src attribute, against the files that Git tracks, so a
  target that is untracked or differs in case, which GitHub cannot
  serve, fails too. A directory resolves, and its anchors are those of
  its README. An empty target, such as that of [Self](), links to its
  own document.
- It resolves each anchor against the heading slugs that GitHub
  generates, including the numeric suffixes of repeated headings, and
  against HTML id and name attributes. Other files accept only line
  anchors, such as #L10, within their length.
- It scans the blocks of each document as CommonMark nests them in
  block quotes and list items, with lazy continuation lines, so that,
  as on GitHub, front matter, fenced and indented code, code spans, HTML
  comments and blocks, escaped brackets, and invalid reference
  definitions hold no links, while the definitions and headings inside
  block quotes and list items count.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 15, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or an
  HTML href or src attribute, against the files that Git tracks, so a
  target that is untracked or differs in case, which GitHub cannot
  serve, fails too. A directory resolves, and its anchors are those of
  its README, whatever the README's case. An empty target, such as that
  of [Self](), links to its own document.
- It resolves each anchor against the heading slugs that GitHub
  generates, including the numeric suffixes of repeated headings, and
  against HTML id and name attributes. Other files accept only line
  anchors, such as #L10, within their length.
- It scans the blocks of each document as CommonMark nests them in
  block quotes and list items, with lazy continuation lines, and strips
  their markers before it parses inline content. As on GitHub, front
  matter, fenced and indented code, code spans, HTML comments and
  blocks, and escaped brackets and tags hold no links, while the
  definitions and headings inside block quotes and list items count.
- It parses each reference definition at the start of a paragraph
  whole, with its title, before the inline content that follows, and
  accepts any destination without spaces, as GitHub does. A reference
  link in a heading keeps its brackets in the anchor when no definition
  holds its label.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 15, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or an
  HTML href or src attribute, against the files that Git tracks, so a
  target that is untracked or differs in case, which GitHub cannot
  serve, fails too. A directory resolves, and its anchors are those of
  its README, whatever the README's case. An empty target, such as that
  of [Self](), links to its own document.
- It resolves each anchor against the heading slugs that GitHub
  generates, including the numeric suffixes of repeated headings, and
  against HTML id and name attributes. Other files accept only line
  anchors, such as #L10, within their length.
- It scans the blocks of each document as CommonMark nests them in
  block quotes, list items, and GitHub's footnote definitions, with
  lazy continuation lines, and strips their markers before it parses
  inline content. As on GitHub, front matter, fenced and indented code,
  code spans, HTML comments and blocks, the attributes of raw HTML
  tags, link titles, and escaped brackets and tags hold no links, while
  the definitions and headings inside those containers count. A
  footnote renders, with its fn- and fnref- anchors, only when inline
  content cites it.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 15, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or an
  HTML href or src attribute, against the files that Git tracks, so a
  target that is untracked or differs in case, which GitHub cannot
  serve, fails too. A directory resolves, and its anchors are those of
  its README, whatever the README's case. An empty target, such as that
  of [Self](), links to its own document.
- It resolves each anchor against the heading slugs that GitHub
  generates, including the numeric suffixes of repeated headings, and
  against HTML id and name attributes. Other files accept only line
  anchors, such as #L10, within their length.
- It scans the blocks of each document as CommonMark nests them in
  block quotes, list items, and GitHub's footnote definitions, with
  lazy continuation lines, and strips their markers before it parses
  inline content. As on GitHub, front matter, fenced and indented code,
  code spans, HTML comments and blocks, the attributes of raw HTML
  tags, link titles, image descriptions, and escaped brackets and tags
  hold no links, while the definitions and headings inside those
  containers count. A footnote renders, with its fn- and fnref-
  anchors, only when inline content cites it.
- It matches brackets as CommonMark does, so links do not nest: an
  inner link leaves the brackets around it literal. Each cell of a
  table parses alone, and the cells past the header's count, which
  GitHub drops, hold no links.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 15, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or an
  HTML href or src attribute, against the files that Git tracks, so a
  target that is untracked or differs in case, which GitHub cannot
  serve, fails too. A directory resolves, and its anchors are those of
  its README, whatever the README's case. An empty target, such as that
  of [Self](), links to its own document.
- It resolves each anchor against the slugs that GitHub generates from
  the rendered text of the headings, including the numeric suffixes of
  repeated headings, and against HTML id and name attributes. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as CommonMark nests them in
  block quotes, list items, and GitHub's footnote definitions, with
  lazy continuation lines, and strips their markers before it parses
  inline content. As on GitHub, front matter, fenced and indented code,
  and HTML comments and blocks hold no links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code
  spans, autolinks, and raw HTML are atomic, each destination is
  consumed with its title, and brackets match so that links do not nest
  and an image's description holds no links. A footnote renders, with
  its fn- and fnref- anchors, only where inline content cites it. Each
  cell of a table parses alone, and the cells past the header's count,
  which GitHub drops, hold no links.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 15, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or an
  HTML href or src attribute, against the files that Git tracks, so a
  target that is untracked or differs in case, which GitHub cannot
  serve, fails too. A directory resolves, and its anchors are those of
  its README, whatever the README's case. An empty target, such as that
  of [Self](), links to its own document.
- It resolves each anchor against the slugs that GitHub generates from
  the rendered text of the headings, including the numeric suffixes of
  repeated headings, and against HTML id and name attributes. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code
  spans, autolinks, and raw HTML are atomic, each destination is
  consumed with its title, and brackets match so that links do not nest
  and an image's description holds no links. A footnote renders, with
  its fn- and fnref- anchors, only where inline content cites it. Each
  cell of a table parses alone, and the cells past the header's count,
  which GitHub drops, hold no links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label.
- It decodes a destination as cmark does, its character references
  before its backslash escapes, and an HTML attribute as GitHub does,
  which keeps its backslashes, and its named references without
  semicolons, literal.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document.
- It resolves each anchor against the slugs that GitHub generates from
  the rendered text of the Markdown and HTML headings, including the
  numeric suffixes of repeated headings, and against the id and name
  attributes of the HTML elements that GitHub keeps. Other files accept
  only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code
  spans, autolinks, and raw HTML are atomic, each destination is
  consumed with its title, and brackets match so that links do not nest
  and an image's description holds no links. A footnote renders, with
  its fn- and fnref- anchors, only where inline content cites it. Each
  cell of a table parses alone, and the cells past the header's count,
  which GitHub drops, hold no links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label.
- It decodes a destination as cmark does, its character references
  before its backslash escapes, and an HTML attribute as GitHub does,
  which keeps its backslashes, and its named references without
  semicolons, literal.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document.
- It resolves each anchor against the slugs that GitHub generates from
  the rendered text of the Markdown and HTML headings, including the
  numeric suffixes of repeated headings, and against the id and name
  attributes of the HTML elements that GitHub keeps. Other files accept
  only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code
  spans, autolinks, and raw HTML are atomic, each destination is
  consumed with its title, and brackets match so that links do not nest
  and an image's description holds no links. A footnote renders, with
  its fn- and fnref- anchors, only where inline content cites it. Each
  cell of a table parses alone, and the cells past the header's count,
  which GitHub drops, hold no links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label, and the delimiters of emphasis and
  strikethrough match as cmark-gfm matches them, those of each link's
  text alone.
- It decodes a destination as cmark does, its character references
  before its backslash escapes, and an HTML attribute as GitHub does,
  which keeps its backslashes, and its named references without
  semicolons, literal.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor against the slugs that GitHub generates from
  the rendered text of the Markdown and HTML headings, including the
  numeric suffixes of repeated headings, and against the id and name
  attributes of the HTML elements that GitHub keeps. Other files accept
  only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code
  spans, autolinks, and raw HTML are atomic, each destination is
  consumed with its title, and brackets match so that links do not nest
  and an image's description holds no links. A footnote renders, with
  its fn- and fnref- anchors, only where inline content cites it. A
  table is a block of its own, which ends as GitHub's table extension
  ends it, and each of its cells parses alone, while the cells past the
  header's count, which GitHub drops, hold no links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label, and the delimiters of emphasis and
  strikethrough match as cmark-gfm matches them, those of each link's
  text alone.
- It decodes a destination as cmark does, its character references
  before its backslash escapes, and an HTML attribute as GitHub does,
  which keeps its backslashes, and its named references without
  semicolons, literal.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor against the slugs that GitHub generates from
  the rendered text of the Markdown and HTML headings, including the
  numeric suffixes of repeated headings, and against the id and name
  attributes of the HTML elements that GitHub keeps. Other files accept
  only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code
  spans, autolinks, and raw HTML are atomic, each destination is
  consumed with its title, and brackets match so that links do not nest
  and an image's description holds no links. A footnote renders, with
  its fn- and fnref- anchors, only where inline content cites it. A
  table is a block of its own, which ends as GitHub's table extension
  ends it, and each of its cells parses alone, while the cells past the
  header's count, which GitHub drops, hold no links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label, and the delimiters of emphasis and
  strikethrough match as cmark-gfm matches them, those of each link's
  text alone.
- It decodes a destination as cmark does, its character references
  before its backslash escapes, and an HTML attribute as GitHub does,
  which keeps its backslashes, and its named references without
  semicolons, literal.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor, in lowercase as GitHub's script looks it up,
  against the slugs that GitHub generates from the rendered text of the
  Markdown and HTML headings, including the numeric suffixes of repeated
  headings, and against the id and name attributes of the HTML elements
  that GitHub keeps. Other files accept only line anchors, such as #L10,
  within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label, a citation renders as its
  footnote's number, and the delimiters of emphasis and strikethrough
  match as cmark-gfm matches them, those of each link's text alone.
- It decodes a destination as cmark does, its character references
  before its backslash escapes, and an HTML attribute as GitHub does,
  which keeps its backslashes, and its named references without
  semicolons, literal.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor, in lowercase as GitHub's script looks it up,
  against the slugs that GitHub generates from the rendered text of the
  Markdown and HTML headings, including the numeric suffixes of repeated
  headings, and against the id and name attributes of the HTML elements
  that GitHub keeps. Other files accept only line anchors, such as #L10,
  within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label, a citation renders as its
  footnote's number, and the delimiters of emphasis and strikethrough
  match as cmark-gfm matches them, those of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph
  whole, within that paragraph, with its title, before the inline
  content that follows, and accepts any destination without spaces, as
  GitHub does. In heading text, a reference link or image resolves only
  when a definition holds its label, a citation renders as its
  footnote's number, and the delimiters of emphasis and strikethrough
  match as cmark-gfm matches them, those of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- As GitHub does, it slugs the text that each heading holds in the tree
  that libxml2's HTML parser builds from the rendered HTML. A start tag
  closes only the elements that libxml2 lets it close, so a paragraph's
  start tag closes a heading while a code block's does not, an end tag
  closes no element of a higher priority than its own, and a heading
  closes the heading that it starts in, so the text of a heading may run
  on across blocks.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- As GitHub does, it slugs the text that each heading holds in the tree
  that libxml2's HTML parser builds from the whole rendered document. A
  start tag closes only the elements that libxml2 lets it close, so a
  paragraph's start tag closes a heading while a code block's does not,
  an end tag closes no element of a higher priority than its own, and a
  heading closes the heading that it starts in. The text of a heading
  may thus run on across blocks, and the end tag of an element that an
  earlier block leaves open, such as a div, may close it.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- As GitHub does, it slugs the text that each heading holds in the tree
  that libxml2's HTML parser builds from the whole rendered document. A
  start tag closes only the elements that libxml2 lets it close, so a
  paragraph's start tag closes a heading while a code block's does not,
  an end tag closes no element of a higher priority than its own, and a
  heading closes the heading that it starts in. The text of a heading
  may thus run on across blocks, and the end tag of an element that an
  earlier block leaves open, such as a div, may close it.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- As GitHub does, it slugs the text that each heading holds in the tree
  that libxml2's HTML parser builds from the whole rendered document,
  where the paragraphs of tight lists, as cmark finds them, have no
  tags. A start tag closes only the elements that libxml2 lets it close,
  so a paragraph's start tag closes a heading while a code block's does
  not, an end tag closes no element of a higher priority than its own,
  and a heading closes the heading that it starts in. The text of a
  heading may thus run on across blocks, and the end tag of an element
  that an earlier block leaves open, such as a div, may close it.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 16, 2026
Nothing in CI checked the repository's relative Markdown links. #529
moved the design documentation, numbered its sections, and retargeted
every inbound link by hand, while an older link had already broken: the
MXC integration table in aci_edge_sandboxes/README.md linked the anchor
native-proxy-and-forwarded-ports of a heading that #512 had renamed to
"Native proxy and host loopback". GitHub opens a page at its top when an
anchor is missing, so such links break silently, and so do comments
that cite a document by path, as the citations of the removed time ABI
specification did (#562).

Add scripts/check_markdown_links.py, a standard-library check, and run
it in check-quality, whose job runs for every pull request that is
ready for review, including the documentation-only ones that are most
likely to break a link:

- It resolves every relative link and image target of the tracked
  Markdown files, whether an inline link, a reference definition, or
  the href of an HTML link or src of an image, against the files that
  Git tracks, so a target that is untracked or differs in case, which
  GitHub cannot serve, fails too. A directory resolves, and its anchors
  are those of its README, whatever the README's case. An empty target,
  such as that of [Self](), links to its own document, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. Other
  files accept only line anchors, such as #L10, within their length.
- It scans the blocks of each document as cmark nests them in block
  quotes, list items, and GitHub's footnote definitions, with lazy
  continuation lines and tabs that advance to the next multiple of four
  columns, and strips their markers before it parses inline content. As
  on GitHub, front matter, fenced and indented code, and the seven kinds
  of HTML blocks hold no Markdown links, while the definitions and
  headings inside those containers count.
- It parses inline content left to right as CommonMark does: code spans,
  autolinks, including GitHub's bare ones, and raw HTML are atomic, each
  destination is consumed with its title, and brackets match so that
  links do not nest and an image's description holds no links. A
  footnote renders, with its fn- and fnref- anchors, only where inline
  content cites it. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while the cells past the header's count, which GitHub drops, hold no
  links.
- It tokenizes the raw HTML, in the order that GitHub renders it, as an
  HTML parser does, to find the tags whose attributes hold targets and
  anchors. A comment hides everything that renders up to its end, even
  in later blocks, while a CDATA section, processing instruction, or
  declaration ends at its first >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- As GitHub does, it slugs the text that each heading holds in the tree
  that libxml2's HTML parser builds from the whole rendered document,
  where the paragraphs of tight lists, as cmark finds them, have no
  tags. A start tag closes only the elements that libxml2 lets it close,
  so a paragraph's start tag closes a heading while a code block's does
  not, an end tag closes no element of a higher priority than its own,
  and a heading closes the heading that it starts in. The text of a
  heading may thus run on across blocks, and the end tag of an element
  that an earlier block leaves open, such as a div, may close it.
- It parses each reference definition at the start of a paragraph whole,
  within that paragraph, with its title, before the inline content that
  follows, and accepts any destination without spaces, as GitHub does. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with its
  character references before its backslash escapes, and an HTML
  attribute as GitHub does, which keeps its backslashes, and its named
  references without semicolons, literal, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- Links into the openvmm submodule resolve against its pinned commit
  when it is checked out and are skipped otherwise, as in the quality
  job, whose checkout does not fetch submodules.
- It also resolves, from the repository root, every doc/ path, with any
  anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists
  the test fixtures that name missing documents, such as doc/notes.md
  in scripts/test_nvx_tools.py, and an entry that its file no longer
  needs fails the check. A mention followed by a character that could
  extend the path, such as the * of a pattern, is ambiguous and is
  skipped.
- It reports each problem with its file and line, as an error
  annotation in GitHub Actions, and then exits with status 1.

scripts/test_markdown_links.py covers the slugs, the parsing, the
resolution in temporary repositories, including a submodule, the
fixtures, and the command line, and validate-nvx runs it on Linux and
Windows.

Retarget the README link to native-proxy-and-host-loopback, the only
broken link that the check found, and document the check in
doc/contribute.md, doc/setup.md, and doc/project-structure.md.

Fixes #563

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support bidirectional host-loopback access

2 participants