Repository navigation
Adopt rift#1517: spawn-mode imposters inherit --host 127.0.0.1 #321
Description
RiftProcess.buildCommand always passes --host (SpawnOptions.host, default 127.0.0.1). From the rift release carrying achird-labs/rift#1517, host-less imposters bind that host, as Mountebank's do, so spawn-mode imposters become loopback-only. A system under test running in a container (host.docker.internal, host-gateway) can no longer reach them.
- Document this against that engine release.
- Grep the ITs for container-to-spawned-imposter traffic.
- Add a
SpawnOptionsnote pointing at per-imposterhost("0.0.0.0").
rift-java-testcontainers passes no --host and is unaffected.
Provenance: rift#1517 (PR achird-labs/rift#1541).
Triage
Verdict: build, docs and one IT only. Gate it on the first rift release after 0.22.0, and land it in the engine-bump PR. No SDK behaviour change. Route: Sonnet.
Engine facts (checked against v0.22.0 and origin/master @ 08f7bef2)
- rift#1517 is fixed by PR rift#1541, merge commit
08f7bef2, merged 2026-10-16.git tag --contains 08f7bef2is empty, so no release carries it yet. v0.22.0(the current pin): a host-less imposter binds0.0.0.0whatever--hostsays (crates/rift-mock-core/src/imposter/manager.rs:884, and:1188-1192on rebind).--hostonly moves the admin API.origin/master:ImposterManager::with_default_host(manager.rs:489) andbind_host_for(:495, used at:939and:1260) make a host-less imposter bind the server's--host. An imposter's ownhoststill wins, and the inherited host is never written into the config (soGET,?replayableand the datadir are unchanged). It is wired only in the CLI server:crates/rift-http-proxy/src/server.rs:997.with_default_host(cli.host.clone()).--local-onlypins the admin listener to loopback (server.rs:1421) but does not narrow imposter binds. Under the SDK default (--host 127.0.0.1 --local-only), the next release therefore puts host-less imposters on127.0.0.1only.- Embedded (FFI) builds
ImposterManager::new()with no default host (crates/rift-ffi/src/lib.rs:307,:2408;with_default_hostis never called there). Embedded mode is unaffected, andadminHostdoes not leak into imposters. - The Docker image gets no
--host, soRiftContaineris unaffected (rift-java-testcontainers/.../RiftContainer.javasets noMB_HOSTor--host). - The only wire-visible change is the
hostdescription text insdk-conformance/schema/imposter.schema.json. The schema IT (adopt: validate DSL output against the engine's imposter JSON Schema (sdk-conformance schema/imposter.schema.json) #314) validates shape, not descriptions.
rift-java today
RiftProcess.buildCommandalways passes--host opts.host()(rift-java-core/.../spawn/RiftProcess.java:211-212). The default is127.0.0.1(SpawnOptions.java:162), andlocalOnlydefaults to true.SpawnOptions.Builder.hostjavadoc (SpawnOptions.java:200-206) says it is "the interface the engine's admin API binds" and thatlocalOnly(true)"makes the engine bind loopback whatever this says". After #1517 the second claim is wrong for imposters:hostsets the bind of every host-less imposter even with localOnly on. The same wording appears indocs/design/sdk-api.md:185-186.- The SDK's own
uri()stays correct. A host-less imposter resolves throughConnectOptions.defaultHostResolverto the admin host (ConnectOptions.java:155-158), and that host is what the imposter now binds. An explicit host is honoured byImposterImpl(ImposterImpl.java:59,:78-80). No SDK code path breaks. - Grep for container-to-spawned-imposter traffic (
host.docker.internal,host-gateway,exposeHostPorts,withExtraHost,NetworkInterface,getLocalHost()acrossrift-java-*/src): no IT or conformance test reaches a spawned imposter from a container or a non-loopback address.RiftContainerForwardTargetITgoes container to container. Thehost.docker.internalhits (docs/intercept.md:357,InterceptOptionsAttachTest.java:56) are about the intercept listener, which has its own explicitInterceptOptions.host("0.0.0.0")and is not an imposter. - Stale comments, all correct for 0.22.0 and needing an update with the bump:
Ipv6HostIT.java:70("The engine binds an imposter on 0.0.0.0 ... whatever the admin host is"), and theImposterUriHostTest.java:18-21class doc ("default 0.0.0.0 ... whatever interface the admin API uses"). rift-scalamirrorsSpawnOptionsinbridge/src/main/scala/rift/bridge/Config.scala:150-153(host = "127.0.0.1",localOnly = true). It inherits the behaviour, and its scaladoc forhostneeds the same note. No code change is needed there.
Decision
- The issue's premise is correct: spawn-mode host-less imposters become loopback-only on the next engine release. Embedded and Testcontainers are unaffected. Two additions:
- "Grep the ITs" turns up nothing that breaks. The only change in this repo is documentation plus stale comments.
- The SpawnOptions javadoc is not only incomplete but becomes wrong. Its "localOnly makes it loopback whatever this says" does not hold for imposters.
- The change does belong in the SDK, as documentation only. The engine's new default is the Mountebank-compatible, safer one, and the SDK's own default (
127.0.0.1+localOnly) signals "keep it local". Do not have the SDK drop--hostor override it to keep the old all-interfaces imposter bind. That would silently undo an upstream security fix. - Do not add a spawn-wide "imposter host" option: the engine has no separate flag for one. Do not recommend
SpawnOptions.host("0.0.0.0")as the container recipe either. It does widen host-less imposters, butRiftProcess.java:61then builds the admin URI ashttp://0.0.0.0:port, which fails to connect on Windows. Per-imposterhost("0.0.0.0")is the recipe.
Design
Replace the SpawnOptions.Builder.host javadoc (SpawnOptions.java:200-206) with:
/**
* The engine's {@code --host}, default {@code 127.0.0.1}: an IP literal, including IPv6 {@code ::1}
* bare or bracketed (a bare one needs rift ≥ 0.18.0). The admin URI is built from it, bracketed
* when it is IPv6. {@link #localOnly(boolean)} (on by default) pins the admin API to loopback
* whatever this says.
*
* <p>From rift <NEXT> (rift#1517), an imposter created without its own {@code host} also binds
* this address, as in Mountebank, so by default spawned imposters are loopback-only and
* {@code localOnly} does not change that. A system under test in a container
* ({@code host.docker.internal}, host-gateway) must reach an imposter that names its own interface:
* {@code imposter("x").host("0.0.0.0")}. Before that release, host-less imposters bind every
* interface whatever this says.
*/<NEXT> is the actual version of the first release after 0.22.0. Mirror the text in docs/design/sdk-api.md:185-186, and add a short "Reaching a spawned imposter from a container" note to docs/testcontainers.md, next to the existing .host("0.0.0.0") intercept example at :121.
Implementation plan
-
pom.xml:<rift.engine.version>bump to the release carrying 08f7bef2 (done by the bump PR; this issue rides on it) -
rift-java-core/src/main/java/io/github/achirdlabs/rift/SpawnOptions.java: javadoc above -
docs/design/sdk-api.md: SpawnOptionshostcomment -
docs/testcontainers.md(ordocs/index.mdspawn section): container-reachability note with a per-imposterhost("0.0.0.0")snippet -
rift-java-conformance/src/test/java/io/github/achirdlabs/rift/conformance/Ipv6HostIT.java:70: fix the comment (host-less imposters now follow the spawn--host) -
rift-java-core/src/test/java/io/github/achirdlabs/rift/ImposterUriHostTest.java:18-21: fix the class doc - new
rift-java-conformance/src/test/java/io/github/achirdlabs/rift/conformance/SpawnHostInheritanceIT.java(below) - CHANGELOG/release notes: "spawn-mode host-less imposters are now loopback-only (engine rift#1517)"
- follow-up on rift-scala: same note on
SpawnConfig.hostscaladoc (bridge/.../Config.scala:150)
Tests
- Unit tests: none needed.
RiftProcessTestalready pins--hostin argv (RiftProcessTest.java:25,:38), and argv does not change. - Live IT
SpawnHostInheritanceIT, SPAWN lane (gatedTo(ConformanceTransport.SPAWN, ...), needsRIFT_IT=1), skipped when the engine version is below the release (EngineVersion.atLeast(version, "<NEXT>")):- On a default
Rift.spawn(), a host-less imposter answers atimposter.uri()(127.0.0.1) and refuses on[::1]when IPv6 loopback exists. This is the same probe as the engine'smanager.rsdefault_hosttests. - The same imposter with
.host("0.0.0.0")answers on a non-loopback local IPv4 address when one exists (pick the first site-local address fromNetworkInterface, orassumeTrueif there is none), and the host-less one refuses there.
- On a default
- Embedded: no new test. Optionally add an assertion to an existing embedded IT (surefire) that a host-less imposter on
adminHost("127.0.0.1")is still reachable on a non-loopback address, which pins "FFI does not inherit".
Acceptance criteria
- SpawnOptions javadoc and
sdk-api.mdsay thathostsets the bind of host-less imposters from the named release, thatlocalOnlydoes not narrow it, and that per-imposterhost("0.0.0.0")is the container recipe. - Stale "binds 0.0.0.0 whatever the admin host" comments are gone.
SpawnHostInheritanceITpasses against the bumped engine withRIFT_IT=1, and is skipped on 0.22.0.- No change to
RiftProcess.buildCommand,SpawnOptionsdefaults, embedded or Testcontainers behaviour.
Gating
Requires the first rift release after 0.22.0 (the one containing 08f7bef2). The javadoc has to name that version, and the IT can only pass on it, so everything ships in or right after the engine-bump PR. Nothing needs to land now: on the 0.22.0 pin the current docs are still accurate.
Activity
EtaCassiopeia commentedon Oct 17, 2026
Amendment from the 0.23.0 impact survey: rift#1523 (PR achird-labs/rift#1543) makes
--local-onlyrefuse non-loopback clients on imposters too, not only on the admin API. The SDK's spawn default islocalOnly=true, so on Linux a container that reaches the host through the bridge (172.17.0.1) is refused even withhost("0.0.0.0")on the imposter. Docker Desktop is still admitted, because its traffic arrives on loopback.The container recipe this issue documents must therefore say both:
host("0.0.0.0")on the imposter;SpawnOptions.builder().localOnly(false).The
SpawnOptions.hostjavadoc, theIpv6HostIT.java:70comment, theImposterUriHostTestclass doc andsdk-api.mdalso describe the pre-0.23 binding and need updating. rift 0.23.0 is now released (it contains #1541 and #1543), so theblockedlabel comes off once the pin bump merges.EtaCassiopeia commentedon Oct 17, 2026
Second amendment (from the triage of #323, which is closed as a duplicate of this issue): rift#1523 also affects the work planned here. Verified against rift-java @
e7fc2bcand rift v0.23.0.Items to add
--host 127.0.0.1#321's planned IT fails on 0.23.0 as written.SpawnHostInheritanceITcase 2 expects a.host("0.0.0.0")imposter on a defaultRift.spawn()to answer on a non-loopback local IPv4 address.localOnly=true.Rift.spawn(SpawnOptions.builder().localOnly(false).build())..host("0.0.0.0")imposter on that site-local address. The connection is accepted and then closed with no response. Read it to EOF, or catch the IOException. Then assertnumberOfRequests == 0through the admin API. This fails on 0.22.0 (the request is served), so gate it withEngineVersion.atLeast(version, "0.23.0"). UseassumeTruewhen there is no site-local address, as case 2 does.Builder.localOnly(boolean)Javadoc, and have thehostJavadoc link to it:hostJavadoc's "localOnly pins the admin API to loopback" to "pins the admin API to loopback and, from 0.23.0, refuses non-loopback imposter clients". Mirror both insdk-api.md:187-189.localOnly.docs/intercept.md:365-375(a container that proxies throughhost.docker.internal) keeps working under the default, because the intercept listener is unfiltered. Add one sentence there.Tests
These are the IT changes from items 1–2:
rift-java-conformance/.../SpawnHostInheritanceIT.java, SPAWN lane,RIFT_IT=1. No unit tests are needed: argv does not change (RiftProcessTest.java:27,:38).Docs / release notes
Items 3–4 above. The release note for #321 gains: "
localOnly(default on) now also refuses non-loopback clients on imposters (engine rift#1523); a Linux container needslocalOnly(false)".