You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Restore loopback port publishing in the llama.cpp qualification runner #8667
Current main has a deterministic llama.cpp qualification-runner regression: the runner expects to expose the protected server through a loopback-only Docker port, but buildServerContainerArgv() returns arguments with no --publish entry.
This issue is pre-existing on main and independent of the OpenShell v0.0.101 upgrade in #8660. It should not block #8660.
The assertion performs pure argument construction. It has no Docker operation, asynchronous work, or concurrency, so this specific failure is not a race condition.
Cause
#8615 intentionally removed port publishing from the shared llama.cpp host-local materializer because normal product launches delegate host bridging to the Docker lifecycle provider.
The qualification runner does not use that lifecycle provider. It launches the container directly, then calls docker port and probes the resolved loopback port. Its request-guard path therefore still needs to add its own loopback-only --publish argument.
#8591 updated the qualification expectation to require the port mapping, but the runner adapter still delegates to the non-publishing shared materializer.
Proposed minimal fix
Keep the shared materializer and lifecycle architecture unchanged.
In scripts/checks/run-llama-cpp-dgx-spark-qualification.mts, make buildServerContainerArgv() insert the qualification-only Docker argument before the image reference:
Without hostPort: --publish 127.0.0.1::8081
With hostPort: 8081: --publish 127.0.0.1:8081:8081
Do not change the assertion to expect an empty result. That would leave the live qualification path unable to resolve or probe the server.
Acceptance criteria
The qualification runner emits an ephemeral loopback mapping when no host port is requested.
It emits the requested fixed loopback mapping when hostPort is provided.
The ordinary host-local materializer continues to leave bridging to the lifecycle provider.
The targeted qualification-runner and host-local runtime tests pass.
Any separate fixed-port availability race is handled independently and does not expand this fix.
buildServerContainerArgv() keeps delegating to the shared buildLlamaCppRequestGuardDockerArgv(), then splices one qualification-only --publish pair into the returned argv immediately before the image reference:
With hostPort: 8081: --publish 127.0.0.1:8081:8081
Position is load-bearing. In the request-guard argv the image reference is immediately preceded by --entrypoint <guardPath>, so the pair lands between the guard path and the image. Anything after the image reference would be passed to the request guard as a program argument instead of to Docker, which would fail quietly rather than loudly.
Nothing under src/ changed. The shared materializer and the Docker lifecycle provider are untouched, so ordinary product launches still leave host bridging to the lifecycle provider — which, as this issue notes, actively rejects a published container via the must not publish ownership rollback from #8544.
Acceptance criteria
Criterion
Status
Ephemeral mapping when no host port is requested
Done — 127.0.0.1::8081
Requested fixed mapping when hostPort is provided
Done — 127.0.0.1:8081:8081
Ordinary materializer still leaves bridging to the lifecycle provider
Done — pinned by host-local-runtime.test.ts (16/16) and the lifecycle provider's suite (41/41), both untouched
Targeted qualification-runner and host-local runtime tests pass
Done — qualification runner 12/12, and the failing assertion this issue reports is now green
The issue asks not to change the assertion to expect an empty result, and #8676 does not. But there is a second, separate assertion in the same test that also needed changing, in the opposite direction:
That one passed on main only because nothing was published at all — it asserted the bug. It now expects ["127.0.0.1:8081:8081"], matching the hostPort behavior this issue specifies.
While strengthening it, one further gap turned up: because the test passes hostPort: 8081 and serve.port is also 8081, toEqual(["127.0.0.1:8081:8081"]) cannot distinguish an implementation that renders the requested host port from one that duplicates the container port onto both sides. #8676 therefore adds a row with a host port that differs from the served port, and mutation testing confirms it is the only assertion that catches that specific bug.
Two adjacent findings, deliberately not fixed here
.mts files are exempt from formatting enforcement. The prekbiome-format hook's files pattern ends in .*\.ts$, which does not match a .mts extension, while biome.json includes **/*.mts. Both npm run validate:pr and the prek run --from-ref invocation in .github/workflows/pr.yaml inherit the gap, so formatting in every .mts file under scripts/checks/ and tools/e2e/ is unchecked. Happy to open a separate issue if that is worth tracking.
resolveLoopbackPort() accepts only 4-and-5-digit ports. Its regex is /^127\.0\.0\.1:([1-9][0-9]{3,4})$/u, while positiveInteger(contract.serve.port, ..., 65_535) permits any port from 1. A recipe declaring a shorter port would publish fine and then fail to resolve. Not reachable today, since the pinned recipe declares port: 8081.
One incidental improvement worth noting: restoring the mapping also re-arms a cleanup assertion that had become vacuous. cleanupOwnedRuntime() computes loopbackListenerClosed: loopbackPort === undefined || !(await tcpOpen(loopbackPort)); on main, resolveLoopbackPort() always threw, so loopbackPort stayed undefined and that teardown evidence field was unconditionally true.
The broad npm test gate is deferred to CI — a local run on an 8 GB development host produced only memory-pressure timeouts in files unrelated to this change, which pass in isolation. Details are in the PR's Verification section.
Activity
AzeelSajjad commentedon Aug 10, 2026
Opened #8676 for this.
What it does
buildServerContainerArgv()keeps delegating to the sharedbuildLlamaCppRequestGuardDockerArgv(), then splices one qualification-only--publishpair into the returned argv immediately before the image reference:hostPort:--publish 127.0.0.1::8081hostPort: 8081:--publish 127.0.0.1:8081:8081Position is load-bearing. In the request-guard argv the image reference is immediately preceded by
--entrypoint <guardPath>, so the pair lands between the guard path and the image. Anything after the image reference would be passed to the request guard as a program argument instead of to Docker, which would fail quietly rather than loudly.Nothing under
src/changed. The shared materializer and the Docker lifecycle provider are untouched, so ordinary product launches still leave host bridging to the lifecycle provider — which, as this issue notes, actively rejects a published container via themust not publishownership rollback from #8544.Acceptance criteria
127.0.0.1::8081hostPortis provided127.0.0.1:8081:8081host-local-runtime.test.ts(16/16) and the lifecycle provider's suite (41/41), both untouchedOne correction to the issue's proposed fix
The issue asks not to change the assertion to expect an empty result, and #8676 does not. But there is a second, separate assertion in the same test that also needed changing, in the opposite direction:
That one passed on
mainonly because nothing was published at all — it asserted the bug. It now expects["127.0.0.1:8081:8081"], matching thehostPortbehavior this issue specifies.While strengthening it, one further gap turned up: because the test passes
hostPort: 8081andserve.portis also8081,toEqual(["127.0.0.1:8081:8081"])cannot distinguish an implementation that renders the requested host port from one that duplicates the container port onto both sides. #8676 therefore adds a row with a host port that differs from the served port, and mutation testing confirms it is the only assertion that catches that specific bug.Two adjacent findings, deliberately not fixed here
.mtsfiles are exempt from formatting enforcement. Theprekbiome-formathook'sfilespattern ends in.*\.ts$, which does not match a.mtsextension, whilebiome.jsonincludes**/*.mts. Bothnpm run validate:prand theprek run --from-refinvocation in.github/workflows/pr.yamlinherit the gap, so formatting in every.mtsfile underscripts/checks/andtools/e2e/is unchecked. Happy to open a separate issue if that is worth tracking.resolveLoopbackPort()accepts only 4-and-5-digit ports. Its regex is/^127\.0\.0\.1:([1-9][0-9]{3,4})$/u, whilepositiveInteger(contract.serve.port, ..., 65_535)permits any port from 1. A recipe declaring a shorter port would publish fine and then fail to resolve. Not reachable today, since the pinned recipe declaresport: 8081.One incidental improvement worth noting: restoring the mapping also re-arms a cleanup assertion that had become vacuous.
cleanupOwnedRuntime()computesloopbackListenerClosed: loopbackPort === undefined || !(await tcpOpen(loopbackPort)); onmain,resolveLoopbackPort()always threw, soloopbackPortstayedundefinedand that teardown evidence field was unconditionallytrue.The broad
npm testgate is deferred to CI — a local run on an 8 GB development host produced only memory-pressure timeouts in files unrelated to this change, which pass in isolation. Details are in the PR's Verification section.fix(inference): restore qualification loopback publish (#8672)