diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java
index 0e4e34a2..1c8aaa6a 100644
--- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java
@@ -127,13 +127,22 @@ public class WikiKnowledgeBaseService {
* KB just drops out). An agent with no scope rows stays workspace-wide,
* preserving the pre-scoping behavior for every existing agent.
*
+ * An agent flagged {@code wiki_disabled=true} sees no KBs at all — the
+ * opt-out toggle wins over both workspace visibility and any leftover
+ * binding rows (the UI clears the rows when the flag is set, so the
+ * zero-row state must not fall through to "unrestricted").
+ *
* This is the single choke point for KB access: {@code wiki_list_kbs},
- * {@link #findVisibleById}, {@link #findAllByName} and
- * {@link #resolvePrimaryKb} all read through here, so narrowing it scopes
- * every wiki tool at once.
+ * {@link #findVisibleById}, {@link #findAllByName},
+ * {@link #resolvePrimaryKb}, the system-prompt wiki context, and the
+ * per-turn relevant-page injection all read through here, so narrowing
+ * it scopes every wiki surface at once.
*/
public List listByAgentId(Long agentId) {
AgentEntity agent = getAgentOrNull(agentId);
+ if (agent != null && Boolean.TRUE.equals(agent.getWikiDisabled())) {
+ return List.of();
+ }
List workspaceKbs = (agent == null || agent.getWorkspaceId() == null)
? listAll()
: listByWorkspace(agent.getWorkspaceId());
diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java
index 7be62c78..552ed3d5 100644
--- a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java
+++ b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java
@@ -15,6 +15,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -308,4 +309,38 @@ class WikiKnowledgeBaseServiceTest {
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(300L);
}
+
+ // ==================== wiki_disabled opt-out (issue #551) ====================
+ //
+ // The "use no knowledge base" toggle sets mate_agent.wiki_disabled=true
+ // and clears the binding rows. Zero rows normally means "unrestricted",
+ // so the flag must short-circuit BEFORE the scope fallback — otherwise
+ // the opted-out agent sees every workspace KB again and the system
+ // prompt keeps injecting wiki content.
+
+ @Test
+ @DisplayName("wiki_disabled hides every KB: list, primary, and by-id lookups all come back empty")
+ void wikiDisabledHidesAllKbs() {
+ AgentEntity optedOut = agent(7L, 1L, 100L);
+ optedOut.setWikiDisabled(true);
+ when(agentMapper.selectById(7L)).thenReturn(optedOut);
+
+ assertThat(service.listByAgentId(7L)).isEmpty();
+ assertThat(service.resolvePrimaryKb(7L)).isNull();
+ assertThat(service.findVisibleById(7L, 100L)).isNull();
+ verify(kbMapper, never()).selectList(any());
+ }
+
+ @Test
+ @DisplayName("wiki_disabled wins over leftover binding rows")
+ void wikiDisabledWinsOverLeftoverBindings() {
+ bindScope(100L);
+ AgentEntity optedOut = agent(7L, 1L, 100L);
+ optedOut.setWikiDisabled(true);
+ when(agentMapper.selectById(7L)).thenReturn(optedOut);
+ when(kbMapper.selectList(any())).thenReturn(List.of(
+ kb(100L, null, 1L, "Bound KB")));
+
+ assertThat(service.listByAgentId(7L)).isEmpty();
+ }
}