Skip to content
Merged
8 changes: 8 additions & 0 deletions apps/dispatcher/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ export interface Env {
*/
readonly SANDBOX_PREVIEW_HOSTNAME?: string;

/**
* The Worker-wide sandbox transport for a repo no `sandbox.transport:<repo>`
* key pins: `http`, `websocket` or `rpc`, and `rpc` when unset or not a
* transport name. The dispatcher pins the resolved transport onto every
* container (`resolveSandboxTransport`). A var, not a secret.
*/
readonly SANDBOX_TRANSPORT?: string;

/**
* The dispatcher's own public origin (e.g.
* `https://flare-dispatch.<account>.workers.dev`, or a custom domain) —
Expand Down
23 changes: 8 additions & 15 deletions apps/dispatcher/src/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,17 +569,11 @@ export class RunWorkflow extends WorkflowEntrypoint<Env> {
}
}

// Per-repo container transport — the canary knob for `rpc`.
//
// The SDK's streaming file APIs exist only on its `rpc` client (`http` is
// the "route-based compatibility" path), which is why the R2 dependency
// cache misses on every run: its restore hands `writeFile` a stream, the SDK
// routes any stream to `writeFileStream`, and that raises off `rpc`.
// `SANDBOX_TRANSPORT=rpc` as a Worker var would fix it for every repo at
// once; this lets one prove it first.
//
// A value the SDK does not recognise is ignored by it with a warning, so a
// typo degrades to the default rather than breaking the run.
// Per-repo container transport pin. It outranks the Worker-wide
// `SANDBOX_TRANSPORT` in either direction, and with neither set containers
// run on `rpc`, the one transport that streams a cache archive in
// (`resolveSandboxTransport`). A value that is not a transport name is
// skipped, so a typo falls through rather than breaking the run.
// Container path only — the substrate owns its own transport, so a run
// there should not pay a KV read to be told about one it cannot use.
const sandboxTransport =
Expand Down Expand Up @@ -716,10 +710,9 @@ export class RunWorkflow extends WorkflowEntrypoint<Env> {
? { aiGatewayAuthToken: this.env.AI_GATEWAY_AUTH_TOKEN }
: {}),
sandboxPreviewHostname: this.env.SANDBOX_PREVIEW_HOSTNAME,
...(sandboxTransport === "rpc" ||
sandboxTransport === "websocket" ||
sandboxTransport === "http"
? { sandboxTransport }
...(sandboxTransport != null ? { sandboxTransport } : {}),
...(this.env.SANDBOX_TRANSPORT !== undefined
? { sandboxTransportDefault: this.env.SANDBOX_TRANSPORT }
: {}),
...(publicOrigin !== undefined ? { publicOrigin } : {}),
...(logsBaseUrl !== undefined ? { logsViewerBase: logsBaseUrl } : {}),
Expand Down
64 changes: 64 additions & 0 deletions infra/Dockerfile.sandbox-demo
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,70 @@ RUN if [ "$WITH_RUST" = "true" ]; then \
echo "WITH_RUST=false — no Rust toolchain baked. Lean image."; \
fi

# ── Prebuilt DuckDB shared library (gated by the WITH_RUST build arg) ───────
#
# specs/adr/0007-cost-efficient-execution.md, "Prebuilt heavy native
# dependencies in the image". A cold `cargo test` of a workspace on the
# `duckdb` crate spends most of its time compiling DuckDB's C++ from source.
# This layer ships the release's `libduckdb.so` and headers, pinned by
# SHA-256, under /opt/duckdb, so a consumer that links DuckDB skips that
# compile. The static archive in the release zip is left out: the layer
# serves dynamic linking only, and the archive doubles the layer's size.
#
# libduckdb-sys (1.10505.x, build.rs) reads DUCKDB_LIB_DIR and
# DUCKDB_INCLUDE_DIR only in its `build_linked` path, compiled under
# `#[cfg(not(feature = "bundled"))]`. A consumer that builds with the
# `bundled` feature compiles from source and ignores both, so exporting them
# changes nothing for it. In linked mode the build script emits
# `rustc-link-search=$DUCKDB_LIB_DIR` and `rustc-link-lib=dylib=duckdb`, which
# links but does not make the library loadable at runtime; the ld.so.conf.d
# entry below puts /opt/duckdb/lib on the loader path for every exec user.
#
# Consumer opt-in, owned by the consumer:
# - build `duckdb` / `libduckdb-sys` without the `bundled` feature in its CI
# run command (release builds may stay bundled);
# - assert the loaded library's version at startup (`SELECT version()` or
# `duckdb_library_version()` against the pinned crate's DuckDB version),
# because a linked library of the wrong version compiles cleanly and
# misbehaves at runtime;
# - move DUCKDB_VERSION and the digest below together with its crate pin:
# libduckdb-sys 1.10505.x pairs with DuckDB v1.5.5.
#
# In a linked Linux build DUCKDB_LIB_DIR wins over every other source,
# `DUCKDB_DOWNLOAD_LIB=1` included: a consumer whose crate pin differs from
# DUCKDB_VERSION links this library, not the one its crate would download,
# unless its run command unsets DUCKDB_LIB_DIR first.
#
# The ENV paths are set in every variant; with WITH_RUST=false the directory
# is absent and nothing in the image reads them. The release asset is
# linux-amd64 only, matching the container platform; any other build
# architecture fails the image build.
ARG DUCKDB_VERSION=v1.5.5
ENV DUCKDB_LIB_DIR=/opt/duckdb/lib \
DUCKDB_INCLUDE_DIR=/opt/duckdb/include
RUN if [ "$WITH_RUST" = "true" ]; then \
set -eux; \
arch="$(uname -m)"; \
case "$arch" in \
x86_64) sha256=1fb8ce388157d84a25abe685a8a2520bf00c00321821968e4bb398fd766e7abb ;; \
*) echo "libduckdb: unsupported architecture $arch" >&2; exit 1 ;; \
esac; \
archive="/tmp/libduckdb-${DUCKDB_VERSION}-linux-amd64.zip"; \
curl -fsSL -o "$archive" \
"https://github.com/duckdb/duckdb/releases/download/${DUCKDB_VERSION}/libduckdb-linux-amd64.zip"; \
echo "${sha256} ${archive}" | sha256sum -c -; \
mkdir -p "$DUCKDB_LIB_DIR" "$DUCKDB_INCLUDE_DIR"; \
unzip -q -j "$archive" libduckdb.so -d "$DUCKDB_LIB_DIR"; \
unzip -q -j "$archive" duckdb.h duckdb.hpp duckdb_extension.h -d "$DUCKDB_INCLUDE_DIR"; \
rm -f "$archive"; \
chmod -R a+rX /opt/duckdb; \
echo "$DUCKDB_LIB_DIR" > /etc/ld.so.conf.d/duckdb.conf; \
ldconfig; \
ldconfig -p | grep -q "libduckdb.so "; \
else \
echo "WITH_RUST=false — no prebuilt DuckDB baked. Lean image."; \
fi

# ── demo-agent layer ────────────────────────────────────────────────────────
#
# The `product-demo` run's bundled CLI (record start/stop, play, gif,
Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/primitives/install-cached.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ type ToolSpec = {
readonly paths: readonly string[];
/** Base the `paths` are packed from, when the tool's home is not the checkout. */
readonly base?: string;
/**
* Command run after every restore, hit or miss, when `paths` cannot hold
* the whole installed tree. A pnpm workspace links each member's
* dependencies into its own `packages/<x>/node_modules`, which the root
* `node_modules` archive does not carry; a hit without a relink leaves
* every member unable to import its dependencies. After a miss the install
* already ran, so the relink finds the tree up to date and returns at once.
*/
readonly relink?: string;
};

// Exported so the invariant above is assertable. It is the contract this
Expand All @@ -60,6 +69,7 @@ export const TOOLS = {
// NOT `.pnpm-store`: nothing creates one in a checkout, and `tar czf` exits
// 2 on a missing member — listing it disabled the whole pnpm save.
paths: ["node_modules"],
relink: "pnpm install --frozen-lockfile --prefer-offline",
},
npm: { lockfile: "package-lock.json", install: "npm ci", paths: ["node_modules"] },
cargo: {
Expand Down Expand Up @@ -117,7 +127,7 @@ export const installCached = (opts: {
return;
}
const spec: ToolSpec = TOOLS[tool];
const { lockfile, install, paths, base } = spec;
const { lockfile, install, paths, base, relink } = spec;

// Hash the lockfile inside the container to key the R2 cache entry.
const hash = yield* sandbox.exec({
Expand All @@ -144,4 +154,14 @@ export const installCached = (opts: {
...(opts.redactValues !== undefined ? { redactValues: opts.redactValues } : {}),
}),
});

if (relink !== undefined) {
yield* sandbox.exec({
cwd: opts.dir,
container: opts.container,
command: relink,
...(opts.env !== undefined ? { env: opts.env } : {}),
...(opts.redactValues !== undefined ? { redactValues: opts.redactValues } : {}),
});
}
});
37 changes: 37 additions & 0 deletions packages/core/src/primitives/primitives.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// covers `workspace`, `installCached`, `sharded`, `bootApp`, `probeHttp`.

import { Effect } from "effect";
import { Cache } from "../services/cache";
import { sandbox } from "../services/sandbox";
import { describe, expect, it } from "vitest";
import { bootApp } from "./boot-app";
Expand Down Expand Up @@ -155,6 +156,42 @@ describe("installCached", () => {
expect(cmds.some((c) => c.includes("pnpm install"))).toBe(true);
});

// PROOF (a pnpm hit still relinks the workspace): the archive holds the
// root `node_modules` only, while pnpm links each workspace member's
// dependencies into `packages/<x>/node_modules`. A hit that skipped every
// install left those members unable to import anything.
it("relinks a pnpm workspace after a cache hit, without the full install", async () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: {
"test -f pnpm-lock.yaml": { exitCode: 0 },
"sha256sum pnpm-lock.yaml": { exitCode: 0, stdout: "feedface\n" },
"pnpm install --frozen-lockfile --prefer-offline": { exitCode: 0 },
},
});
const hit = {
restoreOr: () => Effect.void,
save: () => Effect.void,
} as unknown as Cache["Type"];

await Effect.runPromise(
installCached({ container: { id: "c-1" }, dir: "/workspace/repo" }).pipe(
Effect.provideService(Cache, hit),
Effect.provide(layer),
),
);

const cmds = handles.sandbox.execs.map((e) => String(e.command));
expect(cmds).toContain("pnpm install --frozen-lockfile --prefer-offline");
expect(cmds).not.toContain("pnpm install --frozen-lockfile");
});

it("relinks nothing for a tool whose cached paths hold the whole tree", () => {
expect(TOOLS.pnpm.relink).toBe("pnpm install --frozen-lockfile --prefer-offline");
expect("relink" in TOOLS.npm).toBe(false);
expect("relink" in TOOLS.cargo).toBe(false);
expect("relink" in TOOLS.uv).toBe(false);
});

it("no recognized lockfile → no install run", async () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: {
Expand Down
Loading
Loading