Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b83af63
feat(runtime): add extension lifecycle kernel
xxhZs Aug 13, 2026
b53bfd2
test(runtime): exercise extension lifecycle system flows
xxhZs Aug 13, 2026
05fafe2
feat(runtime): add extension tool contributions
xxhZs Aug 14, 2026
ee5897c
feat(runtime-host): wire extension tool runtime
xxhZs Aug 14, 2026
08cfa84
feat(runtime-host): add trusted extension control plane
xxhZs Aug 14, 2026
ca96ed2
test(runtime-host): exercise trusted extension end to end
xxhZs Aug 14, 2026
ea89b10
feat(runtime-host): add installable tool packages
xxhZs Aug 14, 2026
004fca5
fix(runtime-host): harden tool package model integration
xxhZs Aug 14, 2026
40e4890
feat(runtime-host): let subagents author tool candidates
xxhZs Aug 14, 2026
82fa50b
fix(runtime-host): harden agent-authored tool retries
xxhZs Aug 14, 2026
05b6da9
fix(runtime-host): align extension identity validation
xxhZs Aug 14, 2026
2d32d6d
feat(extension): add lifecycle-managed UI contributions
xxhZs Aug 14, 2026
6ccbd09
feat(extension): close UI import and host bridge loop
xxhZs Aug 14, 2026
c4f99d7
fix(extension): make imported UI executable
xxhZs Aug 14, 2026
7eb0ae9
feat(extension): publish tool results to ui state
xxhZs Aug 14, 2026
522db5f
feat(extension): add native UI sidecar panels
xxhZs Aug 14, 2026
a3f8706
feat(extensions): compose UI and tools as one revision
xxhZs Aug 14, 2026
bc504c0
fix(extensions): exclude git metadata from revisions
xxhZs Aug 14, 2026
c77cec6
feat(extensions): replace the complete desktop root
xxhZs Aug 14, 2026
d80ccd2
feat(graph): add bounded UI author routing
xxhZs Aug 15, 2026
8a06021
feat(extensions): add composable UI slots
xxhZs Aug 15, 2026
f007f70
feat(extensions): complete unified plugin platform
xxhZs Aug 15, 2026
3998a28
feat(runtime): add trusted pre-tool-use hooks
xxhZs Aug 12, 2026
73e60bb
test(runtime): cover configured hook denial end to end
xxhZs Aug 12, 2026
4906c30
fix(runtime): enforce hook lifecycle bounds
xxhZs Aug 13, 2026
63889e3
feat(extensions): add typed runtime hook contributions
xxhZs Aug 15, 2026
589c064
feat(extensions): close unified package authoring loop
xxhZs Aug 16, 2026
23dd1cc
fix(extensions): isolate UI package previews
xxhZs Aug 16, 2026
9d31f8c
fix(extensions): preserve hook lifecycle across updates
xxhZs Aug 16, 2026
11a3af9
feat(extensions): add generic event listener contributions
xxhZs Aug 17, 2026
628f5ba
feat(extensions): add composable serverless runtime
xxhZs Aug 17, 2026
09b3710
fix(extensions): harden dispatch and timer execution
xxhZs Aug 17, 2026
2384d42
refactor(extensions): run trusted plugins in process
xxhZs Aug 17, 2026
7b16610
refactor(extensions): adopt hierarchical runtime contexts
xxhZs Aug 18, 2026
adf3b9d
fix(extensions): close lifecycle consistency gaps
xxhZs Aug 18, 2026
a8cc6f6
refactor(extensions): unify plugin composition runtime
xxhZs Aug 18, 2026
2b31619
refactor(extensions): internalize plugin runtime kernel
xxhZs Aug 18, 2026
a69078f
refactor extension composition through entry tree operations
xxhZs Aug 18, 2026
538634f
route extension mutations through composition apply
xxhZs Aug 18, 2026
80293cf
remove last-good extension revision state
xxhZs Aug 18, 2026
f08365b
align event management with entry tree semantics
xxhZs Aug 18, 2026
675d7b9
refactor(extensions): fold configuration into entry state
xxhZs Aug 18, 2026
fa73009
refactor(extensions): project catalog reads from composition
xxhZs Aug 18, 2026
6ca8938
refactor(extensions): converge composition protocol and runtime consu…
xxhZs Aug 18, 2026
27dd2d4
docs(extensions): define canonical package control surface
xxhZs Aug 18, 2026
c1baee0
refactor(runtime): route extension projections through services
xxhZs Aug 18, 2026
23bd59e
refactor(extensions): remove legacy contribution management
xxhZs Aug 18, 2026
67cb43a
refactor(extensions): complete entry and fiber tree architecture
xxhZs Aug 19, 2026
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import type { IpcHandler } from '../ipc-reconnect-policy.js';
import type { DesktopRuntimeHostClient } from '../runtime-host-client.js';
import { registerRuntimeHostUiExtensionsIpc } from '../runtime-host-ui-extensions-ipc-main.js';

test('user import previews, confirms, installs, and enables one trusted UI and Event package', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-ui-import-'));
try {
await mkdir(join(root, 'documents'));
await mkdir(join(root, 'host'));
await mkdir(join(root, 'dist'));
await writeFile(
join(root, 'maka.extension.json'),
JSON.stringify({
schemaVersion: 1,
id: 'dev.maka.user.ui',
runtime: {
entry: 'dist/index.mjs',
tools: [],
events: [
{
name: 'dev.maka.user.ui.changed',
description: 'UI changed.',
payloadSchema: { type: 'object' },
},
],
listeners: [
{ id: 'changed', event: 'dev.maka.user.ui.changed', handler: 'changed' },
],
services: [],
timers: [],
permissions: { workspace: 'none', network: false },
},
ui: {
contributions: [
{ id: 'root', surface: 'app.root', priority: 1, document: 'documents/root.html' },
],
host: { entry: 'host/service.mjs', methods: [{ name: 'hello', handler: 'hello' }] },
permissions: { network: false, hostState: true, sessionAccess: true },
},
}),
);
await writeFile(join(root, 'documents', 'root.html'), '<main>hello</main>');
await writeFile(join(root, 'host', 'service.mjs'), 'export default { hello: () => "world" };');
await writeFile(
join(root, 'dist', 'index.mjs'),
'export default { changed: () => undefined };',
);
const handlers = new Map<string, IpcHandler>();
const requests: Array<{ operation: string; input: unknown }> = [];
const client = {
request: async (operation: string, input: unknown) => {
requests.push({ operation, input });
if (operation === 'extension.package.install') {
return { extensionId: 'dev.maka.user.ui', toolNames: [], uiContributionIds: ['root'], eventContributionIds: ['event:dev.maka.user.ui.changed', 'listener:dev.maka.user.ui.changed:changed'] };
}
if (operation === 'extension.composition.query') return { extensions: [], entries: [] };
if (operation === 'extension.composition.mutate') return { entry: null };
throw new Error(`unexpected ${operation}`);
},
} as unknown as DesktopRuntimeHostClient;
registerRuntimeHostUiExtensionsIpc({
ipcMain: {
handle: (channel, listener) => handlers.set(channel, listener),
handleReconnectableRead: (channel, listener) => handlers.set(channel, listener),
},
client,
mainWindowController: {
showOpenDialog: async () => ({ canceled: false, filePaths: [root] }),
showMessageBox: async () => ({ response: 0, checkboxChecked: false }),
} as never,
allowLocalPaths: true,
});
const handler = handlers.get('ui-extensions:importLocal');
assert.ok(handler);
assert.deepEqual(await handler({} as never), { ok: true, extensionId: 'dev.maka.user.ui' });
assert.equal(requests[0]?.operation, 'extension.package.install');
const mutations = requests.filter(({ operation }) => operation === 'extension.composition.mutate');
assert.equal(mutations.length, 2);
assert.deepEqual(mutations[0], {
operation: 'extension.composition.mutate',
input: {
kind: 'enable',
entryId: mutations[0] && (mutations[0].input as { entryId: string }).entryId,
scopeId: 'desktop-ui',
extensionId: 'dev.maka.user.ui',
},
});
assert.equal((mutations[1]?.input as { scopeId?: string } | undefined)?.scopeId, 'profile');
} finally {
await rm(root, { recursive: true, force: true });
}
});
194 changes: 194 additions & 0 deletions apps/desktop/src/main/__tests__/ui-extension-host.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
import assert from 'node:assert/strict';
import { afterEach, describe, test } from 'node:test';
import { parseHTML } from 'linkedom';
import { act, createElement, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import type { ExtensionUiContributionProjection } from '@maka/runtime-host/protocol';
import { selectUiSnapshots, UiExtensionSlot, UiExtensionSlotProvider } from '../../renderer/ui-extension-host.js';
import { withUiSandboxPolicy } from '../ui-extension-frame-document.js';
import { createUiExtensionFrameRequestHandler } from '../ui-extension-frame-protocol.js';
import { uiExtensionFrameUrl } from '../../renderer/ui-extension-frame-url.js';

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
HTMLElement: globalThis.HTMLElement,
HTMLIFrameElement: globalThis.HTMLIFrameElement,
IS_REACT_ACT_ENVIRONMENT: (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
.IS_REACT_ACT_ENVIRONMENT,
};

afterEach(() => {
Object.assign(globalThis, originalGlobals);
});

describe('Desktop UI extension shell', () => {
test('selects one deterministic root and ordered independent overlays', () => {
const selected = selectUiSnapshots(null, [
item('low', 'app.root', 1),
item('overlay-b', 'app.overlay', 20),
item('high', 'app.root', 100),
item('overlay-a', 'app.overlay', 20),
item('settings', 'app.slot', 30, 'settings.content'),
item('conversation', 'app.slot', 40, 'conversation.header'),
]);
assert.equal(selected.root.id, 'high');
assert.deepEqual(selected.overlays.map(({ id }) => id), ['overlay-a', 'overlay-b']);
assert.deepEqual(selected.slots.map(({ id }) => id), ['conversation', 'settings']);
});

test('updates one slot without remounting the official root', async () => {
const { document, window } = parseHTML('<div id="root"></div>');
Object.assign(globalThis, {
document,
window,
HTMLElement: window.HTMLElement,
HTMLIFrameElement: window.HTMLIFrameElement ?? class HTMLIFrameElement {},
IS_REACT_ACT_ENVIRONMENT: true,
});
const container = document.querySelector('#root');
assert.ok(container);
const root = createRoot(container);
let mounts = 0;
let unmounts = 0;
function OfficialRootProbe() {
useEffect(() => {
mounts += 1;
return () => {
unmounts += 1;
};
}, []);
return createElement('main', { 'data-official-root': true });
}
const render = async (generation: number) => {
await act(async () => {
root.render(
createElement(
UiExtensionSlotProvider,
{
contributions: [
{ ...item('status', 'app.slot', 10, 'conversation.header'), generation },
],
onSafeMode: () => undefined,
},
createElement(OfficialRootProbe),
createElement(UiExtensionSlot, { name: 'conversation.header' }),
),
);
await Promise.resolve();
});
};

await render(1);
await render(2);
assert.equal(mounts, 1);
assert.equal(unmounts, 0);
assert.equal(container.querySelectorAll('iframe').length, 1);
await act(async () => root.unmount());
assert.equal(unmounts, 1);
});

test('injects an offline CSP by default and only opens declared network lanes', () => {
const offline = withUiSandboxPolicy('<html><head></head><body>Hello</body></html>', false);
assert.match(offline, /connect-src 'none'/);
assert.match(offline, /frame-src 'none'/);
assert.ok(offline.indexOf('Content-Security-Policy') < offline.indexOf('</head>'));
const online = withUiSandboxPolicy('<main>Hello</main>', true);
assert.match(online, /connect-src https: wss:/);
assert.match(online, /form-action 'none'/);
});

test('injects the narrow Host SDK only for an admitted frame token', () => {
const plain = withUiSandboxPolicy('<main>Hello</main>', false);
assert.doesNotMatch(plain, /makaUI/);
const bridged = withUiSandboxPolicy('<main>Hello</main>', false, 'test-token', [
'workspace.body',
]);
assert.match(bridged, /maka-ui-bridge\/v1/);
assert.match(bridged, /maka-ui-bridge-ready\/v1/);
assert.match(bridged, /maka-ui-host-ready\/v1/);
assert.match(bridged, /queued\.push/);
assert.match(bridged, /setInterval\(announce,50\)/);
assert.match(bridged, /clearInterval\(retry\)/);
assert.match(bridged, /getState/);
assert.match(bridged, /setState/);
assert.match(bridged, /deleteState/);
assert.match(bridged, /invoke/);
assert.match(bridged, /session_list/);
assert.match(bridged, /session_send/);
assert.match(bridged, /session_stop/);
assert.match(bridged, /safe_mode/);
assert.match(bridged, /maka-ui-slot-layout\/v1/);
assert.match(bridged, /data-maka-slot/);
assert.match(bridged, /workspace\.body/);
assert.match(bridged, /getConfig/);
assert.match(bridged, /test-token/);
});

test('serves active UI bytes from an isolated scheme instead of srcdoc CSP inheritance', async () => {
const token = '12345678-1234-4123-8123-123456789abc';
const contribution = item('root', 'app.root', 1);
const url = uiExtensionFrameUrl({
scopeId: 'desktop-ui',
entryId: contribution.entryId,
extensionId: contribution.extensionId,
generation: contribution.generation,
contributionId: contribution.id,
token,
});
const handler = createUiExtensionFrameRequestHandler(() => ({
request: async () => ({
scopeId: 'desktop-ui',
digest: 'sha256-test',
contributions: [{ ...contribution, hostState: true }],
}),
}));
const response = await handler(new Request(url));
assert.equal(response.status, 200);
assert.match(response.headers.get('content-security-policy') ?? '', /script-src 'unsafe-inline'/);
assert.match(await response.text(), /makaUI/);
});

test('injects the emergency recovery bridge even without extension permissions', async () => {
const token = '12345678-1234-4123-8123-123456789abc';
const contribution = item('root', 'app.root', 1);
const handler = createUiExtensionFrameRequestHandler(() => ({
request: async () => ({
scopeId: 'desktop-ui',
digest: 'sha256-test',
contributions: [contribution],
}),
}));
const response = await handler(new Request(uiExtensionFrameUrl({
scopeId: 'desktop-ui',
entryId: contribution.entryId,
extensionId: contribution.extensionId,
generation: contribution.generation,
contributionId: contribution.id,
token,
})));
const document = await response.text();
assert.match(document, /safe_mode/);
assert.match(document, /makaUI/);
});
});

function item(
id: string,
surface: 'app.root' | 'app.overlay' | 'app.slot',
priority: number,
slot?: string,
): ExtensionUiContributionProjection {
return {
entryId: `entry-${id}`,
extensionId: 'demo',
generation: 1,
id,
surface,
...(slot ? { slot } : {}),
priority,
document: '<p>demo</p>',
documentSha256: 'sha256',
network: false,
};
}
41 changes: 41 additions & 0 deletions apps/desktop/src/main/__tests__/ui-plugin-runtime.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { ExtensionUiContributionProjection } from '@maka/runtime-host/protocol';
import { UiPluginRuntime } from '../../renderer/ui-plugin-runtime.js';

test('Client Cordis tree updates one UI entry without remounting siblings', async () => {
const runtime = new UiPluginRuntime();
const first = contribution('first', 1, 'one');
const sibling = contribution('sibling', 1, 'sibling');
await runtime.reconcile([first, sibling]);
assert.deepEqual(runtime.inspect().map(({ entryId, generation }) => [entryId, generation]), [
['first', 1],
['sibling', 1],
]);
await runtime.reconcile([contribution('first', 2, 'two'), sibling]);
assert.deepEqual(runtime.inspect().map(({ entryId, generation }) => [entryId, generation]), [
['first', 2],
['sibling', 1],
]);
await runtime.reconcile([sibling]);
assert.deepEqual(runtime.inspect().map(({ entryId }) => entryId), ['sibling']);
await runtime.close();
});

function contribution(
entryId: string,
generation: number,
id: string,
): ExtensionUiContributionProjection {
return Object.freeze({
entryId,
extensionId: `fixture.${entryId}`,
generation,
id,
surface: 'app.overlay',
priority: 0,
document: '<!doctype html>',
documentSha256: `${id}-${generation}`,
network: false,
});
}
6 changes: 6 additions & 0 deletions apps/desktop/src/main/main-window.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ export interface MainWindowController {
setTitleBarOverlayTheme(sender: Electron.WebContents, theme: unknown): void;
showOpenDialog(options: Electron.OpenDialogOptions): Promise<Electron.OpenDialogReturnValue>;
showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue>;
showMessageBox(options: Electron.MessageBoxOptions): Promise<Electron.MessageBoxReturnValue>;
getBrowserViews(): BrowserViewManager<BrowserViewController>;
disposeBrowserViews(): Promise<void>;
hasOpenWindows(): boolean;
Expand DownExpand Up@@ -501,6 +502,11 @@ export function createMainWindowController(deps: MainWindowControllerDeps): Main
? dialog.showSaveDialog(mainWindow, options)
: dialog.showSaveDialog(options);
},
showMessageBox(options) {
return mainWindow
? dialog.showMessageBox(mainWindow, options)
: dialog.showMessageBox(options);
},
getBrowserViews,
disposeBrowserViews,
hasOpenWindows() {
Expand Down
15 changes: 14 additions & 1 deletion apps/desktop/src/main/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
import { app, dialog } from 'electron';
import { app, dialog, protocol } from 'electron';
import { installMainProcessLogCapture } from './main-process-diagnostics.js';
import { isIsolatedE2e } from './startup-context.js';

installMainProcessLogCapture();

protocol.registerSchemesAsPrivileged([
{
scheme: 'maka-ui',
privileges: {
standard: true,
secure: true,
supportFetchAPI: false,
corsEnabled: false,
bypassCSP: false,
},
},
]);

// The macOS app menu title and app.getName() consumers read this name. Set it
// before ready, unchanged from its historical pre-ready position.
//
Expand Down
Loading