Skip to content

Commit ea2a91c

Browse files
authored
feat(desktop): add Runtime Host Session execution IPC (#2149)
1 parent 31896d7 commit ea2a91c

30 files changed

Lines changed: 2607 additions & 53 deletions

‎apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,24 @@ test('treats empty configuration patches as read-only lookups', async () => {
200200
]);
201201
});
202202

203+
test('reads the bounded Host execution boundary summary', async () => {
204+
const { client, requests } = clientWithResponses([
205+
{ kind: 'managed', access: 'read_only', revision: 4 },
206+
]);
207+
208+
assert.deepEqual(await client.readExecutionBoundary('session-1'), {
209+
kind: 'managed',
210+
access: 'read_only',
211+
revision: 4,
212+
});
213+
assert.deepEqual(requests, [
214+
{
215+
operation: 'session.execution_boundary.query',
216+
input: { sessionId: 'session-1' },
217+
},
218+
]);
219+
});
220+
203221
test('binds message controls to the current Host Epoch', async () => {
204222
const { client, requests } = clientWithResponses([
205223
{ disposition: 'steering', queueRevision: 2 },
@@ -264,6 +282,118 @@ test('binds message controls to the current Host Epoch', async () => {
264282
]);
265283
});
266284

285+
test('uploads Attachment bytes in bounded Host chunks and commits the digest', async () => {
286+
const bytes = new Uint8Array(48 * 1024 + 3).fill(7);
287+
const attachment = {
288+
kind: 'file' as const,
289+
name: 'notes.txt',
290+
mimeType: 'text/plain',
291+
bytes: bytes.byteLength,
292+
ref: {
293+
kind: 'session_file' as const,
294+
sessionId: 'session-1',
295+
relativePath: 'artifact-1',
296+
},
297+
};
298+
const { client, requests } = clientWithResponses([
299+
{ kind: 'upload_opened', nextOffset: 0 },
300+
{ kind: 'chunk_accepted', nextOffset: 48 * 1024 },
301+
{ kind: 'chunk_accepted', nextOffset: bytes.byteLength },
302+
{ kind: 'committed', attachment },
303+
]);
304+
305+
assert.deepEqual(
306+
await client.ingestAttachment({
307+
sessionId: 'session-1',
308+
uploadId: 'upload-1',
309+
name: 'notes.txt',
310+
mimeType: 'text/plain',
311+
content: bytes,
312+
}),
313+
attachment,
314+
);
315+
assert.deepEqual(
316+
requests.map(({ operation, input }) => ({
317+
operation,
318+
kind: (input as { kind?: string }).kind,
319+
offset: (input as { offset?: number }).offset,
320+
})),
321+
[
322+
{ operation: 'artifact.ingest', kind: 'begin', offset: undefined },
323+
{ operation: 'artifact.ingest', kind: 'chunk', offset: 0 },
324+
{ operation: 'artifact.ingest', kind: 'chunk', offset: 48 * 1024 },
325+
{ operation: 'artifact.ingest', kind: 'commit', offset: undefined },
326+
],
327+
);
328+
const begin = requests[0]?.input as { contentSha256: string; totalBytes: number };
329+
assert.equal(begin.totalBytes, bytes.byteLength);
330+
assert.match(begin.contentSha256, /^sha256:[a-f0-9]{64}$/);
331+
});
332+
333+
test('aborts an opened Attachment upload when a later chunk fails', async () => {
334+
const requests: RecordedRequest[] = [];
335+
const connection = {
336+
hostEpoch: 'host-current',
337+
request: async <K extends OperationKey>(operation: K, input: OperationInput<K>) => {
338+
requests.push({ operation, input });
339+
if ((input as { kind?: string }).kind === 'begin') {
340+
return { kind: 'upload_opened', nextOffset: 0 };
341+
}
342+
if ((input as { kind?: string }).kind === 'abort') {
343+
return { kind: 'upload_aborted', uploadId: 'upload-1' };
344+
}
345+
throw new Error('chunk failed');
346+
},
347+
close: async () => undefined,
348+
} as unknown as RuntimeHostConnection;
349+
const client = new DesktopRuntimeHostClient(connection);
350+
351+
await assert.rejects(
352+
() =>
353+
client.ingestAttachment({
354+
sessionId: 'session-1',
355+
uploadId: 'upload-1',
356+
name: 'notes.txt',
357+
mimeType: 'text/plain',
358+
content: new Uint8Array([1]),
359+
}),
360+
/chunk failed/,
361+
);
362+
assert.deepEqual(
363+
requests.map(({ input }) => (input as { kind: string }).kind),
364+
['begin', 'chunk', 'abort'],
365+
);
366+
});
367+
368+
test('accepts an idempotently committed Attachment upload at begin', async () => {
369+
const attachment = {
370+
kind: 'file' as const,
371+
name: 'notes.txt',
372+
mimeType: 'text/plain',
373+
bytes: 1,
374+
ref: {
375+
kind: 'session_file' as const,
376+
sessionId: 'session-1',
377+
relativePath: 'artifact-1',
378+
},
379+
};
380+
const { client, requests } = clientWithResponses([
381+
{ kind: 'committed', uploadId: 'upload-1', attachment },
382+
]);
383+
384+
assert.deepEqual(
385+
await client.ingestAttachment({
386+
sessionId: 'session-1',
387+
uploadId: 'upload-1',
388+
name: 'notes.txt',
389+
mimeType: 'text/plain',
390+
content: new Uint8Array([1]),
391+
}),
392+
attachment,
393+
);
394+
assert.equal(requests.length, 1);
395+
});
396+
267397
test('fails explicitly when the Host cannot represent a legacy Session', async () => {
268398
const { client } = clientWithResponses([
269399
{

‎apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
tryAcquireInteractiveRootOwner,
2121
} from '@maka/storage/root-authority';
2222
import { DesktopRuntimeHostClient } from '../runtime-host-client.js';
23+
import { createAttachmentApprovalRegistry } from '../attachment-approval.js';
2324
import { registerRuntimeHostSessionCatalogIpc } from '../runtime-host-session-catalog-ipc-main.js';
25+
import { registerRuntimeHostSessionExecutionIpc } from '../runtime-host-session-execution-ipc-main.js';
26+
import { RuntimeHostSessionObserver } from '../runtime-host-session-observer.js';
2427

2528
test('drives Desktop Session operations through a real Runtime Host connection', async () => {
2629
const base = await mkdtemp(join(tmpdir(), 'maka-desktop-host-client-'));
@@ -222,6 +225,106 @@ test('drives the renderer Session catalog facade through real UDS framing', asyn
222225
}
223226
});
224227

228+
test('drives the renderer Session execution facade through real UDS framing', async () => {
229+
const base = await mkdtemp(join(tmpdir(), 'maka-desktop-host-execution-ipc-'));
230+
let host: RuntimeHostKernel | undefined;
231+
try {
232+
const capability = await resolveStorageRoot({ path: base, kind: 'interactive' });
233+
const owner = await tryAcquireInteractiveRootOwner(capability);
234+
assert.ok(owner);
235+
const projected = session('session-1');
236+
host = await RuntimeHostKernel.start({
237+
owner,
238+
idleGraceMs: 10_000,
239+
compositionFactory: async () => ({
240+
handlers: handlers({
241+
'session.catalog.query': async (input) => ({
242+
ok: true,
243+
result: {
244+
kind: 'session',
245+
session: input.kind === 'get' && input.sessionId === projected.id ? projected : null,
246+
},
247+
}),
248+
'session.execution_boundary.query': async (input) => {
249+
assert.equal(input.sessionId, projected.id);
250+
return {
251+
ok: true,
252+
result: { kind: 'managed', access: 'read_only', revision: 2 },
253+
};
254+
},
255+
'turn.start': async (input) => {
256+
assert.equal(input.sessionId, projected.id);
257+
assert.equal(input.content.text, 'Run through the Host');
258+
return {
259+
ok: true,
260+
result: {
261+
sessionId: input.sessionId,
262+
turnId: input.turnId,
263+
runId: 'run-1',
264+
status: 'running',
265+
},
266+
};
267+
},
268+
}),
269+
beginDrain() {},
270+
async recover() {},
271+
async close() {},
272+
}),
273+
});
274+
const connected = await connectRuntimeHost({
275+
rootPath: base,
276+
surface: 'desktop',
277+
protocol: {
278+
min: RUNTIME_HOST_PROTOCOL_VERSION,
279+
max: RUNTIME_HOST_PROTOCOL_VERSION,
280+
},
281+
});
282+
assert.equal(connected.kind, 'connected');
283+
if (connected.kind !== 'connected') throw new Error('Desktop did not connect to Runtime Host');
284+
const client = new DesktopRuntimeHostClient(connected.connection);
285+
const observer = new RuntimeHostSessionObserver({ client, emitSessionsChanged() {} });
286+
const ipc = ipcHarness();
287+
registerRuntimeHostSessionExecutionIpc(
288+
{
289+
client,
290+
observer,
291+
attachmentApprovals: createAttachmentApprovalRegistry(),
292+
emitSessionsChanged() {},
293+
stat: async () => ({ size: 0 }),
294+
resizeImage: async (bytes) => bytes,
295+
newId: () => 'turn-1',
296+
},
297+
ipc,
298+
);
299+
300+
assert.deepEqual(await ipc.invoke('sessions:readExecutionBoundary', projected.id), {
301+
kind: 'managed',
302+
access: 'read_only',
303+
revision: 2,
304+
});
305+
assert.deepEqual(
306+
await ipc.invoke('sessions:send', projected.id, {
307+
type: 'send',
308+
turnId: 'turn-1',
309+
text: 'Run through the Host',
310+
}),
311+
{
312+
ok: true,
313+
turnId: 'turn-1',
314+
attachments: [],
315+
inlineReferences: [],
316+
skillInvocation: { loaded: [], failed: [], receipts: [] },
317+
},
318+
);
319+
320+
await observer.close();
321+
await client.close();
322+
} finally {
323+
await host?.close().catch(() => undefined);
324+
await rm(base, { recursive: true, force: true });
325+
}
326+
});
327+
225328
type TestHandlers = Partial<RuntimeHostComposition['handlers']>;
226329

227330
function handlers(overrides: TestHandlers): RuntimeHostComposition['handlers'] {

0 commit comments

Comments
 (0)