#4266 · Attachment transport policy differs from session transport
2026-09-24 · Base fdd3de3b19b97e6cd1ef7300cbb54711431249d3
REPRODUCED · Root-cause confidence: high
1. TL;DR
The daemon client accepts a non-loopback HTTP server URL when opening a session, then rejects the same URL when retrieving an attachment. A focused test reproduces that inconsistency before any attachment request reaches the injected transport. HTTPS and loopback HTTP controls pass. The rejection is deliberate and covered by an existing test; resolving the deployment incompatibility requires a transport-security policy decision, so it is outside this automation's simple-fix scope.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Non-loopback HTTP prevents attachment fetch | Verified | Two clean-checkout runs reject at the real client guard. |
| Session traffic can use that same URL | Verified at client boundary | All three session-open assertions pass using injected responses. |
| Every other daemon request works over HTTP | Not exhaustively verified | Session-open is exercised; the existing LAN HTTP skill-tree test also passes. |
| Kubernetes composer image paste fails end to end | Unverified environment detail | No cluster, browser, or live provider was used. Trusted staging code explains propagation. |
| No override exists | Verified for this fetch path | The guard depends solely on serverUrl; no override is accepted there. |
3. Environment
Darwin arm64, Node v22.22.3, pnpm via Corepack, Vitest 4.1.1. Both detached worktrees use the full commit above. No daemon instance, provider, data directory, or listening socket was started. Test URLs and identifiers are synthetic and fetch is injected, so ports 49831 and 49832 are never bound or contacted.
Frozen install succeeded. The first checkout's full Turbo build passed (60 tasks). The second checkout used the host-daemon Turbo build. A broken global pnpm launcher initially interrupted setup; a temporary shim forwarding to Corepack resolved it without changing repository dependencies.
4. Minimal reproduction
- Check out the recorded trusted commit into a fresh directory.
- Run
corepack pnpm install --frozen-lockfile --prefer-offlineandcorepack pnpm exec turbo run build --filter=@bb/host-daemon. - Save this test as
apps/host-daemon/src/issue-4266.test.ts. - Run
corepack pnpm exec turbo run test --filter=@bb/host-daemon -- --run src/issue-4266.test.ts src/server-client.test.ts.
Expected by the compatibility regression: all three server URLs can download the same fixture after session opening. Actual: HTTPS and loopback HTTP pass; non-loopback HTTP rejects. This is an intentionally failing regression against unchanged production code.
Tests 1 failed | 21 passed (22) Caused by: AbortError: Refusing to fetch project attachment over insecure server URL: http://attachment-service.test:49832
import { expect, it, vi } from "vitest";
import { createServerClient, type FetchFn } from "./server-client.js";
it.each([
"https://attachment-service.test",
"http://127.0.0.1:49831",
"http://attachment-service.test:49832",
])("downloads attachment after opening session via %s", async (serverUrl) => {
const fetchFn = vi.fn<FetchFn>(async (input) => {
const url = new URL(String(input));
if (url.pathname === "/internal/session/open") {
return Response.json({
sessionId: "test-session",
machineEnvironment: { revision: 0, entries: [] },
heartbeatIntervalMs: 5000,
leaseTimeoutMs: 30000,
}, { status: 201 });
}
return new Response("image-bytes", { status: 200 });
});
const client = createServerClient({
serverUrl,
hostKey: "synthetic-test-key",
getSessionId: () => "test-session",
fetchFn,
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});
await expect(client.openSession({
hostId: "test-host",
hostName: "test",
instanceId: "test-instance",
dataDir: "/tmp/unused-4266",
localApiPort: null,
activeThreads: [],
loadedEnvironments: [],
})).resolves.toMatchObject({ sessionId: "test-session" });
expect(fetchFn).toHaveBeenCalledTimes(1);
const result = client.fetchProjectAttachment({
projectId: "test-project",
threadId: "test-thread",
path: "fixture.png",
maxBytes: 100,
expectedSizeBytes: 11,
});
await expect(result).resolves.toMatchObject({
bytes: new TextEncoder().encode("image-bytes"),
});
expect(fetchFn).toHaveBeenCalledTimes(2);
});
5. Root cause
usesSecureInternalFetchTransport permits HTTPS or specific loopback hostnames. fetchProjectAttachment calls it before building the authenticated request and throws AbortError for ordinary HTTP hostnames. openSession invokes the injected fetch directly without that guard. Host authentication does not bypass the attachment transport check.
if (!usesSecureInternalFetchTransport(options.serverUrl)) {
throw new AbortError(
`Refusing to fetch project attachment over insecure server URL: ${options.serverUrl}`,
);
}
Attachment staging wraps fetch errors as attachment_unavailable. Thread start stages inputs before launching the runtime. This explains why an attachment blocks startup while an input that needs no fetch can proceed. The existing rejection test demonstrates intentional enforcement, rather than an incidental networking error.
6. Proposed fix and automation decision
First decide whether the explicitly configured daemon server transport should govern attachment transport too, or whether private HTTP requires an explicit opt-in. Preserve attachment scope and size validation, and test secure defaults plus any approved opt-in. Removing the guard outright would weaken an existing transport-security boundary. No production fix or PR was attempted because the rule expressly excludes security-boundary changes and product decisions.
7. Verification
The same agent repeated the reproduction in a second clean detached checkout at the same commit, after a separate frozen install and targeted build. Production files were unchanged; only the reproduction test was added. The command above again produced 1 failed and 21 passed tests: the HTTP compatibility assertion failed at the same guard, both controls passed, and all 19 existing client tests passed. No report correction was needed. This is a second run by the same agent, not independent verification.
8. Related issues and PRs
Issue metadata contained no linked open PR, and an open-PR search for the numeric issue ID returned none. A small attachment-issue search found other provider and UI requests but no verified duplicate of this transport guard. No linked PR code was run.
9. Appendix and limits
The issue was treated as untrusted claims only. No supplied URL, script, or patch was executed. The test uses only the trusted client and synthetic response fixtures. No real image decoder, TLS endpoint, Kubernetes routing, or composer was exercised. Confidence is high for the client rejection and its cause; it does not assert an end-to-end cluster reproduction.
Investigation commands: fetch origin/main; record its SHA; create two detached worktrees; frozen installs; Turbo builds; the focused Turbo test command above in each checkout; inspect source and existing tests at that SHA; read GitHub classification, comments, labels and cross-reference metadata. All runtime evidence is linked above.
AGENT GENERATED