You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by ku...@apache.org on 2026/09/14 09:43:54 UTC

(maka) branch main updated: feat(runtime): add plugin-backed Session executors (#5283)

This is an automated email from the ASF dual-hosted git repository.

likun666661 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new f32cf2b482 feat(runtime): add plugin-backed Session executors (#5283)
f32cf2b482 is described below

commit f32cf2b482a17f235b92e2775ed2672c2775e219
Author: Xinhao Xu <84...@users.noreply.github.com>
AuthorDate: Mon Sep 14 17:43:51 2026 +0800

    feat(runtime): add plugin-backed Session executors (#5283)
    
    * feat(runtime): add plugin-backed Session executors
    
    Allow Host plugins to register scoped black-box executors through ctx.executors and route their output through the existing Session event stream. Persist executor selection across ordinary Sessions, WorkHub-created work, child agents, Graph operators, and Session revisions without exposing Maka tools to the external runtime.
    
    Generated-by: Codex
    
    * feat(runtime): harden plugin executor capabilities
---
 .../runtime-host-session-catalog-ipc-main.test.ts  |  20 +
 .../src/main/__tests__/session-local.test.ts       |  41 ++
 .../main/runtime-host-session-catalog-ipc-main.ts  |  11 +-
 apps/desktop/src/main/session-local-service.ts     |  16 +-
 packages/core/package.json                         |   1 +
 .../src/__tests__/agent-graph-schedule.test.ts     |  23 +
 packages/core/src/__tests__/executor-id.test.ts    |  30 ++
 .../__tests__/runtime-invocation-opened.test.ts    |  22 +
 .../src/__tests__/session-send-projection.test.ts  |  17 +
 packages/core/src/agent-graph-schedule.ts          |  21 +-
 packages/core/src/executor-id.ts                   |  24 +
 packages/core/src/runtime-event.ts                 |  40 +-
 packages/core/src/runtime-inputs.ts                |  10 +-
 packages/core/src/session-send-projection.ts       |  12 +-
 packages/core/src/session.ts                       |  14 +-
 packages/eval/src/harbor-maka-subject.ts           |   5 +-
 .../__tests__/host-session-availability.test.ts    |  41 ++
 .../src/__tests__/plugin-platform.test.ts          |  52 ++
 .../__tests__/session-catalog-coordinator.test.ts  |  82 ++++
 .../src/__tests__/session-catalog-protocol.test.ts |  44 ++
 .../workhub-coordination-protocol.test.ts          |  24 +
 .../src/client/session-catalog-summary.ts          |   1 +
 packages/runtime-host/src/protocol/index.ts        |   3 +-
 .../runtime-host/src/protocol/plugin-platform.ts   |  69 ++-
 .../runtime-host/src/protocol/session-catalog.ts   |  43 +-
 .../src/server/child-agent-composition.ts          |   1 +
 .../src/server/execution-composition.ts            |  60 ++-
 .../src/server/host-session-availability.ts        |  17 +-
 .../src/server/plugin-platform-coordinator.ts      |  16 +-
 .../runtime-host/src/server/plugin-platform.ts     |  11 +
 .../src/server/session-catalog-coordinator.ts      |  97 +++-
 .../src/server/session-revision-coordinator.ts     |   3 +
 packages/runtime/package.json                      |   2 +
 .../src/__tests__/plugin-executor-backend.test.ts  | 209 ++++++++
 .../src/__tests__/plugin-executor-service.test.ts  | 234 +++++++++
 .../runtime/src/__tests__/session-manager.test.ts  | 143 +++++-
 .../runtime/src/__tests__/subagent-tools.test.ts   |   4 +
 packages/runtime/src/agent-run.ts                  |  18 +-
 packages/runtime/src/ai-sdk-compaction.ts          |   5 +-
 packages/runtime/src/history-compaction.ts         |   7 +-
 packages/runtime/src/plugin-agent-service.ts       |   1 +
 packages/runtime/src/plugin-executor-backend.ts    | 388 +++++++++++++++
 packages/runtime/src/plugin-executor-service.ts    | 529 +++++++++++++++++++++
 packages/runtime/src/runtime-invocation-route.ts   |  58 +++
 packages/runtime/src/runtime-kernel.ts             |  26 +-
 packages/runtime/src/session-manager.ts            | 176 ++++---
 .../runtime/src/stream-graph-schedule-reconcile.ts |   1 +
 .../runtime/src/stream-graph-supervisor-tools.ts   |  33 +-
 packages/runtime/src/subagent-tools.ts             |   8 +
 packages/runtime/src/tool-runtime.ts               |   3 +
 .../storage/src/__tests__/session-store.test.ts    |  28 ++
 packages/storage/src/legacy-run-header.ts          |   4 +-
 packages/storage/src/session-store.ts              |  15 +-
 .../storage/src/sqlite-session-metadata-store.ts   |   3 +-
 54 files changed, 2581 insertions(+), 185 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts
index 9cb9e8b6ec..0f7cff7040 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-session-catalog-ipc-main.test.ts
@@ -62,6 +62,26 @@ test('session creation forwards the caller name for a mode that carries none', a
   );
 });
 
+test('session creation forwards a plugin executor without a model target', async () => {
+  const creates: SessionCreateInput[] = [];
+  const ipc = ipcHarness();
+  registerRuntimeHostSessionCatalogIpc(createDeps(creates), ipc as unknown as IpcMain);
+
+  await ipc.invoke('sessions:create', { executorId: 'codex.app-server' });
+
+  assert.equal(creates[0]?.executorId, 'codex.app-server');
+  assert.equal(creates[0]?.modelTarget, undefined);
+  await assert.rejects(
+    ipc.invoke('sessions:create', {
+      executorId: 'codex',
+      llmConnectionId: 'connection-1',
+      llmConnectionSlug: 'openai',
+      model: 'gpt-5',
+    }),
+    /cannot include a model target/,
+  );
+});
+
 type IpcHandler = Parameters<Pick<IpcMain, 'handle'>['handle']>[1];
 
 function ipcHarness() {
diff --git a/apps/desktop/src/main/__tests__/session-local.test.ts b/apps/desktop/src/main/__tests__/session-local.test.ts
index 3e5e33ecd2..7e05626ee1 100644
--- a/apps/desktop/src/main/__tests__/session-local.test.ts
+++ b/apps/desktop/src/main/__tests__/session-local.test.ts
@@ -638,6 +638,47 @@ test('attachment retries across restart reuse committed uploads and release stag
   assert.deepEqual(db.store.stagedAttachments('authority', 'message-1'), []);
 });
 
+test('local creation preserves a plugin executor in the pending Session projection', async (t) => {
+  const { store, beforeClose } = await database(t);
+  const target: DesktopSessionLocalTarget = {
+    partition: 'authority',
+    profileId: 'profile',
+    scope: { hostId: 'root', targetEpoch: 'target' },
+  };
+  const service = new DesktopSessionLocalService(store, {
+    targets: () => [target],
+    changed() {},
+    onError: (error) => assert.fail(String(error)),
+  });
+  beforeClose.push(() => service.close());
+  type Ipc = Parameters<typeof registerDesktopSessionLocalIpc>[0]['ipcMain'];
+  let create!: Parameters<Ipc['handle']>[1];
+  registerDesktopSessionLocalIpc({
+    ipcMain: {
+      handle: (channel, handler) => {
+        if (channel === 'session-local:create') create = handler;
+      },
+    },
+    service,
+    approvals: createAttachmentApprovalRegistry(),
+    resizeImage: async (bytes) => bytes,
+    resolveWorkspace: async () => ({ kind: 'host_path', path: '/workspace' }),
+    changed() {},
+  });
+
+  const summary = (await create(
+    {} as IpcMainInvokeEvent,
+    target.scope,
+    { executorId: 'codex.app-server' },
+  )) as DesktopSessionSummaryInput;
+  assert.equal(summary.backend, 'plugin-executor');
+  assert.equal(summary.executorId, 'codex.app-server');
+  assert.equal(summary.llmConnectionId, undefined);
+  assert.equal(summary.llmConnectionSlug, 'executor:codex.app-server');
+  assert.equal(summary.model, 'codex.app-server');
+  assert.equal(store.creation(target.partition, summary.id)?.executorId, 'codex.app-server');
+});
+
 test('local submit preserves picked-file approvals until durable admission succeeds', async (t) => {
   const { store, path, beforeClose } = await database(t);
   const file = join(path, '..', 'picked.txt');
diff --git a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
index a539f2aaef..a221e8fc03 100644
--- a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
@@ -322,12 +322,21 @@ function normalizeSessionListFilter(value: unknown): SessionListFilter | undefin
 
 export function resolveDesktopSessionCreateInput(input: CreateSessionRequestInput | undefined, sessionId: string, workspace: WorkspaceTarget): SessionCreateInput {
   const request = resolveCreateSessionRequest(input);
+  const executorId = normalizeOptionalString(input?.executorId, 'executor id');
+  if (
+    executorId &&
+    (input?.llmConnectionId !== undefined ||
+      input?.llmConnectionSlug !== undefined ||
+      input?.model !== undefined)
+  ) {
+    throw new Error('Plugin executor selection cannot include a model target');
+  }
   return {
     sessionId, workspace,
     ...(request.mode === undefined ? {} : { mode: request.mode }),
     name: request.name,
     ...(request.labels === undefined ? {} : { labels: request.labels }),
-    modelTarget: normalizeModelTarget(input),
+    ...(executorId ? { executorId } : { modelTarget: normalizeModelTarget(input) }),
     ...normalizeCreateThinkingLevel(input?.thinkingLevel),
     ...(request.mode !== undefined || request.permissionMode === undefined ? {} : { permissionMode: request.permissionMode }),
     collaborationMode: request.collaborationMode,
diff --git a/apps/desktop/src/main/session-local-service.ts b/apps/desktop/src/main/session-local-service.ts
index 15805d74b6..a8624a04fb 100644
--- a/apps/desktop/src/main/session-local-service.ts
+++ b/apps/desktop/src/main/session-local-service.ts
@@ -537,10 +537,18 @@ export function registerDesktopSessionLocalIpc(deps: {
         labels: [...(creation.labels ?? [])],
         hasUnread: false,
         status: 'active',
-        backend: 'ai-sdk',
-        llmConnectionSlug: input.llmConnectionSlug ?? '',
-        model: input.model ?? '',
-        ...(input.llmConnectionId ? { llmConnectionId: input.llmConnectionId } : {}),
+        backend: creation.executorId ? 'plugin-executor' : 'ai-sdk',
+        ...(creation.executorId
+          ? {
+              executorId: creation.executorId,
+              llmConnectionSlug: `executor:${creation.executorId}`,
+              model: creation.executorId,
+            }
+          : {
+              llmConnectionSlug: input.llmConnectionSlug ?? '',
+              model: input.model ?? '',
+              ...(input.llmConnectionId ? { llmConnectionId: input.llmConnectionId } : {}),
+            }),
         connectionLocked: false,
         permissionMode: creation.permissionMode ?? 'ask',
         collaborationMode: creation.collaborationMode,
diff --git a/packages/core/package.json b/packages/core/package.json
index 4a167a083f..331eec147c 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -7,6 +7,7 @@
   "sideEffects": false,
   "private": true,
   "exports": {
+    "./executor-id": "./dist/executor-id.js",
     "./durable-tool-result-projection": "./dist/durable-tool-result-projection.js",
     "./model-projection-transition": "./dist/model-projection-transition.js",
     "./canonical-runtime-event": "./dist/canonical-runtime-event.js",
diff --git a/packages/core/src/__tests__/agent-graph-schedule.test.ts b/packages/core/src/__tests__/agent-graph-schedule.test.ts
index c50b36440f..01d8d6efe2 100644
--- a/packages/core/src/__tests__/agent-graph-schedule.test.ts
+++ b/packages/core/src/__tests__/agent-graph-schedule.test.ts
@@ -34,6 +34,29 @@ describe('agent graph schedule contract', () => {
     assert.equal(isAgentGraphScheduleUpdateRequest(request), true);
   });
 
+  test('accepts a plugin executor only on newly created graph targets', () => {
+    const request = scheduleRequest();
+    request.addWork[0]!.target = {
+      kind: 'agent',
+      agentId: 'fact-checker',
+      executorId: 'codex.app-server',
+    };
+    assert.equal(isAgentGraphScheduleUpdateRequest(request), true);
+
+    request.addWork[0]!.target = {
+      kind: 'agent',
+      agentId: 'fact-checker',
+      executorId: 'invalid executor',
+    };
+    assert.equal(isAgentGraphScheduleUpdateRequest(request), false);
+    request.addWork[0]!.target = {
+      kind: 'operator',
+      operatorId: 'existing-operator',
+      executorId: 'codex',
+    } as never;
+    assert.equal(isAgentGraphScheduleUpdateRequest(request), false);
+  });
+
   test('rejects ambiguous, duplicate, empty, and add-plus-finish updates', () => {
     assert.equal(
       isAgentGraphScheduleUpdateRequest({
diff --git a/packages/core/src/__tests__/executor-id.test.ts b/packages/core/src/__tests__/executor-id.test.ts
new file mode 100644
index 0000000000..9abbe94066
--- /dev/null
+++ b/packages/core/src/__tests__/executor-id.test.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { isExecutorId } from '../executor-id.js';
+
+test('executor ids use one bounded canonical grammar', () => {
+  assert.equal(isExecutorId('codex.app-server:v1'), true);
+  assert.equal(isExecutorId(`a${'b'.repeat(127)}`), true);
+  for (const value of ['', '1codex', 'codex/app', `a${'b'.repeat(128)}`, null]) {
+    assert.equal(isExecutorId(value), false);
+  }
+});
diff --git a/packages/core/src/__tests__/runtime-invocation-opened.test.ts b/packages/core/src/__tests__/runtime-invocation-opened.test.ts
index 205b345a3d..910b72ed4b 100644
--- a/packages/core/src/__tests__/runtime-invocation-opened.test.ts
+++ b/packages/core/src/__tests__/runtime-invocation-opened.test.ts
@@ -117,6 +117,28 @@ describe('invocation_opened content contract', () => {
     );
   });
 
+  test('binds a plugin executor route to its exact activation generation', () => {
+    const route = {
+      provenance: 'runtime',
+      backendKind: 'plugin-executor',
+      executorId: 'codex.app-server',
+      llmConnectionSlug: 'executor:codex.app-server',
+      modelId: 'codex.app-server',
+      providerStateIdentity: DIGEST,
+    } as const;
+    assert.deepEqual(decodeRuntimeInvocationOpened(opening({ route })).route, route);
+    assert.throws(() =>
+      decodeRuntimeInvocationOpened(
+        opening({ route: { ...route, providerStateIdentity: undefined } as never }),
+      ),
+    );
+    assert.throws(() =>
+      decodeRuntimeInvocationOpened(
+        opening({ route: { ...route, llmConnectionId: 'not-an-executor-route' } as never }),
+      ),
+    );
+  });
+
   test('accepts every root authority the runtime can open, and no mixture of them', () => {
     for (const root of [
       { kind: 'user' },
diff --git a/packages/core/src/__tests__/session-send-projection.test.ts b/packages/core/src/__tests__/session-send-projection.test.ts
index d450ff8d0c..45527b69b5 100644
--- a/packages/core/src/__tests__/session-send-projection.test.ts
+++ b/packages/core/src/__tests__/session-send-projection.test.ts
@@ -62,6 +62,23 @@ describe('projectSessionSendOutcome — exact Connection identity', () => {
     assert.deepEqual(projectSessionSendOutcome(input()), { kind: 'ready' });
   });
 
+  it('does not require a Maka model connection for a plugin executor Session', () => {
+    assert.deepEqual(
+      projectSessionSendOutcome(
+        input({
+          session: {
+            backend: 'plugin-executor',
+            llmConnectionSlug: 'executor:codex',
+            model: 'codex',
+            connectionLocked: false,
+          },
+          connections: [],
+        }),
+      ),
+      { kind: 'ready' },
+    );
+  });
+
   it('blocks a legacy Session until the user explicitly selects an account', () => {
     const current = input();
     assert.deepEqual(
diff --git a/packages/core/src/agent-graph-schedule.ts b/packages/core/src/agent-graph-schedule.ts
index a41849a58f..2e60348949 100644
--- a/packages/core/src/agent-graph-schedule.ts
+++ b/packages/core/src/agent-graph-schedule.ts
@@ -24,6 +24,7 @@ import type {
 } from './agent-graph-control.js';
 import type { AgentGraphTopologyStore } from './agent-graph-topology.js';
 import type { OrchestrationMode } from './orchestration.js';
+import { isExecutorId } from './executor-id.js';
 
 export const AGENT_GRAPH_SCHEDULE_UPDATE_SCHEMA_VERSION = 1 as const;
 
@@ -48,10 +49,12 @@ export type AgentGraphWorkTarget =
   | {
       kind: 'agent';
       agentId: string;
+      executorId?: string;
     }
   | {
       kind: 'preset';
       presetId: string;
+      executorId?: string;
     }
   | {
       kind: 'operator';
@@ -346,16 +349,26 @@ function isSelectedResultInput(value: unknown): value is AgentGraphSelectedResul
 function isWorkTarget(value: unknown): value is AgentGraphWorkTarget {
   if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
   if (
-    isExactRecord(value, ['kind', 'agentId']) &&
+    isExactRecord(value, [
+      'kind',
+      'agentId',
+      ...(hasOwn(value, 'executorId') ? ['executorId'] : []),
+    ]) &&
     value.kind === 'agent' &&
-    isOpaqueIdentity(value.agentId)
+    isOpaqueIdentity(value.agentId) &&
+    (value.executorId === undefined || isExecutorId(value.executorId))
   ) {
     return true;
   }
   if (
-    isExactRecord(value, ['kind', 'presetId']) &&
+    isExactRecord(value, [
+      'kind',
+      'presetId',
+      ...(hasOwn(value, 'executorId') ? ['executorId'] : []),
+    ]) &&
     value.kind === 'preset' &&
-    isOpaqueIdentity(value.presetId)
+    isOpaqueIdentity(value.presetId) &&
+    (value.executorId === undefined || isExecutorId(value.executorId))
   ) {
     return true;
   }
diff --git a/packages/core/src/executor-id.ts b/packages/core/src/executor-id.ts
new file mode 100644
index 0000000000..db59c2ca3a
--- /dev/null
+++ b/packages/core/src/executor-id.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const EXECUTOR_ID_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/u;
+
+export function isExecutorId(value: unknown): value is string {
+  return typeof value === 'string' && EXECUTOR_ID_PATTERN.test(value);
+}
diff --git a/packages/core/src/runtime-event.ts b/packages/core/src/runtime-event.ts
index 635b5257fe..77643ee0c4 100644
--- a/packages/core/src/runtime-event.ts
+++ b/packages/core/src/runtime-event.ts
@@ -70,6 +70,7 @@ import {
   type OrchestrationMode,
 } from './orchestration.js';
 import { isToolMode, type ToolMode } from './tool-mode.js';
+import { isExecutorId } from './executor-id.js';
 import {
   isRuntimeSystemNoteKind,
   type PersistedBackendKind,
@@ -267,13 +268,22 @@ export interface RuntimeEventErrorContent {
 export type RuntimeInvocationRoute =
   | {
       provenance: 'runtime';
-      backendKind: PersistedBackendKind;
+      backendKind: Exclude<PersistedBackendKind, 'plugin-executor'>;
       llmConnectionId: string;
       llmConnectionSlug: string;
       modelId: string;
       /** Frozen provider endpoint and credential ownership; absent on non-provider runs. */
       providerStateIdentity?: `sha256:${string}`;
     }
+  | {
+      provenance: 'runtime';
+      backendKind: 'plugin-executor';
+      executorId: string;
+      llmConnectionSlug: string;
+      modelId: string;
+      /** Frozen plugin package, entry point, and activation generation. */
+      providerStateIdentity: `sha256:${string}`;
+    }
   | {
       provenance: 'unknown';
       backendKind: PersistedBackendKind;
@@ -760,12 +770,25 @@ const INVOCATION_OPENED_CONTENT_SHAPE = defineObjectShape<RuntimeEventInvocation
   ['kind', 'protocol', 'route', 'configuration', 'root', 'source'],
   ['lineage'],
 );
-const INVOCATION_ROUTE_RUNTIME_SHAPE = defineObjectShape<
-  Extract<RuntimeInvocationRoute, { provenance: 'runtime' }>
+const INVOCATION_ROUTE_RUNTIME_MODEL_SHAPE = defineObjectShape<
+  Extract<RuntimeInvocationRoute, { provenance: 'runtime'; llmConnectionId: string }>
 >()(
   ['provenance', 'backendKind', 'llmConnectionId', 'llmConnectionSlug', 'modelId'],
   ['providerStateIdentity'],
 );
+const INVOCATION_ROUTE_RUNTIME_EXECUTOR_SHAPE = defineObjectShape<
+  Extract<RuntimeInvocationRoute, { provenance: 'runtime'; backendKind: 'plugin-executor' }>
+>()(
+  [
+    'provenance',
+    'backendKind',
+    'executorId',
+    'llmConnectionSlug',
+    'modelId',
+    'providerStateIdentity',
+  ],
+  [],
+);
 const INVOCATION_ROUTE_UNKNOWN_SHAPE = defineObjectShape<
   Extract<RuntimeInvocationRoute, { provenance: 'unknown' }>
 >()(['provenance', 'backendKind', 'llmConnectionSlug', 'modelId'], []);
@@ -1157,8 +1180,15 @@ function isRuntimeInvocationRoute(value: unknown): value is RuntimeInvocationRou
     return false;
   }
   if (value.provenance === 'runtime') {
+    if (value.backendKind === 'plugin-executor') {
+      return (
+        hasExactShape(value, INVOCATION_ROUTE_RUNTIME_EXECUTOR_SHAPE) &&
+        isExecutorId(value.executorId) &&
+        isSha256Digest(value.providerStateIdentity)
+      );
+    }
     return (
-      hasExactShape(value, INVOCATION_ROUTE_RUNTIME_SHAPE) &&
+      hasExactShape(value, INVOCATION_ROUTE_RUNTIME_MODEL_SHAPE) &&
       isNonEmptyString(value.llmConnectionId) &&
       (value.providerStateIdentity === undefined || isSha256Digest(value.providerStateIdentity))
     );
@@ -1167,7 +1197,7 @@ function isRuntimeInvocationRoute(value: unknown): value is RuntimeInvocationRou
 }
 
 function isPersistedBackendKind(value: unknown): value is PersistedBackendKind {
-  return value === 'ai-sdk' || value === 'fake';
+  return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
 }
 
 function isRuntimeInvocationConfiguration(value: unknown): value is RuntimeInvocationConfiguration {
diff --git a/packages/core/src/runtime-inputs.ts b/packages/core/src/runtime-inputs.ts
index 42bccd337a..caecc3ae68 100644
--- a/packages/core/src/runtime-inputs.ts
+++ b/packages/core/src/runtime-inputs.ts
@@ -53,14 +53,8 @@ export interface CreateSessionInput {
   projectId?: string | null;
   /** If omitted, runtime auto-derives a placeholder; users may rename later. */
   name?: string;
-  /**
-   * No `backend`: a live build has exactly one, so the field carried no choice
-   * — only the chance of writing the retired `'fake'` into a new row (#3211).
-   * The store stamps every new header instead. Sessions derived from an older
-   * one (branch, revision, subagent) no longer inherit its backend; a copy of a
-   * legacy row is a real session whose connection slug resolves to nothing,
-   * which is what the readiness projection already says about it.
-   */
+  /** Named plugin executor. When present, model fields are retained only as display placeholders. */
+  executorId?: string;
   /** Immutable Connection entity identity. Omitted only while copying legacy state. */
   llmConnectionId?: string;
   llmConnectionSlug: string;
diff --git a/packages/core/src/session-send-projection.ts b/packages/core/src/session-send-projection.ts
index 83648d8270..dfe52ed341 100644
--- a/packages/core/src/session-send-projection.ts
+++ b/packages/core/src/session-send-projection.ts
@@ -27,11 +27,12 @@
  * whether that exact target looks usable for presentation and readiness checks.
  *
  * The compatibility rules are:
- *   1. The session's own connection must pass `isConnectionReady` with
+ *   1. Plugin executor Sessions do not depend on Maka model connections.
+ *   2. The session's own connection must pass `isConnectionReady` with
  *      the sticky session model.
- *   2. Legacy Sessions without an immutable connection id are blocked until
+ *   3. Legacy Sessions without an immutable connection id are blocked until
  *      the user explicitly selects an account.
- *   3. A missing id, slug mismatch, or unusable exact connection is blocked.
+ *   4. A missing id, slug mismatch, or unusable exact connection is blocked.
  *
  * `lastTestStatus` deliberately plays no part here (E4): telemetry about
  * a past credential test must not gate send, so it must not gate the
@@ -46,8 +47,8 @@ export interface SessionSendProjectionSession {
   /**
    * Session backend kind. `string` (not `PersistedBackendKind`) so legacy
    * on-disk values like `'claude'` are surfaced exactly as the JSONL stored
-   * them; only `'fake'` is special-cased, everything else goes through the
-   * normal connection readiness gate.
+   * them; known backends are handled explicitly and unknown legacy values go
+   * through the normal connection readiness gate.
    */
   backend: string;
   llmConnectionId?: string;
@@ -127,6 +128,7 @@ function ownConnectionBlockReason(
   | 'legacy_connection_identity'
   | 'connection_identity_mismatch'
   | undefined {
+  if (session.backend === 'plugin-executor') return undefined;
   if (session.backend === 'fake') return 'fake_backend';
   if (!session.llmConnectionId) return 'legacy_connection_identity';
   const identified =
diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
index 76bcc27a4c..1ced6886bd 100644
--- a/packages/core/src/session.ts
+++ b/packages/core/src/session.ts
@@ -18,6 +18,7 @@
  */
 
 import { isWorkHubActionReceipt, type WorkHubActionReceipt } from './workhub-action-result.js';
+import { isExecutorId } from './executor-id.js';
 
 import {
   MODEL_FAILURE_MESSAGE_MAX_BYTES,
@@ -297,6 +298,8 @@ export interface SessionHeader {
 
   // Backend / model config
   backend: PersistedBackendKind;
+  /** Named black-box executor contributed by a plugin. Present exactly for plugin-executor. */
+  executorId?: string;
   /** Immutable Connection entity identity. Optional only on legacy Session records. */
   llmConnectionId?: string;
   llmConnectionSlug: string;
@@ -344,7 +347,7 @@ export function isWorkHubCoordinationSessionTarget(
  * shipped build may choose it, so it is not a member here. Values read back
  * from durable state use {@link PersistedBackendKind} instead.
  */
-export type BackendKind = 'ai-sdk';
+export type BackendKind = 'ai-sdk' | 'plugin-executor';
 
 /**
  * The backend value a persisted record may carry.
@@ -408,6 +411,7 @@ export interface SessionSummary {
   revisionIndex?: number;
   revisionState?: 'preparing' | 'committed';
   backend: PersistedBackendKind;
+  executorId?: string;
   /** Immutable Connection entity identity. Optional only on legacy summaries. */
   llmConnectionId?: string;
   llmConnectionSlug: string;
@@ -945,6 +949,8 @@ export type WorkHubDelegationWorkspace =
 
 /** User-selected creation defaults; never applied to an existing Work. */
 export interface WorkHubCreateDefaults {
+  /** Named plugin executor for the new Session. Mutually exclusive with model. */
+  readonly executorId?: string;
   readonly model?: {
     readonly llmConnectionId: string;
     readonly llmConnectionSlug: string;
@@ -956,10 +962,14 @@ export interface WorkHubCreateDefaults {
 export function isWorkHubCreateDefaults(value: unknown): value is WorkHubCreateDefaults {
   if (
     !isRecord(value) ||
-    Object.keys(value).some((key) => key !== 'model' && key !== 'permissionMode')
+    Object.keys(value).some(
+      (key) => key !== 'executorId' && key !== 'model' && key !== 'permissionMode',
+    )
   )
     return false;
   if (value.permissionMode !== undefined && !isPermissionMode(value.permissionMode)) return false;
+  if (value.executorId !== undefined && !isExecutorId(value.executorId)) return false;
+  if (value.executorId !== undefined && value.model !== undefined) return false;
   if (value.model === undefined) return true;
   const model = value.model;
   return (
diff --git a/packages/eval/src/harbor-maka-subject.ts b/packages/eval/src/harbor-maka-subject.ts
index fbd82b2b2c..38981900b1 100644
--- a/packages/eval/src/harbor-maka-subject.ts
+++ b/packages/eval/src/harbor-maka-subject.ts
@@ -19,8 +19,7 @@
 
 import { mkdir, writeFile } from 'node:fs/promises';
 import { dirname, join } from 'node:path';
-import { runHostedExecution } from '@maka/runtime-host/client';
-import type { HostedExecutionStartInput } from '@maka/runtime-host/protocol';
+import { runHostedExecution, type RunHostedExecutionInput } from '@maka/runtime-host/client';
 import { captureMakaRuntimeArtifacts, writeMakaArtifactCollectionError } from './maka-artifacts.js';
 import { makaEvalRuntimePolicyDocument } from './maka-runtime-policy.js';
 import { takeRelayResultToken, writeRelayResult } from './relay-result-frame.js';
@@ -32,7 +31,7 @@ const payload = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64url').toStr
   artifactRoot: string;
   baseUrl: string;
   hostSettlementTimeoutMs: number;
-  execution: HostedExecutionStartInput;
+  execution: RunHostedExecutionInput['execution'];
 };
 const abort = new AbortController();
 let artifactCapture = Promise.resolve();
diff --git a/packages/runtime-host/src/__tests__/host-session-availability.test.ts b/packages/runtime-host/src/__tests__/host-session-availability.test.ts
index bed59d36f9..5bf3190cd0 100644
--- a/packages/runtime-host/src/__tests__/host-session-availability.test.ts
+++ b/packages/runtime-host/src/__tests__/host-session-availability.test.ts
@@ -25,8 +25,11 @@ import {
 } from '@maka/core/session';
 import {
   runtimeHostExecutionUnavailableReason,
+  runtimeHostConversationCopyUnavailableReason,
   runtimeHostSafeBoundaryContinuationUnavailableReason,
   LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON,
+  PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON,
+  PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON,
   WORKHUB_COORDINATION_EXECUTION_UNAVAILABLE_REASON,
   WORKHUB_COORDINATION_TARGET_UNAVAILABLE_REASON,
 } from '../server/host-session-availability.js';
@@ -140,6 +143,44 @@ test('legacy Session identity cannot enter Host execution before explicit accoun
   );
 });
 
+test('plugin executor Sessions do not require a Maka model connection identity', () => {
+  assert.equal(
+    runtimeHostExecutionUnavailableReason(
+      {
+        ...base,
+        id: 'plugin-executor-session',
+        role: undefined,
+        llmConnectionId: undefined,
+        backend: 'plugin-executor',
+      },
+      { kind: 'external_message' },
+    ),
+    undefined,
+  );
+});
+
+test('plugin executor Sessions fail closed for safe-boundary continuation', () => {
+  assert.equal(
+    runtimeHostSafeBoundaryContinuationUnavailableReason({
+      ...base,
+      id: 'plugin-executor-session',
+      role: undefined,
+      subagentParent: undefined,
+      llmConnectionId: undefined,
+      backend: 'plugin-executor',
+    }),
+    PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON,
+  );
+});
+
+test('plugin executor Sessions fail closed for branch and revision copies', () => {
+  assert.equal(
+    runtimeHostConversationCopyUnavailableReason({ backend: 'plugin-executor' }),
+    PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON,
+  );
+  assert.equal(runtimeHostConversationCopyUnavailableReason({ backend: 'ai-sdk' }), undefined);
+});
+
 test('legacy Session identity cannot resume a safe-boundary continuation', () => {
   assert.equal(
     runtimeHostSafeBoundaryContinuationUnavailableReason({
diff --git a/packages/runtime-host/src/__tests__/plugin-platform.test.ts b/packages/runtime-host/src/__tests__/plugin-platform.test.ts
index c250197926..b93bcd1a0c 100644
--- a/packages/runtime-host/src/__tests__/plugin-platform.test.ts
+++ b/packages/runtime-host/src/__tests__/plugin-platform.test.ts
@@ -93,6 +93,7 @@ function createPlatform(
     ...(options.tools ? { tools: options.tools } : {}),
     ...(options.systemPrompt ? { systemPrompt: options.systemPrompt } : {}),
     ...(options.commands ? { commands: options.commands } : {}),
+    ...(options.executors ? { executors: options.executors } : {}),
   });
   testPlatformInternals.set(platform, { composition, packages, store });
   return platform;
@@ -696,6 +697,57 @@ test('Plugin Platform query projects scoped Command contributions for clients',
   }
 });
 
+test('Plugin Platform query projects scoped Executor contributions for clients', async () => {
+  const root = await mkdtemp(join(tmpdir(), 'maka-plugin-executor-inspection-'));
+  try {
+    const platform = createPlatform(join(root, 'control'), {
+      executors: {
+        inspect: () => [
+          {
+            entryId: 'executor-entry',
+            scopeId: 'profile',
+            extensionId: 'executor-package',
+            generation: 4,
+            id: 'codex',
+            displayName: 'Codex',
+            capabilities: { thinking: true, toolActivity: false },
+          },
+        ],
+      },
+    });
+    const coordinator = new HostPluginPlatformCoordinator(platform);
+    await platform.recover();
+
+    assert.deepEqual(
+      await coordinator.handlers['plugin.platform.query'](
+        { view: 'executors', rootId: 'profile' },
+        null as never,
+      ),
+      {
+        ok: true,
+        result: {
+          view: 'executors',
+          items: [
+            {
+              entryId: 'executor-entry',
+              scopeId: 'profile',
+              extensionId: 'executor-package',
+              generation: 4,
+              id: 'codex',
+              displayName: 'Codex',
+              capabilities: { thinking: true, toolActivity: false },
+            },
+          ],
+          nextCursor: null,
+        },
+      },
+    );
+    await platform.close();
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});
+
 test('Plugin Platform query pages reserve a cursor even when the complete final result fits', async () => {
   const root = await mkdtemp(join(tmpdir(), 'maka-plugin-query-budget-'));
   try {
diff --git a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
index 4e4dfd44b3..409d09698e 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
@@ -747,6 +747,84 @@ test('creation on a relay connection honours declared levels via the catalog pro
   assert.equal(persistedConnectionId, 'connection-1');
 });
 
+test('plugin executor creation bypasses model resolution and persists the executor route', async () => {
+  let persistedInput: Parameters<CatalogStores['createStableSession']>[0]['input'] | undefined;
+  const externalHeader = (sessionId: string): SessionHeader => {
+    const { llmConnectionId: _connectionId, ...base } = sessionHeader(sessionId, ['user-label']);
+    return {
+      ...base,
+      backend: 'plugin-executor',
+      executorId: 'codex',
+      llmConnectionSlug: 'executor:codex',
+      model: 'codex',
+    };
+  };
+  const fixture = createFixture({
+    connection: {
+      onResolve: () => assert.fail('Plugin executor creation must not resolve a Maka model'),
+    },
+    stores: {
+      createStableSession: async (args) => {
+        persistedInput = args.input;
+        return {
+          kind: 'existing' as const,
+          record: headerSnapshot(externalHeader(args.sessionId), 1),
+        };
+      },
+      readCatalogRecord: async (sessionId) => catalogRecord(externalHeader(sessionId), 1),
+    },
+  });
+
+  const outcome = await fixture.coordinator.handlers['session.create'](
+    {
+      sessionId: fixture.sessionId,
+      workspace: { kind: 'host_path', path: process.cwd() },
+      executorId: 'codex',
+    },
+    context,
+  );
+
+  assert.equal(outcome.ok, true, JSON.stringify(outcome));
+  assert.equal(persistedInput?.executorId, 'codex');
+  assert.equal(persistedInput?.llmConnectionId, undefined);
+  assert.equal(persistedInput?.llmConnectionSlug, 'executor:codex');
+  assert.equal(persistedInput?.model, 'codex');
+  if (outcome.ok && !('kind' in outcome.result)) {
+    assert.equal(outcome.result.backend, 'plugin-executor');
+    assert.equal(outcome.result.executorId, 'codex');
+  }
+});
+
+test('plugin executor creation fails before persistence when the executor is unavailable', async () => {
+  let createAttempts = 0;
+  const fixture = createFixture({
+    assertExecutorAvailable: () => {
+      throw new Error('not installed');
+    },
+    stores: {
+      createStableSession: async () => {
+        createAttempts += 1;
+        throw new Error('must not persist');
+      },
+    },
+  });
+
+  const outcome = await fixture.coordinator.handlers['session.create'](
+    {
+      sessionId: fixture.sessionId,
+      workspace: { kind: 'host_path', path: process.cwd() },
+      executorId: 'missing',
+    },
+    context,
+  );
+
+  assert.deepEqual(outcome, {
+    ok: false,
+    error: { code: 'operation_unavailable', message: 'Plugin executor is unavailable: missing' },
+  });
+  assert.equal(createAttempts, 0);
+});
+
 test('creation admits the enabled bootstrap DeepSeek model before discovery', async () => {
   const modelId = 'deepseek-v4-flash';
   let createAttempts = 0;
@@ -1895,6 +1973,7 @@ function createFixture(
     readonly onProjectChanged?: () => void;
     readonly legacyConnectionIdentity?: boolean;
     readonly header?: Partial<SessionHeader>;
+    readonly assertExecutorAvailable?: (sessionId: string, executorId: string) => void;
   } = {},
 ) {
   const sessionId = 'session-1';
@@ -1984,6 +2063,9 @@ function createFixture(
     requestDrain: () => {
       drains += 1;
     },
+    ...(options.assertExecutorAvailable
+      ? { assertExecutorAvailable: options.assertExecutorAvailable }
+      : {}),
   });
   return {
     coordinator,
diff --git a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
index c70ebb66ce..f1c05b5e1b 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
@@ -269,6 +269,50 @@ describe('Session catalog protocol', () => {
       },
     );
 
+    assert.deepEqual(
+      decodeClientFrame({
+        requestId: 'request-executor',
+        operation: 'session.create',
+        input: {
+          sessionId: 'session-executor',
+          workspace: { kind: 'project', projectId: 'project-1' },
+          executorId: 'codex.app-server',
+        },
+      }),
+      {
+        requestId: 'request-executor',
+        operation: 'session.create',
+        input: {
+          sessionId: 'session-executor',
+          workspace: { kind: 'project', projectId: 'project-1' },
+          executorId: 'codex.app-server',
+        },
+      },
+    );
+
+    for (const input of [
+      {
+        sessionId: 'session-missing-route',
+        workspace: { kind: 'project', projectId: 'project-1' },
+      },
+      {
+        sessionId: 'session-ambiguous-route',
+        workspace: { kind: 'project', projectId: 'project-1' },
+        executorId: 'codex',
+        modelTarget: { kind: 'default' },
+      },
+    ]) {
+      assert.throws(
+        () =>
+          decodeClientFrame({
+            requestId: 'request-invalid-executor',
+            operation: 'session.create',
+            input,
+          }),
+        isProtocolError,
+      );
+    }
+
     assert.deepEqual(
       decodeClientFrame({
         requestId: 'request-3',
diff --git a/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts b/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
index 79863e3b81..eafaffab63 100644
--- a/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
@@ -106,6 +106,30 @@ test('WorkHub model actions cannot supply user authority or attachment locators'
   assert.equal(REMOTE_OWNER_OPERATION_GRANTS.includes('workhub.coordination.actFromTurn'), true);
 });
 
+test('WorkHub new Sessions accept a plugin executor as their creation default', () => {
+  const input = {
+    turnId: 'active-model-turn',
+    actionId: 'tool-call-external',
+    proposal: { disposition: 'create_new', title: 'External audit' },
+    delegationText: 'Inspect the login retries',
+    create: { workspace: { kind: 'project', projectId: 'maka' } },
+    newWorkDefaults: { executorId: 'codex.app-server', permissionMode: 'ask' },
+  };
+  assert.deepEqual(decodeWorkHubCoordinationActFromTurnInput(input), input);
+  for (const newWorkDefaults of [
+    {
+      executorId: 'codex',
+      model: { llmConnectionId: 'conn', llmConnectionSlug: 'test', model: 'model' },
+    },
+    { executorId: 'invalid executor' },
+  ]) {
+    assert.throws(
+      () => decodeWorkHubCoordinationActFromTurnInput({ ...input, newWorkDefaults }),
+      RuntimeHostProtocolError,
+    );
+  }
+});
+
 test('delegation content is optional, bounded, and unavailable to stop or resume', () => {
   const input = {
     actionId: 'delegate-content',
diff --git a/packages/runtime-host/src/client/session-catalog-summary.ts b/packages/runtime-host/src/client/session-catalog-summary.ts
index 10d50521db..49752c1394 100644
--- a/packages/runtime-host/src/client/session-catalog-summary.ts
+++ b/packages/runtime-host/src/client/session-catalog-summary.ts
@@ -60,6 +60,7 @@ export function projectSessionCatalogSummary(
     ...(session.revisionIndex === undefined ? {} : { revisionIndex: session.revisionIndex }),
     ...(session.revisionState === undefined ? {} : { revisionState: session.revisionState }),
     backend: session.backend,
+    ...(session.executorId ? { executorId: session.executorId } : {}),
     ...(session.llmConnectionId === null ? {} : { llmConnectionId: session.llmConnectionId }),
     llmConnectionSlug: session.llmConnectionSlug,
     connectionLocked: session.connectionLocked,
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 3372e7bd30..e2c277cc06 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -101,7 +101,8 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
 export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
 // Increment when the same protocol version no longer guarantees safe Client-Host
 // interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 152 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 153 as const;
+// 153: Sessions may select plugin executors and Plugin Platform queries expose them.
 // 152: Assistant completions and transcript rows preserve interrupted responses.
 // 151: WorkHub selects and delegates through a durable Host Form interaction.
 // 150: Message admission accepts an empty-text Message that carries a quote or
diff --git a/packages/runtime-host/src/protocol/plugin-platform.ts b/packages/runtime-host/src/protocol/plugin-platform.ts
index cf6914ff8b..1215195fd1 100644
--- a/packages/runtime-host/src/protocol/plugin-platform.ts
+++ b/packages/runtime-host/src/protocol/plugin-platform.ts
@@ -28,6 +28,7 @@ import {
 } from '@maka/runtime/plugin-runtime';
 import type { PluginToolInspection } from '@maka/runtime/plugin-tool-service';
 import type { PluginCommandInspection } from '@maka/runtime/plugin-command-service';
+import type { PluginExecutorInspection } from '@maka/runtime/plugin-executor-service';
 import {
   requireCount,
   requireEncodedByteLimit,
@@ -88,7 +89,14 @@ export interface PluginPackageProjection {
 }
 
 export interface PluginPlatformQueryInput {
-  readonly view: 'status' | 'packages' | 'entries' | 'tools' | 'commands' | 'failures';
+  readonly view:
+    | 'status'
+    | 'packages'
+    | 'entries'
+    | 'tools'
+    | 'commands'
+    | 'executors'
+    | 'failures';
   readonly rootId?: MakaPluginRootId;
   readonly cursor?: string;
   readonly limit?: number;
@@ -127,6 +135,11 @@ export type PluginPlatformQueryResult =
       readonly items: readonly PluginCommandInspection[];
       readonly nextCursor: string | null;
     }
+  | {
+      readonly view: 'executors';
+      readonly items: readonly PluginExecutorInspection[];
+      readonly nextCursor: string | null;
+    }
   | {
       readonly view: 'failures';
       readonly items: readonly PluginPlatformFailureProjection[];
@@ -268,7 +281,7 @@ function decodePluginPlatformQueryInput(value: unknown): PluginPlatformQueryInpu
     ['rootId', 'cursor', 'limit'],
   );
   if (
-    !['status', 'packages', 'entries', 'tools', 'commands', 'failures'].includes(
+    !['status', 'packages', 'entries', 'tools', 'commands', 'executors', 'failures'].includes(
       input.view as string,
     )
   ) {
@@ -292,9 +305,15 @@ function decodePluginPlatformQueryInput(value: unknown): PluginPlatformQueryInpu
   ) {
     throw invalidProtocolFrame('Plugin Platform status query does not accept paging');
   }
-  if (input.rootId !== undefined && view !== 'entries' && view !== 'tools' && view !== 'commands') {
+  if (
+    input.rootId !== undefined &&
+    view !== 'entries' &&
+    view !== 'tools' &&
+    view !== 'commands' &&
+    view !== 'executors'
+  ) {
     throw invalidProtocolFrame(
-      'Plugin root identity is only valid for Entry, Tool, and Command queries',
+      'Plugin root identity is only valid for Entry, Tool, Command, and Executor queries',
     );
   }
   let rootId: MakaPluginRootId | undefined;
@@ -367,7 +386,9 @@ function decodePluginPlatformQueryResult(value: unknown): PluginPlatformQueryRes
     if (
       !Array.isArray(output.items) ||
       output.items.length > 64 ||
-      !['packages', 'entries', 'tools', 'commands', 'failures'].includes(view as string)
+      !['packages', 'entries', 'tools', 'commands', 'executors', 'failures'].includes(
+        view as string,
+      )
     ) {
       throw invalidProtocolFrame('Invalid Plugin Platform page');
     }
@@ -384,7 +405,9 @@ function decodePluginPlatformQueryResult(value: unknown): PluginPlatformQueryRes
             ? { view, items: output.items.map(decodeToolInspection), nextCursor }
             : view === 'commands'
               ? { view, items: output.items.map(decodeCommandInspection), nextCursor }
-              : { view: 'failures', items: output.items.map(decodePlatformFailure), nextCursor };
+              : view === 'executors'
+                ? { view, items: output.items.map(decodeExecutorInspection), nextCursor }
+                : { view: 'failures', items: output.items.map(decodePlatformFailure), nextCursor };
   }
   requireEncodedByteLimit(
     decoded,
@@ -418,6 +441,40 @@ function decodeCommandInspection(value: unknown): PluginCommandInspection {
   };
 }
 
+function decodeExecutorInspection(value: unknown): PluginExecutorInspection {
+  const item = requireExactRecord(value, 'Plugin Executor inspection', [
+    'entryId',
+    'scopeId',
+    'extensionId',
+    'generation',
+    'id',
+    'displayName',
+    'capabilities',
+  ]);
+  const capabilities = requireExactRecord(item.capabilities, 'Plugin Executor capabilities', [
+    'thinking',
+    'toolActivity',
+  ]);
+  if (
+    typeof capabilities.thinking !== 'boolean' ||
+    typeof capabilities.toolActivity !== 'boolean'
+  ) {
+    throw invalidProtocolFrame('Invalid Plugin Executor capabilities');
+  }
+  return {
+    entryId: requireId(item.entryId, 'Plugin Entry identity'),
+    scopeId: requireString(item.scopeId, 'Plugin scope identity', 256),
+    extensionId: requireId(item.extensionId, 'Plugin package identity'),
+    generation: requireCount(item.generation, 'Plugin generation'),
+    id: requireString(item.id, 'Plugin Executor id', 128),
+    displayName: requireString(item.displayName, 'Plugin Executor display name', 256),
+    capabilities: {
+      thinking: capabilities.thinking,
+      toolActivity: capabilities.toolActivity,
+    },
+  };
+}
+
 function decodePlatformFailure(value: unknown): PluginPlatformFailureProjection {
   const failure = requireShapedRecord(
     value,
diff --git a/packages/runtime-host/src/protocol/session-catalog.ts b/packages/runtime-host/src/protocol/session-catalog.ts
index 0820a6b693..eb65be7e54 100644
--- a/packages/runtime-host/src/protocol/session-catalog.ts
+++ b/packages/runtime-host/src/protocol/session-catalog.ts
@@ -31,6 +31,7 @@ import {
   type SessionToolProfile,
 } from '@maka/core/session';
 import { isThinkingLevel, type ThinkingLevel } from '@maka/core/model-thinking';
+import { isExecutorId } from '@maka/core/executor-id';
 import type { ExecutionBoundarySummary } from '@maka/core/sandbox-boundary';
 export type { ExecutionBoundarySummary } from '@maka/core/sandbox-boundary';
 import {
@@ -123,6 +124,7 @@ const PROJECTION_FIELDS = [
   'revisionOfTurnId',
   'revisionIndex',
   'revisionState',
+  'executorId',
   'thinkingLevel',
   'lastReadMessageId',
   'liveRunState',
@@ -154,7 +156,10 @@ export interface SessionCreateInput {
   readonly mode?: SessionStartMode;
   readonly name?: string;
   readonly labels?: readonly string[];
-  readonly modelTarget: SessionModelTarget;
+  /** Required for native execution and omitted for a plugin executor. */
+  readonly modelTarget?: SessionModelTarget;
+  /** Named black-box executor contributed by a Host plugin. */
+  readonly executorId?: string;
   readonly thinkingLevel?: ThinkingLevel;
   readonly toolProfile?: SessionToolProfile;
   readonly permissionMode?: PermissionMode;
@@ -236,6 +241,7 @@ export interface SessionCatalogProjection {
   readonly revisionIndex?: number;
   readonly revisionState?: 'preparing' | 'committed';
   readonly backend: PersistedBackendKind;
+  readonly executorId?: string;
   readonly llmConnectionId: string | null;
   readonly llmConnectionSlug: string;
   readonly connectionLocked: boolean;
@@ -513,11 +519,13 @@ export function decodeSessionCreateInput(value: unknown): SessionCreateInput {
   const input = requireShapedRecord(
     value,
     'Session create input',
-    ['sessionId', 'workspace', 'modelTarget'],
+    ['sessionId', 'workspace'],
     [
       'mode',
       'name',
       'labels',
+      'modelTarget',
+      'executorId',
       'thinkingLevel',
       'toolProfile',
       'permissionMode',
@@ -525,13 +533,21 @@ export function decodeSessionCreateInput(value: unknown): SessionCreateInput {
       'orchestrationMode',
     ],
   );
+  const executorId = Object.hasOwn(input, 'executorId')
+    ? executorIdValue(input.executorId)
+    : undefined;
+  const target = Object.hasOwn(input, 'modelTarget') ? modelTarget(input.modelTarget) : undefined;
+  if ((executorId === undefined) === (target === undefined)) {
+    throw invalidProtocolFrame('Session creation requires exactly one model target or executor id');
+  }
   return {
     sessionId: requireEntityId(input.sessionId, 'sessionId'),
     workspace: decodeWorkspaceTarget(input.workspace),
     ...(Object.hasOwn(input, 'mode') ? { mode: sessionStartMode(input.mode) } : {}),
     ...(Object.hasOwn(input, 'name') ? { name: sessionName(input.name) } : {}),
     ...(Object.hasOwn(input, 'labels') ? { labels: labels(input.labels) } : {}),
-    modelTarget: modelTarget(input.modelTarget),
+    ...(target ? { modelTarget: target } : {}),
+    ...(executorId ? { executorId } : {}),
     ...(Object.hasOwn(input, 'thinkingLevel')
       ? { thinkingLevel: thinkingLevel(input.thinkingLevel) }
       : {}),
@@ -760,6 +776,7 @@ export function decodeSessionCatalogProjection(value: unknown): SessionCatalogPr
     ...optionalRevisionIndex(record),
     ...optionalRevisionState(record),
     backend: backend(record.backend),
+    ...optionalExecutorId(record),
     llmConnectionId:
       record.llmConnectionId === null
         ? null
@@ -776,6 +793,9 @@ export function decodeSessionCatalogProjection(value: unknown): SessionCatalogPr
     collaborationMode: collaborationMode(record.collaborationMode),
     orchestrationMode: orchestrationMode(record.orchestrationMode),
   };
+  if ((projection.backend === 'plugin-executor') !== (projection.executorId !== undefined)) {
+    throw invalidProtocolFrame('Session executor identity does not match its backend');
+  }
   requireEncodedByteLimit(
     projection,
     'Session catalog projection',
@@ -997,12 +1017,27 @@ function optionalThinkingLevel(
 // header's durable backend, and rows written by builds that shipped
 // FakeBackend still hold it (#3211).
 function backend(value: unknown): SessionCatalogProjection['backend'] {
-  if (value !== 'ai-sdk' && value !== 'fake') {
+  if (value !== 'ai-sdk' && value !== 'plugin-executor' && value !== 'fake') {
     throw invalidProtocolFrame('Invalid Session backend');
   }
   return value;
 }
 
+function executorIdValue(value: unknown): string {
+  const id = requireUtf8String(value, 'Executor id', 128);
+  if (!isExecutorId(id)) {
+    throw invalidProtocolFrame('Invalid Executor id');
+  }
+  return id;
+}
+
+function optionalExecutorId(
+  record: Record<string, unknown>,
+): Pick<SessionCatalogProjection, 'executorId'> | Record<string, never> {
+  if (record.executorId === undefined) return {};
+  return { executorId: executorIdValue(record.executorId) };
+}
+
 function thinkingLevel(value: unknown): ThinkingLevel {
   if (!isThinkingLevel(value)) throw invalidProtocolFrame('Invalid Session thinking level');
   return value;
diff --git a/packages/runtime-host/src/server/child-agent-composition.ts b/packages/runtime-host/src/server/child-agent-composition.ts
index 461da00d2d..793b65a084 100644
--- a/packages/runtime-host/src/server/child-agent-composition.ts
+++ b/packages/runtime-host/src/server/child-agent-composition.ts
@@ -77,6 +77,7 @@ export function bindHostChildAgentBackend(
         },
         agentProfile: input.agentProfile,
         ...(input.subagentId ? { subagentId: input.subagentId } : {}),
+        ...(input.executorId ? { executorId: input.executorId } : {}),
         prompt: input.prompt,
         ...(input.swarm ? { swarm: input.swarm } : {}),
         abortSignal: input.abortSignal,
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index bba8259bbf..265f1e69d3 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -91,6 +91,8 @@ import { PluginAttachmentService } from '@maka/runtime/plugin-attachment-service
 import { PluginApprovalService } from '@maka/runtime/plugin-approval-service';
 import { PluginFilesystemService } from '@maka/runtime/plugin-fs-service';
 import { PluginLlmService } from '@maka/runtime/plugin-llm-service';
+import { PluginExecutorBackend } from '@maka/runtime/plugin-executor-backend';
+import { PluginExecutorService } from '@maka/runtime/plugin-executor-service';
 import { PluginShellService } from '@maka/runtime/plugin-shell-service';
 import { PluginUserQuestionService } from '@maka/runtime/plugin-user-question-service';
 import { PluginWebService } from '@maka/runtime/plugin-web-service';
@@ -336,6 +338,7 @@ export async function createExecutionRuntimeHostComposition(
   let goalExecutions: HostGoalExecutionCoordinator | undefined;
   let pluginPlatform: HostPluginPlatform | undefined;
   let manager: SessionManager | undefined;
+  let invalidatePluginExecutorBackends: () => void = () => undefined;
   let modelMetadataRefresh: ReturnType<typeof startHostModelMetadataRefresh> | undefined;
   let archiveEvidence: Awaited<ReturnType<typeof openToolResultArchiveEvidenceReader>> | undefined;
   try {
@@ -346,6 +349,11 @@ export async function createExecutionRuntimeHostComposition(
     new PluginUserQuestionService(pluginRoot, pluginAgents);
     const pluginFilesystem = new PluginFilesystemService(pluginRoot, pluginAgents);
     const pluginLlm = new PluginLlmService(pluginRoot, pluginAgents);
+    const pluginExecutors = new PluginExecutorService(pluginRoot, {
+      // Retirement aborts its generation before backend disposal begins, so the
+      // terminal event preserves executor-retired instead of looking like Stop.
+      onChanged: () => queueMicrotask(invalidatePluginExecutorBackends),
+    });
     const pluginShellEnv = new PluginShellEnvService(pluginRoot);
     const pluginShell = new PluginShellService(pluginRoot, pluginAgents, pluginShellEnv);
     const pluginWeb = new PluginWebService(pluginRoot, pluginAgents);
@@ -369,6 +377,7 @@ export async function createExecutionRuntimeHostComposition(
       tools: pluginTools,
       systemPrompt: pluginSystemPrompt,
       commands: pluginCommands,
+      executors: pluginExecutors,
     });
     const pluginPlatformCoordinator = new HostPluginPlatformCoordinator(pluginPlatform);
     const openedProjectCatalog = storage.projectCatalog;
@@ -1055,6 +1064,23 @@ export async function createExecutionRuntimeHostComposition(
         prepare: (backendContext) => prepareHostAiSdkBackend(hostAiSdkBackendInput(backendContext)),
       },
     );
+    backends.register('plugin-executor', {
+      prepare: async (backendContext) => {
+        const executorId = backendContext.header.executorId;
+        if (!executorId) throw new Error('Plugin executor Session is missing its executor id');
+        const binding = pluginExecutors.bind(backendContext.sessionId, executorId);
+        return {
+          providerStateIdentity: binding.providerStateIdentity,
+          build: (factoryContext) =>
+            new PluginExecutorBackend({
+              sessionId: factoryContext.sessionId,
+              cwd: factoryContext.header.cwd,
+              ...(factoryContext.systemPrompt ? { instructions: factoryContext.systemPrompt } : {}),
+              binding,
+            }),
+        };
+      },
+    });
     const runtimeAuthority: RuntimeHostedRootAuthority = {
       bindRun: (identity) => messages.bindRun(identity),
       executeRoot: (input) =>
@@ -1108,6 +1134,7 @@ export async function createExecutionRuntimeHostComposition(
     };
     resolveAvailableToolNames = async (sessionId: string): Promise<string[]> => {
       const header = await stores.sessionStore.readHeaderSnapshot(sessionId);
+      if (header.backend === 'plugin-executor') return [];
       if (header.subagentRuntime) {
         if (!header.subagentParent) {
           throw new Error('Subagent runtime snapshot requires a linked child session');
@@ -1300,6 +1327,14 @@ export async function createExecutionRuntimeHostComposition(
       toolBoundaryProtocol: stores.runtimeEventStore.toolBoundaryProtocol,
       backends,
       subagentCatalog,
+      assertChildExecutorAvailable: (parentSessionId, executorId) => {
+        const identity = pluginExecutors.identity(parentSessionId, executorId);
+        if (identity.scopeId !== 'profile') {
+          throw new Error(
+            `Session-scoped executor cannot be inherited by a child Session: ${executorId}`,
+          );
+        }
+      },
       newId: randomUUID,
       now: Date.now,
       safeBoundaryResumeEnabled: process.env.MAKA_RUNTIME_SAFE_BOUNDARY_RESUME === '1',
@@ -1401,6 +1436,7 @@ export async function createExecutionRuntimeHostComposition(
     const registerBackendInvalidation = (): void => {
       observeBackendInvalidation(requireSessionManager(manager).refreshIdleBackends());
     };
+    invalidatePluginExecutorBackends = registerBackendInvalidation;
     const registerConfigurationMutation = (): void => {
       hostChanges.publishConfiguration();
       registerBackendInvalidation();
@@ -1684,6 +1720,7 @@ export async function createExecutionRuntimeHostComposition(
         return new Promise((resolve, reject) => {
           void spawn({
             agentProfile: options.agentProfile ?? 'implementation',
+            ...(options.executorId ? { executorId: options.executorId } : {}),
             prompt: options.prompt!,
             ...(options.signal ? { abortSignal: options.signal } : {}),
             onReady: (ready) =>
@@ -1940,6 +1977,9 @@ export async function createExecutionRuntimeHostComposition(
       continuity: continuityCoordinator,
       workspaceResolver,
       requestDrain: context.requestDrain,
+      assertExecutorAvailable: (sessionId, executorId) => {
+        pluginExecutors.identity(sessionId, executorId);
+      },
       ...(context.sessionAccessAuthority
         ? { sessionAccessAuthority: context.sessionAccessAuthority }
         : {}),
@@ -2135,14 +2175,18 @@ export async function createExecutionRuntimeHostComposition(
                   sessionId: input.targetSessionId,
                   workspace: input.create.workspace,
                   name: input.create.title,
-                  modelTarget: input.create.defaults?.model
-                    ? {
-                        kind: 'explicit',
-                        connectionId: input.create.defaults.model.llmConnectionId,
-                        connectionSlug: input.create.defaults.model.llmConnectionSlug,
-                        model: input.create.defaults.model.model,
-                      }
-                    : { kind: 'default' },
+                  ...(input.create.defaults?.executorId
+                    ? { executorId: input.create.defaults.executorId }
+                    : {
+                        modelTarget: input.create.defaults?.model
+                          ? {
+                              kind: 'explicit' as const,
+                              connectionId: input.create.defaults.model.llmConnectionId,
+                              connectionSlug: input.create.defaults.model.llmConnectionSlug,
+                              model: input.create.defaults.model.model,
+                            }
+                          : ({ kind: 'default' } as const),
+                      }),
                   ...(input.create.defaults?.permissionMode
                     ? { permissionMode: input.create.defaults.permissionMode }
                     : {}),
diff --git a/packages/runtime-host/src/server/host-session-availability.ts b/packages/runtime-host/src/server/host-session-availability.ts
index 2cff50c8f0..5874aea198 100644
--- a/packages/runtime-host/src/server/host-session-availability.ts
+++ b/packages/runtime-host/src/server/host-session-availability.ts
@@ -37,6 +37,16 @@ export const WORKHUB_COORDINATION_TARGET_UNAVAILABLE_REASON =
   'WorkHub Coordination execution requires the reserved Coordination Session';
 export const LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON =
   'This Session requires an explicit account selection before it can run.';
+export const PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON =
+  'Plugin executor Sessions cannot resume without provider-owned durable continuation support.';
+export const PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON =
+  'Plugin executor Sessions cannot be copied without provider-owned conversation cloning';
+
+export function runtimeHostConversationCopyUnavailableReason(
+  header: Pick<SessionHeader, 'backend'>,
+): string | undefined {
+  return header.backend === 'plugin-executor' ? PLUGIN_EXECUTOR_COPY_UNAVAILABLE_REASON : undefined;
+}
 
 export function runtimeHostExternalTurnUnavailableReason(
   header: Pick<
@@ -64,9 +74,12 @@ export function runtimeHostSafeBoundaryContinuationUnavailableReason(
       ? WORKHUB_COORDINATION_EXECUTION_UNAVAILABLE_REASON
       : undefined) ??
     (header.transcriptLedgerVersion === 0 ? IMPORT_STAGING_UNAVAILABLE_REASON : undefined) ??
-    (header.llmConnectionId === undefined && header.backend !== 'fake'
+    (header.llmConnectionId === undefined && header.backend === 'ai-sdk'
       ? LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON
       : undefined) ??
+    (header.backend === 'plugin-executor'
+      ? PLUGIN_EXECUTOR_CONTINUATION_UNAVAILABLE_REASON
+      : undefined) ??
     (header.subagentParent ? CHILD_CONTINUATION_UNAVAILABLE_REASON : undefined)
   );
 }
@@ -111,7 +124,7 @@ export function runtimeHostExecutionUnavailableReason(
       ? WORKHUB_COORDINATION_EXECUTION_UNAVAILABLE_REASON
       : undefined) ??
     (header.transcriptLedgerVersion === 0 ? IMPORT_STAGING_UNAVAILABLE_REASON : undefined) ??
-    (header.llmConnectionId === undefined && header.backend !== 'fake'
+    (header.llmConnectionId === undefined && header.backend === 'ai-sdk'
       ? LEGACY_CONNECTION_IDENTITY_EXECUTION_UNAVAILABLE_REASON
       : undefined) ??
     (header.collaborationMode === 'plan' &&
diff --git a/packages/runtime-host/src/server/plugin-platform-coordinator.ts b/packages/runtime-host/src/server/plugin-platform-coordinator.ts
index 889fd266e0..3ad2b6528d 100644
--- a/packages/runtime-host/src/server/plugin-platform-coordinator.ts
+++ b/packages/runtime-host/src/server/plugin-platform-coordinator.ts
@@ -27,6 +27,7 @@ import {
 } from '@maka/runtime/plugin-runtime';
 import type { PluginToolInspection } from '@maka/runtime/plugin-tool-service';
 import type { PluginCommandInspection } from '@maka/runtime/plugin-command-service';
+import type { PluginExecutorInspection } from '@maka/runtime/plugin-executor-service';
 import type {
   OperationOutcome,
   PluginPackageExportInput,
@@ -90,6 +91,12 @@ export class HostPluginPlatformCoordinator {
             result: boundedPage('commands', this.platform.inspectCommands(input.rootId), input),
           };
         }
+        if (input.view === 'executors') {
+          return {
+            ok: true,
+            result: boundedPage('executors', this.platform.inspectExecutors(input.rootId), input),
+          };
+        }
         if (input.view === 'failures') {
           return { ok: true, result: boundedPage('failures', failures, input) };
         }
@@ -198,13 +205,18 @@ function boundedPage(
   values: readonly PluginCommandInspection[],
   input: PluginPlatformQueryInput,
 ): Extract<PluginPlatformQueryResult, { readonly view: 'commands' }>;
+function boundedPage(
+  view: 'executors',
+  values: readonly PluginExecutorInspection[],
+  input: PluginPlatformQueryInput,
+): Extract<PluginPlatformQueryResult, { readonly view: 'executors' }>;
 function boundedPage(
   view: 'failures',
   values: readonly PluginPlatformFailureProjection[],
   input: PluginPlatformQueryInput,
 ): Extract<PluginPlatformQueryResult, { readonly view: 'failures' }>;
 function boundedPage<T>(
-  view: 'packages' | 'entries' | 'tools' | 'commands' | 'failures',
+  view: 'packages' | 'entries' | 'tools' | 'commands' | 'executors' | 'failures',
   values: readonly T[],
   input: PluginPlatformQueryInput,
 ): PluginPlatformQueryResult {
@@ -240,7 +252,7 @@ function boundedPage<T>(
 
 interface PageCursor {
   readonly version: 1;
-  readonly view: 'packages' | 'entries' | 'tools' | 'commands' | 'failures';
+  readonly view: 'packages' | 'entries' | 'tools' | 'commands' | 'executors' | 'failures';
   readonly rootId?: string;
   readonly digest: string;
   readonly offset: number;
diff --git a/packages/runtime-host/src/server/plugin-platform.ts b/packages/runtime-host/src/server/plugin-platform.ts
index 98c4a6309c..cdf59765e3 100644
--- a/packages/runtime-host/src/server/plugin-platform.ts
+++ b/packages/runtime-host/src/server/plugin-platform.ts
@@ -36,6 +36,7 @@ import type { ExtensionPackageManifest } from './extension-package-manifest.js';
 import type { PluginToolInspection } from '@maka/runtime/plugin-tool-service';
 import type { PluginSystemPromptInspection } from '@maka/runtime/plugin-system-prompt-service';
 import type { PluginCommandInspection } from '@maka/runtime/plugin-command-service';
+import type { PluginExecutorInspection } from '@maka/runtime/plugin-executor-service';
 import { validateExtensionConfiguration } from './extension-package-manifest.js';
 import { recoverExtensionBundleImports } from './extension-bundle.js';
 import { loadPluginCompositionPatch } from './plugin-composition-patch.js';
@@ -82,6 +83,9 @@ export interface HostPluginPlatformOptions {
     inspect(rootId?: MakaPluginRootId): readonly PluginSystemPromptInspection[];
   };
   readonly commands?: { inspect(rootId?: MakaPluginRootId): readonly PluginCommandInspection[] };
+  readonly executors?: {
+    inspect(rootId?: MakaPluginRootId): readonly PluginExecutorInspection[];
+  };
 }
 
 export interface HostPluginPlatformFailure {
@@ -120,6 +124,7 @@ export class HostPluginPlatform {
   readonly #tools?: HostPluginPlatformOptions['tools'];
   readonly #systemPrompt?: HostPluginPlatformOptions['systemPrompt'];
   readonly #commands?: HostPluginPlatformOptions['commands'];
+  readonly #executors?: HostPluginPlatformOptions['executors'];
 
   #authority: PersistedPluginComposition = emptyCompositionAuthority();
   #desired: MakaCompositionState = emptyCompositionState();
@@ -147,6 +152,7 @@ export class HostPluginPlatform {
     this.#tools = options.tools;
     this.#systemPrompt = options.systemPrompt;
     this.#commands = options.commands;
+    this.#executors = options.executors;
   }
 
   async recover(): Promise<void> {
@@ -499,6 +505,11 @@ export class HostPluginPlatform {
     return this.#commands?.inspect(rootId) ?? Object.freeze([]);
   }
 
+  inspectExecutors(rootId?: MakaPluginRootId): readonly PluginExecutorInspection[] {
+    this.#assertReadable();
+    return this.#executors?.inspect(rootId) ?? Object.freeze([]);
+  }
+
   async status(): Promise<{
     readonly phase: PluginPlatformPhase;
     readonly authorityEpoch: number;
diff --git a/packages/runtime-host/src/server/session-catalog-coordinator.ts b/packages/runtime-host/src/server/session-catalog-coordinator.ts
index face1032bd..a9a62b22c1 100644
--- a/packages/runtime-host/src/server/session-catalog-coordinator.ts
+++ b/packages/runtime-host/src/server/session-catalog-coordinator.ts
@@ -30,6 +30,7 @@ import {
   type ExecutionBoundarySummary,
 } from '@maka/core/sandbox-boundary';
 import type { CreateSessionInput } from '@maka/core/runtime-inputs';
+import { isExecutorId } from '@maka/core/executor-id';
 import type { ToolMode } from '@maka/core/tool-mode';
 import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/core/runtime-policy';
 import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/session-name';
@@ -138,8 +139,9 @@ type SessionConfigurationAuthority = Pick<
 type SessionContinuity = Pick<SessionContinuityCoordinator, 'refreshCanonical'>;
 
 interface ResolvedSessionConfiguration {
-  readonly backend: 'ai-sdk';
-  readonly llmConnectionId?: string;
+  readonly backend: 'ai-sdk' | 'plugin-executor';
+  readonly executorId: string | undefined;
+  readonly llmConnectionId: string | undefined;
   readonly llmConnectionSlug: string;
   readonly model: string;
   readonly thinkingLevel: SessionHeader['thinkingLevel'];
@@ -188,6 +190,7 @@ export interface HostSessionCatalogCoordinatorOptions {
   readonly continuity: SessionContinuity;
   readonly workspaceResolver: HostWorkspaceResolver;
   readonly requestDrain: () => void;
+  readonly assertExecutorAvailable?: (sessionId: string, executorId: string) => void;
   readonly sessionAccessAuthority?: Pick<
     RuntimeHostAccessAuthority,
     'activeSessionGrantForPrincipal'
@@ -299,6 +302,7 @@ export class HostSessionCatalogCoordinator {
   readonly #continuity: SessionContinuity;
   readonly #workspaceResolver: HostWorkspaceResolver;
   readonly #requestDrain: () => void;
+  readonly #assertExecutorAvailable: ((sessionId: string, executorId: string) => void) | undefined;
   readonly #sessionAccessAuthority:
     | Pick<RuntimeHostAccessAuthority, 'activeSessionGrantForPrincipal'>
     | undefined;
@@ -312,6 +316,7 @@ export class HostSessionCatalogCoordinator {
     this.#continuity = options.continuity;
     this.#workspaceResolver = options.workspaceResolver;
     this.#requestDrain = options.requestDrain;
+    this.#assertExecutorAvailable = options.assertExecutorAvailable;
     this.#sessionAccessAuthority = options.sessionAccessAuthority;
   }
 
@@ -368,7 +373,7 @@ export class HostSessionCatalogCoordinator {
     const prepared = await prepareCreate(input);
     return this.#workspaceResolver.runWithUsageRecorded(input.workspace, async (workspace) => {
       const [model, policy] = await Promise.all([
-        this.#resolveModel(input.modelTarget, input.thinkingLevel),
+        this.#resolveCreateExecution(input),
         this.#readRuntimePolicy(),
       ]);
       return {
@@ -379,7 +384,8 @@ export class HostSessionCatalogCoordinator {
           ...(workspace.projectId === null ? {} : { projectId: workspace.projectId }),
           name: prepared.name,
           labels: [...prepared.labels],
-          llmConnectionId: model.connectionId,
+          ...(model.executorId ? { executorId: model.executorId } : {}),
+          ...(model.connectionId ? { llmConnectionId: model.connectionId } : {}),
           llmConnectionSlug: model.connectionSlug,
           model: model.model,
           ...(input.thinkingLevel === undefined ? {} : { thinkingLevel: input.thinkingLevel }),
@@ -607,7 +613,7 @@ export class HostSessionCatalogCoordinator {
           input.workspace,
           async (workspace) => {
             const [model, policy] = await Promise.all([
-              this.#resolveModel(input.modelTarget, input.thinkingLevel),
+              this.#resolveCreateExecution(input),
               this.#readRuntimePolicy(),
             ]);
             const createInput: CreateSessionInput = {
@@ -615,7 +621,8 @@ export class HostSessionCatalogCoordinator {
               ...(workspace.projectId === null ? {} : { projectId: workspace.projectId }),
               name: prepared.name,
               labels: [...prepared.labels],
-              llmConnectionId: model.connectionId,
+              ...(model.executorId ? { executorId: model.executorId } : {}),
+              ...(model.connectionId ? { llmConnectionId: model.connectionId } : {}),
               llmConnectionSlug: model.connectionSlug,
               model: model.model,
               ...(input.thinkingLevel === undefined ? {} : { thinkingLevel: input.thinkingLevel }),
@@ -1211,7 +1218,17 @@ export class HostSessionCatalogCoordinator {
     current: SessionHeader,
     patch: SessionConfigurationUpdateInput['patch'],
   ): Promise<ResolvedSessionConfiguration> {
-    if (current.llmConnectionId === undefined && patch.modelTarget === undefined) {
+    if (current.backend === 'fake' && patch.modelTarget === undefined) {
+      throw new SessionOperationFailure(
+        'operation_conflict',
+        'Legacy test backend configuration requires an explicit account selection',
+      );
+    }
+    if (
+      current.backend !== 'plugin-executor' &&
+      current.llmConnectionId === undefined &&
+      patch.modelTarget === undefined
+    ) {
       throw new SessionOperationFailure(
         'operation_conflict',
         'Legacy Session configuration requires an explicit account selection',
@@ -1245,8 +1262,9 @@ export class HostSessionCatalogCoordinator {
       );
     }
     return {
-      backend: 'ai-sdk',
-      ...(model.connectionId === undefined ? {} : { llmConnectionId: model.connectionId }),
+      backend: patch.modelTarget || current.backend === 'ai-sdk' ? 'ai-sdk' : 'plugin-executor',
+      executorId: patch.modelTarget ? undefined : current.executorId,
+      llmConnectionId: model.connectionId,
       llmConnectionSlug: model.connectionSlug,
       model: model.model,
       thinkingLevel,
@@ -1257,6 +1275,36 @@ export class HostSessionCatalogCoordinator {
     };
   }
 
+  async #resolveCreateExecution(input: SessionCreateInput): Promise<{
+    readonly executorId?: string;
+    readonly connectionId?: string;
+    readonly connectionSlug: string;
+    readonly model: string;
+  }> {
+    if (input.executorId) {
+      try {
+        this.#assertExecutorAvailable?.(input.sessionId, input.executorId);
+      } catch {
+        throw new SessionOperationFailure(
+          'operation_unavailable',
+          `Plugin executor is unavailable: ${input.executorId}`,
+        );
+      }
+      return {
+        executorId: input.executorId,
+        connectionSlug: `executor:${input.executorId}`,
+        model: input.executorId,
+      };
+    }
+    if (!input.modelTarget) {
+      throw new SessionOperationFailure(
+        'invalid_request',
+        'Session creation requires a model target or executor id',
+      );
+    }
+    return await this.#resolveModel(input.modelTarget, input.thinkingLevel);
+  }
+
   async #readRuntimePolicy(): Promise<
     Awaited<ReturnType<SessionRuntimePolicyStores['runtimePolicy']['getSnapshot']>>
   > {
@@ -1273,7 +1321,8 @@ function sessionConfigurationMatches(
   configuration: ResolvedSessionConfiguration,
 ): boolean {
   return (
-    header.backend === 'ai-sdk' &&
+    header.backend === configuration.backend &&
+    header.executorId === configuration.executorId &&
     header.llmConnectionId === configuration.llmConnectionId &&
     header.llmConnectionSlug === configuration.llmConnectionSlug &&
     header.model === configuration.model &&
@@ -1302,6 +1351,15 @@ interface PreparedSessionCreate {
 }
 
 async function prepareCreate(input: SessionCreateInput): Promise<PreparedSessionCreate> {
+  if ((input.executorId === undefined) === (input.modelTarget === undefined)) {
+    throw new SessionOperationFailure(
+      'invalid_request',
+      'Session creation requires exactly one model target or executor id',
+    );
+  }
+  if (input.executorId !== undefined && !isExecutorId(input.executorId)) {
+    throw new SessionOperationFailure('invalid_request', 'Session executor id is invalid');
+  }
   if (input.labels?.some(isExecutionSemanticLabel)) {
     throw new SessionOperationFailure(
       'invalid_request',
@@ -1337,14 +1395,16 @@ function createRequestFingerprint(
       : ['host_path', input.workspace.path],
     prepared.name,
     prepared.labels,
-    input.modelTarget.kind === 'default'
-      ? ['default']
-      : [
-          'explicit',
-          input.modelTarget.connectionId,
-          input.modelTarget.connectionSlug,
-          input.modelTarget.model,
-        ],
+    input.executorId
+      ? ['executor', input.executorId]
+      : input.modelTarget?.kind === 'default'
+        ? ['default']
+        : [
+            'explicit',
+            input.modelTarget!.connectionId,
+            input.modelTarget!.connectionSlug,
+            input.modelTarget!.model,
+          ],
     input.thinkingLevel ?? null,
     input.toolProfile ?? null,
     prepared.permissionMode ?? ['runtime_default'],
@@ -1417,6 +1477,7 @@ export function projectSessionCatalogRecord(
     ...(header.revisionIndex === undefined ? {} : { revisionIndex: header.revisionIndex }),
     ...(header.revisionState === undefined ? {} : { revisionState: header.revisionState }),
     backend: header.backend,
+    ...(header.executorId ? { executorId: header.executorId } : {}),
     llmConnectionId: header.llmConnectionId ?? null,
     llmConnectionSlug: header.llmConnectionSlug,
     connectionLocked: header.connectionLocked,
diff --git a/packages/runtime-host/src/server/session-revision-coordinator.ts b/packages/runtime-host/src/server/session-revision-coordinator.ts
index 74625e621f..dec9a386a5 100644
--- a/packages/runtime-host/src/server/session-revision-coordinator.ts
+++ b/packages/runtime-host/src/server/session-revision-coordinator.ts
@@ -30,6 +30,7 @@ import {
   type SessionHeader,
   type StoredMessage,
 } from '@maka/core/session';
+import { runtimeHostConversationCopyUnavailableReason } from './host-session-availability.js';
 import {
   archivedToolResultContainsLinkedChildReferences,
   archivedToolResultContainsConversationOwnedReferences,
@@ -345,6 +346,8 @@ export class HostSessionRevisionCoordinator {
         'Deep Research Sessions cannot be copied without an exact research ledger boundary',
       );
     }
+    const copyUnavailableReason = runtimeHostConversationCopyUnavailableReason(sourceHeader);
+    if (copyUnavailableReason) return copyFailure('operation_unavailable', copyUnavailableReason);
     if (kind !== 'side_conversation' && this.options.isSessionActive(input.sourceSessionId)) {
       return copyFailure('session_busy', 'Source Session has an active Turn');
     }
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 915c7c8f4d..b45bb28223 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -84,6 +84,8 @@
     "./plugin-composition-loader": "./dist/plugin-composition-loader.js",
     "./plugin-command-service": "./dist/plugin-command-service.js",
     "./plugin-data-services": "./dist/plugin-data-services.js",
+    "./plugin-executor-backend": "./dist/plugin-executor-backend.js",
+    "./plugin-executor-service": "./dist/plugin-executor-service.js",
     "./plugin-goal-service": "./dist/plugin-goal-service.js",
     "./plugin-lsp-service": "./dist/plugin-lsp-service.js",
     "./plugin-session-query-service": "./dist/plugin-session-query-service.js",
diff --git a/packages/runtime/src/__tests__/plugin-executor-backend.test.ts b/packages/runtime/src/__tests__/plugin-executor-backend.test.ts
new file mode 100644
index 0000000000..cb0174d007
--- /dev/null
+++ b/packages/runtime/src/__tests__/plugin-executor-backend.test.ts
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import type { SessionEvent } from '@maka/core/events';
+import { PluginExecutorBackend } from '../plugin-executor-backend.js';
+import { Context } from '../plugin-kernel.js';
+import { PluginExecutorService } from '../plugin-executor-service.js';
+
+test('executor backend converts plugin output and result to ordinary Session events', async () => {
+  const { root, binding } = fixture(async (request, context) => {
+    assert.equal(request.instructions, 'child instructions');
+    context.emit({ type: 'output_delta', text: 'hel' });
+    return { status: 'completed', text: 'hello' };
+  });
+  const backend = new PluginExecutorBackend({
+    sessionId: 'session-a',
+    cwd: '/workspace',
+    instructions: 'child instructions',
+    binding,
+    newId: ids(),
+    now: () => 42,
+  });
+
+  const events = await collect(backend.send({ turnId: 'turn-a', runId: 'run-a', text: 'task' }));
+  assert.deepEqual(
+    events.map((event) => event.type),
+    ['text_delta', 'text_complete', 'complete'],
+  );
+  assert.equal(events[0]?.turnId, 'turn-a');
+  assert.equal(events[0]?.type === 'text_delta' ? events[0].text : undefined, 'hel');
+  assert.equal(events[1]?.type === 'text_complete' ? events[1].text : undefined, 'hello');
+  assert.equal(events[2]?.type === 'complete' ? events[2].stopReason : undefined, 'end_turn');
+  await root.fiber.dispose();
+});
+
+test('executor backend turns stop into abort and terminal events', async () => {
+  let started!: () => void;
+  const ready = new Promise<void>((resolve) => {
+    started = resolve;
+  });
+  const { root, binding } = fixture(async (_request, context) => {
+    started();
+    await new Promise<void>((resolve) => context.signal.addEventListener('abort', () => resolve()));
+    return { status: 'cancelled' };
+  });
+  const backend = new PluginExecutorBackend({
+    sessionId: 'session-a',
+    cwd: '/workspace',
+    binding,
+  });
+
+  const eventsPromise = collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+  await ready;
+  await backend.stop('user_stop');
+  const events = await eventsPromise;
+  assert.deepEqual(
+    events.map((event) => event.type),
+    ['abort', 'complete'],
+  );
+  assert.equal(events[1]?.type === 'complete' ? events[1].stopReason : undefined, 'user_stop');
+  await root.fiber.dispose();
+});
+
+test('executor backend projects optional thinking and external tool activity', async () => {
+  const { root, binding } = fixture(
+    async (_request, context) => {
+      context.emit({ type: 'thinking_delta', text: 'considering' });
+      context.emit({
+        type: 'tool_start',
+        toolCallId: 'external-1',
+        name: 'search',
+        input: { query: 'maka' },
+        activityKind: 'search',
+      });
+      context.emit({ type: 'tool_progress', toolCallId: 'external-1', text: 'working' });
+      context.emit({ type: 'tool_result', toolCallId: 'external-1', text: 'found' });
+      return { status: 'completed', text: 'done' };
+    },
+    { thinking: true, toolActivity: true },
+  );
+  const backend = new PluginExecutorBackend({
+    sessionId: 'session-a',
+    cwd: '/workspace',
+    binding,
+    newId: ids(),
+    now: () => 42,
+  });
+
+  const events = await collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+  assert.deepEqual(
+    events.map((event) => event.type),
+    [
+      'thinking_delta',
+      'tool_start',
+      'tool_progress',
+      'tool_result',
+      'thinking_complete',
+      'text_complete',
+      'complete',
+    ],
+  );
+  assert.equal(events[1]?.type === 'tool_start' ? events[1].providerExecuted : undefined, true);
+  const stepId = events[0]?.type === 'thinking_delta' ? events[0].messageId : undefined;
+  assert.equal(events[1]?.type === 'tool_start' ? events[1].stepId : undefined, stepId);
+  assert.equal(events[4]?.type === 'thinking_complete' ? events[4].messageId : undefined, stepId);
+  assert.equal(events[5]?.type === 'text_complete' ? events[5].messageId : undefined, stepId);
+  await root.fiber.dispose();
+});
+
+test('executor retirement remains cancellation and is surfaced as a crash abort', async () => {
+  let started!: () => void;
+  const ready = new Promise<void>((resolve) => {
+    started = resolve;
+  });
+  const { root, binding, dispose } = fixture(async (_request, context) => {
+    started();
+    await new Promise<void>((_resolve, reject) =>
+      context.signal.addEventListener('abort', () => reject(context.signal.reason)),
+    );
+    return { status: 'completed', text: 'unreachable' };
+  });
+  const backend = new PluginExecutorBackend({
+    sessionId: 'session-a',
+    cwd: '/workspace',
+    binding,
+  });
+
+  const eventsPromise = collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+  await ready;
+  await dispose();
+  const events = await eventsPromise;
+  assert.equal(events[0]?.type === 'abort' ? events[0].reason : undefined, 'crash');
+  assert.equal(events[1]?.type === 'complete' ? events[1].stopReason : undefined, 'user_stop');
+  await root.fiber.dispose();
+});
+
+test('executor failure closes rich output before publishing its terminal error', async () => {
+  const { root, binding } = fixture(
+    async (_request, context) => {
+      context.emit({ type: 'thinking_delta', text: 'partial thought' });
+      context.emit({ type: 'tool_start', toolCallId: 'external-1', name: 'search' });
+      throw new Error('provider crashed');
+    },
+    { thinking: true, toolActivity: true },
+  );
+  const backend = new PluginExecutorBackend({
+    sessionId: 'session-a',
+    cwd: '/workspace',
+    binding,
+    newId: ids(),
+    now: () => 42,
+  });
+
+  const events = await collect(backend.send({ turnId: 'turn-a', text: 'task' }));
+  assert.deepEqual(
+    events.map((event) => event.type),
+    ['thinking_delta', 'tool_start', 'thinking_complete', 'tool_result', 'error', 'complete'],
+  );
+  assert.equal(events[3]?.type === 'tool_result' ? events[3].isError : undefined, true);
+  assert.equal(events[4]?.type === 'error' ? events[4].message : undefined, 'provider crashed');
+  await root.fiber.dispose();
+});
+
+function fixture(
+  execute: Parameters<PluginExecutorService['register']>[0]['execute'],
+  capabilities?: Parameters<PluginExecutorService['register']>[0]['capabilities'],
+): {
+  root: Context;
+  binding: ReturnType<PluginExecutorService['bind']>;
+  dispose: ReturnType<PluginExecutorService['register']>;
+} {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  const dispose = root
+    .extend({
+      maka: { rootId: 'profile', packageId: 'fixture', entryId: 'provider', generation: 1 },
+    })
+    .executors.register({ id: 'remote', execute, ...(capabilities ? { capabilities } : {}) });
+  return { root, binding: service.bind('session-a', 'remote'), dispose };
+}
+
+function ids(): () => string {
+  let value = 0;
+  return () => `id-${++value}`;
+}
+
+async function collect(events: AsyncIterable<SessionEvent>): Promise<SessionEvent[]> {
+  const result: SessionEvent[] = [];
+  for await (const event of events) result.push(event);
+  return result;
+}
diff --git a/packages/runtime/src/__tests__/plugin-executor-service.test.ts b/packages/runtime/src/__tests__/plugin-executor-service.test.ts
new file mode 100644
index 0000000000..bd2862e8d3
--- /dev/null
+++ b/packages/runtime/src/__tests__/plugin-executor-service.test.ts
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { Context } from '../plugin-kernel.js';
+import { PluginExecutorService } from '../plugin-executor-service.js';
+import { MakaPluginTransactionBuffer } from '../plugin-runtime.js';
+
+test('executors are scoped and pass black-box output without an Agent invocation', async () => {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  const profile = plugin(root, 'profile', 'profile-provider', 1);
+  const session = plugin(root, 'session:session-a', 'session-provider', 1);
+  profile.executors.register({
+    id: 'remote',
+    displayName: 'Remote',
+    execute: async (request, context) => {
+      context.emit({ type: 'output_delta', text: 'working' });
+      return { status: 'completed', text: `${request.sessionId}:${request.text}` };
+    },
+  });
+  session.executors.register({
+    id: 'private',
+    execute: async () => ({ status: 'completed', text: 'private' }),
+  });
+
+  const output: string[] = [];
+  assert.deepEqual(
+    await service.execute('remote', request('session-b'), {
+      onEvent: (event) => {
+        if (event.type === 'output_delta') output.push(event.text);
+      },
+    }),
+    { status: 'completed', text: 'session-b:hello' },
+  );
+  assert.deepEqual(output, ['working']);
+  assert.deepEqual(
+    service.list('session-a').map((item) => item.id),
+    ['private', 'remote'],
+  );
+  assert.deepEqual(
+    service.list('session-b').map((item) => item.id),
+    ['remote'],
+  );
+  await assert.rejects(() => service.execute('private', request('session-b')), /unavailable/u);
+  await root.fiber.dispose();
+});
+
+test('executor registration is transactional across hot reload', async () => {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  const previous = plugin(root, 'profile', 'provider', 1);
+  const disposePrevious = previous.executors.register(provider('previous'));
+  const candidateOwner = plugin(root, 'profile', 'provider', 2);
+  const transaction = new MakaPluginTransactionBuffer(candidateOwner);
+  const candidate = candidateOwner.extend({ makaTransaction: transaction });
+  const disposeCandidate = candidate.executors.register(provider('candidate'));
+
+  assert.equal(await executeText(service), 'previous');
+  await transaction.commit();
+  assert.equal(await executeText(service), 'candidate');
+  await disposePrevious();
+  assert.equal(await executeText(service), 'candidate');
+  await disposeCandidate();
+  await assert.rejects(() => executeText(service), /unavailable/u);
+  await root.fiber.dispose();
+});
+
+test('retiring an executor aborts and drains its active calls', async () => {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  const owner = plugin(root, 'profile', 'provider', 1);
+  let observed: AbortSignal | undefined;
+  let started!: () => void;
+  const startedPromise = new Promise<void>((resolve) => {
+    started = resolve;
+  });
+  const dispose = owner.executors.register({
+    id: 'remote',
+    execute: async (_request, context) => {
+      observed = context.signal;
+      started();
+      await new Promise<void>((resolve) =>
+        context.signal.addEventListener('abort', () => resolve()),
+      );
+      return { status: 'cancelled' };
+    },
+  });
+  const execution = service.execute('remote', request('session-a'));
+  await startedPromise;
+  await dispose();
+  assert.equal(observed?.aborted, true);
+  assert.deepEqual(await execution, {
+    status: 'cancelled',
+    source: 'executor_retired',
+    reason: 'Executor was retired: remote',
+  });
+  await root.fiber.dispose();
+});
+
+test('executor registration binds prototype methods to the provider instance', async () => {
+  class ClassExecutor {
+    readonly id = 'remote';
+    readonly prefix = 'class';
+
+    async execute() {
+      return { status: 'completed' as const, text: `${this.prefix}:ok` };
+    }
+  }
+
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  plugin(root, 'profile', 'provider', 1).executors.register(new ClassExecutor());
+  assert.equal(await executeText(service), 'class:ok');
+  await root.fiber.dispose();
+});
+
+test('executor bindings pin one provider generation', async () => {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  const previous = plugin(root, 'profile', 'provider', 1);
+  const disposePrevious = previous.executors.register(provider('previous'));
+  const previousBinding = service.bind('session-a', 'remote');
+  const candidateOwner = plugin(root, 'profile', 'provider', 2);
+  const transaction = new MakaPluginTransactionBuffer(candidateOwner);
+  const candidate = candidateOwner.extend({ makaTransaction: transaction });
+  candidate.executors.register(provider('candidate'));
+  await transaction.commit();
+
+  assert.equal((await previousBinding.execute(request('session-a'))).status, 'completed');
+  assert.equal(await executeText(service), 'candidate');
+  await disposePrevious();
+  assert.deepEqual(await previousBinding.execute(request('session-a')), {
+    status: 'cancelled',
+    source: 'executor_retired',
+    reason: 'Executor was retired: remote',
+  });
+  await root.fiber.dispose();
+});
+
+test('executor completion after caller cancellation is normalized to cancelled', async () => {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  const owner = plugin(root, 'profile', 'provider', 1);
+  let started!: () => void;
+  const ready = new Promise<void>((resolve) => {
+    started = resolve;
+  });
+  owner.executors.register({
+    id: 'remote',
+    execute: async (_request, context) => {
+      started();
+      await new Promise<void>((resolve) =>
+        context.signal.addEventListener('abort', () => resolve()),
+      );
+      return { status: 'completed', text: 'late success' };
+    },
+  });
+  const abort = new AbortController();
+  const execution = service.execute('remote', request('session-a'), { signal: abort.signal });
+  await ready;
+  abort.abort(new Error('redirect'));
+  assert.deepEqual(await execution, { status: 'cancelled', source: 'caller', reason: 'redirect' });
+  await root.fiber.dispose();
+});
+
+test('executor rich events require an explicitly declared capability', async () => {
+  const root = new Context();
+  const service = new PluginExecutorService(root);
+  plugin(root, 'profile', 'provider', 1).executors.register({
+    id: 'remote',
+    execute: async (_request, context) => {
+      context.emit({ type: 'thinking_delta', text: 'undeclared' });
+      return { status: 'completed', text: 'unreachable' };
+    },
+  });
+
+  await assert.rejects(
+    () => service.execute('remote', request('session-a')),
+    /invalid or undeclared/u,
+  );
+  await root.fiber.dispose();
+});
+
+function plugin(
+  root: Context,
+  rootId: 'profile' | `session:${string}`,
+  entryId: string,
+  generation: number,
+) {
+  return root.extend({
+    maka: { rootId, packageId: 'fixture', entryId, generation },
+  });
+}
+
+function provider(text: string) {
+  return {
+    id: 'remote',
+    execute: async () => ({ status: 'completed' as const, text }),
+  };
+}
+
+function request(sessionId: string) {
+  return {
+    sessionId,
+    turnId: 'turn-a',
+    conversationKey: sessionId,
+    text: 'hello',
+    cwd: '/workspace',
+  };
+}
+
+async function executeText(service: PluginExecutorService): Promise<string> {
+  const result = await service.execute('remote', request('session-a'));
+  assert.equal(result.status, 'completed');
+  return result.text;
+}
diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts
index 7c525616ec..9204c0994a 100644
--- a/packages/runtime/src/__tests__/session-manager.test.ts
+++ b/packages/runtime/src/__tests__/session-manager.test.ts
@@ -747,6 +747,56 @@ describe('SessionManager graph operator provisioning', () => {
     assert.deepStrictEqual(await runStore.listSessionInvocations(result.header.id), []);
   });
 
+  test('provisions a graph child on an explicit plugin executor without Maka tools', async () => {
+    const store = new MemorySessionStore();
+    const runStore = new MemoryAgentRunStore();
+    const checkedExecutors: string[] = [];
+    const manager = new SessionManager({
+      store,
+      runStore,
+      runtimeEventStore: runStore,
+      backends: new BackendRegistry(),
+      childTools: [],
+      assertChildExecutorAvailable: (parentSessionId, executorId) => {
+        checkedExecutors.push(`${parentSessionId}:${executorId}`);
+      },
+      newId: nextId(),
+      now: nextNow(40),
+    });
+    const parent = await manager.createSession(makeInput());
+    await seedInvocationFromHeader(
+      runStore,
+      makeRunHeader({
+        sessionId: parent.id,
+        runId: 'supervisor-run',
+        turnId: 'supervisor-turn',
+      }),
+    );
+
+    const result = await manager.provisionAgentGraphOperator({
+      graphId: 'graph-external',
+      workId: `graph_work_${'4'.repeat(32)}`,
+      agentId: LOCAL_READ_AGENT_ID,
+      executorId: 'codex',
+      operatorId: `graph_operator_${'5'.repeat(32)}`,
+      source: {
+        sessionId: parent.id,
+        runId: 'supervisor-run',
+        turnId: 'supervisor-turn',
+        toolCallId: 'schedule-tool',
+      },
+      edges: [],
+      expectedScheduleRevision: 1,
+    });
+
+    assert.strictEqual(result.header.backend, 'plugin-executor');
+    assert.strictEqual(result.header.executorId, 'codex');
+    assert.strictEqual(result.header.llmConnectionId, undefined);
+    assert.strictEqual(result.header.llmConnectionSlug, 'executor:codex');
+    assert.deepStrictEqual(result.header.subagentRuntime?.toolNames, []);
+    assert.deepStrictEqual(checkedExecutors, [`${parent.id}:codex`]);
+  });
+
   test('keeps four large graph branches and a replacement off the supervisor data plane', async () => {
     const store = new MemorySessionStore();
     const runStore = new MemoryAgentRunStore();
@@ -2273,6 +2323,87 @@ describe('SessionManager claimed graph intent execution', () => {
 });
 
 describe('SessionManager child-session runtime primitive', () => {
+  test('runs an explicitly delegated child through a plugin executor with no Maka tools', async () => {
+    const store = new MemorySessionStore();
+    const runStore = new MemoryAgentRunStore();
+    const backends = new BackendRegistry();
+    const parentGate = makeGate();
+    let childContext: BackendFactoryContext | undefined;
+    const checkedExecutors: string[] = [];
+    backends.register('ai-sdk', (ctx) => new TestBackend(ctx, parentGate));
+    backends.register('plugin-executor', (ctx) => {
+      childContext = ctx;
+      return {
+        kind: 'plugin-executor' as const,
+        sessionId: ctx.sessionId,
+        async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
+          yield {
+            type: 'text_complete',
+            id: 'external-complete-text',
+            turnId: input.turnId,
+            ts: 1,
+            messageId: 'external-message',
+            text: 'external result',
+          };
+          yield {
+            type: 'complete',
+            id: 'external-complete',
+            turnId: input.turnId,
+            ts: 2,
+            stopReason: 'end_turn',
+          };
+        },
+        async stop() {},
+        async respondToSandboxBoundary() {},
+        async dispose() {},
+      };
+    });
+    const manager = new SessionManager({
+      store,
+      runStore,
+      runtimeEventStore: runStore,
+      backends,
+      childTools: [],
+      assertChildExecutorAvailable: (parentSessionId, executorId) => {
+        checkedExecutors.push(`${parentSessionId}:${executorId}`);
+      },
+      newId: nextId(),
+      now: nextNow(80),
+    });
+    const parent = await manager.createSession(makeInput());
+    const parentTurn = manager
+      .sendMessage(parent.id, { turnId: 'parent-turn', text: 'delegate externally' })
+      [Symbol.asyncIterator]();
+    await parentTurn.next();
+    const [parentRun] = await runStore.listSessionInvocations(parent.id);
+    if (!parentRun) throw new Error('parent run was not recorded');
+
+    const result = await manager.spawnChildSession(parent.id, {
+      spawnedBy: {
+        parentRunId: parentRun.runId,
+        parentTurnId: parentRun.turnId,
+        toolCallId: 'tool-call-external',
+      },
+      agentProfile: LOCAL_READ_AGENT_PROFILE,
+      executorId: 'codex',
+      prompt: 'inspect through codex',
+    });
+    const child = await store.readHeader(result.childSessionId);
+
+    assert.strictEqual(result.status, 'completed');
+    assert.strictEqual(result.summary, 'external result');
+    assert.strictEqual(child.backend, 'plugin-executor');
+    assert.strictEqual(child.executorId, 'codex');
+    assert.strictEqual(child.llmConnectionId, undefined);
+    assert.deepStrictEqual(child.subagentRuntime?.toolNames, []);
+    assert.deepStrictEqual(childContext?.tools, []);
+    assert.strictEqual(childContext?.systemPrompt, LOCAL_READ_AGENT_DEFINITION.systemPrompt);
+    assert.deepStrictEqual(checkedExecutors, [`${parent.id}:codex`]);
+
+    parentGate.release();
+    while (!(await parentTurn.next()).done) {}
+  });
+
   test('creates a fresh read-only child with a session-inline first run and no parent history', async () => {
     const store = new MemorySessionStore();
     const runStore = new MemoryAgentRunStore();
@@ -3516,7 +3647,9 @@ describe('SessionManager manual compaction and quiescent session changes', () =>
           {
             runId: sourceRun.runId,
             connectionId:
-              sourceRoute.provenance === 'runtime' ? sourceRoute.llmConnectionId : undefined,
+              sourceRoute.provenance === 'runtime' && sourceRoute.backendKind !== 'plugin-executor'
+                ? sourceRoute.llmConnectionId
+                : undefined,
             modelId: sourceRoute.modelId,
           },
         ],
@@ -12940,7 +13073,8 @@ class CompactingTestBackend extends TestBackend {
       runtimeContextCount: input.runtimeContext.length,
       sourceRoutes: (input.runtimeContextInvocations ?? []).map((run) => ({
         runId: run.runId,
-        ...(run.opening.route.provenance === 'runtime'
+        ...(run.opening.route.provenance === 'runtime' &&
+        run.opening.route.backendKind !== 'plugin-executor'
           ? { connectionId: run.opening.route.llmConnectionId }
           : {}),
         modelId: run.opening.route.modelId,
@@ -13756,7 +13890,8 @@ class MemorySessionStore implements SessionStore {
       ...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}),
       ...(input.revisionState ? { revisionState: input.revisionState } : {}),
       hasUnread: false,
-      backend: 'ai-sdk',
+      backend: input.executorId ? 'plugin-executor' : 'ai-sdk',
+      ...(input.executorId ? { executorId: input.executorId } : {}),
       ...(input.llmConnectionId === undefined ? {} : { llmConnectionId: input.llmConnectionId }),
       llmConnectionSlug: input.llmConnectionSlug,
       connectionLocked: input.subagentParent !== undefined,
@@ -15093,7 +15228,7 @@ function testInvocationOpening(header: TestRunHeader): RuntimeEventInvocationOpe
     kind: 'invocation_opened',
     protocol: 'invocation_opened_v1',
     route:
-      header.llmConnectionId === undefined
+      header.llmConnectionId === undefined || header.backendKind === 'plugin-executor'
         ? {
             provenance: 'unknown',
             backendKind: header.backendKind,
diff --git a/packages/runtime/src/__tests__/subagent-tools.test.ts b/packages/runtime/src/__tests__/subagent-tools.test.ts
index 0a26942d44..08434de0f7 100644
--- a/packages/runtime/src/__tests__/subagent-tools.test.ts
+++ b/packages/runtime/src/__tests__/subagent-tools.test.ts
@@ -111,6 +111,7 @@ describe('subagent tools', () => {
     assert.deepStrictEqual(Object.keys(await advertisedProperties(buildSubagentSpawnTool())), [
       'profile',
       'subagent_id',
+      'executor_id',
       'task',
       'write_back',
       'isolation',
@@ -412,6 +413,7 @@ describe('subagent tools', () => {
     const result = await tool.impl(
       {
         profile: LOCAL_READ_AGENT_PROFILE,
+        executor_id: 'codex',
         task: 'Inspect the runtime tests.',
       },
       {
@@ -464,10 +466,12 @@ describe('subagent tools', () => {
     assert.strictEqual(calls.length, 1);
     const call = calls[0] as {
       agentProfile: string;
+      executorId?: string;
       prompt: string;
       onEvent?: (event: SessionEvent) => void;
     };
     assert.strictEqual(call.agentProfile, LOCAL_READ_AGENT_PROFILE);
+    assert.strictEqual(call.executorId, 'codex');
     assert.strictEqual(call.prompt, 'Inspect the runtime tests.');
     assert.strictEqual(typeof call.onEvent, 'function');
     assert.deepStrictEqual(output, [
diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts
index e693903960..eac4fbaed6 100644
--- a/packages/runtime/src/agent-run.ts
+++ b/packages/runtime/src/agent-run.ts
@@ -29,6 +29,7 @@ import type { RuntimeEventStore } from '@maka/core/runtime-event-store';
 import { isRuntimeHandoffPause, type RuntimeHandoffIntent } from '@maka/core/runtime-handoff';
 import { RunHandoffGate, type RunHandoffRequest } from './run-handoff-gate.js';
 import { preserveHandoffOpening } from './runtime-resume.js';
+import { runtimeInvocationRouteForHeader } from './runtime-invocation-route.js';
 import type {
   RequestCompositionSnapshot,
   RequestCompositionSnapshotInput,
@@ -1444,22 +1445,7 @@ export class AgentRun {
     const opening: RuntimeEventInvocationOpenedContent = {
       kind: 'invocation_opened',
       protocol: 'invocation_opened_v1',
-      route:
-        this.header.llmConnectionId === undefined
-          ? {
-              provenance: 'unknown',
-              backendKind: this.header.backend,
-              llmConnectionSlug: this.header.llmConnectionSlug,
-              modelId: this.header.model,
-            }
-          : {
-              provenance: 'runtime',
-              backendKind: this.header.backend,
-              llmConnectionId: this.header.llmConnectionId,
-              llmConnectionSlug: this.header.llmConnectionSlug,
-              modelId: this.header.model,
-              ...(providerStateIdentity ? { providerStateIdentity } : {}),
-            },
+      route: runtimeInvocationRouteForHeader(this.header, providerStateIdentity),
       configuration: {
         cwd: this.header.cwd,
         permissionMode: this.header.permissionMode,
diff --git a/packages/runtime/src/ai-sdk-compaction.ts b/packages/runtime/src/ai-sdk-compaction.ts
index a515fe02bd..6c5004ee37 100644
--- a/packages/runtime/src/ai-sdk-compaction.ts
+++ b/packages/runtime/src/ai-sdk-compaction.ts
@@ -492,7 +492,9 @@ export class AiSdkCompaction {
         const route = invocation.opening.route;
         return {
           runId: invocation.runId,
-          ...(route.provenance === 'runtime' ? { connectionId: route.llmConnectionId } : {}),
+          ...(route.provenance === 'runtime' && route.backendKind !== 'plugin-executor'
+            ? { connectionId: route.llmConnectionId }
+            : {}),
           modelId: route.modelId,
         };
       })
@@ -1498,6 +1500,7 @@ function persistedRequestAnchor(
     const route = invocations.find((candidate) => candidate.runId === event?.runId)?.opening.route;
     if (
       route?.provenance !== 'runtime' ||
+      route.backendKind === 'plugin-executor' ||
       route.modelId !== modelId ||
       route.llmConnectionId !== connectionId
     ) {
diff --git a/packages/runtime/src/history-compaction.ts b/packages/runtime/src/history-compaction.ts
index 69210d4893..6c4a15edc1 100644
--- a/packages/runtime/src/history-compaction.ts
+++ b/packages/runtime/src/history-compaction.ts
@@ -255,7 +255,12 @@ function acceptedInputBoundary(
   const onRoute = (event: RuntimeEvent | undefined): boolean => {
     if (event?.role !== 'model') return false;
     const opened = invocations.find((candidate) => candidate.runId === event.runId)?.opening.route;
-    if (opened?.provenance !== 'runtime' || opened.modelId !== route.modelId) return false;
+    if (
+      opened?.provenance !== 'runtime' ||
+      opened.backendKind === 'plugin-executor' ||
+      opened.modelId !== route.modelId
+    )
+      return false;
     return opened.llmConnectionId === route.connectionId;
   };
   let index = -1;
diff --git a/packages/runtime/src/plugin-agent-service.ts b/packages/runtime/src/plugin-agent-service.ts
index 727ee4639b..ea298d1da1 100644
--- a/packages/runtime/src/plugin-agent-service.ts
+++ b/packages/runtime/src/plugin-agent-service.ts
@@ -57,6 +57,7 @@ export interface PluginAgentCreateOptions {
   readonly cwd?: string;
   readonly prompt?: string;
   readonly agentProfile?: AgentProfile;
+  readonly executorId?: string;
   readonly model?: string;
   readonly permissionMode?: PermissionMode;
   readonly signal?: AbortSignal;
diff --git a/packages/runtime/src/plugin-executor-backend.ts b/packages/runtime/src/plugin-executor-backend.ts
new file mode 100644
index 0000000000..4bedece094
--- /dev/null
+++ b/packages/runtime/src/plugin-executor-backend.ts
@@ -0,0 +1,388 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { randomUUID } from 'node:crypto';
+import type { SessionEvent } from '@maka/core/events';
+import type { AgentBackend, BackendSendInput } from '@maka/core/backend-types';
+import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
+import type { UserQuestionResponse } from '@maka/core/user-question';
+import { AsyncEventQueue } from './async-queue.js';
+import type {
+  PluginExecutorBinding,
+  PluginExecutorOutputEvent,
+  PluginExecutorResult,
+} from './plugin-executor-service.js';
+
+interface ActiveExecution {
+  readonly abort: AbortController;
+  readonly settled: Promise<void>;
+}
+
+export interface PluginExecutorBackendInput {
+  readonly sessionId: string;
+  readonly cwd: string;
+  readonly instructions?: string;
+  readonly binding: PluginExecutorBinding;
+  readonly newId?: () => string;
+  readonly now?: () => number;
+}
+
+/** Converts a small plugin executor contract into Maka's existing Run event stream. */
+export class PluginExecutorBackend implements AgentBackend {
+  readonly kind = 'plugin-executor' as const;
+  readonly sessionId: string;
+  readonly #cwd: string;
+  readonly #instructions?: string;
+  readonly #binding: PluginExecutorBinding;
+  readonly #newId: () => string;
+  readonly #now: () => number;
+  readonly #active = new Set<ActiveExecution>();
+  #disposed = false;
+
+  constructor(input: PluginExecutorBackendInput) {
+    this.sessionId = input.sessionId;
+    this.#cwd = input.cwd;
+    this.#instructions = input.instructions;
+    this.#binding = input.binding;
+    this.#newId = input.newId ?? randomUUID;
+    this.#now = input.now ?? Date.now;
+  }
+
+  async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
+    if (this.#disposed) throw new Error('Plugin executor backend is disposed');
+    const abort = new AbortController();
+    const queue = new AsyncEventQueue<SessionEvent>();
+    const messageId = this.#newId();
+    const producer = this.#produce(input, messageId, abort.signal, queue).finally(() =>
+      queue.close(),
+    );
+    const active: ActiveExecution = { abort, settled: producer };
+    this.#active.add(active);
+    try {
+      for await (const event of queue) {
+        yield event;
+        queue.ackConsumed();
+      }
+      await producer;
+    } finally {
+      queue.noteConsumerDetached();
+      abort.abort(new Error('Plugin executor event consumer detached'));
+      await producer.catch(() => undefined);
+      this.#active.delete(active);
+    }
+  }
+
+  async stop(reason: 'user_stop' | 'redirect'): Promise<void> {
+    const active = [...this.#active];
+    for (const execution of active) execution.abort.abort(new Error(reason));
+    await Promise.allSettled(active.map((execution) => execution.settled));
+  }
+
+  async respondToSandboxBoundary(_response: SandboxBoundaryResponse): Promise<void> {
+    throw new Error('Plugin executor does not expose Maka sandbox-boundary requests');
+  }
+
+  async respondToUserQuestion(_response: UserQuestionResponse): Promise<void> {
+    throw new Error('Plugin executor does not expose Maka user-question requests');
+  }
+
+  async dispose(): Promise<void> {
+    if (this.#disposed) return;
+    this.#disposed = true;
+    await this.stop('user_stop');
+  }
+
+  async #produce(
+    input: BackendSendInput,
+    messageId: string,
+    signal: AbortSignal,
+    queue: AsyncEventQueue<SessionEvent>,
+  ): Promise<void> {
+    const turnId = input.turnId;
+    let thinkingText = '';
+    const toolUseIds = new Map<string, string>();
+    let result: PluginExecutorResult | undefined;
+    let failure: unknown;
+    let failed = false;
+    try {
+      result = await this.#binding.execute(
+        {
+          sessionId: this.sessionId,
+          turnId,
+          ...(input.runId ? { runId: input.runId } : {}),
+          conversationKey: this.sessionId,
+          text: input.text,
+          cwd: this.#cwd,
+          ...(this.#instructions ? { instructions: this.#instructions } : {}),
+          ...(input.attachments ? { attachments: input.attachments } : {}),
+          ...(input.directoryReferences ? { directoryReferences: input.directoryReferences } : {}),
+          ...(input.quotes ? { quotes: input.quotes } : {}),
+        },
+        {
+          signal,
+          onEvent: (event) => {
+            if (event.type === 'thinking_delta') thinkingText += event.text;
+            this.#publishOutputEvent(turnId, messageId, event, toolUseIds, queue);
+          },
+        },
+      );
+    } catch (error) {
+      failed = true;
+      failure = error;
+    }
+
+    this.#closeOptionalOutput(turnId, messageId, thinkingText, toolUseIds, queue);
+    if (failed) {
+      if (signal.aborted)
+        this.#publishCancellation(turnId, { status: 'cancelled', source: 'caller' }, queue);
+      else
+        this.#publishFailure(
+          turnId,
+          failure instanceof Error ? failure.message : 'External executor failed',
+          undefined,
+          false,
+          queue,
+        );
+      return;
+    }
+    if (result === undefined) {
+      this.#publishFailure(
+        turnId,
+        'External executor returned no terminal result',
+        undefined,
+        false,
+        queue,
+      );
+      return;
+    }
+    this.#publishResult(turnId, messageId, result, queue);
+  }
+
+  #closeOptionalOutput(
+    turnId: string,
+    messageId: string,
+    thinkingText: string,
+    toolUseIds: Map<string, string>,
+    queue: AsyncEventQueue<SessionEvent>,
+  ): void {
+    if (thinkingText) {
+      queue.push({
+        type: 'thinking_complete',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        messageId,
+        text: thinkingText,
+      });
+    }
+    for (const toolUseId of toolUseIds.values()) {
+      queue.push({
+        type: 'tool_result',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        toolUseId,
+        providerExecuted: true,
+        isError: true,
+        content: { kind: 'text', text: 'External executor ended before reporting a tool result' },
+      });
+    }
+  }
+
+  #publishResult(
+    turnId: string,
+    messageId: string,
+    result: PluginExecutorResult,
+    queue: AsyncEventQueue<SessionEvent>,
+  ): void {
+    if (result.status === 'completed') {
+      queue.push({
+        type: 'text_complete',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        messageId,
+        text: result.text,
+      });
+      queue.push({
+        type: 'complete',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        stopReason: 'end_turn',
+      });
+      return;
+    }
+    if (result.status === 'cancelled') {
+      this.#publishCancellation(turnId, result, queue);
+      return;
+    }
+    this.#publishFailure(turnId, result.message, result.code, result.recoverable ?? false, queue);
+  }
+
+  #publishCancellation(
+    turnId: string,
+    result: Extract<PluginExecutorResult, { status: 'cancelled' }>,
+    queue: AsyncEventQueue<SessionEvent>,
+  ): void {
+    const reason = cancellationEventReason(result);
+    queue.push({
+      type: 'abort',
+      id: this.#newId(),
+      turnId,
+      ts: this.#now(),
+      reason,
+    });
+    queue.push({
+      type: 'complete',
+      id: this.#newId(),
+      turnId,
+      ts: this.#now(),
+      stopReason: 'user_stop',
+    });
+  }
+
+  #publishOutputEvent(
+    turnId: string,
+    messageId: string,
+    event: PluginExecutorOutputEvent,
+    toolUseIds: Map<string, string>,
+    queue: AsyncEventQueue<SessionEvent>,
+  ): void {
+    if (event.type === 'output_delta') {
+      if (!event.text) return;
+      queue.push({
+        type: 'text_delta',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        messageId,
+        text: event.text,
+      });
+      return;
+    }
+    if (event.type === 'thinking_delta') {
+      if (!event.text) return;
+      queue.push({
+        type: 'thinking_delta',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        messageId,
+        text: event.text,
+      });
+      return;
+    }
+    if (event.type === 'tool_start') {
+      const previousToolUseId = toolUseIds.get(event.toolCallId);
+      if (previousToolUseId) {
+        queue.push({
+          type: 'tool_result',
+          id: this.#newId(),
+          turnId,
+          ts: this.#now(),
+          toolUseId: previousToolUseId,
+          providerExecuted: true,
+          isError: true,
+          content: { kind: 'text', text: 'External executor reused an active tool call id' },
+        });
+      }
+      const toolUseId = this.#newId();
+      toolUseIds.set(event.toolCallId, toolUseId);
+      queue.push({
+        type: 'tool_start',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        toolUseId,
+        toolName: event.name,
+        args: event.input ?? {},
+        providerExecuted: true,
+        stepId: messageId,
+        ...(event.displayName === undefined ? {} : { displayName: event.displayName }),
+        ...(event.activityKind === undefined ? {} : { activityKind: event.activityKind }),
+      });
+      return;
+    }
+    const toolUseId = toolUseIds.get(event.toolCallId);
+    if (!toolUseId) return;
+    if (event.type === 'tool_progress') {
+      if (!event.text) return;
+      queue.push({
+        type: 'tool_progress',
+        id: this.#newId(),
+        turnId,
+        ts: this.#now(),
+        toolUseId,
+        chunk: event.text,
+      });
+      return;
+    }
+    toolUseIds.delete(event.toolCallId);
+    queue.push({
+      type: 'tool_result',
+      id: this.#newId(),
+      turnId,
+      ts: this.#now(),
+      toolUseId,
+      providerExecuted: true,
+      isError: event.isError ?? false,
+      content: { kind: 'text', text: event.text },
+    });
+  }
+
+  #publishFailure(
+    turnId: string,
+    message: string,
+    code: string | undefined,
+    recoverable: boolean,
+    queue: AsyncEventQueue<SessionEvent>,
+  ): void {
+    queue.push({
+      type: 'error',
+      id: this.#newId(),
+      turnId,
+      ts: this.#now(),
+      recoverable,
+      ...(code ? { code, reason: code } : {}),
+      message: boundedMessage(message),
+    });
+    queue.push({
+      type: 'complete',
+      id: this.#newId(),
+      turnId,
+      ts: this.#now(),
+      stopReason: 'error',
+    });
+  }
+}
+
+function boundedMessage(value: string): string {
+  if (value.length <= 8_192) return value;
+  return `${value.slice(0, 8_191)}…`;
+}
+
+function cancellationEventReason(
+  result: Extract<PluginExecutorResult, { status: 'cancelled' }>,
+): 'user_stop' | 'redirect' | 'timeout' | 'crash' {
+  if (result.reason === 'redirect') return 'redirect';
+  if (result.reason === 'timeout') return 'timeout';
+  if (result.source === 'executor_retired') return 'crash';
+  return 'user_stop';
+}
diff --git a/packages/runtime/src/plugin-executor-service.ts b/packages/runtime/src/plugin-executor-service.ts
new file mode 100644
index 0000000000..26eb19fe39
--- /dev/null
+++ b/packages/runtime/src/plugin-executor-service.ts
@@ -0,0 +1,529 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createHash } from 'node:crypto';
+import type {
+  AttachmentRef,
+  DirectoryReference,
+  QuoteRef,
+  ToolActivityKind,
+} from '@maka/core/events';
+import { TOOL_ACTIVITY_KINDS } from '@maka/core/events';
+import { isExecutorId } from '@maka/core/executor-id';
+import { Service, type Context, type Disposable } from './plugin-kernel.js';
+import {
+  MakaPluginRuntimeError,
+  pluginIdentity,
+  registerPluginContribution,
+  type MakaContributionIdentity,
+  type MakaPluginRootId,
+} from './plugin-runtime.js';
+import { PluginScopeRegistry } from './plugin-scope-registry.js';
+
+declare module './plugin-kernel.js' {
+  interface Context {
+    readonly executors: PluginExecutorService;
+  }
+}
+
+export interface PluginExecutorRequest {
+  readonly sessionId: string;
+  readonly turnId: string;
+  readonly runId?: string;
+  /** Stable key a provider may use to retain its own external conversation. */
+  readonly conversationKey: string;
+  readonly text: string;
+  readonly cwd: string;
+  /** Child-agent instruction when this request belongs to a linked child Session. */
+  readonly instructions?: string;
+  readonly attachments?: readonly AttachmentRef[];
+  readonly directoryReferences?: readonly DirectoryReference[];
+  readonly quotes?: readonly QuoteRef[];
+}
+
+/** Optional presentation capabilities. Text output and terminal results are always supported. */
+export interface PluginExecutorCapabilities {
+  readonly thinking?: boolean;
+  readonly toolActivity?: boolean;
+}
+
+export type PluginExecutorOutputEvent =
+  | { readonly type: 'output_delta'; readonly text: string }
+  | { readonly type: 'thinking_delta'; readonly text: string }
+  | {
+      readonly type: 'tool_start';
+      readonly toolCallId: string;
+      readonly name: string;
+      readonly input?: unknown;
+      readonly displayName?: string;
+      readonly activityKind?: ToolActivityKind;
+    }
+  | { readonly type: 'tool_progress'; readonly toolCallId: string; readonly text: string }
+  | {
+      readonly type: 'tool_result';
+      readonly toolCallId: string;
+      readonly text: string;
+      readonly isError?: boolean;
+    };
+
+export type PluginExecutorCancellationSource = 'provider' | 'caller' | 'executor_retired';
+
+export type PluginExecutorResult =
+  | { readonly status: 'completed'; readonly text: string }
+  | {
+      readonly status: 'cancelled';
+      readonly reason?: string;
+      /** Service-owned provenance; provider-supplied values are ignored. */
+      readonly source?: PluginExecutorCancellationSource;
+    }
+  | {
+      readonly status: 'failed';
+      readonly message: string;
+      readonly code?: string;
+      readonly recoverable?: boolean;
+    };
+
+export interface PluginExecutorContext {
+  readonly signal: AbortSignal;
+  emit(event: PluginExecutorOutputEvent): void;
+}
+
+/** A black-box executor contributed by one Host plugin. */
+export interface PluginExecutorProvider {
+  readonly id: string;
+  readonly displayName?: string;
+  readonly capabilities?: PluginExecutorCapabilities;
+  execute(
+    request: Readonly<PluginExecutorRequest>,
+    context: PluginExecutorContext,
+  ): Promise<PluginExecutorResult>;
+}
+
+export interface PluginExecutorExecutionOptions {
+  readonly signal?: AbortSignal;
+  readonly onEvent?: (event: PluginExecutorOutputEvent) => void;
+}
+
+export interface PluginExecutorInspection extends MakaContributionIdentity {
+  readonly id: string;
+  readonly displayName: string;
+  readonly capabilities: Readonly<Required<PluginExecutorCapabilities>>;
+}
+
+/** Generation-pinned handle used by one prepared backend instance. */
+export interface PluginExecutorBinding {
+  readonly identity: PluginExecutorInspection;
+  readonly providerStateIdentity: `sha256:${string}`;
+  execute(
+    request: PluginExecutorRequest,
+    options?: PluginExecutorExecutionOptions,
+  ): Promise<PluginExecutorResult>;
+}
+
+export interface PluginExecutorServiceOptions {
+  readonly onChanged?: (rootId: MakaPluginRootId) => void;
+}
+
+interface RegisteredExecutor extends MakaContributionIdentity {
+  readonly provider: PluginExecutorProvider;
+  readonly token: symbol;
+  readonly active: Set<ActiveExecution>;
+  retired: boolean;
+}
+
+interface ActiveExecution {
+  readonly abort: AbortController;
+  readonly settled: Promise<void>;
+}
+
+class ExecutorRetiredAbort extends Error {
+  constructor(executorId: string) {
+    super(`Executor was retired: ${executorId}`);
+    this.name = 'ExecutorRetiredAbort';
+  }
+}
+
+/**
+ * Scoped black-box execution registry.
+ *
+ * The service owns only registration, visibility, cancellation, and result
+ * validation. Protocol processes, credentials, external conversation ids, and
+ * tools remain private to the contributing plugin.
+ */
+export class PluginExecutorService extends Service {
+  private readonly registry = new PluginScopeRegistry<RegisteredExecutor>();
+  private readonly onChanged: ((rootId: MakaPluginRootId) => void) | undefined;
+
+  constructor(ctx: Context, options: PluginExecutorServiceOptions = {}) {
+    super(ctx, 'executors');
+    this.onChanged = options.onChanged;
+  }
+
+  register(provider: PluginExecutorProvider): Disposable<Promise<void>> {
+    validateProvider(provider);
+    const identity = pluginIdentity(this.ctx);
+    return registerPluginContribution(this.ctx, `executors.register(${provider.id})`, () => {
+      const rootId = identity.scopeId as MakaPluginRootId;
+      const existing = this.registry.get(rootId, provider.id);
+      if (existing && existing.entryId !== identity.entryId) {
+        throw new MakaPluginRuntimeError(
+          'activation_failed',
+          `Executor is already registered in this scope: ${provider.id}`,
+        );
+      }
+      const capabilities = normalizeCapabilities(provider.capabilities);
+      const registeredProvider: PluginExecutorProvider = Object.freeze({
+        id: provider.id,
+        ...(provider.displayName === undefined ? {} : { displayName: provider.displayName }),
+        capabilities,
+        execute: provider.execute.bind(provider),
+      });
+      const entry: RegisteredExecutor = {
+        ...identity,
+        provider: registeredProvider,
+        token: Symbol(provider.id),
+        active: new Set(),
+        retired: false,
+      };
+      return this.registry.publish(rootId, provider.id, entry, {
+        ...(this.onChanged ? { onChanged: this.onChanged } : {}),
+        onRetired: async (retired) => {
+          for (const execution of retired.active) {
+            execution.abort.abort(new ExecutorRetiredAbort(retired.provider.id));
+          }
+          await Promise.allSettled([...retired.active].map((execution) => execution.settled));
+        },
+      });
+    });
+  }
+
+  list(sessionId: string): readonly PluginExecutorInspection[] {
+    assertSessionId(sessionId);
+    return Object.freeze(
+      [...this.registry.visible(sessionId).values()]
+        .sort((left, right) => left.provider.id.localeCompare(right.provider.id))
+        .map(({ provider, token: _token, active: _active, retired: _retired, ...identity }) =>
+          Object.freeze({
+            ...identity,
+            id: provider.id,
+            displayName: provider.displayName?.trim() || provider.id,
+            capabilities: normalizeCapabilities(provider.capabilities),
+          }),
+        ),
+    );
+  }
+
+  inspect(rootId?: MakaPluginRootId): readonly PluginExecutorInspection[] {
+    const seen = new Set<RegisteredExecutor>();
+    return Object.freeze(
+      [...this.registry.entries(rootId)]
+        .filter((entry) => !seen.has(entry) && Boolean(seen.add(entry)))
+        .sort((left, right) => left.provider.id.localeCompare(right.provider.id))
+        .map(({ provider, token: _token, active: _active, retired: _retired, ...identity }) =>
+          Object.freeze({
+            ...identity,
+            id: provider.id,
+            displayName: provider.displayName?.trim() || provider.id,
+            capabilities: normalizeCapabilities(provider.capabilities),
+          }),
+        ),
+    );
+  }
+
+  identity(sessionId: string, executorId: string): PluginExecutorInspection {
+    return this.identityForEntry(this.entry(sessionId, executorId));
+  }
+
+  bind(sessionId: string, executorId: string): PluginExecutorBinding {
+    const entry = this.entry(sessionId, executorId);
+    const identity = this.identityForEntry(entry);
+    return Object.freeze({
+      identity,
+      providerStateIdentity: providerStateIdentity(identity),
+      execute: (request: PluginExecutorRequest, options: PluginExecutorExecutionOptions = {}) => {
+        if (request.sessionId !== sessionId) {
+          throw new Error('Executor binding cannot cross Session scope');
+        }
+        return this.executeEntry(entry, request, options);
+      },
+    });
+  }
+
+  async execute(
+    executorId: string,
+    request: PluginExecutorRequest,
+    options: PluginExecutorExecutionOptions = {},
+  ): Promise<PluginExecutorResult> {
+    const normalizedRequest = normalizeRequest(request);
+    return this.executeEntry(
+      this.entry(normalizedRequest.sessionId, executorId),
+      normalizedRequest,
+      options,
+    );
+  }
+
+  private async executeEntry(
+    entry: RegisteredExecutor,
+    request: PluginExecutorRequest,
+    options: PluginExecutorExecutionOptions,
+  ): Promise<PluginExecutorResult> {
+    const normalizedRequest = normalizeRequest(request);
+    const abort = new AbortController();
+    const signal = options.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal;
+    let settle!: () => void;
+    const settled = new Promise<void>((resolve) => {
+      settle = resolve;
+    });
+    const active: ActiveExecution = { abort, settled };
+    entry.active.add(active);
+    try {
+      if (entry.retired) return cancelledResult(new ExecutorRetiredAbort(entry.provider.id));
+      try {
+        const result = await entry.provider.execute(normalizedRequest, {
+          signal,
+          emit: (event) => {
+            if (signal.aborted || entry.retired) return;
+            const normalized = normalizeOutputEvent(event, entry.provider.capabilities);
+            try {
+              options.onEvent?.(normalized);
+            } catch {
+              // A presentation observer must not change external execution.
+            }
+          },
+        });
+        if (signal.aborted) return cancelledResult(signal.reason);
+        return normalizeResult(result);
+      } catch (error) {
+        if (signal.aborted) return cancelledResult(signal.reason);
+        throw error;
+      }
+    } finally {
+      entry.active.delete(active);
+      settle();
+    }
+  }
+
+  private entry(sessionId: string, executorId: string): RegisteredExecutor {
+    assertSessionId(sessionId);
+    if (!isExecutorId(executorId)) throw new TypeError('Executor id is invalid');
+    const entry = this.registry.visible(sessionId).get(executorId);
+    if (!entry || entry.retired) throw new Error(`Executor is unavailable: ${executorId}`);
+    return entry;
+  }
+
+  private identityForEntry(entry: RegisteredExecutor): PluginExecutorInspection {
+    return Object.freeze({
+      entryId: entry.entryId,
+      scopeId: entry.scopeId,
+      extensionId: entry.extensionId,
+      generation: entry.generation,
+      id: entry.provider.id,
+      displayName: entry.provider.displayName?.trim() || entry.provider.id,
+      capabilities: normalizeCapabilities(entry.provider.capabilities),
+    });
+  }
+}
+
+function validateProvider(provider: PluginExecutorProvider): void {
+  if (!provider || typeof provider !== 'object')
+    throw new TypeError('Executor provider is required');
+  if (!isExecutorId(provider.id)) throw new TypeError('Executor id is invalid');
+  if (typeof provider.execute !== 'function') {
+    throw new TypeError(`Executor implementation is invalid: ${provider.id}`);
+  }
+  if (
+    provider.displayName !== undefined &&
+    (typeof provider.displayName !== 'string' || !provider.displayName.trim())
+  ) {
+    throw new TypeError(`Executor display name is invalid: ${provider.id}`);
+  }
+}
+
+function assertSessionId(value: string): void {
+  if (!value || /[\0\r\n]/u.test(value)) throw new TypeError('Session id is invalid');
+}
+
+function normalizeRequest(request: PluginExecutorRequest): Readonly<PluginExecutorRequest> {
+  if (!request || typeof request !== 'object') throw new TypeError('Executor request is required');
+  assertSessionId(request.sessionId);
+  for (const [label, value] of [
+    ['turnId', request.turnId],
+    ['conversationKey', request.conversationKey],
+    ['cwd', request.cwd],
+  ] as const) {
+    if (!value || /[\0\r\n]/u.test(value)) throw new TypeError(`Executor ${label} is invalid`);
+  }
+  if (typeof request.text !== 'string') throw new TypeError('Executor request text is invalid');
+  if (request.runId !== undefined && (!request.runId || /[\0\r\n]/u.test(request.runId))) {
+    throw new TypeError('Executor runId is invalid');
+  }
+  if (request.instructions !== undefined && typeof request.instructions !== 'string') {
+    throw new TypeError('Executor instructions are invalid');
+  }
+  return Object.freeze({
+    ...request,
+    ...(request.attachments ? { attachments: Object.freeze([...request.attachments]) } : {}),
+    ...(request.directoryReferences
+      ? { directoryReferences: Object.freeze([...request.directoryReferences]) }
+      : {}),
+    ...(request.quotes ? { quotes: Object.freeze([...request.quotes]) } : {}),
+  });
+}
+
+function normalizeCapabilities(
+  value: PluginExecutorCapabilities | undefined,
+): Readonly<Required<PluginExecutorCapabilities>> {
+  if (
+    value !== undefined &&
+    (!value ||
+      typeof value !== 'object' ||
+      (value.thinking !== undefined && typeof value.thinking !== 'boolean') ||
+      (value.toolActivity !== undefined && typeof value.toolActivity !== 'boolean'))
+  ) {
+    throw new TypeError('Executor capabilities are invalid');
+  }
+  return Object.freeze({
+    thinking: value?.thinking === true,
+    toolActivity: value?.toolActivity === true,
+  });
+}
+
+function normalizeOutputEvent(
+  event: PluginExecutorOutputEvent,
+  capabilities: PluginExecutorCapabilities | undefined,
+): PluginExecutorOutputEvent {
+  if (!event || typeof event !== 'object') throw new TypeError('Executor output event is invalid');
+  if (event.type === 'output_delta' && typeof event.text === 'string') {
+    return Object.freeze({ type: event.type, text: event.text });
+  }
+  if (
+    event.type === 'thinking_delta' &&
+    capabilities?.thinking === true &&
+    isSafeEventText(event.text)
+  ) {
+    return Object.freeze({ type: event.type, text: event.text });
+  }
+  if (
+    event.type === 'tool_start' &&
+    capabilities?.toolActivity === true &&
+    isSafeEventId(event.toolCallId) &&
+    isSafeEventId(event.name) &&
+    (event.displayName === undefined || isSafeEventText(event.displayName)) &&
+    (event.activityKind === undefined || TOOL_ACTIVITY_KINDS.includes(event.activityKind))
+  ) {
+    return Object.freeze({
+      type: event.type,
+      toolCallId: event.toolCallId,
+      name: event.name,
+      ...(event.input === undefined ? {} : { input: structuredClone(event.input) }),
+      ...(event.displayName === undefined ? {} : { displayName: event.displayName }),
+      ...(event.activityKind === undefined ? {} : { activityKind: event.activityKind }),
+    });
+  }
+  if (
+    event.type === 'tool_progress' &&
+    capabilities?.toolActivity === true &&
+    isSafeEventId(event.toolCallId) &&
+    isSafeEventText(event.text)
+  ) {
+    return Object.freeze({ type: event.type, toolCallId: event.toolCallId, text: event.text });
+  }
+  if (
+    event.type === 'tool_result' &&
+    capabilities?.toolActivity === true &&
+    isSafeEventId(event.toolCallId) &&
+    isSafeEventText(event.text) &&
+    (event.isError === undefined || typeof event.isError === 'boolean')
+  ) {
+    return Object.freeze({
+      type: event.type,
+      toolCallId: event.toolCallId,
+      text: event.text,
+      ...(event.isError === undefined ? {} : { isError: event.isError }),
+    });
+  }
+  throw new TypeError('Executor output event is invalid or undeclared');
+}
+
+function normalizeResult(result: PluginExecutorResult): PluginExecutorResult {
+  if (!result || typeof result !== 'object') throw new TypeError('Executor result is invalid');
+  if (result.status === 'completed' && typeof result.text === 'string') {
+    return Object.freeze({ status: result.status, text: result.text });
+  }
+  if (
+    result.status === 'cancelled' &&
+    (result.reason === undefined || typeof result.reason === 'string')
+  ) {
+    return Object.freeze({
+      status: result.status,
+      ...(result.reason === undefined ? {} : { reason: result.reason }),
+      source: 'provider',
+    });
+  }
+  if (
+    result.status === 'failed' &&
+    typeof result.message === 'string' &&
+    (result.code === undefined || typeof result.code === 'string') &&
+    (result.recoverable === undefined || typeof result.recoverable === 'boolean')
+  ) {
+    return Object.freeze({
+      status: result.status,
+      message: result.message,
+      ...(result.code === undefined ? {} : { code: result.code }),
+      ...(result.recoverable === undefined ? {} : { recoverable: result.recoverable }),
+    });
+  }
+  throw new TypeError('Executor result is invalid');
+}
+
+function providerStateIdentity(identity: PluginExecutorInspection): `sha256:${string}` {
+  return `sha256:${createHash('sha256')
+    .update(
+      JSON.stringify([
+        'plugin-executor.v1',
+        identity.id,
+        identity.extensionId,
+        identity.entryId,
+        identity.generation,
+      ]),
+    )
+    .digest('hex')}`;
+}
+
+function cancelledResult(reason: unknown): PluginExecutorResult {
+  const source: PluginExecutorCancellationSource =
+    reason instanceof ExecutorRetiredAbort ? 'executor_retired' : 'caller';
+  const message =
+    reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : undefined;
+  return Object.freeze({
+    status: 'cancelled',
+    source,
+    ...(message ? { reason: message } : {}),
+  });
+}
+
+function isSafeEventId(value: unknown): value is string {
+  return (
+    typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value)
+  );
+}
+
+function isSafeEventText(value: unknown): value is string {
+  return typeof value === 'string' && value.length <= 8_192 && !/[\0\r]/u.test(value);
+}
diff --git a/packages/runtime/src/runtime-invocation-route.ts b/packages/runtime/src/runtime-invocation-route.ts
new file mode 100644
index 0000000000..87e53cff8a
--- /dev/null
+++ b/packages/runtime/src/runtime-invocation-route.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { isExecutorId } from '@maka/core/executor-id';
+import type { RuntimeInvocationRoute } from '@maka/core/runtime-event';
+import type { SessionHeader } from '@maka/core/session';
+
+export function runtimeInvocationRouteForHeader(
+  header: SessionHeader,
+  providerStateIdentity: `sha256:${string}` | undefined,
+): RuntimeInvocationRoute {
+  if (
+    header.backend === 'plugin-executor' &&
+    isExecutorId(header.executorId) &&
+    providerStateIdentity
+  ) {
+    return {
+      provenance: 'runtime',
+      backendKind: header.backend,
+      executorId: header.executorId,
+      llmConnectionSlug: header.llmConnectionSlug,
+      modelId: header.model,
+      providerStateIdentity,
+    };
+  }
+  if (header.backend !== 'plugin-executor' && header.llmConnectionId !== undefined) {
+    return {
+      provenance: 'runtime',
+      backendKind: header.backend,
+      llmConnectionId: header.llmConnectionId,
+      llmConnectionSlug: header.llmConnectionSlug,
+      modelId: header.model,
+      ...(providerStateIdentity ? { providerStateIdentity } : {}),
+    };
+  }
+  return {
+    provenance: 'unknown',
+    backendKind: header.backend,
+    llmConnectionSlug: header.llmConnectionSlug,
+    modelId: header.model,
+  };
+}
diff --git a/packages/runtime/src/runtime-kernel.ts b/packages/runtime/src/runtime-kernel.ts
index 83217db13c..efb5327a28 100644
--- a/packages/runtime/src/runtime-kernel.ts
+++ b/packages/runtime/src/runtime-kernel.ts
@@ -151,6 +151,7 @@ import {
 import { DeliveryAckQueue, isDeliveryAckQueueClosed } from './delivery-ack-queue.js';
 import { runtimeHandoffPause, type RuntimeHandoffIntent } from '@maka/core/runtime-handoff';
 import { preserveHandoffOpening } from './runtime-resume.js';
+import { runtimeInvocationRouteForHeader } from './runtime-invocation-route.js';
 import type { AgentRunHandoffRequest } from './agent-run.js';
 
 export interface RuntimeKernelLike {
@@ -2679,6 +2680,12 @@ export class RuntimeKernel implements RuntimeKernelLike {
     if (!header.subagentParent) {
       throw new Error('Subagent runtime snapshot requires a linked child session');
     }
+    if (header.backend === 'plugin-executor') {
+      return {
+        systemPrompt: snapshot.systemPrompt,
+        tools: [],
+      };
+    }
     const snapshotDefinition = {
       id: snapshot.agentId,
       permissionMode: header.permissionMode,
@@ -3146,24 +3153,7 @@ function continuationTargetOpeningForExecution(input: {
   const opening: RuntimeEventInvocationOpenedContent = {
     kind: 'invocation_opened',
     protocol: 'invocation_opened_v1',
-    route:
-      sessionHeader.llmConnectionId === undefined
-        ? {
-            provenance: 'unknown',
-            backendKind: sessionHeader.backend,
-            llmConnectionSlug: sessionHeader.llmConnectionSlug,
-            modelId: sessionHeader.model,
-          }
-        : {
-            provenance: 'runtime',
-            backendKind: sessionHeader.backend,
-            llmConnectionId: sessionHeader.llmConnectionId,
-            llmConnectionSlug: sessionHeader.llmConnectionSlug,
-            modelId: sessionHeader.model,
-            ...(input.targetProviderStateIdentity
-              ? { providerStateIdentity: input.targetProviderStateIdentity }
-              : {}),
-          },
+    route: runtimeInvocationRouteForHeader(sessionHeader, input.targetProviderStateIdentity),
     configuration: {
       cwd: sessionHeader.cwd,
       permissionMode: sessionHeader.permissionMode,
diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts
index f6635e2a3c..214c6efc2c 100644
--- a/packages/runtime/src/session-manager.ts
+++ b/packages/runtime/src/session-manager.ts
@@ -308,6 +308,8 @@ export interface SpawnChildSessionInput {
   agentProfile: AgentProfile;
   /** User-approved catalog selector. The runtime resolves its frozen model target. */
   subagentId?: string;
+  /** Optional plugin executor. Non-preset children inherit the parent's executor. */
+  executorId?: string;
   prompt: string;
   name?: string;
   turnId?: string;
@@ -353,6 +355,7 @@ export interface ProvisionAgentGraphOperatorInput {
   workId: string;
   agentId?: string;
   subagentId?: string;
+  executorId?: string;
   operatorId: string;
   source: AgentGraphScheduleUpdateSource;
   edges: AgentGraphProvisionedEdge[];
@@ -524,6 +527,7 @@ export interface SessionConfigurationStoreUpdate {
   readonly expectedVersion: number;
   readonly configuration: {
     readonly backend: SessionHeader['backend'];
+    readonly executorId?: string;
     readonly llmConnectionId?: string;
     readonly llmConnectionSlug: string;
     readonly connectionLocked: boolean;
@@ -820,6 +824,8 @@ interface SessionManagerBaseDeps {
     list(): Promise<SubagentPresetListItem[]>;
     resolve(id: string): Promise<ResolvedSubagentPreset>;
   };
+  /** Host gate for an executor that must remain visible in a newly created child Session. */
+  assertChildExecutorAvailable?: (parentSessionId: string, executorId: string) => void;
   /** Host-owned filesystem isolation for worktree-backed child Sessions. */
   worktreeChildExecutor?: SubagentWorktreeExecutor;
   listArtifactsForTurn?: (sessionId: string, turnId: string) => Promise<ArtifactRecord[]>;
@@ -1035,6 +1041,20 @@ export class SessionManager {
     });
   }
 
+  private async resolveChildToolNames(
+    parentSessionId: string,
+    parentHeader: SessionHeader,
+    definition: AgentDefinition,
+  ): Promise<string[]> {
+    const availableChildTools = await this.childToolsForSession(parentSessionId);
+    assertAgentDefinitionRunnable({
+      definition,
+      tools: availableChildTools,
+      worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
+    });
+    return buildToolsForAgentDefinition(availableChildTools, definition).map((tool) => tool.name);
+  }
+
   private async finalizeAndListChildTurnArtifacts(
     sessionId: string,
     turnId: string,
@@ -2610,15 +2630,13 @@ export class SessionManager {
     const definition = resolvedPreset
       ? requireBuiltinAgentDefinitionByProfile(resolvedPreset.profile)
       : requireBuiltinAgentDefinition(input.agentId!);
-    const availableChildTools = await this.childToolsForSession(input.source.sessionId);
-    assertAgentDefinitionRunnable({
-      definition,
-      tools: availableChildTools,
-      worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
-    });
-    const resolvedToolNames = buildToolsForAgentDefinition(availableChildTools, definition).map(
-      (tool) => tool.name,
-    );
+    const executorId = input.executorId ?? (resolvedPreset ? undefined : parentHeader.executorId);
+    if (executorId) {
+      this.deps.assertChildExecutorAvailable?.(input.source.sessionId, executorId);
+    }
+    const resolvedToolNames = executorId
+      ? []
+      : await this.resolveChildToolNames(input.source.sessionId, parentHeader, definition);
     const childPermissionMode =
       parentHeader.permissionMode === 'bypass' ? 'bypass' : definition.permissionMode;
 
@@ -2646,6 +2664,7 @@ export class SessionManager {
         toolNames: resolvedToolNames,
         categoryPolicy: {},
         systemPrompt: definition.systemPrompt,
+        executorId: executorId ?? null,
         ...(resolvedPreset
           ? {
               preset: {
@@ -2682,20 +2701,28 @@ export class SessionManager {
         cwd: workspace?.worktreePath ?? parentHeader.cwd,
         ...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}),
         name: resolvedPreset?.name ?? definition.name,
-        ...(resolvedPreset
-          ? { llmConnectionId: resolvedPreset.connectionId }
-          : parentHeader.llmConnectionId === undefined
-            ? {}
-            : { llmConnectionId: parentHeader.llmConnectionId }),
-        llmConnectionSlug: resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
-        model: resolvedPreset?.model ?? parentHeader.model,
-        ...(resolvedPreset
-          ? resolvedPreset.thinkingLevel !== undefined
-            ? { thinkingLevel: resolvedPreset.thinkingLevel }
-            : {}
-          : parentHeader.thinkingLevel !== undefined
-            ? { thinkingLevel: parentHeader.thinkingLevel }
-            : {}),
+        ...(executorId
+          ? {
+              executorId,
+              llmConnectionSlug: `executor:${executorId}`,
+              model: executorId,
+            }
+          : {
+              ...(resolvedPreset
+                ? { llmConnectionId: resolvedPreset.connectionId }
+                : parentHeader.llmConnectionId === undefined
+                  ? {}
+                  : { llmConnectionId: parentHeader.llmConnectionId }),
+              llmConnectionSlug: resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
+              model: resolvedPreset?.model ?? parentHeader.model,
+              ...(resolvedPreset
+                ? resolvedPreset.thinkingLevel !== undefined
+                  ? { thinkingLevel: resolvedPreset.thinkingLevel }
+                  : {}
+                : parentHeader.thinkingLevel !== undefined
+                  ? { thinkingLevel: parentHeader.thinkingLevel }
+                  : {}),
+            }),
         permissionMode: childPermissionMode,
         collaborationMode: 'agent',
         orchestrationMode: 'default',
@@ -3203,15 +3230,12 @@ export class SessionManager {
     this.assertActiveParentRun(parentSessionId, parentRun, input.spawnedBy.parentTurnId);
 
     const definition = requireBuiltinAgentDefinitionByProfile(input.agentProfile);
-    const availableChildTools = await this.childToolsForSession(parentSessionId);
-    assertAgentDefinitionRunnable({
-      definition,
-      tools: availableChildTools,
-      worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
-    });
-    const resolvedToolNames = buildToolsForAgentDefinition(availableChildTools, definition).map(
-      (tool) => tool.name,
-    );
+    const executorId =
+      input.executorId ?? (input.resolvedPreset ? undefined : parentHeader.executorId);
+    if (executorId) this.deps.assertChildExecutorAvailable?.(parentSessionId, executorId);
+    const resolvedToolNames = executorId
+      ? []
+      : await this.resolveChildToolNames(parentSessionId, parentHeader, definition);
 
     const proposedTurnId = input.turnId ?? this.deps.newId();
     const proposedRunId = input.runId ?? this.deps.newId();
@@ -3225,20 +3249,29 @@ export class SessionManager {
         cwd: workspace?.worktreePath ?? parentHeader.cwd,
         ...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}),
         name: input.name ?? input.resolvedPreset?.name ?? definition.name,
-        ...(input.resolvedPreset
-          ? { llmConnectionId: input.resolvedPreset.connectionId }
-          : parentHeader.llmConnectionId === undefined
-            ? {}
-            : { llmConnectionId: parentHeader.llmConnectionId }),
-        llmConnectionSlug: input.resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
-        model: input.resolvedPreset?.model ?? parentHeader.model,
-        ...(input.resolvedPreset
-          ? input.resolvedPreset.thinkingLevel !== undefined
-            ? { thinkingLevel: input.resolvedPreset.thinkingLevel }
-            : {}
-          : parentHeader.thinkingLevel !== undefined
-            ? { thinkingLevel: parentHeader.thinkingLevel }
-            : {}),
+        ...(executorId
+          ? {
+              executorId,
+              llmConnectionSlug: `executor:${executorId}`,
+              model: executorId,
+            }
+          : {
+              ...(input.resolvedPreset
+                ? { llmConnectionId: input.resolvedPreset.connectionId }
+                : parentHeader.llmConnectionId === undefined
+                  ? {}
+                  : { llmConnectionId: parentHeader.llmConnectionId }),
+              llmConnectionSlug:
+                input.resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
+              model: input.resolvedPreset?.model ?? parentHeader.model,
+              ...(input.resolvedPreset
+                ? input.resolvedPreset.thinkingLevel !== undefined
+                  ? { thinkingLevel: input.resolvedPreset.thinkingLevel }
+                  : {}
+                : parentHeader.thinkingLevel !== undefined
+                  ? { thinkingLevel: parentHeader.thinkingLevel }
+                  : {}),
+            }),
         permissionMode: definition.permissionMode,
         collaborationMode: 'agent',
         orchestrationMode: 'default',
@@ -3956,7 +3989,7 @@ export class SessionManager {
       kind: 'invocation_opened',
       protocol: 'invocation_opened_v1',
       route:
-        session.llmConnectionId === undefined
+        session.llmConnectionId === undefined || session.backend === 'plugin-executor'
           ? {
               provenance: 'unknown',
               backendKind: session.backend,
@@ -5029,6 +5062,7 @@ export function headerToSummary(h: SessionHeader): SessionSummary {
     ...(h.revisionIndex !== undefined ? { revisionIndex: h.revisionIndex } : {}),
     ...(h.revisionState ? { revisionState: h.revisionState } : {}),
     backend: h.backend,
+    ...(h.executorId ? { executorId: h.executorId } : {}),
     ...(h.llmConnectionId === undefined ? {} : { llmConnectionId: h.llmConnectionId }),
     llmConnectionSlug: h.llmConnectionSlug,
     connectionLocked: h.connectionLocked,
@@ -5124,33 +5158,47 @@ function childSessionRequestFingerprint(
   parentSessionId: string,
   input: Pick<
     ResolvedSpawnChildSessionInput,
-    'spawnedBy' | 'agentProfile' | 'prompt' | 'swarm' | 'resolvedPreset'
+    'spawnedBy' | 'agentProfile' | 'executorId' | 'prompt' | 'swarm' | 'resolvedPreset'
   >,
 ): string {
-  const payload = input.resolvedPreset
+  const payload = input.executorId
     ? [
-        2,
+        3,
         parentSessionId,
         input.spawnedBy.parentRunId,
         input.spawnedBy.parentTurnId,
         input.spawnedBy.toolCallId,
         input.agentProfile,
-        input.resolvedPreset,
+        input.executorId,
+        input.resolvedPreset ?? null,
         input.prompt,
         input.swarm?.swarmId ?? null,
         input.swarm?.itemId ?? null,
       ]
-    : [
-        1,
-        parentSessionId,
-        input.spawnedBy.parentRunId,
-        input.spawnedBy.parentTurnId,
-        input.spawnedBy.toolCallId,
-        input.agentProfile,
-        input.prompt,
-        input.swarm?.swarmId ?? null,
-        input.swarm?.itemId ?? null,
-      ];
+    : input.resolvedPreset
+      ? [
+          2,
+          parentSessionId,
+          input.spawnedBy.parentRunId,
+          input.spawnedBy.parentTurnId,
+          input.spawnedBy.toolCallId,
+          input.agentProfile,
+          input.resolvedPreset,
+          input.prompt,
+          input.swarm?.swarmId ?? null,
+          input.swarm?.itemId ?? null,
+        ]
+      : [
+          1,
+          parentSessionId,
+          input.spawnedBy.parentRunId,
+          input.spawnedBy.parentTurnId,
+          input.spawnedBy.toolCallId,
+          input.agentProfile,
+          input.prompt,
+          input.swarm?.swarmId ?? null,
+          input.swarm?.itemId ?? null,
+        ];
   return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
 }
 
@@ -5201,7 +5249,8 @@ function sessionConfigurationWithPermissionMode(
 ): SessionConfigurationTransitionRequest['configuration'] {
   return {
     backend: header.backend,
-    ...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }),
+    executorId: header.executorId,
+    llmConnectionId: header.llmConnectionId,
     llmConnectionSlug: header.llmConnectionSlug,
     connectionLocked: header.connectionLocked,
     model: header.model,
@@ -5218,6 +5267,7 @@ function sessionConfigurationMatchesExceptPermissionMode(
 ): boolean {
   return (
     header.backend === configuration.backend &&
+    header.executorId === configuration.executorId &&
     header.llmConnectionId === configuration.llmConnectionId &&
     header.llmConnectionSlug === configuration.llmConnectionSlug &&
     header.connectionLocked === configuration.connectionLocked &&
diff --git a/packages/runtime/src/stream-graph-schedule-reconcile.ts b/packages/runtime/src/stream-graph-schedule-reconcile.ts
index f282f2fb55..78e30b63e9 100644
--- a/packages/runtime/src/stream-graph-schedule-reconcile.ts
+++ b/packages/runtime/src/stream-graph-schedule-reconcile.ts
@@ -958,6 +958,7 @@ function buildOperatorProvisionInput(
     ...(work.target.kind === 'preset'
       ? { subagentId: work.target.presetId }
       : { agentId: work.target.agentId }),
+    ...(work.target.executorId ? { executorId: work.target.executorId } : {}),
     operatorId,
     source,
     edges,
diff --git a/packages/runtime/src/stream-graph-supervisor-tools.ts b/packages/runtime/src/stream-graph-supervisor-tools.ts
index fef66a8d9c..b93fba2ece 100644
--- a/packages/runtime/src/stream-graph-supervisor-tools.ts
+++ b/packages/runtime/src/stream-graph-supervisor-tools.ts
@@ -18,6 +18,7 @@
  */
 
 import { z } from 'zod';
+import { isExecutorId } from '@maka/core/executor-id';
 import {
   AGENT_GRAPH_SCHEDULE_MAX_ADD_WORK,
   AGENT_GRAPH_SCHEDULE_MAX_INPUT_IDS,
@@ -65,6 +66,8 @@ const identitySchema = z
   .max(256)
   .refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'Identity contains control characters');
 
+const executorIdSchema = z.string().refine(isExecutorId, 'Invalid plugin executor id');
+
 const cursorSchema = z
   .string()
   .trim()
@@ -95,6 +98,9 @@ const addWorkSchema = z.preprocess(
         .describe(
           'Runtime id of an EXISTING graph operator returned by view_agent_graph. Use only for follow-up work; set operator_id OR agent_id, never both.',
         ),
+      executor_id: executorIdSchema
+        .optional()
+        .describe('Plugin executor id for a newly created agent or preset target.'),
       instruction: z.string().trim().min(1).max(AGENT_GRAPH_SCHEDULE_MAX_INSTRUCTION_CHARS),
       input_ids: z
         .array(identitySchema)
@@ -332,6 +338,7 @@ function cleanAddWorkInput(input: unknown): unknown {
   if (cleaned.target_kind === 'existing_operator') {
     delete cleaned.agent_id;
     delete cleaned.subagent_id;
+    delete cleaned.executor_id;
   }
   if (cleaned.replacement_mode === 'none') delete cleaned.replaces;
   return cleaned;
@@ -373,6 +380,7 @@ export interface UpdateAgentGraphToolInput {
     agent_id?: string;
     subagent_id?: string;
     operator_id?: string;
+    executor_id?: string;
     instruction: string;
     input_ids?: string[];
     selected_result_inputs?: Array<{
@@ -1101,12 +1109,21 @@ function normalizeWorkTarget(input: {
   agent_id?: string;
   subagent_id?: string;
   operator_id?: string;
+  executor_id?: string;
 }): AgentGraphWorkTarget {
   if (input.target_kind === 'new_agent') {
-    return { kind: 'agent', agentId: requireIdentity(input.agent_id, 'agent id') };
+    return {
+      kind: 'agent',
+      agentId: requireIdentity(input.agent_id, 'agent id'),
+      ...(input.executor_id ? { executorId: input.executor_id } : {}),
+    };
   }
   if (input.target_kind === 'new_preset') {
-    return { kind: 'preset', presetId: requireIdentity(input.subagent_id, 'subagent preset id') };
+    return {
+      kind: 'preset',
+      presetId: requireIdentity(input.subagent_id, 'subagent preset id'),
+      ...(input.executor_id ? { executorId: input.executor_id } : {}),
+    };
   }
   if (input.target_kind === 'existing_operator') {
     return {
@@ -1120,10 +1137,18 @@ function normalizeWorkTarget(input: {
     throw new Error('Exactly one of subagent_id, agent_id, or operator_id is required');
   }
   if (input.subagent_id) {
-    return { kind: 'preset', presetId: requireIdentity(input.subagent_id, 'subagent preset id') };
+    return {
+      kind: 'preset',
+      presetId: requireIdentity(input.subagent_id, 'subagent preset id'),
+      ...(input.executor_id ? { executorId: input.executor_id } : {}),
+    };
   }
   return input.agent_id
-    ? { kind: 'agent', agentId: requireIdentity(input.agent_id, 'agent id') }
+    ? {
+        kind: 'agent',
+        agentId: requireIdentity(input.agent_id, 'agent id'),
+        ...(input.executor_id ? { executorId: input.executor_id } : {}),
+      }
     : { kind: 'operator', operatorId: requireIdentity(input.operator_id, 'operator id') };
 }
 
diff --git a/packages/runtime/src/subagent-tools.ts b/packages/runtime/src/subagent-tools.ts
index ade65bd679..29982e8dba 100644
--- a/packages/runtime/src/subagent-tools.ts
+++ b/packages/runtime/src/subagent-tools.ts
@@ -18,6 +18,7 @@
  */
 
 import { z } from 'zod';
+import { isExecutorId } from '@maka/core/executor-id';
 import { decodeCanonicalToolResultContent } from '@maka/core/tool-result-record-schema';
 import { isSafeSubagentPresetId } from '@maka/core/subagent-settings';
 import { type ToolResultContent } from '@maka/core/events';
@@ -93,6 +94,7 @@ export function buildSubagentSpawnTool(
   {
     profile?: string;
     subagent_id?: string;
+    executor_id?: string;
     task: string;
     write_back?: string;
     isolation?: string;
@@ -118,6 +120,11 @@ export function buildSubagentSpawnTool(
             .refine(isSafeSubagentPresetId)
             .optional()
             .describe('User-approved subagent preset id from agent_list.'),
+          executor_id: z
+            .string()
+            .refine(isExecutorId)
+            .optional()
+            .describe('Plugin executor id for this child task.'),
           task: z
             .string()
             .min(1)
@@ -202,6 +209,7 @@ export function buildSubagentSpawnTool(
           await ctx.spawnChildSession({
             agentProfile: definition.profile,
             ...(input.subagent_id ? { subagentId: input.subagent_id } : {}),
+            ...(input.executor_id ? { executorId: input.executor_id } : {}),
             prompt: input.task,
             onEvent: (event) => progress.observe(event),
           }),
diff --git a/packages/runtime/src/tool-runtime.ts b/packages/runtime/src/tool-runtime.ts
index fa8b628c89..b282185fe5 100644
--- a/packages/runtime/src/tool-runtime.ts
+++ b/packages/runtime/src/tool-runtime.ts
@@ -282,6 +282,7 @@ export interface MakaToolContext {
   spawnChildSession?: (input: {
     agentProfile: AgentProfile;
     subagentId?: string;
+    executorId?: string;
     prompt: string;
     /** Optional swarm identity, scoped to the owning tool call. */
     swarm?: {
@@ -419,6 +420,7 @@ export interface ToolRuntimeInput {
     toolCallId: string;
     agentProfile: AgentProfile;
     subagentId?: string;
+    executorId?: string;
     prompt: string;
     swarm?: {
       swarmId: string;
@@ -2572,6 +2574,7 @@ export class ToolRuntime {
                     toolCallId: input.toolUseId,
                     agentProfile: spawnInput.agentProfile,
                     ...(spawnInput.subagentId ? { subagentId: spawnInput.subagentId } : {}),
+                    ...(spawnInput.executorId ? { executorId: spawnInput.executorId } : {}),
                     prompt: spawnInput.prompt,
                     ...(spawnInput.swarm ? { swarm: spawnInput.swarm } : {}),
                     abortSignal,
diff --git a/packages/storage/src/__tests__/session-store.test.ts b/packages/storage/src/__tests__/session-store.test.ts
index 582590be52..dbad1e6bd0 100644
--- a/packages/storage/src/__tests__/session-store.test.ts
+++ b/packages/storage/src/__tests__/session-store.test.ts
@@ -58,6 +58,34 @@ describe('SQLite SessionStore', () => {
     }
   });
 
+  test('persists a plugin executor route across reloads and catalog projection', async () => {
+    const root = await mkdtemp(join(tmpdir(), 'maka-plugin-executor-route-'));
+    let store = createSessionStore(root);
+    try {
+      const created = await store.create(
+        makeInput({
+          cwd: root,
+          executorId: 'codex',
+          llmConnectionSlug: 'executor:codex',
+          model: 'codex',
+        }),
+      );
+      assert.equal(created.backend, 'plugin-executor');
+      assert.equal(created.executorId, 'codex');
+      assert.equal(created.llmConnectionId, undefined);
+
+      await store.close?.();
+      store = createSessionStore(root);
+      const reloaded = await store.readHeader(created.id);
+      assert.equal(reloaded.backend, 'plugin-executor');
+      assert.equal(reloaded.executorId, 'codex');
+      assert.equal((await store.list())[0]?.executorId, 'codex');
+    } finally {
+      await store.close?.();
+      await rm(root, { recursive: true, force: true });
+    }
+  });
+
   test('requires the reserved WorkHub Coordination identity and role together', async () => {
     const root = await mkdtemp(join(tmpdir(), 'maka-workhub-coordination-identity-role-'));
     const store = createSessionStore(root);
diff --git a/packages/storage/src/legacy-run-header.ts b/packages/storage/src/legacy-run-header.ts
index 7136b7df95..38e4bf3403 100644
--- a/packages/storage/src/legacy-run-header.ts
+++ b/packages/storage/src/legacy-run-header.ts
@@ -353,7 +353,7 @@ export function invocationOpeningFromLegacyRunHeader(
 }
 
 function invocationRouteFromLegacyRunHeader(header: LegacyRunHeader): RuntimeInvocationRoute {
-  if (header.llmConnectionId === undefined) {
+  if (header.llmConnectionId === undefined || header.backendKind === 'plugin-executor') {
     return {
       provenance: 'unknown',
       backendKind: header.backendKind,
@@ -446,7 +446,7 @@ function isLegacyContinuationSource(value: unknown): value is LegacyContinuation
 
 /** `'fake'` stays accepted: runs written by builds that shipped FakeBackend must keep decoding (#3211). */
 function isPersistedBackendKind(value: unknown): value is PersistedBackendKind {
-  return value === 'ai-sdk' || value === 'fake';
+  return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
 }
 
 function isSha256Digest(value: unknown): value is `sha256:${string}` {
diff --git a/packages/storage/src/session-store.ts b/packages/storage/src/session-store.ts
index 2679e6b21e..9ddf142933 100644
--- a/packages/storage/src/session-store.ts
+++ b/packages/storage/src/session-store.ts
@@ -36,6 +36,7 @@ import {
   OPERATIONAL_STATE_DATABASE_NAME,
 } from './operational-state-store.js';
 import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/session-name';
+import { isExecutorId } from '@maka/core/executor-id';
 import {
   decodeCanonicalMessage,
   deriveTurnRecords,
@@ -1340,7 +1341,8 @@ function buildSessionHeader(
     ...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}),
     ...(input.revisionState ? { revisionState: input.revisionState } : {}),
     hasUnread: false,
-    backend: 'ai-sdk',
+    backend: input.executorId ? 'plugin-executor' : 'ai-sdk',
+    ...(input.executorId ? { executorId: input.executorId } : {}),
     ...(input.llmConnectionId === undefined ? {} : { llmConnectionId: input.llmConnectionId }),
     llmConnectionSlug: input.llmConnectionSlug,
     // A subagent Session's route is chosen by the spawn that created it and is
@@ -1405,6 +1407,7 @@ export function normalizeSessionHeader(
     (header.lastReadMessageId === undefined || typeof header.lastReadMessageId === 'string') &&
     typeof header.hasUnread === 'boolean' &&
     isPersistedBackendKind(header.backend) &&
+    isValidExecutorSelection(header) &&
     (header.llmConnectionId === undefined ||
       (typeof header.llmConnectionId === 'string' && header.llmConnectionId.length > 0)) &&
     typeof header.llmConnectionSlug === 'string' &&
@@ -1566,7 +1569,14 @@ function isValidSubagentSessionLineage(header: SessionHeader): boolean {
  * FakeBackend fail `normalizeSessionHeader` and read back as malformed (#3211).
  */
 function isPersistedBackendKind(value: unknown): value is SessionHeader['backend'] {
-  return value === 'ai-sdk' || value === 'fake';
+  return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
+}
+
+function isValidExecutorSelection(header: SessionHeader): boolean {
+  if (header.backend === 'plugin-executor') {
+    return isExecutorId(header.executorId);
+  }
+  return header.executorId === undefined;
 }
 
 function isFiniteNumber(value: unknown): value is number {
@@ -1641,6 +1651,7 @@ function toSummary(header: SessionHeader): SessionSummary {
     ...(header.revisionIndex !== undefined ? { revisionIndex: header.revisionIndex } : {}),
     ...(header.revisionState ? { revisionState: header.revisionState } : {}),
     backend: header.backend,
+    ...(header.executorId ? { executorId: header.executorId } : {}),
     ...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }),
     llmConnectionSlug: header.llmConnectionSlug,
     connectionLocked: header.connectionLocked,
diff --git a/packages/storage/src/sqlite-session-metadata-store.ts b/packages/storage/src/sqlite-session-metadata-store.ts
index 73d1194aa6..fe7ca77ff3 100644
--- a/packages/storage/src/sqlite-session-metadata-store.ts
+++ b/packages/storage/src/sqlite-session-metadata-store.ts
@@ -391,7 +391,8 @@ export interface SessionConfigurationMetadataUpdate {
   readonly expectedVersion: number;
   readonly configuration: {
     readonly backend: SessionHeader['backend'];
-    readonly llmConnectionId: string;
+    readonly executorId?: string;
+    readonly llmConnectionId?: string;
     readonly llmConnectionSlug: string;
     readonly connectionLocked: boolean;
     readonly model: string;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org