Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/api/src/openshell-gateway-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,17 @@ export type GatewayInfo = z.output<typeof GatewayInfoSchema>;
export const SandboxInfoSchema = z.object({
id: z.string(),
name: z.string(),
phase: z.enum(['Provisioning', 'Ready', 'Error', 'Deleting', 'Unknown', 'Unspecified']),
phase: z.enum([
'Provisioning',
'Ready',
'Error',
'Deleting',
'Unknown',
'Unspecified',
'Starting',
'Stopping',
'Stopped',
]),
created_at: z
.string()
.transform(ts => {
Expand Down
25 changes: 16 additions & 9 deletions packages/main/src/plugin/acp/acp-session-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ const apiSender: ApiSenderType = {

const openshellCli: OpenshellCli = {
getCliPath: vi.fn().mockReturnValue('/usr/bin/openshell'),
listSandboxes: vi.fn(),
uploadToSandbox: vi.fn(),
} as unknown as OpenshellCli;

const mockSandboxList = vi.fn();
const sdkSandbox = {
execInteractive: vi.fn(),
list: mockSandboxList,
};
const openshellSdkClientManager = {
getClient: vi.fn().mockResolvedValue({ sandbox: sdkSandbox }),
Expand Down Expand Up @@ -104,8 +105,8 @@ describe('AcpSessionManager', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(directories.getAcpSessionsDirectory).mockReturnValue(FAKE_SESSIONS_DIR);
vi.mocked(openshellCli.listSandboxes).mockResolvedValue([]);
vi.mocked(openshellSdkClientManager.getClient).mockResolvedValue({ sandbox: sdkSandbox } as never);
mockSandboxList.mockResolvedValue([]);
manager = new AcpSessionManager(apiSender, openshellCli, agentRegistry, directories, openshellSdkClientManager);
});

Expand Down Expand Up @@ -853,8 +854,8 @@ describe('AcpSessionManager', () => {
events: [],
}),
);
vi.mocked(openshellCli.listSandboxes).mockResolvedValue([
{ id: 'other-id', name: 'other-sandbox', phase: 'Ready' },
mockSandboxList.mockResolvedValue([
{ id: 'other-id', name: 'other-sandbox', phase: 'ready', labels: {}, resourceVersion: '1' },
]);

await manager.init();
Expand Down Expand Up @@ -883,7 +884,9 @@ describe('AcpSessionManager', () => {
events: [],
}),
);
vi.mocked(openshellCli.listSandboxes).mockResolvedValue([{ id: 'sb-id', name: 'my-sandbox', phase: 'Ready' }]);
mockSandboxList.mockResolvedValue([
{ id: 'sb-id', name: 'my-sandbox', phase: 'ready', labels: {}, resourceVersion: '1' },
]);

await manager.init();

Expand Down Expand Up @@ -911,7 +914,9 @@ describe('AcpSessionManager', () => {
events: [],
}),
);
vi.mocked(openshellCli.listSandboxes).mockResolvedValue([{ id: 'sb-id', name: 'my-sandbox', phase: 'Deleting' }]);
mockSandboxList.mockResolvedValue([
{ id: 'sb-id', name: 'my-sandbox', phase: 'deleting', labels: {}, resourceVersion: '1' },
]);

await manager.init();

Expand Down Expand Up @@ -939,7 +944,7 @@ describe('AcpSessionManager', () => {
events: [],
}),
);
vi.mocked(openshellCli.listSandboxes).mockRejectedValue(new Error('CLI not found'));
mockSandboxList.mockRejectedValue(new Error('CLI not found'));

await manager.init();

Expand Down Expand Up @@ -969,7 +974,7 @@ describe('AcpSessionManager', () => {
events: [],
}),
);
vi.mocked(openshellCli.listSandboxes).mockResolvedValue([]);
mockSandboxList.mockResolvedValue([]);

await manager.init();

Expand Down Expand Up @@ -1240,7 +1245,9 @@ describe('AcpSessionManager', () => {

const agent = createAgentInfo();
vi.mocked(agentRegistry.getAgent).mockResolvedValue(agent);
vi.mocked(openshellCli.listSandboxes).mockResolvedValue([createSandbox()]);
mockSandboxList.mockResolvedValue([
{ id: 'sandbox-1', name: 'test-sandbox', phase: 'ready', labels: {}, resourceVersion: '1' },
]);

type ExecStreamEvent = { stream: 'stdout' | 'stderr'; data: Buffer } | { type: 'exit'; exitCode: number };
const events: ExecStreamEvent[] = [];
Expand Down
19 changes: 14 additions & 5 deletions packages/main/src/plugin/acp/acp-session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { AgentRegistry } from '/@/plugin/agent-registry.js';
import { Directories } from '/@/plugin/directories.js';
import { OpenshellCli } from '/@/plugin/openshell-cli/openshell-cli.js';
import { OpenshellSdkClientManager } from '/@/plugin/openshell-cli/openshell-sdk-client-manager.js';
import { mapSdkSandboxRef } from '/@/plugin/openshell-cli/openshell-sdk-sandbox-mapper.js';
import type {
AcpAttachment,
AcpElicitationResponseData,
Expand All @@ -50,6 +51,8 @@ import { createAcpDebug } from './acp-debug.js';

const MAX_STDERR_LINES = 100;
const PTY_COLS = 65_535;

// eslint-disable-next-line sonarjs/publicly-writable-directories
const ATTACHMENT_UPLOAD_DIR = '/sandbox/.kaiden-attachments';

const debugPty = createAcpDebug('pty');
Expand Down Expand Up @@ -92,7 +95,7 @@ export class AcpSessionManager {
@inject(OpenshellCli) private readonly openshellCli: OpenshellCli,
@inject(AgentRegistry) private readonly agentRegistry: AgentRegistry,
@inject(Directories) private readonly directories: Directories,
@inject(OpenshellSdkClientManager) private readonly openshellSdkClientManager: OpenshellSdkClientManager,
@inject(OpenshellSdkClientManager) private readonly sdkClientManager: OpenshellSdkClientManager,
) {}

async init(): Promise<void> {
Expand Down Expand Up @@ -179,7 +182,7 @@ export class AcpSessionManager {
}

async createSession(options: AcpSessionCreateOptions): Promise<AcpSessionInfo> {
const sandboxes = await this.openshellCli.listSandboxes();
const sandboxes = await this.#listSandboxes();
const sandbox = sandboxes.find(s => s.name === options.sandboxName);
if (!sandbox) {
throw new Error(`Sandbox "${options.sandboxName}" not found`);
Expand All @@ -196,7 +199,7 @@ export class AcpSessionManager {
debugPty(`${sandbox.name} execInteractive: ${command.join(' ')}`);

const abortController = new AbortController();
const sdkClient = await this.openshellSdkClientManager.getClient(gatewayName);
const sdkClient = await this.sdkClientManager.getClient(gatewayName);
const execSession = await sdkClient.sandbox.execInteractive(sandbox.name, command, {
tty: false,
cols: PTY_COLS,
Expand Down Expand Up @@ -691,7 +694,7 @@ export class AcpSessionManager {
debugPty(`${session.info.sandboxName} reconnecting via execInteractive: ${session.agentCommand.join(' ')}`);

const abortController = new AbortController();
const sdkClient = await this.openshellSdkClientManager.getClient(session.gatewayName);
const sdkClient = await this.sdkClientManager.getClient(session.gatewayName);
const execSession = await sdkClient.sandbox.execInteractive(session.info.sandboxName, session.agentCommand, {
tty: false,
cols: PTY_COLS,
Expand Down Expand Up @@ -1299,7 +1302,7 @@ export class AcpSessionManager {
private async validateSandboxes(): Promise<void> {
if (this.sessions.size === 0) return;
try {
const sandboxes = await this.openshellCli.listSandboxes();
const sandboxes = await this.#listSandboxes();
const readySandboxes = new Map(sandboxes.filter(s => s.phase === 'Ready').map(s => [s.name, s.id]));
for (const session of this.sessions.values()) {
if (readySandboxes.has(session.info.sandboxName)) {
Expand Down Expand Up @@ -1340,4 +1343,10 @@ export class AcpSessionManager {
// file may not exist
}
}

async #listSandboxes(): Promise<SandboxInfo[]> {
const client = await this.sdkClientManager.getClient();
const refs = await client.sandbox.list();
return refs.map(mapSdkSandboxRef);
}
}
Loading
Loading