Skip to content

Commit 92ab1b7

Browse files
committed
test(sandbox): cover relay lifecycle races
1 parent 8348336 commit 92ab1b7

9 files changed

Lines changed: 491 additions & 62 deletions

File tree

packages/agentos-sandbox/src/provider.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type SandboxAgentProviderOptions = Omit<
1616
interface SandboxAgentTransportInternals {
1717
baseUrl: string;
1818
token?: string;
19-
defaultHeaders?: HeadersInit;
19+
defaultHeaders?: RequestInit["headers"];
2020
fetcher?: typeof globalThis.fetch;
2121
awaitHealthy?(signal?: AbortSignal): Promise<void>;
2222
}
@@ -62,7 +62,9 @@ async function requestThroughSandboxAgent(
6262
);
6363
}
6464
const headers = new Headers(transport.defaultHeaders);
65-
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
65+
new Headers(init.headers).forEach((value, name) => {
66+
headers.set(name, value);
67+
});
6668
if (transport.token) {
6769
headers.set("authorization", `Bearer ${transport.token}`);
6870
}

packages/agentos-sandbox/tests/vm-integration.test.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe("VM integration", () => {
4747
software: [common],
4848
sandbox: {
4949
mountPath: "/sandbox",
50-
idleTimeoutMs: 25,
50+
idleTimeoutMs: 500,
5151
provider: {
5252
start: async () => {
5353
providerStarts += 1;
@@ -119,9 +119,7 @@ describe("VM integration", () => {
119119
new TextEncoder().encode("first"),
120120
);
121121
expect(
122-
new TextDecoder().decode(
123-
await vm.readFile("/sandbox/generation.txt"),
124-
),
122+
new TextDecoder().decode(await vm.readFile("/sandbox/generation.txt")),
125123
).toBe("first");
126124
expect(providerStarts).toBe(1);
127125

@@ -130,15 +128,32 @@ describe("VM integration", () => {
130128
{ path: "/generation.txt" },
131129
new TextEncoder().encode("second"),
132130
);
133-
for (let attempt = 0; attempt < 50 && providerDisposals === 0; attempt++) {
131+
for (
132+
let attempt = 0;
133+
attempt < 100 && providerDisposals === 0;
134+
attempt++
135+
) {
134136
await new Promise((resolve) => setTimeout(resolve, 10));
135137
}
136138
expect(providerDisposals).toBe(1);
137-
expect(
138-
new TextDecoder().decode(
139-
await vm.readFile("/sandbox/generation.txt"),
139+
const [secondContent, bindingResult] = await Promise.all([
140+
vm.readFile("/sandbox/generation.txt"),
141+
vm.process.execFile(
142+
"agentos-sandbox",
143+
["run-command", "--command", "echo", "--args", "second-generation"],
144+
{ output: { capture: "all" } },
140145
),
141-
).toBe("second");
146+
]);
147+
expect(new TextDecoder().decode(secondContent)).toBe("second");
148+
expect(bindingResult.exitCode).toBe(0);
149+
expect(JSON.parse(bindingResult.stdout)).toEqual(
150+
expect.objectContaining({
151+
ok: true,
152+
result: expect.objectContaining({
153+
stdout: expect.stringContaining("second-generation"),
154+
}),
155+
}),
156+
);
142157
expect(providerStarts).toBe(2);
143158
} finally {
144159
await replacement.stop();

packages/core/src/index.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,43 @@
11
// @rivet-dev/agentos
22

33
export { AgentOs, AgentOsSidecar } from "./agent-os.js";
4-
export type * from "./language-execution.js";
4+
export {
5+
isPackageDescriptor,
6+
OPT_AGENTOS_BIN,
7+
OPT_AGENTOS_ROOT,
8+
tryReadAgentosPackageManifest,
9+
} from "./agentos-package.js";
10+
export type { Binding, BindingExample, Bindings } from "./bindings.js";
11+
export {
12+
binding,
13+
bindings,
14+
MAX_BINDING_DESCRIPTION_LENGTH,
15+
validateBindings,
16+
} from "./bindings.js";
517
export {
618
CronManager,
719
InvalidScheduleError,
820
PastScheduleError,
921
TimerScheduleDriver,
1022
} from "./cron/index.js";
1123
export { createHostDirBackend, nodeModulesMount } from "./host-dir-mount.js";
12-
export {
13-
binding,
14-
MAX_BINDING_DESCRIPTION_LENGTH,
15-
bindings,
16-
validateBindings,
17-
} from "./bindings.js";
18-
export type { Binding, BindingExample, Bindings } from "./bindings.js";
24+
export type * from "./language-execution.js";
25+
export { createSnapshotExport } from "./layers.js";
1926
export {
2027
agentOsLimitsSchema,
2128
agentOsOptionFieldSchemas,
2229
agentOsOptionsSchema,
2330
bindingSchema,
31+
bindingsSchema,
2432
mountConfigSchema,
2533
nativeMountConfigSchema,
2634
parseAgentOsOptions,
2735
permissionsSchema,
2836
rootFilesystemConfigSchema,
2937
sharedSidecarConfigSchema,
3038
sidecarConfigSchema,
31-
bindingsSchema,
3239
} from "./options-schema.js";
33-
export { createSnapshotExport } from "./layers.js";
3440
export { defineSoftware } from "./packages.js";
35-
export {
36-
isPackageDescriptor,
37-
OPT_AGENTOS_BIN,
38-
OPT_AGENTOS_ROOT,
39-
tryReadAgentosPackageManifest,
40-
} from "./agentos-package.js";
41-
export { KernelError } from "./runtime-compat.js";
4241
export type {
4342
ExecOptions,
4443
ExecResult,
@@ -48,6 +47,7 @@ export type {
4847
VirtualDirEntry,
4948
VirtualStat,
5049
} from "./runtime.js";
50+
export { KernelError } from "./runtime-compat.js";
5151
export {
5252
createSandboxBindings,
5353
createSandboxFs,

packages/core/src/sandbox-relay.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ function mergeUpstreamHeaders(
162162
const serializable = client as AgentOsSandboxClient &
163163
SerializableSandboxClient;
164164
const headers = new Headers(serializable.defaultHeaders);
165-
requestHeaders.forEach((value, name) => headers.set(name, value));
165+
requestHeaders.forEach((value, name) => {
166+
headers.set(name, value);
167+
});
166168
if (serializable.token) {
167169
headers.set("authorization", `Bearer ${serializable.token}`);
168170
}
@@ -236,7 +238,10 @@ export async function createSandboxRelay(
236238
const token = randomBytes(32).toString("base64url");
237239
const maxConcurrentRequests =
238240
options.maxConcurrentRequests ?? DEFAULT_MAX_RELAY_REQUESTS;
239-
if (!Number.isSafeInteger(maxConcurrentRequests) || maxConcurrentRequests <= 0) {
241+
if (
242+
!Number.isSafeInteger(maxConcurrentRequests) ||
243+
maxConcurrentRequests <= 0
244+
) {
240245
throw new Error("sandbox.maxRelayRequests must be a positive safe integer");
241246
}
242247

@@ -283,8 +288,7 @@ export async function createSandboxRelay(
283288
activeRequests += 1;
284289
if (
285290
!warnedNearCapacity &&
286-
activeRequests * 100 >=
287-
maxConcurrentRequests * RELAY_WARNING_PERCENT
291+
activeRequests * 100 >= maxConcurrentRequests * RELAY_WARNING_PERCENT
288292
) {
289293
warnedNearCapacity = true;
290294
console.warn(
@@ -308,7 +312,7 @@ export async function createSandboxRelay(
308312
signal: abortController.signal,
309313
...(hasBody
310314
? {
311-
body: Readable.toWeb(request) as unknown as BodyInit,
315+
body: Readable.toWeb(request) as never,
312316
duplex: "half" as const,
313317
}
314318
: {}),

packages/core/src/sandbox.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ class SandboxClientController implements SandboxRelayClientController {
203203
if (!this.#provider) {
204204
throw new Error("Sandbox client is not available");
205205
}
206-
if (this.#stopPromise) await this.#stopPromise;
206+
if (this.#stopPromise !== undefined) await this.#stopPromise;
207207
if (this.#current) return this.#current;
208-
if (this.#startPromise) return await this.#startPromise;
208+
if (this.#startPromise !== undefined) return await this.#startPromise;
209209

210210
const startPromise = this.#startProvider();
211211
this.#startPromise = startPromise;
@@ -307,13 +307,15 @@ class SandboxClientController implements SandboxRelayClientController {
307307
this.#disposed ||
308308
this.#activeOperations !== 0 ||
309309
!this.#current ||
310-
this.#stopPromise
310+
this.#stopPromise !== undefined
311311
) {
312312
return;
313313
}
314314
const client = this.#current;
315315
this.#current = undefined;
316-
const stopPromise = Promise.resolve(client.dispose?.()).then(() => undefined);
316+
const stopPromise = Promise.resolve(client.dispose?.()).then(
317+
() => undefined,
318+
);
317319
this.#stopPromise = stopPromise;
318320
try {
319321
await stopPromise;
@@ -416,9 +418,11 @@ function normalizeHeaders(
416418
);
417419
}
418420

419-
function getSerializableClientConfig(
420-
client: AgentOsSandboxClient,
421-
): Pick<SandboxMountPluginConfig, "baseUrl" | "token" | "headers"> {
421+
function getSerializableClientConfig(client: AgentOsSandboxClient): {
422+
baseUrl: string;
423+
token?: string;
424+
headers?: Record<string, string>;
425+
} {
422426
const serializable = client as unknown as SerializableSandboxClient;
423427
const baseUrl = serializable.baseUrl?.trim().replace(/\/+$/, "");
424428
if (!baseUrl) {

0 commit comments

Comments
 (0)