Skip to content

Commit bdac5bd

Browse files
authored
fix(cloud-agent): make stale workspace cleanup runtime-aware (#3656)
1 parent f4b967a commit bdac5bd

7 files changed

Lines changed: 298 additions & 106 deletions

File tree

services/cloud-agent-next/src/agent-sandbox/cloudflare/cloudflare-agent-sandbox.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,78 @@ describe('CloudflareAgentSandbox', () => {
146146
ensureBootstrapWrapper.mockRestore();
147147
});
148148

149+
it('reclaims stale bootstrap workspaces without inspecting Docker', async () => {
150+
const bootstrapSession = {};
151+
const createSession = vi.fn().mockResolvedValue(bootstrapSession);
152+
const ensureBootstrapWrapper = vi
153+
.spyOn(WrapperClient, 'ensureBootstrapWrapper')
154+
.mockResolvedValueOnce({ client: {} as WrapperClient });
155+
const exec = vi
156+
.fn()
157+
.mockResolvedValueOnce({ exitCode: 1, stdout: '', stderr: '' })
158+
.mockResolvedValueOnce({ exitCode: 0, stdout: '536870912 10485760000\n', stderr: '' })
159+
.mockResolvedValueOnce({
160+
exitCode: 0,
161+
stdout: 'agent_stale-aaaa\nagent_cloudflare\n',
162+
stderr: '',
163+
})
164+
.mockResolvedValueOnce({ exitCode: 0, stdout: '0\n', stderr: '' })
165+
.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' })
166+
.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' })
167+
.mockResolvedValueOnce({ exitCode: 0, stdout: '3145728000 10485760000\n', stderr: '' });
168+
const sandbox = new CloudflareAgentSandbox({} as Env, metadata(), {
169+
resolveSandbox: () =>
170+
({
171+
exec,
172+
listProcesses: vi.fn().mockResolvedValue([]),
173+
createSession,
174+
}) as unknown as SandboxInstance,
175+
});
176+
177+
await expect(sandbox.ensureWrapper(ensureRequest())).resolves.toMatchObject({
178+
status: 'wrapper-running',
179+
});
180+
expect(exec.mock.calls.every(call => !call[0].includes('docker'))).toBe(true);
181+
expect(createSession).toHaveBeenCalled();
182+
ensureBootstrapWrapper.mockRestore();
183+
});
184+
185+
it('keeps unresolved DIND bootstrap cleanup fail-closed', async () => {
186+
const unresolvedDindMetadata = {
187+
...metadata(),
188+
workspace: { sandboxId: 'dind-unresolved' },
189+
} satisfies SessionMetadata;
190+
const request = ensureRequest();
191+
request.plan.workspace = { sandboxId: 'dind-unresolved', metadata: unresolvedDindMetadata };
192+
const exec = vi
193+
.fn()
194+
.mockResolvedValueOnce({ exitCode: 1, stdout: '', stderr: '' })
195+
.mockResolvedValueOnce({ exitCode: 0, stdout: '536870912 10485760000\n', stderr: '' })
196+
.mockResolvedValueOnce({ exitCode: 0, stdout: 'agent_stale-aaaa\n', stderr: '' })
197+
.mockResolvedValueOnce({
198+
exitCode: 0,
199+
stdout: '/run/user/1000/docker.sock',
200+
stderr: '',
201+
})
202+
.mockRejectedValueOnce(new Error('docker inspection unavailable'))
203+
.mockResolvedValueOnce({ exitCode: 0, stdout: '536870912 10485760000\n', stderr: '' });
204+
const sandbox = new CloudflareAgentSandbox({} as Env, unresolvedDindMetadata, {
205+
resolveSandbox: () =>
206+
({
207+
exec,
208+
listProcesses: vi.fn().mockResolvedValue([]),
209+
createSession: vi.fn(),
210+
}) as unknown as SandboxInstance,
211+
});
212+
213+
await expect(sandbox.ensureWrapper(request)).rejects.toBeInstanceOf(
214+
WorkspaceCapacityAdmissionRejectedError
215+
);
216+
expect(exec.mock.calls[4][0]).toContain('docker ps');
217+
expect(exec.mock.calls.every(call => !call[0].includes('stat'))).toBe(true);
218+
expect(exec.mock.calls.every(call => !call[0].includes('rm -rf'))).toBe(true);
219+
});
220+
149221
it('passes a leased physical identity into bootstrap startup', async () => {
150222
const bootstrapSession = {};
151223
const ensureBootstrapWrapper = vi

services/cloud-agent-next/src/agent-sandbox/cloudflare/cloudflare-agent-sandbox.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,9 @@ export class CloudflareAgentSandbox implements AgentSandbox {
230230
const workspaceWarm = await this.workspaceHasGit(sandbox, prepared.context.workspacePath);
231231
if (!workspaceWarm) {
232232
request.onProgress?.('disk_check', 'Checking disk space...');
233-
await checkDiskAndCleanBeforeSetup(sandbox, orgId, userId, sessionId);
233+
await checkDiskAndCleanBeforeSetup(sandbox, orgId, userId, sessionId, {
234+
inspectContainers: sandboxId.startsWith('dind-'),
235+
});
234236
}
235237
request.onProgress?.('kilo_server', 'Starting Kilo...');
236238
const bootstrapSession = await sandbox.createSession({

services/cloud-agent-next/src/kilo/wrapper-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export async function findWrapperForSession(
164164
* `kilo.agentSession=<id>`. The published port we want is buried in the
165165
* `Ports` column (`0.0.0.0:5xxx->5xxx/tcp` or `127.0.0.1:5xxx->5xxx/tcp`).
166166
*/
167-
type LabeledWrapperRow = {
167+
export type LabeledWrapperRow = {
168168
containerId: string;
169169
agentSessionId: string;
170170
port?: number;

services/cloud-agent-next/src/session-service.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,13 @@ describe('SessionService.prepareWorkspace', () => {
286286
onProgress: progress,
287287
});
288288

289+
expect(workspaceMocks.checkDiskAndCleanBeforeSetup).toHaveBeenCalledWith(
290+
sandbox,
291+
undefined,
292+
'user_test',
293+
'agent_test',
294+
{ inspectContainers: false }
295+
);
289296
expect(workspaceMocks.cloneGitRepo).toHaveBeenCalledWith(
290297
session,
291298
'/workspace/user/sessions/agent_test',
@@ -375,11 +382,57 @@ describe('SessionService.prepareWorkspace', () => {
375382
})
376383
).rejects.toBe(rejection);
377384

385+
expect(workspaceMocks.checkDiskAndCleanBeforeSetup).toHaveBeenCalledWith(
386+
sandbox,
387+
undefined,
388+
'user_test',
389+
'agent_test',
390+
{ inspectContainers: true }
391+
);
378392
expect(workspaceMocks.setupWorkspace).not.toHaveBeenCalled();
379393
expect(sandbox.createSessionMock).not.toHaveBeenCalled();
380394
expect(devcontainerMocks.bringUpDevContainer).not.toHaveBeenCalled();
381395
});
382396

397+
it('keeps requested devcontainer cleanup fail-closed when the sandbox ID is not DIND', async () => {
398+
const session = createSession(false);
399+
const sandbox = createSandbox(session);
400+
const metadata = {
401+
...createMetadata(),
402+
workspace: {
403+
sandboxId: 'usr-abcdef' as const,
404+
devcontainerRequested: true,
405+
},
406+
} satisfies CloudAgentSessionState;
407+
const rejection = new WorkspaceCapacityAdmissionRejectedError({
408+
availableMB: 512,
409+
thresholdMB: 2048,
410+
cleaned: 0,
411+
skipped: 1,
412+
});
413+
workspaceMocks.checkDiskAndCleanBeforeSetup.mockRejectedValueOnce(rejection);
414+
415+
await expect(
416+
new SessionService().prepareWorkspace({
417+
sandbox,
418+
sandboxId: 'usr-abcdef',
419+
userId: 'user_test',
420+
sessionId: 'agent_test' as SessionId,
421+
env: createEnv(),
422+
metadata,
423+
kilocodeModel: 'test-model',
424+
})
425+
).rejects.toBe(rejection);
426+
427+
expect(workspaceMocks.checkDiskAndCleanBeforeSetup).toHaveBeenCalledWith(
428+
sandbox,
429+
undefined,
430+
'user_test',
431+
'agent_test',
432+
{ inspectContainers: true }
433+
);
434+
});
435+
383436
it('hydrates requested devcontainer metadata while preparing a cold DIND workspace', async () => {
384437
const session = createSession(false);
385438
const writeFile = vi.fn().mockResolvedValue(undefined);

services/cloud-agent-next/src/session-service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,12 @@ export class SessionService {
17031703
}
17041704

17051705
onProgress?.('disk_check', 'Checking disk space…');
1706-
await checkDiskAndCleanBeforeSetup(sandbox, orgId, userId, sessionId);
1706+
await checkDiskAndCleanBeforeSetup(sandbox, orgId, userId, sessionId, {
1707+
inspectContainers:
1708+
sandboxId.startsWith('dind-') ||
1709+
metadata.workspace?.devcontainerRequested === true ||
1710+
metadata.devcontainer !== undefined,
1711+
});
17071712

17081713
onProgress?.('workspace_setup', 'Setting up workspace…');
17091714
await setupWorkspace(sandbox, userId, orgId, sessionId);

0 commit comments

Comments
 (0)