The identity forwarded to opt-in MCP servers was a one-dimensional string
(ChatOrigin.requesterId): a MateClaw username for web logins, but a webchat
visitorId for visitors and an IM sender id for IM — indistinguishable to the
REST backend. The signed-token mode (d204b702) made this worse: an RS256
signature over an unauthenticated visitorId reads as "MateClaw authenticated
this user" to any backend that trusts the signature.
Introduce an identity-typing dimension at McpIdentityForwardService:
- classify() branches on ChatOrigin: authenticated (web login, sub=immutable
userId), anonymous (webchat visitor, trust=anonymous), external (IM sender,
trust=external), or none (cron/system → nothing injected, fail-closed).
- mint() adds `trust` and `channel_type` claims; plaintext value is prefixed
`trust:subject` so backends can tell the kinds apart without a JWT.
The immutable userId reaches resolve() without coupling it to the user store:
JwtAuthFilter stamps user.id into auth.setDetails() (both JWT and PAT paths),
and ChatController.memoryOrigin carries it on a new ChatOrigin.requesterUserId
field (only-add, per the record's evolution rule).
Resolves the webchat semantic mismatch raised in #459 and the "sub should be
an immutable user id" follow-up. 82 tests green (4 identity classes covered
with claim assertions + full ChatOrigin/MCP regression).
(cherry picked from commit b5d2cfbf98b39848d7139c743a0b81fea71e8ffe)
Lets an employee pin an ordered fallback chain of (provider, model) entries; the same provider may appear multiple times with different models. Build-time dedup keys on exact (provider, model).
Strict OpenAI-compatible providers reject the /chat/completions request
with HTTP 400 when an assistant message in history carries a tool call
whose function.arguments is not parseable JSON. Normalize blank or
non-JSON arguments to "{}" at the send chokepoint so streaming,
history-replay, and older-persisted tool calls all stay well-formed.
Backend (SourceEvidenceLedger):
- appendWikiSourceTable now normalizes existing source lines in-place to
canonical "[N] Title - section - page N" format instead of skipping them
- Added replaceSourceLine helper that matches a full source line by regex
and replaces it with the canonical form
- When source lines exist without a "来源:" header, automatically insert
one so the frontend preprocessor can locate the source table
Frontend (useMarkdownRenderer):
- Added data-citation-index / data-citation-title to DOMPurify whitelist
- Added preprocessWikiCitations preprocessor: parses the canonical source
table to build an index-to-title map, replaces [n] markers in the answer
body with clickable <a> links, and wraps entire source-table rows so the
full line is clickable
- Integrated into the render pipeline after wikilink substitution and
before Marked parsing
Frontend (useGlobalWikilinkClick):
- Extended the click delegation selector to match both .wiki-link and
.wiki-citation elements
- Title extraction falls back: data-citation-title || data-wiki-title
Tests: added three test cases for source-line normalization, idempotency,
and automatic header insertion
Issue #304. Operators who want an agent with NO knowledge base had no
way to express it: leaving the KB picker empty fell through to "inherit
workspace-wide" (every KB visible), so the agent ended up ingesting
every KB's context. This adds the same opt-out toggle that
skills_disabled (V126) / tools_disabled already provide.
Backend:
- V154 migration (h2 + mysql + kingbase): mate_agent.wiki_disabled
BOOLEAN/TINYINT/SMALLINT NOT NULL DEFAULT FALSE. Legacy agents stay
bit-identical.
- AgentEntity.wikiDisabled: Boolean field, @TableField("wiki_disabled").
- AgentBindingService.getBoundKbIds: short-circuit at the top —
wiki_disabled=true returns Set.of() regardless of binding rows. Mirrors
the precedence contract of getBoundSkillIds vs skills_disabled.
- AgentBindingService.setKbBindings: a non-empty save auto-clears a
stale wiki_disabled flag (same contract as setSkillBindings /
setToolBindings on their respective flags). Empty saves leave the flag
untouched — the UI toggle owns the bit, not the binding writer.
- AgentBindingServiceWikiDisabledTest: 5 cases covering all three
return states + the stale-flag auto-clear + empty-save no-op.
Frontend:
- Agents.vue KB picker: add the "此智能体不使用任何知识库" /
"This agent uses no knowledge bases" toggle, mirroring the skills /
tools picker layout. Tab badge shows "Off" when the toggle is on.
- types/index.ts: add Agent.wikiDisabled?: boolean.
- Save logic: when wikiDisabled is on, send an empty KB list (the
setKbs contract then leaves the flag alone server-side, exactly as
setSkills / setTools behave for their opt-out flags).
- i18n (zh + en): new strings for toggle label, hint, badge, and the
scope description shown when the toggle is on.
Stacked on top of #382 (which introduced AgentBindingResolver
.getBoundKbIds). No agent-runtime changes — wiki tools already degrade
cleanly when getBoundKbIds returns Set.of().
- goal: continue (not skip) on max-iterations and evidence-insufficient turns.
A max-iterations turn grants a fresh iteration budget ("hard continuation"),
bounded per run and sized into the graph recursion ceiling, so a task too big
for one budget keeps going instead of stalling until the next user message.
- plan-execute: re-plan the remaining work on a step exception, and on a
signature-based stall (repeated failures / identical results / no usable
result) instead of advancing dependent steps with junk; bounded by a per-run
re-plan cap, with a graduated change-strategy nudge before the hard stop.
- plan-execute: auto-derive a goal from a genuine multi-step plan, seeding the
acceptance criteria from the plan steps, so the goal subsystem engages without
the model calling setGoal; broadcast goal_created so the UI hydrates.
- react: refund the iteration for setup-only rounds (load_skill / enable_tool)
so a tight budget is not eaten by the load-then-use two-step.
- ui: re-fetch the active goal when a turn finishes so a goal created or mutated
mid-conversation surfaces without depending on an SSE event.
- streaming: make retry backoff / total-time budget instance fields with a
test-only seam; clarify that the wall-clock budget (not max-retries) bounds a
sustained SERVER_ERROR loop to ~8 attempts, fixing the slow/flaky retry test.
The knowledge-base trust verification recorded wiki citations from every
non-readFile tool response by sniffing its JSON for a top-level title /
pages / chunks field. Tools like getGoalStatus return a top-level title,
which falsely populated the citation set and then forced [n] citations on
the final answer (otherwise flagged EVIDENCE_INSUFFICIENT). Gate citation
mining on the wiki_* tool name instead.
Likewise, the grounded answer contract (cite-or-refuse) was appended to
every ReasoningNode call unconditionally, degrading general agents that
have no knowledge base. Append it only when the agent has a wiki_* tool
bound, scoping the strict regime to KB-grounded scenarios.
Adds a regression test asserting a non-wiki tool with a top-level title
creates no wiki citations.
Add ErrorClassificationTest regression cases for the AI-gateway retry
hardening: 5xx-before-4xx ordering (a proxy 502 whose body says
"bad request" stays retryable), Chinese / numeric provider billing
patterns, and DNS / TLS infrastructure-fatal detection.
Fix the cert-trust pattern while adding its test: Java's ValidatorException
emits "PKIX path building failed" with an uppercase PKIX and the error
chain is not lower-cased, so the previous lowercase pattern never matched
— an untrusted/expired cert chain fell through to the retryable
SERVER_ERROR bucket and was retried in vain instead of failing over.
Add 5-arg buildContextMessage overload that emits [system-context] Model:
for every origin (web/cron/IM). Legacy 3/4-arg overloads delegate to the
new one with null model args, keeping their output byte-identical.
Also fix pre-existing FeishuMentionTest compile error caused by removed
mentionMatchesAnyAlias/collectMentionIdentifiers methods.
Streaming chat ran render tools on an async thread with no bound request,
so download links lost their host and arrived without a domain. Resolve the
host on the request thread and carry it through ChatOrigin/ToolContext;
falls back to a configurable public-base-url, then a relative path.
Some OpenAI-compatible providers (LM Studio's built-in server, certain
strict-mode vLLM / SGLang deployments) reject 400 "System message must
be at the beginning" when SystemMessages appear after user / assistant
/ tool messages. The reasoning loop currently emits four SystemMessage
segments — main prompt at index 0, skill catalog inserted at index 1,
progress-ledger snapshot and stale-reminder appended at the end of
nonHistoryPrefix after the runtime-context UserMessage. The latter two
violate the strict shape, so conversations on LM Studio 400 on the
first turn (reported in #218).
Add MessageNormalizer: collects every SystemMessage in the outbound
prompt regardless of position, joins their text with a blank-line
separator, and emits a single SystemMessage at index 0. Non-system
messages keep their relative order, so AssistantMessage(tool_calls) ↔
ToolResponseMessage adjacency is preserved verbatim (required by strict
pair validators).
Wire it into doStreamCall as the first pre-egress step so every node
(reasoning, step-execution, summarizing, plan-generation, limit-exceeded)
inherits the fix without per-node changes, and any future node that
emits multiple SystemMessages stays compliant.
The transformation is semantically equivalent on permissive providers
(OpenAI, DashScope, Ollama, DeepSeek, Kimi, Doubao, GLM) — the merged
token sequence matches what they would have seen across N SystemMessages
— and safe on non-OpenAI protocols (Anthropic, Vertex / Gemini), whose
adapters already extract SystemMessages into a top-level system field
and receive an identical payload.
Kill switch: -Dmateclaw.llm.message-normalizer.enabled=false reverts to
the prior behavior for emergency rollback.
Tests: 11 unit tests on MessageNormalizer cover empty / no-system /
canonical / mid-list / tail / blanks / tool-pair preservation / Prompt
option-reference preservation / kill switch. 1 wiring test pins the
call site in doStreamCall. Full vip.mate.agent.** suite (504 tests)
stays green.
Closes#218.
1. Relative parent traversal in shell commands (HIGH)
validateShellCommand only scanned absolute path tokens, so commands
like `cat ../mateclaw/CLAUDE.md`, `cd .. && cat foo`, or
`ln -sf ../bar breakout` had no absolute path to trip the check.
From a workspace cwd that's a real escape — `..` segments resolve
against the JVM cwd at file-tool time and reach anywhere the user
can read.
Add a second pass: any token containing `..` as a path segment is
resolved against the workspace root via root.resolve(token).
normalize(); reject when the result falls outside. In-workspace
traversal like `subdir/../sibling` normalizes back inside and
passes. Identifiers without slashes (e.g. version strings with
`1.2..3`) are not treated as paths.
2. Shell validation and process working directory used different
context sources (MEDIUM)
execute_shell_command validated with the explicit ToolContext, but
buildShellProcess called WorkspacePathGuard.getWorkingDirectory()
(no-arg), which only sees the ThreadLocal fallback. Today the
ToolExecutionExecutor sets both so the discrepancy is latent, but
a future direct Spring AI invocation passing only ToolContext would
validate against one basePath and exec against another. Thread ctx
through buildShellProcess and call getWorkingDirectory(ctx) so
validation and execution agree on a single source of truth.
3. Absolute agent override could disable workspace scoping (MEDIUM)
resolveAgentBasePath accepted an absolute override verbatim, even
when it pointed outside the workspace root. An admin (or any
account with agent-edit permission) could set workspaceBasePath="/"
or another team's repo and bypass workspace boundaries entirely.
When a workspace has its own basePath, require absolute overrides
to sit underneath it. The caller in build() catches the rejection,
logs WARN, and falls back to the workspace basePath so chat stays
available rather than crashing agent construction. When the
workspace has no basePath there's no boundary to enforce, so legacy
behavior is preserved.
Test coverage: WorkspacePathGuardShellTest grows from 17 to 23 (six
new cases for `cd ..`, relative parent traversal, relative symlink
escape, deeper traversal, in-workspace normalization, and the
identifier false-positive guard). AgentGraphBuilderBasePathResolutionTest
grows from 7 to 10 (three new cases for in-workspace absolute,
outside-workspace absolute rejection, and no-workspace legacy
behavior). All 45 sandbox-area tests pass with no regressions.