Skip to content

Commit 4e94e65

Browse files
hotlongCopilot
andcommitted
fix(runtime): route AI/Automation through dispatch() so kernel swap fires
For multi-tenant hosts (ObjectOS), the AutomationServicePlugin and AIServicePlugin live on per-project kernels, not the host. The static route handlers in dispatcher-plugin.ts previously called `dispatcher.handleAutomation()` / read `kernel.__aiRoutes` directly, which always hit the host kernel where these services aren't registered — returning 404. Fix: route all /automation/* handlers through `dispatcher.dispatch()` which already performs the kernel swap (`resolveEnvironmentContext` → `kernelManager.getOrCreate(projectId)`). Then `handleAutomation()` resolves the service on the project kernel as intended. For AI, register method-wildcard catch-alls (`/ai/*`) per scope that dispatch through the same path; `handleAI()` reads the per-project `kernel.__aiRoutes` after the swap, so all routes built dynamically by `AIServicePlugin.start()` (chat, models, conversations, tools, agents, assistants) become reachable on per-project URLs and on hostname-scoped multi-tenant URLs (e.g. crm.objectos.app). All 201 runtime tests pass — the changes preserve the existing behaviour for single-kernel hosts (kernelManager unset → dispatch falls through to defaultKernel, same as before). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2856f6b commit 4e94e65

1 file changed

Lines changed: 52 additions & 14 deletions

File tree

packages/runtime/src/dispatcher-plugin.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -648,14 +648,15 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
648648
// ── Automation ──────────────────────────────────────────────
649649
// Registered at both `${prefix}/automation/...` and
650650
// `${prefix}/projects/:projectId/automation/...` when project
651-
// scoping is enabled. Handlers surface `req.params.projectId` to
652-
// the HttpDispatcher through the `request` context so downstream
653-
// resolution (see HttpDispatcher.resolveEnvironmentContext) can
654-
// pick the right data driver.
651+
// scoping is enabled. Always dispatched through
652+
// `dispatcher.dispatch()` so the multi-kernel host can swap
653+
// to the per-project kernel before resolving the
654+
// `automation` service (which lives on the project kernel,
655+
// not the host kernel, in ObjectOS multi-tenant mode).
655656
const registerAutomationRoutes = (base: string) => {
656657
server!.get(`${base}/automation`, async (req: any, res: any) => {
657658
try {
658-
const result = await dispatcher.handleAutomation('', 'GET', {}, { request: req });
659+
const result = await dispatcher.dispatch('GET', '/automation', undefined, req.query, { request: req });
659660
sendResult(result, res);
660661
} catch (err: any) {
661662
errorResponse(err, res);
@@ -664,7 +665,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
664665

665666
server!.post(`${base}/automation`, async (req: any, res: any) => {
666667
try {
667-
const result = await dispatcher.handleAutomation('', 'POST', req.body, { request: req });
668+
const result = await dispatcher.dispatch('POST', '/automation', req.body, req.query, { request: req });
668669
sendResult(result, res);
669670
} catch (err: any) {
670671
errorResponse(err, res);
@@ -673,7 +674,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
673674

674675
server!.get(`${base}/automation/:name`, async (req: any, res: any) => {
675676
try {
676-
const result = await dispatcher.handleAutomation(`${req.params.name}`, 'GET', {}, { request: req });
677+
const result = await dispatcher.dispatch('GET', `/automation/${req.params.name}`, undefined, req.query, { request: req });
677678
sendResult(result, res);
678679
} catch (err: any) {
679680
errorResponse(err, res);
@@ -682,7 +683,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
682683

683684
server!.put(`${base}/automation/:name`, async (req: any, res: any) => {
684685
try {
685-
const result = await dispatcher.handleAutomation(`${req.params.name}`, 'PUT', req.body, { request: req });
686+
const result = await dispatcher.dispatch('PUT', `/automation/${req.params.name}`, req.body, req.query, { request: req });
686687
sendResult(result, res);
687688
} catch (err: any) {
688689
errorResponse(err, res);
@@ -691,7 +692,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
691692

692693
server!.delete(`${base}/automation/:name`, async (req: any, res: any) => {
693694
try {
694-
const result = await dispatcher.handleAutomation(`${req.params.name}`, 'DELETE', {}, { request: req });
695+
const result = await dispatcher.dispatch('DELETE', `/automation/${req.params.name}`, undefined, req.query, { request: req });
695696
sendResult(result, res);
696697
} catch (err: any) {
697698
errorResponse(err, res);
@@ -700,7 +701,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
700701

701702
server!.post(`${base}/automation/trigger/:name`, async (req: any, res: any) => {
702703
try {
703-
const result = await dispatcher.handleAutomation(`trigger/${req.params.name}`, 'POST', req.body, { request: req });
704+
const result = await dispatcher.dispatch('POST', `/automation/trigger/${req.params.name}`, req.body, req.query, { request: req });
704705
sendResult(result, res);
705706
} catch (err: any) {
706707
errorResponse(err, res);
@@ -709,7 +710,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
709710

710711
server!.post(`${base}/automation/:name/trigger`, async (req: any, res: any) => {
711712
try {
712-
const result = await dispatcher.handleAutomation(`${req.params.name}/trigger`, 'POST', req.body, { request: req });
713+
const result = await dispatcher.dispatch('POST', `/automation/${req.params.name}/trigger`, req.body, req.query, { request: req });
713714
sendResult(result, res);
714715
} catch (err: any) {
715716
errorResponse(err, res);
@@ -718,7 +719,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
718719

719720
server!.post(`${base}/automation/:name/toggle`, async (req: any, res: any) => {
720721
try {
721-
const result = await dispatcher.handleAutomation(`${req.params.name}/toggle`, 'POST', req.body, { request: req });
722+
const result = await dispatcher.dispatch('POST', `/automation/${req.params.name}/toggle`, req.body, req.query, { request: req });
722723
sendResult(result, res);
723724
} catch (err: any) {
724725
errorResponse(err, res);
@@ -727,7 +728,7 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
727728

728729
server!.get(`${base}/automation/:name/runs`, async (req: any, res: any) => {
729730
try {
730-
const result = await dispatcher.handleAutomation(`${req.params.name}/runs`, 'GET', {}, { request: req }, req.query);
731+
const result = await dispatcher.dispatch('GET', `/automation/${req.params.name}/runs`, undefined, req.query, { request: req });
731732
sendResult(result, res);
732733
} catch (err: any) {
733734
errorResponse(err, res);
@@ -736,14 +737,48 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
736737

737738
server!.get(`${base}/automation/:name/runs/:runId`, async (req: any, res: any) => {
738739
try {
739-
const result = await dispatcher.handleAutomation(`${req.params.name}/runs/${req.params.runId}`, 'GET', {}, { request: req });
740+
const result = await dispatcher.dispatch('GET', `/automation/${req.params.name}/runs/${req.params.runId}`, undefined, req.query, { request: req });
740741
sendResult(result, res);
741742
} catch (err: any) {
742743
errorResponse(err, res);
743744
}
744745
});
745746
};
746747

748+
// ── AI / Assistants ─────────────────────────────────────────
749+
// The AI service plugin registers a large, dynamic surface
750+
// (chat, models, conversations, tools, agents, assistants)
751+
// whose exact routes are built at start() time from the
752+
// service's tool / agent registries. To support multi-tenant
753+
// hosts where the AI service lives on per-project kernels,
754+
// mount a method-wildcard catch-all that always dispatches
755+
// through `dispatcher.dispatch()` — that triggers the kernel
756+
// swap and then routes via `handleAI`, which looks up the
757+
// AI service on the current (project) kernel.
758+
const registerAIRoutes = (base: string) => {
759+
const wildcards: Array<['get'|'post'|'delete'|'put', string]> = [
760+
['get', `${base}/ai/*`],
761+
['post', `${base}/ai/*`],
762+
['delete', `${base}/ai/*`],
763+
['put', `${base}/ai/*`],
764+
];
765+
for (const [method, pattern] of wildcards) {
766+
(server as any)![method](pattern, async (req: any, res: any) => {
767+
try {
768+
// Reconstruct the AI subpath without the prefix
769+
// so dispatch() routes via the /ai branch.
770+
const fullPath: string = req.path ?? '';
771+
const idx = fullPath.lastIndexOf('/ai');
772+
const aiSubPath = idx >= 0 ? fullPath.slice(idx) : '/ai';
773+
const result = await dispatcher.dispatch(method.toUpperCase(), aiSubPath, req.body, req.query, { request: req });
774+
sendResult(result, res);
775+
} catch (err: any) {
776+
errorResponse(err, res);
777+
}
778+
});
779+
}
780+
};
781+
747782
// ── Actions (server-registered handlers, e.g. CRM convertLead) ───
748783
// Bridges UI `script` / `modal` actions to ObjectQL handlers
749784
// registered via `engine.registerAction(object, action, fn)`.
@@ -776,12 +811,15 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
776811
if (enableProjectScoping && projectResolution === 'required') {
777812
registerAutomationRoutes(`${prefix}/projects/:projectId`);
778813
registerActionRoutes(`${prefix}/projects/:projectId`);
814+
registerAIRoutes(`${prefix}/projects/:projectId`);
779815
} else {
780816
registerAutomationRoutes(prefix);
781817
registerActionRoutes(prefix);
818+
registerAIRoutes(prefix);
782819
if (enableProjectScoping) {
783820
registerAutomationRoutes(`${prefix}/projects/:projectId`);
784821
registerActionRoutes(`${prefix}/projects/:projectId`);
822+
registerAIRoutes(`${prefix}/projects/:projectId`);
785823
}
786824
}
787825

0 commit comments

Comments
 (0)