You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by li...@apache.org on 2026/10/23 09:56:08 UTC

(maka) branch main updated: fix(assistant): keep the discovery Session to read and report tools (#6256)

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

liugddx 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 ac1d288c40 fix(assistant): keep the discovery Session to read and report tools (#6256)
ac1d288c40 is described below

commit ac1d288c4017d1a37975c54859735195ea22cae1
Author: Guangdong Liu <li...@gmail.com>
AuthorDate: Fri Oct 23 17:56:01 2026 +0800

    fix(assistant): keep the discovery Session to read and report tools (#6256)
---
 .../assistant-initiative-boundary.test.ts          |   4 +-
 .../index-initiative/discovery-capabilities.ts     |  70 ++++++++
 .../src/assistant/index-initiative/host.ts         |   9 +
 packages/runtime-host/src/assistant/service.ts     |  20 ++-
 .../test/discovery-boundary.test.ts                | 193 +++++++++++++++++++++
 .../assistant/index-initiative/test/fixture.ts     |   1 +
 6 files changed, 289 insertions(+), 8 deletions(-)

diff --git a/packages/runtime-host/src/__tests__/assistant-initiative-boundary.test.ts b/packages/runtime-host/src/__tests__/assistant-initiative-boundary.test.ts
index 0fe0de02bd..8bb2facda8 100644
--- a/packages/runtime-host/src/__tests__/assistant-initiative-boundary.test.ts
+++ b/packages/runtime-host/src/__tests__/assistant-initiative-boundary.test.ts
@@ -113,7 +113,7 @@ test('DiscoveryReport reports real mailbox rejection, success and storage failur
       bot,
       systemPrompt: { section() {}, context() {} },
       turns: { beforeFinish() {} },
-      tools: { register: (tool: any) => tools.set(tool.name, tool) },
+      tools: { register: (tool: any) => tools.set(tool.name, tool), filter() {} },
       api: { register() {} },
       onClose: (fn: any) => closers.push(fn),
       onStart: (fn: any) => starters.push(fn),
@@ -283,7 +283,7 @@ test('assistant.bind recovers from startup lookup failure without a Host restart
       bot: new AssistantBotBridge(),
       systemPrompt: { section() {}, context() {} },
       turns: { beforeFinish() {} },
-      tools: { register() {} },
+      tools: { register() {}, filter() {} },
       api: { register: (name: string, handler: any) => apis.set(name, handler) },
       onClose: (fn: any) => closers.push(fn),
       onStart: (fn: any) => starters.push(fn),
diff --git a/packages/runtime-host/src/assistant/index-initiative/discovery-capabilities.ts b/packages/runtime-host/src/assistant/index-initiative/discovery-capabilities.ts
new file mode 100644
index 0000000000..728c65aeea
--- /dev/null
+++ b/packages/runtime-host/src/assistant/index-initiative/discovery-capabilities.ts
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tools the proactive discovery Session must not see or run (#6249, #6250).
+ *
+ * Discovery investigates and reports through DiscoveryReport; it does not create,
+ * adopt or mutate conversation-owned Matters or memory indexes, control initiative
+ * configuration, or deliver to the user directly. Read tools (MatterOverview,
+ * MemoryIndexRead, DiscoveryContext, ...) and source queries stay available.
+ * Whether MemorySourceQuery and MemoryExtract belong here is still an open
+ * design question tracked in the RFC.
+ */
+export const DISCOVERY_EXCLUDED_TOOLS: ReadonlySet<string> = new Set([
+  // Matter creation, adoption and conversation-scoped control.
+  'MatterDelegate',
+  'MatterStart',
+  'MatterAdopt',
+  'MatterControl',
+  'MatterMessage',
+  'MatterTaskControl',
+  'MatterTaskMessage',
+  'MatterReply',
+  'MatterDelegationRecover',
+  'MatterFeedbackRetry',
+  // Matter worker lifecycle tools; valid only inside an activation the Session owns.
+  'MatterRead',
+  'MatterReadFile',
+  'MatterWriteFile',
+  'MatterCheckpoint',
+  'MatterSettle',
+  'MatterConditionTest',
+  'MatterAskBot',
+  // Memory index mutation. Writes belong to index workers and the conversation.
+  'MemoryIndexCreate',
+  'MemoryIndexCreateGroup',
+  'MemoryIndexWrite',
+  'MemoryIndexEdit',
+  'MemoryIndexCheckpoint',
+  'MemoryIndexControl',
+  'MemoryIndexAddSources',
+  'MemoryIndexMaintain',
+  // Initiative configuration is owned by the conversation.
+  'InitiativeEnable',
+  'InitiativeConfigure',
+  'InitiativeControl',
+  // Bot-only coordination tools.
+  'SendUser',
+  'InboxCheck',
+  'InboxList',
+  'InboxRead',
+  'AssistantSetAvatar',
+  'Sleep',
+]);
diff --git a/packages/runtime-host/src/assistant/index-initiative/host.ts b/packages/runtime-host/src/assistant/index-initiative/host.ts
index 257ea7c03e..6af39d7160 100644
--- a/packages/runtime-host/src/assistant/index-initiative/host.ts
+++ b/packages/runtime-host/src/assistant/index-initiative/host.ts
@@ -24,6 +24,7 @@ import { InitiativeStore } from './store.js';
 import { InitiativeController } from './controller.js';
 import { initializeBot } from '../bot/host.js';
 import { registerAssistant } from './assistant.js';
+import { DISCOVERY_EXCLUDED_TOOLS } from './discovery-capabilities.js';
 export async function initializeInitiative(
   ctx: AssistantModuleServices,
   directory: string,
@@ -47,6 +48,14 @@ export async function initializeInitiative(
   ctx.backgroundSessions.register(() =>
     store.get()?.discoverySession ? [store.get().discoverySession] : [],
   );
+  // Discovery investigates and reports; it must not hold conversation-mutating tools.
+  if (!ctx.tools.filter)
+    throw Error('Assistant tool filtering is required to bound discovery Sessions');
+  ctx.tools.filter((sessionId, tools) =>
+    store.get()?.discoverySession === sessionId
+      ? tools.filter((tool) => !DISCOVERY_EXCLUDED_TOOLS.has(tool.name))
+      : tools,
+  );
   ctx.tools.register({
     name: 'DiscoveryReport',
     description:
diff --git a/packages/runtime-host/src/assistant/service.ts b/packages/runtime-host/src/assistant/service.ts
index 742fe75362..f9e79e72ca 100644
--- a/packages/runtime-host/src/assistant/service.ts
+++ b/packages/runtime-host/src/assistant/service.ts
@@ -109,6 +109,17 @@ class AssistantRuntime {
       this.sessionAccess.some((allows) => allows(sessionId))
     );
   }
+  /**
+   * The only visibility policy. Catalogs and execution both use it, so a tool hidden
+   * from a Session cannot run in that Session either.
+   */
+  visibleTools(sessionId: string, tools: readonly MakaTool[]): readonly MakaTool[] {
+    const nativeNames = new Set(this.tools.map((tool) => tool.name));
+    const allowed = this.hasSessionAccess(sessionId)
+      ? tools
+      : tools.filter((tool) => !nativeNames.has(tool.name));
+    return this.toolFilters.reduce((current, filter) => filter(sessionId, current), allowed);
+  }
   private readonly sections: PluginSystemPromptSection[] = [];
   private readonly contexts: PluginSystemPromptSection[] = [];
   private readonly checks: TurnFinishCheck[] = [];
@@ -172,6 +183,8 @@ class AssistantRuntime {
               if (this.closing) throw Error('Personal assistant is shutting down');
               if (!this.hasSessionAccess(call.sessionId))
                 throw Error('Personal assistant tools are unavailable in this session');
+              if (!this.visibleTools(call.sessionId, [tool]).length)
+                throw Error('Personal assistant tool is unavailable in this session');
               return internalTool.impl(input, call);
             },
           });
@@ -503,12 +516,7 @@ export class PersonalAssistantService {
   }
   filterTools(sessionId: string, tools: readonly MakaTool[]): readonly MakaTool[] {
     if (!this.enabled) return tools;
-    const runtime = this.runtime!;
-    const nativeNames = new Set(runtime.tools.map((tool) => tool.name));
-    const allowed = runtime.hasSessionAccess(sessionId)
-      ? tools
-      : tools.filter((tool) => !nativeNames.has(tool.name));
-    return runtime.toolFilters.reduce((current, filter) => filter(sessionId, current), allowed);
+    return this.runtime!.visibleTools(sessionId, tools);
   }
   toolCeiling(sessionId: string): readonly MakaTool[] | undefined {
     return this.enabled && this.runtime!.bot.isWorker(sessionId)
diff --git a/packages/runtime-host/test/assistant/index-initiative/test/discovery-boundary.test.ts b/packages/runtime-host/test/assistant/index-initiative/test/discovery-boundary.test.ts
new file mode 100644
index 0000000000..b1ef10f04c
--- /dev/null
+++ b/packages/runtime-host/test/assistant/index-initiative/test/discovery-boundary.test.ts
@@ -0,0 +1,193 @@
+/*
+ * 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.
+ */
+
+// Regression tests for #6249 and #6250. The proactive discovery Session investigates
+// and reports; it must not create, adopt, or mutate conversation-owned Matters or
+// memory indexes, and must not appear to hold those tools in its catalog.
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { fixture, until } from './fixture.js';
+
+async function setup() {
+  const f = await fixture({ matters: true });
+  await f.remote('assistant.bind', { sessionId: 'owner' });
+  f.setIndexRunner(async () => {});
+  f.setRunner(async () => {});
+  await f.invoke('InitiativeEnable', {});
+  await f.invoke('InitiativeControl', { action: 'check' });
+  await until(async () => !!(await f.invoke('InitiativeStatus')).discoverySession);
+  const discovery = (await f.invoke('InitiativeStatus')).discoverySession as string;
+  return { f, discovery };
+}
+
+test('discovery Session cannot delegate a Matter the conversation cannot see (#6249)', async () => {
+  const { f, discovery } = await setup();
+  try {
+    await assert.rejects(
+      f.invokeAs(discovery, 'MatterDelegate', {
+        taskKey: 'probe',
+        title: 'probe',
+        request: 'probe',
+      }),
+    );
+    const tasks = await f.invokeAs('owner', 'MatterTasks', {});
+    assert.equal((tasks.items ?? []).length, 0);
+  } finally {
+    await f.close();
+  }
+});
+
+test('discovery catalog omits conversation-mutating tools and keeps read and report tools (#6250)', async () => {
+  const { f, discovery } = await setup();
+  try {
+    const visible = f
+      .filterTools(discovery, f.tools.resolve(discovery, []).tools)
+      .map((tool: any) => tool.name);
+    for (const name of [
+      'MatterDelegate',
+      'MatterAdopt',
+      'MatterSettle',
+      'MemoryIndexWrite',
+      'MemoryIndexControl',
+      'SendUser',
+    ])
+      assert.ok(!visible.includes(name), `catalog still exposes ${name}`);
+    for (const name of ['DiscoveryReport', 'DiscoveryContext', 'MemoryIndexRead', 'MatterOverview'])
+      assert.ok(visible.includes(name), `catalog lost ${name}`);
+  } finally {
+    await f.close();
+  }
+});
+
+test('discovery Session cannot write, edit, checkpoint, control or create conversation memory indexes (#6250)', async () => {
+  const { f, discovery } = await setup();
+  try {
+    const range = await f.invoke('MemoryRange', {});
+    const index = await f.invoke('MemoryIndexCreate', {
+      name: 'Owner index',
+      instructions: 'owner',
+      cursor: range.to,
+      background: true,
+    });
+    const indexId = index.index.id;
+    const seededRead = await f.invoke('MemoryIndexRead', { indexId });
+    await f.invoke('MemoryIndexWrite', {
+      indexId,
+      key: 'owner-key',
+      expectedRevision: seededRead.index.revision,
+      text: 'OWNER TEXT',
+    });
+    const seeded = await f.invoke('MemoryIndexRead', { indexId });
+    await assert.rejects(
+      f.invokeAs(discovery, 'MemoryIndexWrite', {
+        indexId,
+        key: 'owner-key',
+        expectedRevision: seeded.index.revision,
+        text: 'INJECTED',
+      }),
+    );
+    await assert.rejects(
+      f.invokeAs(discovery, 'MemoryIndexEdit', {
+        indexId,
+        key: 'owner-key',
+        oldText: 'OWNER',
+        newText: 'INJECTED',
+        expectedRevision: seeded.index.revision,
+      }),
+    );
+    await assert.rejects(
+      f.invokeAs(discovery, 'MemoryIndexCheckpoint', {
+        indexId,
+        rangeId: seeded.range.rangeId,
+        expectedRevision: seeded.index.revision,
+        notes: 'injected',
+        complete: false,
+      }),
+    );
+    await assert.rejects(f.invokeAs(discovery, 'MemoryIndexControl', { indexId, action: 'pause' }));
+    await assert.rejects(
+      f.invokeAs(discovery, 'MemoryIndexCreate', {
+        name: 'Injected',
+        instructions: 'injected',
+        cursor: range.to,
+        background: false,
+      }),
+    );
+    const content = await f.invoke('MemoryIndexContent', { indexId, key: 'owner-key' });
+    assert.equal(content.text, 'OWNER TEXT');
+    assert.equal(
+      (await f.invoke('MemoryIndexRead', { indexId })).index.revision,
+      seeded.index.revision,
+    );
+  } finally {
+    await f.close();
+  }
+});
+
+test('discovery Session cannot adopt an unassigned Matter from the conversation (#6250)', async () => {
+  const { f, discovery } = await setup();
+  try {
+    await f.invokeAs('owner', 'MatterStart', { title: 'legacy', request: 'legacy request' });
+    const before = (await f.invokeAs('owner', 'MatterOverview', {})).legacy ?? [];
+    assert.equal(before.length, 1);
+    await assert.rejects(f.invokeAs(discovery, 'MatterAdopt', { id: before[0].id }));
+    const after = (await f.invokeAs('owner', 'MatterOverview', {})).legacy ?? [];
+    assert.equal(after.length, 1);
+  } finally {
+    await f.close();
+  }
+});
+
+test('discovery Session cannot control or amend a conversation-delegated task (#6250)', async () => {
+  const { f, discovery } = await setup();
+  try {
+    const task = await f.invoke('MatterDelegate', {
+      taskKey: 'owner-task',
+      title: 'owner task',
+      request: 'owner request',
+    });
+    await assert.rejects(
+      f.invokeAs(discovery, 'MatterTaskControl', { id: task.id, action: 'cancel' }),
+    );
+    await assert.rejects(
+      f.invokeAs(discovery, 'MatterTaskMessage', { id: task.id, text: 'injected requirement' }),
+    );
+    assert.notEqual((await f.invoke('MatterTasks', { id: task.id })).status, 'cancelled');
+  } finally {
+    await f.close();
+  }
+});
+
+test('discovery Session keeps read access to conversation memory (regression guard)', async () => {
+  const { f, discovery } = await setup();
+  try {
+    const range = await f.invoke('MemoryRange', {});
+    const index = await f.invoke('MemoryIndexCreate', {
+      name: 'Readable index',
+      instructions: 'readable',
+      cursor: range.to,
+      background: true,
+    });
+    const read = await f.invokeAs(discovery, 'MemoryIndexRead', { indexId: index.index.id });
+    assert.equal(read.index.id, index.index.id);
+    await f.invokeAs(discovery, 'MatterOverview', {});
+  } finally {
+    await f.close();
+  }
+});
diff --git a/packages/runtime-host/test/assistant/index-initiative/test/fixture.ts b/packages/runtime-host/test/assistant/index-initiative/test/fixture.ts
index b23f04f324..8231644c61 100644
--- a/packages/runtime-host/test/assistant/index-initiative/test/fixture.ts
+++ b/packages/runtime-host/test/assistant/index-initiative/test/fixture.ts
@@ -248,6 +248,7 @@ export async function fixture(options: any = {}) {
     workers,
     sessions,
     invokeAs,
+    filterTools: native.filterTools,
     remote: native.remote,
     send: (id: string, text: string) => native.send('owner', id, { text }),
     cancels: () => cancels,


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