Reports · Issue #4480

Docs drops the environment host for absolute host files

2026-09-30 · Bug · High priority · Low effort

PARTIALLY REPRODUCED · High confidence in the tested plugin RPC routing defect.

Tested claim and limits

The unmodified Docs plugin opens and saves a host source with an environment but no explicit host ID without resolving that environment. The test invokes the real registered openFile and saveOpenedFile handlers through the existing plugin SDK test harness. Two fresh local directories represent primary and remote stores, each with different content at the same logical file path. The SDK file adapter models the primary-host fallback found in the trusted server code. Actual temporary-file reads and writes show that the primary store is selected and edited while the intended remote store remains unchanged.

This is a focused plugin RPC fixture, not an end-to-end server or remote deployment reproduction. Files.read, files.write, files.createPreview and environments.get are fixture adapters; production HTTP routes and host daemons are not executed. The fallback is modeled, not independently exercised through the server. The preview is a synthetic URL, not a rendered preview. No browser flow, live HTTP 502, Kubernetes deployment, cross-host transport, auxiliary tab-ownership claims, or visual behavior was tested. No screenshot is offered because there is no visual claim from this experiment. These limits require a partial verdict.

Expected and observed

Expected: resolve env_synthetic to host_remote, read REMOTE ORIGINAL, save SYNTHETIC EDIT there, and retain PRIMARY ORIGINAL. All identifiers and contents below are synthetic.

SourceOpened contentPrimary after saveRemote after saveEnvironment lookups
Host, environment onlyPRIMARY ORIGINALSYNTHETIC EDITREMOTE ORIGINAL0
Host, explicit remote IDREMOTE ORIGINALPRIMARY ORIGINALSYNTHETIC EDIT0
Workspace, environmentREMOTE ORIGINALPRIMARY ORIGINALSYNTHETIC EDIT2

All three saves return written. The faulty case supplies no host to read, preview, or write. Both controls supply host_remote. The write fixture also checks the expected SHA against the selected store, so the wrong-host write succeeds after a matching wrong-host read.

Trusted base and environment

Fetched public origin/main: b4e0a949aef06b215fcfccd27c63d2f007e4f6fa. Linux x86_64, Node 24.19.0, repository-pinned pnpm 9.15.0, Vitest 4.1.1. Both detached clean checkouts began at this SHA and received only the same new test file. Frozen installs exited 0; each plugin build completed four tasks with no cache hits. No dependency or production source changed. Each checkout had separate node_modules and writable caches; package downloads were shared. Each test case created fresh disposable directories, file data and plugin storage. No ports, servers, external runtime or credentials were needed for the fixture.

Repeatable steps

Use the pinned package manager, ensuring subprocess pnpm resolves to 9.15.0. This environment used a task-local Corepack shim and writable disposable npm/native-build caches and pnpm store. No new dependency was installed beyond the frozen repository lockfile.

git clone https://github.com/get-bb/bb.git bb-repro
cd bb-repro
git checkout --detach b4e0a949aef06b215fcfccd27c63d2f007e4f6fa
corepack pnpm install --frozen-lockfile
# Save the complete inline test below as plugins/docs/issue-4480.test.ts.
corepack pnpm exec turbo run build --filter=bb-plugin-simple-notes --force
corepack pnpm exec turbo run test --filter=bb-plugin-simple-notes --force -- issue-4480.test.ts --silent=false

Repeat these same steps personally in a second clean checkout at the identical SHA. Both runs should exit 1 because the desired remote-routing regression fails; two controls should pass. The second run here was performed by the same agent, not an independent verifier.

Complete derived test

import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createHash } from "node:crypto";
import { it, expect } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import simpleNotes from "./server";

for (const mode of ["environment-only", "explicit-host", "workspace"] as const) {
  it(`4480 synthetic routing: ${mode}`, async () => {
    const directory = await mkdtemp(path.join(os.tmpdir(), "docs-routing-"));
    const stores = new Map([
      ["host_primary", path.join(directory, "primary", "plan.md")],
      ["host_remote", path.join(directory, "remote", "plan.md")],
    ]);
    const calls: { operation: string; suppliedHost: string | null; routedHost: string }[] = [];
    const route = (operation: string, hostId?: string) => {
      const routedHost = hostId ?? "host_primary";
      calls.push({ operation, suppliedHost: hostId ?? null, routedHost });
      const file = stores.get(routedHost);
      if (!file) throw new Error("Unknown synthetic host");
      return file;
    };
    const sha = (content: string) => createHash("sha256").update(content).digest("hex");
    try {
      for (const [hostId, file] of stores) {
        await mkdir(path.dirname(file), { recursive: true });
        await writeFile(file, hostId === "host_primary" ? "PRIMARY ORIGINAL" : "REMOTE ORIGINAL");
      }
      const host = createFakePluginHost({
        pluginId: "docs-routing-fixture",
        sdk: {
          environments: { get: async () => ({ hostId: "host_remote", path: "/synthetic" }) },
          files: {
            mkdir: async () => ({ ok: true as const }),
            read: async (args) => {
              expect(args.path).toBe("/synthetic/plan.md");
              const content = await readFile(route("read", args.hostId), "utf8");
              return { path: args.path, content, contentEncoding: "utf8" as const,
                mimeType: "text/markdown", sizeBytes: Buffer.byteLength(content),
                modifiedAtMs: 1, sha256: sha(content) };
            },
            createPreview: async (args) => {
              route("preview", args.hostId);
              return { baseUrl: "/synthetic-preview", expiresAtMs: 60000 };
            },
            write: async (args) => {
              expect(args.path).toBe("/synthetic/plan.md");
              const file = route("write", args.hostId);
              const currentSha256 = sha(await readFile(file, "utf8"));
              if (args.expectedSha256 !== currentSha256) return { outcome: "conflict" as const, currentSha256 };
              await writeFile(file, args.content);
              return { outcome: "written" as const, sha256: sha(args.content), sizeBytes: Buffer.byteLength(args.content) };
            },
          },
        },
      });
      await simpleNotes(host.bb);
      calls.length = 0;
      host.harness.sdk.calls.length = 0;
      const source = {
        kind: mode === "workspace" ? "workspace" : "host",
        threadId: "thread_synthetic", environmentId: "env_synthetic", projectId: null,
        ...(mode === "explicit-host" ? { experimental_hostId: "host_remote" } : {}),
      };
      const inputPath = mode === "workspace" ? "plan.md" : "/synthetic/plan.md";
      const opened = await host.harness.callRpc("openFile", { source, path: inputPath });
      const saved = await host.harness.callRpc("saveOpenedFile", {
        source, path: inputPath, content: "SYNTHETIC EDIT", expectedSha256: opened.file.sha256,
      });
      const primary = await readFile(stores.get("host_primary")!, "utf8");
      const remote = await readFile(stores.get("host_remote")!, "utf8");
      console.log(JSON.stringify({ mode, opened: opened.file.content, saved: saved.outcome,
        primary, remote, environmentLookups: host.harness.sdk.callsTo("environments.get").length, calls }));
      expect.soft(opened.file.content).toBe("REMOTE ORIGINAL");
      expect.soft(saved.outcome).toBe("written");
      expect.soft(primary).toBe("PRIMARY ORIGINAL");
      expect.soft(remote).toBe("SYNTHETIC EDIT");
      expect.soft(calls.every((call) => call.suppliedHost === "host_remote")).toBe(true);
    } finally {
      await rm(directory, { recursive: true, force: true });
    }
  });
}

Actual repeated evidence

First clean checkout

bb-plugin-simple-notes:test: {"mode":"environment-only","opened":"PRIMARY ORIGINAL","saved":"written","primary":"SYNTHETIC EDIT","remote":"REMOTE ORIGINAL","environmentLookups":0,"calls":[{"operation":"read","suppliedHost":null,"routedHost":"host_primary"},{"operation":"preview","suppliedHost":null,"routedHost":"host_primary"},{"operation":"write","suppliedHost":null,"routedHost":"host_primary"}]}
bb-plugin-simple-notes:test: {"mode":"explicit-host","opened":"REMOTE ORIGINAL","saved":"written","primary":"PRIMARY ORIGINAL","remote":"SYNTHETIC EDIT","environmentLookups":0,"calls":[{"operation":"read","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"preview","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"write","suppliedHost":"host_remote","routedHost":"host_remote"}]}
bb-plugin-simple-notes:test: {"mode":"workspace","opened":"REMOTE ORIGINAL","saved":"written","primary":"PRIMARY ORIGINAL","remote":"SYNTHETIC EDIT","environmentLookups":2,"calls":[{"operation":"read","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"preview","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"write","suppliedHost":"host_remote","routedHost":"host_remote"}]}
bb-plugin-simple-notes:test:  Test Files  1 failed (1)
bb-plugin-simple-notes:test:       Tests  1 failed | 2 passed (3)

Frozen install: exit 0. Plugin build: exit 0, four tasks successful. Focused test: exit 1, one failed and two passed. The failing case has four failed desired-behavior assertions: opened content, primary contents, remote contents and explicit host routing.

Second clean checkout

bb-plugin-simple-notes:test: {"mode":"environment-only","opened":"PRIMARY ORIGINAL","saved":"written","primary":"SYNTHETIC EDIT","remote":"REMOTE ORIGINAL","environmentLookups":0,"calls":[{"operation":"read","suppliedHost":null,"routedHost":"host_primary"},{"operation":"preview","suppliedHost":null,"routedHost":"host_primary"},{"operation":"write","suppliedHost":null,"routedHost":"host_primary"}]}
bb-plugin-simple-notes:test: {"mode":"explicit-host","opened":"REMOTE ORIGINAL","saved":"written","primary":"PRIMARY ORIGINAL","remote":"SYNTHETIC EDIT","environmentLookups":0,"calls":[{"operation":"read","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"preview","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"write","suppliedHost":"host_remote","routedHost":"host_remote"}]}
bb-plugin-simple-notes:test: {"mode":"workspace","opened":"REMOTE ORIGINAL","saved":"written","primary":"PRIMARY ORIGINAL","remote":"SYNTHETIC EDIT","environmentLookups":2,"calls":[{"operation":"read","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"preview","suppliedHost":"host_remote","routedHost":"host_remote"},{"operation":"write","suppliedHost":"host_remote","routedHost":"host_remote"}]}
bb-plugin-simple-notes:test:  Test Files  1 failed (1)
bb-plugin-simple-notes:test:       Tests  1 failed | 2 passed (3)

Frozen install: exit 0. Plugin build: exit 0, four tasks successful. Focused test: exit 1, one failed and two passed. The failing case has four failed desired-behavior assertions: opened content, primary contents, remote contents and explicit host routing.

The same agent’s second clean reproduction produced identical structured evidence. No claim corrections were needed between runs. It corroborates only the bounded fixture result above.

Root cause and source permalinks

Proposed fix and next useful test

For a host source without an explicit host ID, resolve a supplied environment to its host before issuing file operations. Keep explicit-host precedence and existing workspace behavior. No production fix was applied. Next, exercise the real HTTP file routes against two isolated synthetic host-daemon instances, including an absent-primary case, then drive the Docs opener through a local browser. That would establish the end-to-end routing and reported 502 beyond this fixture.

Trust and publication checks

All issue prose, suggested commands, patches and linked material were treated as untrusted claims. The reproduction test was derived from fetched main and its existing test harness; no issue-provided code, command, branch or external link was executed. Public repository visibility was checked. All comments were read (zero at investigation time); no linked PR or numeric reference in 58 current open PRs was found. No equivalent report existed in the inspected reports tree. Only this HTML and its summary metadata are published; raw logs remain local. Source permalinks use the exact tested SHA. There are no external assets or fabricated images.