Compass gateway boot wiring
Problem / Intent
Section titled “Problem / Intent”Tracking: RIG-2862 (remainder: boot wiring). Addendum to
compass-server-llm-gateway §T1 and
to the §Global Constraints “Compass boot entrypoint” bullet of
compass-gateway-credential-door-agent-token.
The fork’s compass boot entrypoint (packages/coding-agent/src/cli/gateway-boot.ts
in RigelBuild/oh-my-pi) calls runAuthGatewayCommand({ action: "serve" }).
runServe (packages/coding-agent/src/cli/auth-gateway-cli.ts) throws under
if (!brokerConfig) { with
"`omp auth-gateway serve` requires OMP_AUTH_BROKER_URL …". So the
published image cannot serve under the stack. This record wires the
entrypoint to the Compass Server’s doors:
- agent tokens are verified by
GatewayAuth.VerifyAgentToken; - each owner gets its own
AuthStorageoverCompassAuthCredentialStore; - the stack mints, registers and mounts the
llm-gatewayservice bearer that those door calls need.
Approach
Section titled “Approach”Fork: a compass boot, not a reshaped runServe
Section titled “Fork: a compass boot, not a reshaped runServe”gateway-boot.ts calls startAuthGateway itself with compass adapters.
runServe stays the broker path for upstream omp auth-gateway serve. The
boot reuses registerGatewayShutdown and GATEWAY_MODEL_KINDS, which this
change exports. The gateway adapters live in packages/ai/src/compass/, next
to the store.
- Transport (
wire.ts). The verifier and the store share two helpers:normalizeCompassBaseUrlreplacesassertSecureBaseUrl. It allows https, or http on loopback only.fetchCompassRpcResponsesends Connect JSON with the service bearer andredirect: "error".
agent-verifier.ts: the authorizer. It posts the presented bearer toGatewayAuth/VerifyAgentToken. The mapping follows theAuthGatewayAuthorizerdoc (auth-gateway/types.ts): “nullto answer 401. A throw answers 503 … bound any remote call”.- No bearer →
null, with no call. - 200 →
{ kind: "agent", agentAccountId, ownerUserId }. An empty id throws. unauthenticatedwith messageinvalid agent token→null. The message iserrGatewayTokenInvalid(go/server/gateway_auth.go:var errGatewayTokenInvalid = errors.New("invalid agent token")), and S0 pins it.- Any other result or error → throw (503). A bad service bearer gets
ServiceBearerInterceptor’s message (go/internal/auth/service_door.go:connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))). Mapped to 401, a broken gateway would look like a bad agent token. - The deadline is
AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]). There is no cache (OQ3).
- No bearer →
owner-pool.ts: oneAuthStorageper owner.resolveStorage(caller)callspool.get(caller.ownerUserId). A missing owner throws, which the router answers with 503.-
Open. On first use the pool opens, per owner:
CompassAuthCredentialStore.open(...), with noagentAccountId;new AuthStorage(store);credentials.reload().
The open is single-flight, and a failure is not cached. It runs inside
routeScope, becausecreateAuthGatewayRouter(packages/ai/src/auth-gateway/server.ts) wraps the whole body:return opts.routeScope ? await opts.routeScope(req, body) : await body();. Soopen()sees the agent token. -
Leases. A response stream outlives
route:startAuthGatewaysetsidleTimeout: 255,. A mid-stream OAuth refresh writes back through the store, and a closed store throws (#requestincredential-store.ts:if (this.#closed) throw new CompassRpcError("closed", "Compass credential store is closed");). The rotated refresh token would then be lost. SorouteScoperuns each request in a lease scope, and everygetinside it leases the entry. The lease ends when the body ends, errors or is cancelled. A body-less result or a throw ends it at once. -
Eviction. A 60 s sweep closes an entry only when all of these hold:
- it has no leases;
store.hasPendingWrites()is false (the door record’s pin);- it has been idle 15 min.
Over 256 owners, the sweep also closes LRU entries idle more than 60 s that pass the first two checks.
close()at shutdown closes every entry; a parked write is lost then, a residual the door record accepts. There is no credential-poll timer.
-
routeScope. The door record’s line, plus the lease:(req, run) => { const t = presentedBearer(req); const leased = () => pool.withRequestLease(run); return t ? runWithAgentToken(t, leased) : leased(); }.- Catalog: bundled,
provider/idkeys only (OQ1). TheModelResolvertype(modelId) => Model<Api> | undefinednever sees the caller, and/v1/modelsskips the pool. The boot indexesgetBundledModels()for every bundled provider, keepingGATEWAY_MODEL_KINDSonly. OQ1 option B would replace the resolver with a per-owner one.- No bare ids.
indexModelsByRequestIdwould add a bare-id fallback:if (!modelById.has(model.id)) modelById.set(model.id, model);. Over all providers, that routes a shared bare id to whichever provider comes first. A bare id gets the router’sformatError(404, "invalid_request_error", `Unknown model: ${modelId}`). - A provider missing from the owner’s pool gets a dispatch 401
(
resolveGatewayApiKey).
- No bare ids.
- Inbound auth.
authorizeis the verifier, wrapped inwithEnrollTokenwhenCOMPASS_GATEWAY_ENROLL_TOKEN_FILEis set (OQ5).bearerTokenAuthorizeris not used, and no shared inbound token remains (OQ2, resolved). - Outbound TLS. The store and the verifier share one fetch,
fetch(input, { ...init, tls: { ca } }), withcaread fromCOMPASS_SERVER_CA_FILE. Provider calls keep the default trust store. - Door probe before bind.
/healthzanswers 200 with no checks, sowaitGatewaycannot see a broken door. The boot callsVerifyAgentTokenwith""and exits non-zero unless the answer isinvalid agent token.GatewayTokens.Verify(go/internal/auth/gateway_token.go) returns that before any store read:if !wellFormedToken(token) { return store.GatewayCaller{}, ErrGatewayTokenInvalid }. The probe proves reachability, TLS and a live service bearer, and it reads no credentials.
Stack: service bearer, server address, start order
Section titled “Stack: service bearer, server address, start order”-
Service bearer.
<state>/gateway/service.token(0600), mounted asCOMPASS_SERVICE_TOKEN_FILE. S2 deletes the oldgateway.token, whichensureGatewayTokenfilled with an unregistered hex secret:os.WriteFile(path, []byte(hex.EncodeToString(raw[:])), 0o600). A new name means skew fails before bind, never by adopting that secret. S1 makes the token status check generic overstore.Subject, becauseRunnerTokenStatus(go/internal/runnerhub/mint.go) hardcodesif subj.Kind == store.SubjectRunner && subj.ID == runnerID {. The rules for subject{SubjectService, "llm-gateway"}:- registered → keep;
- unknown → re-register;
- revoked or another subject → mint a fresh one, file first, then
PutTokenHash, and remove the file if the commit fails.
Residuals:
- a revoke mid-run means 503 until the next
up; - an external database plus a wiped state dir leaves old hashes live, as on the runner path.
-
Server address (OQ7-A, resolved). The container dials
https://host.containers.internal:<door port>on both OSes and runs with--network pasta:--map-host-loopback,169.254.1.2. Both mappings land on the host’s127.0.0.1(probe table under Resolved decisions):- Linux: by default pasta does not forward container loopback
(
pastaHostGatewayingo/e2e/fixture.go: “pasta does not forward 127.0.0.1”). The map reaches a host127.0.0.1listener, and-pstill publishes. - darwin: the name resolves to
192.168.127.254, which reaches a Mac127.0.0.1listener under the same pasta spec, and-pstill publishes from that container. - TLS: the name needs a DNS SAN. Today the SANs are
var certHosts = []string{"127.0.0.1", "::1", "localhost"}(go/internal/stack/adapters/cert.go), so the Linux probe failed the hostname check.needsRotationchecks only the key, the parse and the expiry (return !notAfter.After(now.Add(e.rotateWindow))). S2 adds the SAN and makes a missingcertHostsSAN trigger rotation. Every existing anchor then rotates once, on the expiry-rotation path. <state>/tls.crtis mounted read-only as the CA.
- Linux: by default pasta does not forward container loopback
(
-
Door host.
overlapsLoopbackPublish(go/internal/stack/config.go) is too wide here, because it also passes every DNS name:return ip == nil || ip.IsUnspecified() || ip.Equal(net.IPv4(127, 0, 0, 1)). S2 has its own rule (Global Constraints). -
Start order.
spawnChain(stack.go) starts the gateway at step 1e, before the TLS anchor and the server. So on a first run there is no CA file and no store to register the token in. The gateway moves to afterwaitReadyand beforestartRunner.drainChildrenuses a hard-coded list (runner, server, llm-gateway, …), so S2 reorders it to runner → gateway → server. That gain is small: agents are gone by then, and T5 revokes their tokens on stop. Detached down is unchanged (OQ6).
Alternatives considered
Section titled “Alternatives considered”- A
--compassmode inrunServe. This mixes broker and compass wiring in one upstream command. Rejected. - A bare-id index over all providers. It misroutes shared bare ids. Rejected.
- Keeping the shared bearer as a second caller class (OQ2). Its holder would get a pool-less caller. Rejected.
- Reusing
gateway.tokenfor the service bearer. It would adopt the old inbound secret, and skew would stay silent. Rejected. --network host. It drops-pand network isolation. Rejected.--network pasta:-T,<door port>with127.0.0.1. Linux only. On darwin it reaches the VM’s loopback (“Connection reset by peer”). Rejected (OQ7).- An ErrorInfo reason on
VerifyAgentToken. The verifier would have to decode error details from Connect JSON. Rejected for S0’s literal pin. - A verify cache. A revoked token would keep working. Deferred (OQ3).
Global Constraints
Section titled “Global Constraints”-
Public repo. This record and every compass task cite only compass and RigelBuild/oh-my-pi.
-
Fork changes stay seam-shaped. Allowed:
packages/ai/src/compass/(new modules, plus F1’swire.tshelpers and the matchingcredential-store.tstransport refactor);gateway-boot.ts;- one export from
auth-gateway-cli.ts; - option types and call sites in
dispatch.tsandserver.ts, for F2’srouteScopenarrowing; - only if OQ1 picks B: the caller-aware
resolveModelindispatch.ts,server.tsand the sevenroutes/handlers (embeddings, images, rerank, speech, systemone, transcriptions, video). EachbootOpts.resolveModel(...)call receives the admitted caller and awaits the result. This is an injection-point change, not routing core. A caller-awarelistModels(B2) also needs a stable-name amendment first.
No routing-core change, and no change to
auth-storage.tsorpackages/ai/src/auth/. -
Fork base.
rigel-releasewith RIG-5205 (routeScope,d0ac08ac56cc). #146 (RIG-5206) is based on it. -
Server dependency. A default-pinned image needs RIG-5204, because on main
ListCredentialPoolRequeststill requiresagent_account_id. -
Host floor (OQ7-A). The bundled gateway needs passt 2024-08-21 or newer, which added
--map-host-loopback. Ubuntu 24.04’s passt lacks it, and 26.04’s has it (Resolved decisions). Below the floor,upfails at S2’s preflight;--gateway-externalstill works. The existing passt floor inMicroVMFloors(go/internal/hostcheck/hostcheck.go:{Binary: "passt", Fields: []int{2025, 9, 19}, Display: "2025_09_19"}) covers only the microVM backend’spasston PATH, not podman’s pasta. -
Fail before bind. The boot exits non-zero and never binds when:
COMPASS_SERVER_URLis unset or failsnormalizeCompassBaseUrl;- an https URL has no readable CA file;
- the service-token file is unset, empty or holds whitespace (the error
names
COMPASS_SERVICE_TOKEN_FILE); - an enroll-token file is set but invalid;
- the door probe fails.
-
Inbound transport is loopback only. The stack publishes the listener only on host
127.0.0.1:4100(go/internal/stack/gateway_container.go:gatewayHostEndpoint = "127.0.0.1:4100"). So in this record agent tokens cross loopback only, and the listener has no TLS. Parent OQ-4 (“TLS via the stack’s existing anchor when bound beyond loopback”) applies when T5 exposes the gateway to agent containers, and T5 owns it. -
Secrets never logged. No token appears in a log line or an error; only
errorName(error)is logged. -
Env contract. Passed with
-e, never as imageENV. I1 drops theCOMPASS_GATEWAY_TOKEN_FILEENVand its allowlist entry.Name Value Supplier COMPASS_SERVICE_TOKEN_FILE/run/compass/service.tokenS2 COMPASS_SERVER_URLhttps://host.containers.internal:<door port>S2 COMPASS_SERVER_CA_FILE/run/compass/server-ca.crtS2 COMPASS_GATEWAY_BIND0.0.0.0:4000existing COMPASS_GATEWAY_DRAIN_MS20000existing COMPASS_GATEWAY_ENROLL_TOKEN_FILEoptional E2/E3, later -
Door listen host. Both host mappings deliver to the host’s IPv4
127.0.0.1. The door binds throughStack.listenTCP(stack.go), which callsnet.ResolveTCPAddrand thennet.ListenTCP. S2 accepts only hosts that accept on127.0.0.1:127.0.0.1;localhost, which resolves IPv4 first (forResolvein Go’snet/ipsock.go: “IPv4 is preferred, unless addr contains an IPv6 literal”);0.0.0.0,::and an empty host (:port), which bind a wildcard.
Any other host, including
::1,127.0.0.2and every DNS name, errors and names--gateway-external. -
Order. Each line is linear:
- compass: S0 → S1 → S2 → P1;
- fork: #146 → F1 → F2 → F3.
I1 follows F3’s merge to
rigel-release. P1 also waits on I1 and RIG-5204. No Pulumi state is involved.
The Plan is the long section, because it carries the exact interfaces each lane builds against.
S0 — Pin the verify message
Section titled “S0 — Pin the verify message”Lane: compass-server (owns go/server). Add a test in
go/server/gateway_auth_test.go that asserts code Unauthenticated and
message invalid agent token, and names the gateway as consumer.
F1 — Verifier and shared transport (fork #150)
Section titled “F1 — Verifier and shared transport (fork #150)”Lane: compass-gateway. Depends on #146.
export function normalizeCompassBaseUrl(baseUrl: string): string; // replaces assertSecureBaseUrlexport function fetchCompassRpcResponse(opts: { baseUrl: string; service: string; method: string; token: string; fetch: typeof fetch; body: unknown; signal: AbortSignal }): Promise<{ response: Response; text: string }>;// packages/ai/src/compass/agent-verifier.tsexport interface CompassAgentVerifierOptions { baseUrl: string; token: string; fetch?: typeof fetch; timeoutMs?: number }export function createCompassAgentVerifier(opts: CompassAgentVerifierOptions): AuthGatewayAuthorizer;export function probeCompassDoor(opts: CompassAgentVerifierOptions): Promise<void>;// credential-store.ts: #request uses both helpers.Tests cover each mapping row above, abort, the probe, and that no token appears in an error.
F2 — Owner pool
Section titled “F2 — Owner pool”Lane: compass-gateway.
export interface CompassOwnerEntry { storage: AuthStorage; store: { hasPendingWrites(): boolean } }export interface CompassOwnerPoolOptions { open: (ownerUserId: string) => Promise<CompassOwnerEntry>; maxOwners?: number; idleMs?: number; minIdleMs?: number; sweepIntervalMs?: number; now?: () => number }export class CompassOwnerPool { get(ownerUserId: string): Promise<AuthStorage>; withRequestLease(run: () => Promise<Response>): Promise<Response>; // F2 narrows AuthGatewayRouteOptions.routeScope to Response to match. sweep(): void; close(): void;}export function openCompassOwner(opts: CompassAuthCredentialStoreOptions): Promise<CompassOwnerEntry>;Required tests:
- Two-owner isolation. Each owner gets the same instance on repeat calls, and the two owners get different instances. Each selects only its own credential.
- Lease. A stream held past both idle limits, with the cap exceeded, survives. The lease is released, and the owner becomes evictable, when the body ends, when it errors, and when it is cancelled. #151 already has these cases.
- Parked write. An idle owner with a parked write survives the sweep. The next live-token request commits it, using a real store over a fake door.
- Basics: single-flight open, retried after a failure, and closed by
close().
F3 — Compass boot
Section titled “F3 — Compass boot”Lane: compass-gateway.
export interface GatewayBootConfig { serviceTokenFile: string; serverUrl: string; caFile?: string; bind: string; drainMs: number; enrollTokenFile?: string }export function getGatewayBootConfig(env: Readonly<Record<string, string | undefined>>): GatewayBootConfig;export function readTokenFile(path: string, envName: string): Promise<string>; // replaces readGatewayTokenexport function bundledGatewayModels(): Map<string, Model<Api>>;export function runGatewayBoot(env?: Readonly<Record<string, string | undefined>>): Promise<void>;Order: config → token files and CA → installGlobalProxyFetch() →
probeCompassDoor → startAuthGateway(...) → registerGatewayShutdown(handle, drainMs, () => pool.close()).
The required storage option gets a placeholder AuthStorage over a store
that throws on every call.
Tests:
- each fail-before-bind rule;
- the model map holds no bare ids;
- an end-to-end boot against a fake Server: good token → 200, bad token →
401, Server
internal→ 503, bare id → 404, missing provider → 401.
S1 — Service bearer ensure
Section titled “S1 — Service bearer ensure”Lane: compass-server (owns go/internal/stack/adapters; reviews S1, S2, P1).
func TokenStatus(ctx context.Context, r TokenHashResolver, token string, want store.Subject) (TokenState, store.Subject, error) // runnerhub; RunnerTokenStatus wraps ittype ServiceTokenEnsurer interface{ EnsureServiceToken(ctx context.Context, path, serviceID string) error } // stack.Deps.ServiceTokensfunc (e *TokenEnsurer) EnsureServiceToken(ctx context.Context, path, serviceID string) error // adaptersEnsureServiceToken creates the token’s directory at 0700 before its first
write. Tests:
- each Approach rule;
- the file is removed when the commit fails;
- mode 0600;
- fresh state: with no
<state>/gateway/, the call creates it at 0700 and mints the token; - the runner cases stay green.
S2 — Gateway spec, network and order
Section titled “S2 — Gateway spec, network and order”Lane: compass-server. Depends on S1.
const GatewayServiceTokenMountPath, GatewayCAMountPath = "/run/compass/service.token", "/run/compass/server-ca.crt"const gatewayServerHost, gatewayHostLoopbackAddr = "host.containers.internal", "169.254.1.2"type GatewayContainerSpec struct { /* existing */ ServerURL string; NetworkArgs []string; CAFile string }func gatewayContainerSpec(cfg Config, listenAddr string, cert CertResult) (GatewayContainerSpec, error)// adapters/cert.govar certHosts = []string{"127.0.0.1", "::1", "localhost", "host.containers.internal"}func (e *CertEnsurer) needsRotation(certPath, keyPath string, now time.Time) bool // also true when a certHosts SAN is missingTokenFilebecomes<state>/gateway/service.token, mounted atGatewayServiceTokenMountPath.GatewayTokenMountPathand theCOMPASS_GATEWAY_TOKEN_FILEenv are removed.- The spec sets the env table and
NetworkArgs(--network pasta:--map-host-loopback,169.254.1.2). - Mount labels:
- the service token uses
:ro,Z, a private label, like today’sspec.TokenFile+":"+stack.GatewayTokenMountPath+":ro,Z"ingatewayRunArgs; - the CA uses
:ro,z, a shared label, because the host server reads the sametls.crt.
- the service token uses
startGatewayrunsMkdirAll(TokenDir, 0o700), removesgateway.token, then callsEnsureServiceToken, thenStart.drainChildrenorder becomes runner, llm-gateway, server. DetachedliveTargetskeeps its order, anddrainTargetsstill signals the consumer tier together.ensureGatewayTokenis deleted.- The gateway step moves as in the Approach.
- Network preflight. Before
EnsureServiceToken,startGatewayrunspodman run --rm <NetworkArgs> --entrypoint /usr/bin/tini <image> --version. The override is required: the fork’sDockerfile.gatewayhasENTRYPOINT ["/usr/bin/tini", "--"], so without it the run executestini -- --versionand fails on every host. This is a newGatewayContainer.CheckNetwork(ctx, spec) error. On failure,upfails with an error that names passt 2024-08-21,--map-host-loopbackand--gateway-external, and quotes the first two lines of podman’s stderr (pasta’sunrecognized optionline; its full usage text runs to about 150 lines). The run tests the option itself, so it works through the darwin machine too, with no version string to parse.
Tests:
- the argv test pins the service-token mount (
:ro,Z) and the CA mount (:ro,z), and finds noCOMPASS_GATEWAY_TOKEN_FILE; ::1,127.0.0.2andcompass.exampleerror.127.0.0.1,localhost,0.0.0.0,::and an empty host (:port) pass;cert_test.go: a fresh anchor carries the DNS SAN. An unexpired anchor without it rotates once, and the next ensure keeps it;- harness: on fresh state, server ready → token → container start, and the drain stops the gateway before the server.
- the preflight argv is pinned, including
--entrypoint /usr/bin/tiniand a trailing--version. A fake podman that rejects--map-host-loopbackfailsupwith that error, before the token is ensured or the container starts;
I1 — Fork pin, image env and smoke
Section titled “I1 — Fork pin, image env and smoke”Lane: compass-gateway.
- Bump
fork-pin.jsonfrom57f41121ff5e…to therigel-releasecommit that holds F1–F3. - Drop the old
ENVand its allowlist entry. smoke.tsdrops the broker. It runs a fake Server on the host’s127.0.0.1over https, with a throwaway cert forhost.containers.internal(compass-gen-cert -hosts) mounted as the CA. It boots with the S2 network spec and asserts/healthz,/v1/modelsfor a good token, 401 for a bad token, and ano-server-envfault.- compass
.github/workflows/release.yml, jobpublish-gateway-image, runs the smoke onruns-on: ubuntu-latest. Pin it toruns-on: ubuntu-26.04, because 24.04’s passt lacks--map-host-loopbackandubuntu-latestmoves to 26.04 only gradually (Resolved decisions). Add the pinned Go to PATH forcompass-gen-cert. Compass draft #2233 does both.
P1 — Default image pin
Section titled “P1 — Default image pin”Lane: compass-server. Set DefaultGatewayImage to I1’s digest. A podman
test boots the bundled gateway and gets 200 on /v1/models. Its token comes
from auth.NewGatewayTokens(st).Mint directly: only tests call Mint until
T5, so this test is not end-to-end proof.
- S0 — compass-server: pin
invalid agent token. - S1 — compass-server:
TokenStatus;EnsureServiceToken(makes its dir). - S2 — compass-server: spec, mounts,
map-host-loopback, SAN and one-time rotation, door-host rule, order. - F1 — compass-gateway:
wire.tstransport, verifier, door probe (#150). - F2 — compass-gateway: owner pool and its required tests.
- F3 — compass-gateway: compass
runGatewayBoot,readTokenFile. - I1 — compass-gateway: pin bump, image env, broker-free smoke.
- P1 — compass-server: pin
DefaultGatewayImage. - Ledger: claim these with
bun tools/dl-claimwhen this PR is ready; the design pass does not claim them.- DL (server): the
llm-gatewayservice bearer is<state>/gateway/service.token. Agents are admitted only throughVerifyAgentToken; the optional enroll token admits only the enroll caller, on/internal/enroll/*. - DL (server): the bundled gateway reaches the Server at
host.containers.internalthroughpasta:--map-host-loopback, and the stack anchor carries that DNS SAN (OQ7). - DL (server): the boot’s catalog per the OQ1 ruling. Claim after OQ1.
- DL (server): the
Resolved decisions
Section titled “Resolved decisions”OQ2: drop the inbound shared bearer (Matt, 2026-10-12). This reverses
the T1 contract agreed in the RIG-2862 comment of 2026-09-29, under which
the boot loaded the token file /run/compass/gateway.token as the single
inbound bearer. Agents are now admitted only through VerifyAgentToken.
OQ7: A, host.containers.internal on both OSes (Matt, 2026-10-12).
One network spec, --network pasta:--map-host-loopback,169.254.1.2, with the
DNS SAN and the one-time rotation from S2. Option C (bundled gateway
unsupported on darwin) was the fallback. It is moot, because the darwin probe
passed.
Probe evidence. Each probe targets a host listener on 127.0.0.1.
| Host | Network | Target | Result |
|---|---|---|---|
| Linux, rootless podman 5.8.6 | default | 127.0.0.1 |
refused |
| Linux | pasta:-T,<port> |
127.0.0.1 |
reached; -p publishes |
| Linux | pasta:--map-host-loopback,169.254.1.2 |
host.containers.internal |
reached (cert failed: no SAN); -p publishes |
| mattmini, macOS arm64, podman 5.8.6, applehv | default | host.containers.internal → 192.168.127.254 |
reached |
| mattmini | pasta:--map-host-loopback,169.254.1.2 |
host.containers.internal → 192.168.127.254 |
reached |
| mattmini | pasta:-T,51999 |
127.0.0.1 |
“Connection reset by peer” |
| mattmini | default, -p 127.0.0.1:51998:80 |
host → container | reached |
| mattmini | pasta:--map-host-loopback,169.254.1.2, -p 127.0.0.1:51998:8080 |
host → container, and same container → host.containers.internal |
both reached |
The Mac listener was python3 -m http.server 51999 --bind 127.0.0.1. The
probe machine was removed afterwards.
Host floor evidence. A holds, with a floor:
--map-host-loopbacklanded in upstream passt on 2024-08-21 (commit e813a4df, “conf: Allow address remapped to host to be configured”).- Ubuntu noble (24.04) ships passt
0.0~git20240220, and its pasta(1) has no--map-host-loopback. Resolute (26.04, passt 20260120) has it (noble, resolute, Launchpad). - GitHub
ubuntu-latestmoves to 26.04 gradually, from 2026-10-19 to 2026-11-19 (changelog). - This box’s podman 5.8.6 bundles
pasta 2026_07_16, whose--helplists the option.
Open Questions
Section titled “Open Questions”OQ1 (load-bearing): the catalog under per-owner pools.
- A (recommended): every bundled provider by
provider/id, with a dispatch 401 when a credential is missing. - B: a per-owner
ModelRegistry, built in the owner pool, behind theresolveModelwidening that the stable-name record already ratified (Global Constraints, “Fork changes are seam-shaped”):(modelId, identity) => Promise<Model<Api> | undefined>. That record keepslistModels?: () => Iterable<Model<Api>>caller-independent and lists the stable-name vocabulary on it. Two variants:- B1 (recommended if B): the resolver only, pulled forward from
stable-name P1, with the listing unchanged. No amendment is needed.
Cost: the seam change in
dispatch.ts,server.tsand seven route handlers (Global Constraints), plus one caller-specific non-chat route test (embeddings). Dispatch is per owner, but/v1/modelsis not: a listed model the owner holds no credential for gets a 404 from the resolver. - B2: B1 plus a caller-aware listing. This adds a second seam and
changes the
/v1/modelscontract that stable-name discovery relies on, so it needs a stable-name amendment.
- B1 (recommended if B): the resolver only, pulled forward from
stable-name P1, with the listing unchanged. No amendment is needed.
Cost: the seam change in
- C: a union of live pools. It leaks between owners and is nondeterministic.
- D: A, limited to the parent’s day-1 set. This makes a new provider a code change, against RD-6 (“Additional providers are configuration, not code”).
- E: read
GetGatewayModelRegistrynow. This duplicates stable-name P1: a sync resolver would need a boot fetch, a poll and a model mapping.
Decision needed: B (Matt leaning) or A; if B, then B1 or B2.
OQ3 (deferred): verify cache. Revisit after dev measures the load.
OQ4 (resolved): the 401 vs 503 split hangs on one message. No test pinned it before; S0 does now.
OQ5 (deferred): enroll token supply. Nothing reads or writes
COMPASS_GATEWAY_ENROLL_TOKEN_FILE today. Owned by E2/E3 of
compass-gateway-oauth-enrollment.
OQ6 (accepted residual): detached down. drainTargets signals the
runner, server and gateway together, so a write-back during the gateway’s
drain can be lost.
OQ8 (deferred to T5): inbound TLS. The listener is published only on host loopback (Global Constraints). T5 owns TLS once it binds beyond loopback (parent OQ-4).