Skip to content

feat(operator): foundation for an agent that can safely write - #622

Merged
ginccc merged 15 commits into
mainfrom
feat/operator-write-foundation
Jul 30, 2026
Merged

feat(operator): foundation for an agent that can safely write#622
ginccc merged 15 commits into
mainfrom
feat/operator-write-foundation

Conversation

@ginccc

@ginccc ginccc commented Jul 29, 2026

Copy link
Copy Markdown
Member

Groundwork for a workspace operator agent that manages a deployment — creating and updating agents and groups — while acting as the person chatting to it, with that person's permissions.

No new capability is granted here. The operator's endpoint allow-list is untouched and still read-only. What changes is that widening it is now safe, where before it was neither safe nor functional.

Generated writes did not work at all

McpApiToolBuilder.buildBodyTemplate emitted Qute variables for a request body but registered none of them as tool parameters, and AgentOrchestrator builds the tool schema from ApiCall.getParameters() alone. The model therefore had no documented way to fill a body; with strict rendering off those variables render empty.

Every generated POST/PUT/PATCH went out structurally valid and semantically empty. Adding a write endpoint before this fix would have produced garbage requests that fail at the far end rather than at the config.

The body is now one model-written variable

A per-property template looked more helpful and was worse three ways:

  1. Every variable became a required tool parameter — Map<String,String> has nowhere to record optionality, so a PATCH of one field forced the model to restate all the others, turning a partial update into a full overwrite.
  2. Values were substituted into the JSON unescaped (the templating engine runs in TEXT mode), so a value containing a quote could break the body or add fields the schema never declared. For an agent that reads untrusted content and writes through an API, that is an injection boundary.
  3. The HITL card shows tool arguments, so "the arguments are the request" only holds if the body is one of them.

The shape a decomposed template implied now lives in the parameter description, which names each property with its type and marks which are required.

Approval patterns can address the endpoint a tool calls

Tool names come from operationId or a slug and drift when a spec changes — and ToolApprovalGate allows an unmatched call, so a renamed or newly generated write arrived ungated and silently. Method and path were available and discarded one line into registration.

{ "requireApproval": ["http.post:*", "http.put:*", "http.patch:*", "http.delete:*"],
  "exempt": ["http.get:*"] }

gates every mutation without naming a single tool, so a new endpoint cannot arrive ungated. http.post:/agentstore/agents addresses exactly one, because different POSTs carry different weight. Both forms speak the same METHOD /path vocabulary as the endpoint allow-list, so the two can be generated from one source rather than maintained in two.

Design rule: enumerate downward, never upward. Whether something is gated stays in requireApproval/exempt. A missed exemption costs an approval prompt; a missed requirement is an ungated write. The same reasoning restricts the method qualifier to http — the only source whose tools record an endpoint — so mcp.post: is rejected at save time instead of saved as a pattern nothing could ever match.

Backward compatibility was the risk worth designing around: making the source itself http.post would have stopped http:* matching in every existing config, removing gating silently. The source is unchanged; endpoint identity travels in a parallel map behind a classify() overload, and a test asserts http:* still gates every http tool when no endpoint data is present.

MCP tool calls can now run as the chatting user

They previously ran as whatever static credential the config named, and a ${caller:token} there passed through the global-variable and secret resolvers untouched and was sent as the literal placeholder — failing silently rather than closed.

The transport supported this all along: customHeaders has three overloads and EDDI used the constant one. The per-request McpHeadersSupplier overload makes the credential per-call while the client stays cached. The split it produces is the one we want and langchain4j enforces it — initialize and tools/list carry a null invocation context, so discovery cannot run under one user's credential and then be reused for everyone's calls.

Fixed in passing: a privilege bug

MCP clients were cached by URL alone, so two agents naming the same server with different credentials silently shared whichever client was constructed first — the second borrowed the first's authorization. The key now includes a digest of the configured credential: a digest so a literal key never becomes a map key that could reach a heap dump, taken unresolved so configs sharing a vault reference still share a client. No per-user client explosion — a caller-bound config yields one client whose supplier reads the caller per request.

Not solved, deliberately

On an expired MCP session the transport retries initialize() on an HTTP callback thread where the caller binding does not exist. That path now sends the request unauthenticated rather than falling back to the static key under the caller's intent — a visible failure instead of the wrong authority.

Verification

566 tests pass on a clean build across the affected suites; checkstyle clean. Each fix was mutation-checked by reverting it and confirming a test fails.

Worth flagging for reviewers: four tests written during this work were found vacuous by that check and rewritten. Three asserted against a helper or against state seeded through the very wrapper under test; one seeded a thread binding that the fix then restored, so it passed either way. The mutation check caught every one — the tests here are green and verified to fail without their fix.

Review notes

Three earlier claims were corrected during this work, each of which changed a decision:

  • McpCallsConfiguration.toolsWhitelist does not fail closed — filtering is skipped when the list is empty, the same shape as the approval gate.
  • toolApprovals is not agent-only — LlmConfiguration carries a per-task override that fully replaces the agent-level block.
  • A PUT on a config does not reach a running agent: HistorizedResourceStore.update creates version + 1 while agents pin a version, so self-modification takes a chain of write → re-point → redeploy, each independently gated. The thing to guard is whatever can re-point a version reference, not PUT in general.

Next

Per-endpoint approval friction (timeoutPolicy, approvalTimeout and the pause message are still single scalars for every gated tool) · agent-readable documentation (EDDI's MCP client never reads MCP resources, so eddi://docs/* is reachable from a desktop client but not from an agent) · widening the allow-list · the Manager scope picker and approval surface.

Summary by CodeRabbit

  • New Features
    • HITL approvals can now be scoped to HTTP method + normalized endpoint paths (including templated paths).
    • Generated write tools now pass the full request payload as a single requestBody parameter.
  • Bug Fixes
    • Improved MCP discovery/auth isolation, safer cancellation and shutdown, and more reliable retry behavior.
    • Hardened workflow URI parsing and sanitized exception logging to avoid leaking raw messages.
  • Documentation
    • Updated HITL pattern language plus caller-identity and MCP server guidance, including stricter ${caller:token} placement rules.

ginccc added 6 commits July 29, 2026 19:28
A generated POST/PUT/PATCH could not carry a body. buildBodyTemplate emitted
Qute variables — one per schema property, or {requestBody} for the unresolved
$ref that is the common case here — but registered none of them, and
paramDescriptions was only ever filled from path and query parameters.
AgentOrchestrator builds the tool schema from getParameters() alone, so those
variables were invisible to the model; with strict rendering off they render
empty. Every generated write therefore went out structurally valid and
semantically empty, and failed at the far end rather than at the config.

buildBodyTemplate now returns the template together with the variables it
expects, and buildApiCall declares them. Path and query names win a collision:
they are structural, and one Qute variable cannot mean two things.

The whole-body case is also the most reviewable shape, which matters for the
approval gate — what the model wrote is exactly what gets sent, rather than
inputs to a template the approver cannot see.

Three tests, mutation-checked: dropping the registration fails all three.
27 tests pass in McpApiToolBuilderTest.
Approval patterns could only name a tool, and generated tool names come from
operationId or a slug — they say nothing about what the call does and they
drift when the spec changes. Since ToolApprovalGate fails open on an unmatched
name, a renamed or newly generated write arrived ungated and silently.

The method and path were available and discarded one line into registration
(toolSources.put(name, "http")). They now travel alongside, so a pattern may
match a tool three ways: its bare name, source:name, or source.method:path.

    "requireApproval": ["http.post:*", "http.put:*", "http.patch:*", "http.delete:*"]

gates every mutation without naming a single tool, so a newly generated
endpoint cannot arrive ungated. And because different POSTs carry different
weight:

    "requireApproval": ["http.post:/agentstore/agents"]

addresses exactly one. Both forms speak the same METHOD /path vocabulary as the
endpoint allow-list, so the two can be generated from one source instead of
hand-maintained in two.

Backward compatibility was the risk worth designing around: making the source
itself "http.post" would have stopped "http:*" matching in every existing
config, removing gating silently. The source is unchanged and endpoint identity
travels in a parallel map, behind a classify() overload; the previous signature
delegates with an empty map. A test asserts http:* still gates every http tool
when no endpoint data is present.

Two details that would be easy to break later, both pinned by tests: a path
template's braces are literals only because compile() quotes every non-wildcard
segment, and a typo'd method prefix (http.pots:) is rejected rather than
compiled into a pattern that never matches — which would itself be an ungated
write.

522 tests pass on a clean build.
Review found the new validation opened the failure it was written to close.
isMethodQualifiedSource accepted <anyKnownSource>.<method>, but only the http
branch of tool registration records an endpoint — mcp and a2a register a source
alone. So "mcp.post:*" saved cleanly, matched nothing at runtime, and because
the gate allows an unmatched call, every MCP write executed unapproved while
the config looked protected. The error message made it worse by suggesting the
method-qualified form on every unknown-prefix rejection, teaching the shape
that silently does nothing.

The qualifier is now http-only, and the hint says so. Widen it again only
alongside whatever populates endpoints for another source.

Also normalises the stored path. The httpcall config accepts "/a/b", "a/b" and
an absolute URL for the same endpoint (ApiCallExecutor applies the same
leading-slash rule), so storing it verbatim made a pattern written in the
documented shape miss two of the three — and a require-pattern that misses is
an ungated write.

docs/hitl.md documented two pattern forms; there are now three. The
endpoint-qualified form, its http-only restriction, and the path normalisation
are described there.

Both fixes mutation-checked: restoring the any-source qualifier fails a test,
and storing the raw path fails another. 414 tests pass.
Closes the two follow-ups from review together, because they share a cause: a
per-property body template.

- Every body variable became a REQUIRED tool parameter. AgentOrchestrator
  builds the tool schema from ApiCall.parameters and marks all of them
  required, and a Map<String,String> has nowhere to record optionality, so a
  PATCH of one field forced the model to restate every other — a partial update
  became a full overwrite.
- Values were substituted into the JSON unescaped. The templating engine runs
  in TEXT mode and escapes nothing, so a model-supplied value containing a
  quote could break the body or add fields the schema never declared. For an
  agent that reads untrusted content and now writes through an API, that is an
  injection boundary rather than a formatting bug.

Both disappear when the model writes the body itself: one variable, no
substitution boundary, nothing to mark optional. It is also what makes the
approval card honest — the HITL surface shows tool arguments, so "the arguments
are the request" only holds if the body is one of them.

The shape a decomposed template would have implied is preserved in the
parameter description, which now names each property with its type and marks
which are required — the model's only clue about what to write.

Three existing tests asserted the decomposed shape and were rewritten rather
than deleted, since the behaviour changed deliberately. The invariant test no
longer names specific variables: it extracts every variable the template
references and asserts each is declared, so it keeps holding whatever shape the
template takes.

547 tests pass on a clean build.
An MCP tool call ran as whatever static credential the config named, so an
agent could not use MCP "on behalf of the user" the way an apicall can. A
${caller:token} there was passed through GlobalVariableResolver and
SecretResolver unchanged — neither handles the caller namespace — and sent as
the literal placeholder, failing silently rather than closed.

The transport supported this all along. customHeaders has three overloads and
EDDI used the Map one, which langchain4j wraps in a constant lambda; the
McpHeadersSupplier overload is applied per request inside createRequest. Using
it makes the credential per-call while the client stays cached.

The split it produces is the one we want, and the library enforces it: the
initialize handshake and tools/list carry a null invocation context, so
discovery cannot run under a user's credential and then be reused for
everyone's calls. Only tools/call carries the caller. A caller-bound config
therefore sends discovery unauthenticated and lets the server decide.

Also fixes a privilege bug this exposed rather than caused. Clients were cached
by URL alone, so two agents naming the same server with DIFFERENT credentials
silently shared whichever client was built first — the second borrowed the
first's authorization. The key now includes a digest of the configured
credential. Digest, not the value, so a literal key never becomes a map key
that could reach a heap dump; taken unresolved, so configs sharing one vault
reference still share a client. This does not multiply clients per user: a
caller-bound config yields one client whose supplier reads the caller per
request. closeClient closes every client for a URL, since a URL can now hold
more than one.

Worth noting what is NOT solved: on an expired MCP session the transport
retries initialize() on an HttpClient callback thread where the caller binding
does not exist. That path now sends the request unauthenticated instead of
silently falling back to a static key under the caller's intent — visible
failure rather than wrong authority.

My first credential-isolation tests were vacuous: they exercised cacheKey()
directly, so reverting the lookup to config.getUrl() left them all green. The
call site is now pinned by seeding the credential-aware key and requiring a
cache hit — mutation-checked, that reversion fails it.

415 tests pass on a clean build.
…oundation

# Conflicts:
#	src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
#	src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java
Copilot AI review requested due to automatic review settings July 29, 2026 21:41
@ginccc
ginccc requested a review from rolandpickl as a code owner July 29, 2026 21:41
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR changes HTTP request-body modeling to use one whole-body variable, adds endpoint-qualified HITL approval matching, and updates MCP authentication, metrics, caches, and circuit isolation for caller and credential separation. Documentation and tests cover these behaviors.

Changes

Operator write foundation

Layer / File(s) Summary
Whole-body request tool schemas
src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java, src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java
Request bodies use a declared whole-body parameter with schema-derived descriptions, collision-safe naming, and coverage for schema variants.
Endpoint-qualified HITL approval
src/main/java/ai/labs/eddi/engine/hitl/tools/*, src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java, src/test/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGateTest.java, src/test/java/ai/labs/eddi/modules/llm/impl/*Test.java
Approval matching supports normalized HTTP methods and endpoint paths, with endpoint metadata passed from discovery into the approval gate.
Caller-scoped MCP clients and metrics
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java, src/main/java/ai/labs/eddi/engine/security/CallerIdentityResolver.java, src/test/java/ai/labs/eddi/modules/llm/impl/*Test.java
MCP discovery is unauthenticated, tool calls use per-request caller authorization, caches and circuit breakers use credential-derived keys, and discovery outcomes are metered without sensitive tags.
Release documentation
docs/*, AGENTS.md
Documentation records endpoint matching, whole-body tools, caller-bound MCP behavior, and related changelog corrections.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentOrchestrator
  participant ToolApprovalGate
  participant ToolApprovalPatterns
  AgentOrchestrator->>ToolApprovalGate: classify tool call with endpoint metadata
  ToolApprovalGate->>ToolApprovalPatterns: match normalized method and path
  ToolApprovalGate-->>AgentOrchestrator: approval decision
Loading
sequenceDiagram
  participant McpToolProviderManager
  participant CallerIdentityContext
  participant McpServer
  McpToolProviderManager->>McpServer: discover tools without caller token
  McpToolProviderManager->>CallerIdentityContext: resolve caller token
  CallerIdentityContext-->>McpToolProviderManager: authorization header
  McpToolProviderManager->>McpServer: invoke tool with caller token
Loading

Possibly related PRs

  • labsai/EDDI#470: Modifies MCP API-key resolution in the same manager where this PR changes credential-aware authentication and caching.
  • labsai/EDDI#606: Modifies the AgentOrchestrator tool-calling execution path also changed here for endpoint-qualified HITL approval wiring.

Suggested reviewers: rolandpickl, copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title broadly matches the PR’s goal of enabling a safe operator agent with write-related groundwork.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/operator-write-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR lays the groundwork for a workspace “operator” agent that can safely perform future deployment mutations by fixing MCP/http tool generation and HITL approval matching so that (a) generated write requests are actually representable to the model and (b) approval patterns can reliably gate mutations by HTTP method+path rather than unstable tool names.

Changes:

  • Make generated OpenAPI/MCP httpcall request bodies model-supplied via a single whole-body parameter, and ensure body template variables are declared in the tool schema.
  • Add endpoint provenance (method:path) for httpcall tools and extend HITL approval patterns to match http.<method>:/path (with backward compatibility).
  • Allow MCP calls to run under the chatting user via per-request header resolution, and isolate cached MCP clients by URL + credential digest.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java Caller-bound MCP auth, credential-aware client cache keying, and updated close semantics.
src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java Records http tool endpoint provenance and adds endpoint path normalization for approval matching.
src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java Whole-body request template + parameter declaration for model-written request bodies.
src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGate.java Adds endpoint-qualified matching support during tool-call classification.
src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java Validates method-qualified http.<method>: prefixes for approval patterns.
src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java Adds tests for credential-isolated client cache keys.
src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorTest.java Adds tests for endpoint path normalization; updates record constructor usage.
src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorExtendedTest.java Updates record constructor usage.
src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorCoverageTest.java Updates ToolSetup constructor usage.
src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java Updates body template expectations and adds parameter/required-shape assertions.
src/test/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGateTest.java Adds tests covering method/path-qualified patterns and backward compatibility.
docs/hitl.md Documents endpoint-qualified approval patterns and method qualification constraints.
docs/changelog.md Adds a changelog entry describing the operator-write foundation work and rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/hitl.md Outdated
Comment thread src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
…dings

The discovery/invocation split was decided by asking whether a caller was bound
to the thread — but discovery runs inside the turn, on a thread that IS bound.
So initialize and tools/list went out with the first caller's token, and since
the client is cached that session was reused by everyone after them, with a tool
list reflecting one user's permissions offered to the next. langchain4j
distinguishes the two (listTools delegates a null InvocationContext,
McpToolExecutor always builds one) but enforces nothing; EDDI now reads
McpCallContext.invocationContext() and acts on it. Static analysis flagged the
parameter as unused, which it was — the fix was to use it, not drop it.

Four more from the same review round:

- A caller-bound key was put in the header raw while the static path prefixed
  "Bearer ". apiKey is documented as a key/token, so the natural
  apiKey: "${caller:token}" sent a bare token with no scheme and the server
  would reject it. Both paths now prefix, and an author who spells the scheme
  out is not double-prefixed.
- The tool cache was still keyed by URL alone. CachedTools holds executors bound
  to a client, so tools discovered under one credential could be reused under
  another — the same cross-credential leak fixed at the client layer, left open
  one level up. Both now key on the credential, and closeClient sweeps the tool
  cache by prefix rather than dropping a key that no longer exists.
- callerBound was detected with containsReference, which only matches the
  documented ${caller:...} form. A bare {caller:token} looked like a static key
  and was sent as literal text; rejectUnsupportedReference now turns it into the
  clear error it was written for.
- normalizeEndpointPath treated anything starting with "http" as a URL. Narrowed
  to a real http(s):// prefix: an opaque URI such as "httpfoo:bar" has a null
  path, so the loose test collapsed it to empty and the endpoint provenance —
  and the approval pattern's ability to match — vanished.

docs/hitl.md and the validation message both still listed the old allowed
character set; endpoint patterns permit / { } and now say so, in the two places
that had drifted.

The first version of the path test was vacuous: a relative URI keeps its path,
so both implementations agreed on "httpcalls/agents". It now asserts the opaque
case that actually distinguishes them, verified to fail against the old code.

810 tests pass on a clean build.
Copilot AI review requested due to automatic review settings July 29, 2026 23:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java:290

  • The whole-body variable name ("requestBody") can collide with an explicit query/path parameter of the same name. Because body variables are added with putIfAbsent, that collision would skip declaring the body parameter even though the body template still references it, reintroducing the "empty body" failure mode (the model can’t supply an undeclared parameter and Qute renders it empty). Consider renaming the body variable on collision so it’s always declared.
                // built from getParameters() alone (AgentOrchestrator), and with
                // strict-rendering off an undeclared variable renders as empty. The
                // request would go out structurally valid and semantically empty.
                // Path and query names win a collision — those are structural.
                body.variables().forEach(paramDescriptions::putIfAbsent);

@ginccc

ginccc commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java (1)

261-283: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a top-level import instead of the inline FQN for Pattern.

Line 275 uses java.util.regex.Pattern.compile(...) inline rather than importing Pattern at the top of the file.

🧹 Proposed fix
+import java.util.regex.Pattern;
@@
-        var matcher = java.util.regex.Pattern.compile("\\{([A-Za-z0-9_]+)}").matcher(createPet.getRequest().getBody());
+        var matcher = Pattern.compile("\\{([A-Za-z0-9_]+)}").matcher(createPet.getRequest().getBody());

As per coding guidelines, "Use simple names with top-level imports instead of inline fully qualified names; only use an inline FQN to disambiguate conflicting types, and remove unused imports."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java` around
lines 261 - 283, Replace the inline java.util.regex.Pattern reference in
parseAndBuild_requestBodyVariablesAreDeclaredAsParameters with the imported
Pattern simple name, adding the top-level import and removing any now-unused or
conflicting import as needed.

Source: Coding guidelines

src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java (1)

327-351: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale javadoc describes removed per-property decomposition behavior.

The comment at Lines 327-333 ("Produces a Qute-templated JSON body where each property is a template variable... Only handles flat schemas...") describes the old decomposition logic that this PR removes, and now sits orphaned above the new BodyTemplate record instead of buildBodyTemplate. Similarly, the WHOLE_BODY_VARIABLE javadoc (Line 350) says it's "used when the schema has no properties," but buildBodyTemplate now uses it unconditionally for every non-null schema (Line 376). Both are misleading to future maintainers who might reintroduce per-property decomposition based on outdated docs.

📝 Proposed doc fix
-    /**
-     * Build a JSON body template from a schema. Produces a Qute-templated JSON body
-     * where each property is a template variable.
-     * <p>
-     * Note: Only handles flat schemas (direct properties). Nested objects and
-     * arrays fall back to a single {`@code` {requestBody}} template variable.
-     */
     /**
      * A request-body template together with the tool parameters the model must
      * supply to fill it.
@@
     /** Name of the whole-body variable used when the schema has no properties. */
+    /** Name of the single whole-body variable used for every JSON request body. */
     static final String WHOLE_BODY_VARIABLE = "requestBody";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java` around lines
327 - 351, Remove the stale schema-decomposition Javadoc above BodyTemplate, or
move only any still-accurate documentation to the buildBodyTemplate method.
Update WHOLE_BODY_VARIABLE’s Javadoc to state that requestBody is the whole-body
template variable used for every non-null schema, matching buildBodyTemplate’s
current behavior.
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java (3)

385-424: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No new metrics for the credential-isolation/caller-bound auth feature.

This introduces a security-relevant behavior change (per-credential client isolation, per-request caller-bound headers, discovery-vs-invocation authorization split) with no MeterRegistry instrumentation — e.g. a counter for "MCP tool call sent unauthenticated because no caller was bound" (line 617-618) would make silent auth failures in production visible instead of only appearing in DEBUG logs. As per coding guidelines, "Add metrics to new features using Micrometer MeterRegistry, including counters, timers, or gauges as appropriate."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 385 - 424, Instrument the credential-isolation and caller-bound
authorization flow in McpToolProviderManager using the existing Micrometer
MeterRegistry, adding a counter for MCP tool calls sent without a bound caller
at the unauthenticated invocation path around getOrCreateClient and the
caller-header handling. Ensure the counter is incremented whenever that silent
unauthenticated request occurs, while preserving the existing client caching and
authorization behavior.

Source: Coding guidelines


70-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale javadoc on clientCache/toolCache after the rekeying change.

Both field comments still say "keyed by server URL" / "per server URL", but getOrCreateClient/discoverTools now key on cacheKey(serverConfig) (URL + credential digest). This is exactly the distinction the PR is built around (privilege isolation between different credentials on the same URL), so leaving the doc unchanged risks a future maintainer reasoning about the cache with the pre-fix mental model.

📝 Suggested doc update
     /**
-     * Cache of active MCP clients, keyed by server URL. Connections are reused
-     * across conversation turns to avoid reconnect overhead.
+     * Cache of active MCP clients, keyed by {`@link` `#cacheKey`(McpServerConfig)}
+     * (server URL plus a digest of the configured credential). Connections are
+     * reused across conversation turns to avoid reconnect overhead.
      */
     private final Map<String, McpClient> clientCache = new ConcurrentHashMap<>();

     /**
-     * Cache of discovered tool specs/executors per server URL. Finding F12: without
+     * Cache of discovered tool specs/executors, keyed the same way as
+     * {`@link` `#clientCache`}. Finding F12: without
      * this, every conversation turn issued a live {`@code` tools/list} RPC to every
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 70 - 82, Update the Javadoc for clientCache and toolCache to state
that entries are keyed by cacheKey(serverConfig), incorporating the server URL
and credential digest rather than the URL alone. Keep the existing reuse and
discovery-cache behavior descriptions while documenting credential-based
isolation consistently with getOrCreateClient and discoverTools.

403-416: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

"|" delimiter in cacheKey/closeClient isn't escaped against a URL that itself contains "|".

cacheKey builds url + "|" + digest, and closeClient matches on url + "|" as a prefix. If two configured URLs are such that one is a "|"-prefix of another's cache key (e.g. http://x vs. http://x|anonymous), closeClient("http://x") would also tear down the second server's unrelated cached client, since its key also starts with "http://x|". validateServerUrl doesn't reject | in the URL, so this isn't structurally impossible, just very unlikely in practice.

Using a compound key (e.g. a small record/Map.entry(url, digest) as the cache key type) instead of a delimited string would remove the ambiguity entirely.

Also applies to: 630-654

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 403 - 416, The cache key and close-client matching use an ambiguous
delimiter when URLs contain “|”. Update cacheKey and closeClient to use a
structured key containing the URL and credential identity (for example, a record
or Map.Entry) rather than string concatenation and prefix matching, while
preserving anonymous and hashed-credential isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java`:
- Around line 76-80: Update the validation around KNOWN_SOURCES and
isMethodQualifiedSource in ToolApprovalPatterns to reject endpoint-looking
prefixes such as unqualified http or mcp targets unless they use a valid
http.<method> qualification. Require qualified http targets to have a remainder
beginning with / or *, so http.post:agents is rejected while valid path/template
targets remain accepted. Add regression coverage for http:/…, mcp:/…, and
http.post:agents.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java`:
- Around line 386-415: Update describeBodySchema to branch its opening
description on schema.getType(): retain the single JSON object wording for
object schemas, and describe array or primitive schemas according to their
actual top-level shape. Ensure property enumeration and its existing
required/description details remain limited to object schemas so the generated
request-body guidance matches the schema structure.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 601-622: Update createTransport and its
discoverTools/client-construction flow to validate caller-bound apiKey
configurations before caching the transport. When apiKey contains a
${caller:...} reference while caller-identity is disabled, force the existing
discovery or client validation path to resolve and report the failure as
INVALID_CONFIGURATION instead of deferring CallerIdentityException to
authorizationHeader during a request. Preserve current validation for URL and
transport errors.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java`:
- Around line 327-351: Remove the stale schema-decomposition Javadoc above
BodyTemplate, or move only any still-accurate documentation to the
buildBodyTemplate method. Update WHOLE_BODY_VARIABLE’s Javadoc to state that
requestBody is the whole-body template variable used for every non-null schema,
matching buildBodyTemplate’s current behavior.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 385-424: Instrument the credential-isolation and caller-bound
authorization flow in McpToolProviderManager using the existing Micrometer
MeterRegistry, adding a counter for MCP tool calls sent without a bound caller
at the unauthenticated invocation path around getOrCreateClient and the
caller-header handling. Ensure the counter is incremented whenever that silent
unauthenticated request occurs, while preserving the existing client caching and
authorization behavior.
- Around line 70-82: Update the Javadoc for clientCache and toolCache to state
that entries are keyed by cacheKey(serverConfig), incorporating the server URL
and credential digest rather than the URL alone. Keep the existing reuse and
discovery-cache behavior descriptions while documenting credential-based
isolation consistently with getOrCreateClient and discoverTools.
- Around line 403-416: The cache key and close-client matching use an ambiguous
delimiter when URLs contain “|”. Update cacheKey and closeClient to use a
structured key containing the URL and credential identity (for example, a record
or Map.Entry) rather than string concatenation and prefix matching, while
preserving anonymous and hashed-credential isolation.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java`:
- Around line 261-283: Replace the inline java.util.regex.Pattern reference in
parseAndBuild_requestBodyVariablesAreDeclaredAsParameters with the imported
Pattern simple name, adding the top-level import and removing any now-unused or
conflicting import as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d5cfe5f8-9b15-4a44-a61c-0ef9f934835f

📥 Commits

Reviewing files that changed from the base of the PR and between fd0152b and 02d5eea.

📒 Files selected for processing (13)
  • docs/changelog.md
  • docs/hitl.md
  • src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGate.java
  • src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java
  • src/test/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGateTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorCoverageTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorExtendedTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java

Comment thread src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java
ginccc added 2 commits July 30, 2026 09:21
CodeRabbit found two more shapes of the fail-open I have now fixed three times.
Running the validator confirmed all of these saved cleanly and matched nothing,
which for a require-rule leaves the call allowed while the config looks like
protection:

    http:/agentstore/agents   — a path needs a method-qualified prefix
    mcp:/x                    — mcp tools carry no endpoint at all
    http.post:agents          — an endpoint path always begins with '/'

Rather than name a third special case, validation now enforces the invariant in
both directions: an endpoint-shaped target requires http.<method>, and a
method-qualified prefix requires a target that starts with '/' or '*'. Wildcard
prefixes such as http.*:/x are left alone — those do match.

Also from the same review:

- describeBodySchema opened with "a single JSON object" whatever the schema
  declared. For a top-level array the model wraps the payload in braces and the
  API rejects a request the config cannot explain; it now names the real
  container.
- An MCP apiKey holding ${caller:...} with eddi.caller-identity.enabled=false
  threw once per request, forever, for a mistake made once. It is validated
  alongside the URL and transport checks and reported as INVALID_CONFIGURATION,
  where an operator will see it. CallerIdentityResolver gained isEnabled() for
  that.

812 tests pass on a clean build.
Copilot AI review requested due to automatic review settings July 30, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java:356

  • When the OpenAPI media type declares a requestBody but its schema is null, buildBodyTemplate() returns a fixed "{}" and declares no parameters. That recreates the original failure mode for those specs: the model has no way to provide a body and every write goes out semantically empty. Treat a null schema the same as an undecomposable schema by using the whole-body variable and declaring it as a tool parameter.
    private static BodyTemplate buildBodyTemplate(Schema<?> schema) {
        if (schema == null) {
            return new BodyTemplate("{}", Map.of());
        }

src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java:412

  • cacheKey() truncates the SHA-256 digest to 8 bytes (64 bits). Because this key is a privilege boundary (it determines whether two different credentials can share a client), a hash collision would reintroduce credential-sharing across agents. Using the full digest (or at least a larger prefix) avoids collision risk without leaking the credential.
        try {
            var digest = MessageDigest.getInstance("SHA-256").digest(apiKey.getBytes(StandardCharsets.UTF_8));
            return config.getUrl() + "|" + HexFormat.of().formatHex(digest, 0, 8);
        } catch (NoSuchAlgorithmException e) {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java (1)

628-635: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when a caller-bound tool call has no caller context.

A caller-bound credential currently sends the tool request without Authorization when the context is absent. That can execute against an anonymous MCP role rather than the chatting user’s permissions. Throw CallerIdentityException here (or let resolveValue() do so) instead of returning an empty header map.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 628 - 635, Update the no-caller branch in McpToolProviderManager’s
caller-bound credential resolution to fail closed by throwing
CallerIdentityException, or by delegating to resolveValue() so it throws,
instead of returning Map.of(). Preserve the existing behavior for calls with a
valid caller context.
docs/changelog.md (1)

123-132: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the changelog entry separator.

Line [123] starts the next entry without the --- delimiter used between the preceding entries. Add the separator to preserve consistent changelog structure and rendering.

Proposed fix
+---
+
 ## 🔎 fix(llm): review follow-ups — workflow version parse, log sanitization (2026-07-29)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/changelog.md` around lines 123 - 132, Add the missing `---` separator
immediately before the changelog entry beginning with “fix(llm): review
follow-ups,” matching the delimiter structure used between the preceding
entries. Leave the entry content unchanged.
🧹 Nitpick comments (1)
src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java (1)

294-294: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a top-level Pattern import.

Pattern is only referenced once and there is no conflicting type here, so java.util.regex.Pattern.compile(...) should be written with a top-level java.util.regex.Pattern import instead of an inline FQN.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java` at line
294, Import java.util.regex.Pattern at the top of McpApiToolBuilderTest and
update the matcher creation to use Pattern.compile(...) instead of the fully
qualified name.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java`:
- Around line 90-93: Update the validation in ToolApprovalPatterns to reject
wildcard source prefixes targeting endpoint paths when they cannot match any
runtime http.<method> key, including mcp*:/x. Preserve valid wildcard patterns
such as http.*:/x and *:/x, while retaining the existing rejection for
non-method-qualified endpoint patterns.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 466-471: The validateCallerBoundKey method only handles supported
caller references; add validation for malformed or unsupported caller-reference
syntax before createTransport is reached, including forms such as {caller:token}
and unsupported ${caller:...} tokens. Reuse CallerIdentityResolver’s existing
rejection/validation behavior and translate any resulting exception into
IllegalArgumentException so these cases are classified as invalid configuration
rather than CONNECTION_FAILURE.

---

Outside diff comments:
In `@docs/changelog.md`:
- Around line 123-132: Add the missing `---` separator immediately before the
changelog entry beginning with “fix(llm): review follow-ups,” matching the
delimiter structure used between the preceding entries. Leave the entry content
unchanged.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 628-635: Update the no-caller branch in McpToolProviderManager’s
caller-bound credential resolution to fail closed by throwing
CallerIdentityException, or by delegating to resolveValue() so it throws,
instead of returning Map.of(). Preserve the existing behavior for calls with a
valid caller context.

---

Nitpick comments:
In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java`:
- Line 294: Import java.util.regex.Pattern at the top of McpApiToolBuilderTest
and update the matcher creation to use Pattern.compile(...) instead of the fully
qualified name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 301e856d-2258-4a94-821c-d06b1ff07aec

📥 Commits

Reviewing files that changed from the base of the PR and between 02d5eea and d763b32.

📒 Files selected for processing (7)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java
  • src/main/java/ai/labs/eddi/engine/security/CallerIdentityResolver.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java
  • src/test/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGateTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java

Comment thread src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java Outdated
Round two from CodeRabbit and Copilot, four findings.

The wildcard escape hatch in the previous commit was too wide: "mcp*:/x"
contains a '*' so it skipped the path check, yet no endpoint key can ever match
it, so the pattern saved and gated nothing. Rather than reason about glob
shapes, the prefix is now compiled and tried against every http.<method> EDDI
actually emits — the same question the gate asks at runtime. "http.*:/x" and
"*:/x" keep working because they genuinely match.

That is the fifth shape of this one bug. The invariant is simply that a pattern
which cannot match must not save, and it is now checked by construction rather
than by enumerating the ways to get it wrong.

- A declared request body whose media type has no schema returned "{}" and
  declared no variable — recreating, for that spec shape, the empty-body bug
  this branch exists to fix. It now uses the whole-body variable like any other
  undecomposable schema.
- A malformed caller reference ({caller:token}, ${caller:tokn}) reached
  createTransport, threw there, and was recorded as CONNECTION_FAILURE — which
  blames the server and trips the circuit breaker for a mistake the config made.
  It is validated with the URL and transport checks and reported as
  INVALID_CONFIGURATION.
- The client cache key truncated its SHA-256 to 64 bits. That key decides
  whether two credentials share a client, so a collision would reintroduce the
  privilege leak it was added to close; the full digest costs nothing.

813 tests pass on a clean build.
Copilot AI review requested due to automatic review settings July 30, 2026 07:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java:291

  • buildBodyTemplate() always uses the requestBody variable, but buildApiCall() only registers body variables via putIfAbsent. If an OpenAPI operation already defines a query/path parameter named requestBody, the body variable will be skipped, leaving the template variable undeclared again (so the model can’t fill it and the rendered body can still go out empty). Consider renaming the body variable on collision so the template and parameters stay in sync.
                // strict-rendering off an undeclared variable renders as empty. The
                // request would go out structurally valid and semantically empty.
                // Path and query names win a collision — those are structural.
                body.variables().forEach(paramDescriptions::putIfAbsent);
            }

src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java:599

  • withBearerPrefix() uses value.trim() only for the scheme check but then returns the original value, preserving leading/trailing whitespace. That can produce an Authorization header with unexpected spaces (or even a newline if misconfigured). Trimming once and returning the trimmed value keeps behavior predictable.
        if (value == null || value.isBlank()) {
            return value;
        }
        // A scheme is one token followed by a space; anything else is a bare secret.
        return value.trim().matches("(?i)(bearer|basic|token|apikey)\\s.+") ? value : "Bearer " + value;

src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java:2519

  • normalizeEndpointPath() logs the raw configured path on parse failure. Since this value can include templates or other untrusted config text, it’s safer/consistent to pass it through LogSanitizer.sanitize() to avoid log injection (newlines, control chars).
                // Not parseable as a URI — a templated host, most likely. Leave it be:
                // matching something odd is better than throwing during discovery.
                LOGGER.debugf("Could not normalise endpoint path '%s' for approval matching", rawPath);
                return path;

docs/hitl.md:264

  • The docs say each call is tested against source:name first, then the bare name, but the implementation now tries the endpoint-qualified form (source.method:path) before source:name (when endpoint provenance exists). Updating this sentence avoids readers assuming endpoint patterns are evaluated later.
- **`*` is the only wildcard** — it matches any run of characters (including empty). Every other character is a quoted literal, so compilation is **ReDoS-safe**.
- **Source-qualified or bare.** A pattern may carry a known source prefix (`mcp:read_*`, `http:*`) or match the bare tool name (`delete_account`). Each call is tested against `source:name` **first**, then the bare dispatch name — a tool with an unknown source still matches bare-name patterns (fail-safe).
- **Endpoint-qualified** (`http` tools only). A pattern may also address *what a tool calls* rather than what it is named: `http.post:*` matches every POST, `http.post:/agentstore/agents` matches exactly one endpoint. This is the robust form for tools generated from an OpenAPI spec, whose names come from `operationId` or a slug and change when the spec does — `http.post:*` keeps gating every mutation even when a name changes or a new endpoint appears. The path is matched **as the httpcall config declares it**, normalised to a leading slash (an absolute URL contributes only its path).

src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java:266

  • Now that clients/tools are cached per cacheKey(serverConfig) (URL + credential digest), the circuit breaker still keys failures by the bare url (see isCircuitOpen(url) / recordFailure(url) / recordSuccess(url)). If two MCP server entries share a URL but use different credentials, repeated auth failures for one credential can open the circuit for the whole URL and suppress discovery for the other (valid) credential too. Consider keying the circuit breaker state by the same cache key (or URL+credential) to keep isolation consistent.
            // F12: serve the tool list from the TTL cache when it is still fresh —
            // a live tools/list RPC per server per turn was pure overhead. Nothing
            // reaches the cache without having passed validation first, so this is
            // checked before the (potentially DNS-resolving) validation below.
            CachedTools cached = toolCache.get(cacheKey(serverConfig));

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java:290

  • Potential parameter-name collision: the request body is always {requestBody}, but it is only added to ApiCall.parameters via putIfAbsent. If an OpenAPI operation has a query/path parameter named requestBody, the body parameter will be dropped and the model again has no declared way to populate the request body (recreating the original empty-body failure mode for that endpoint).
                // built from getParameters() alone (AgentOrchestrator), and with
                // strict-rendering off an undeclared variable renders as empty. The
                // request would go out structurally valid and semantically empty.
                // Path and query names win a collision — those are structural.
                body.variables().forEach(paramDescriptions::putIfAbsent);

src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java:599

  • withBearerPrefix checks value.trim() to detect an explicit scheme, but then returns the original untrimmed value (or prefixes the untrimmed value). Leading/trailing whitespace in a configured or resolved token will be sent on the wire and can cause authentication failures (e.g., pasted secrets with a trailing newline).
        if (value == null || value.isBlank()) {
            return value;
        }
        // A scheme is one token followed by a space; anything else is a bare secret.
        return value.trim().matches("(?i)(bearer|basic|token|apikey)\\s.+") ? value : "Bearer " + value;

Five findings from the review round on the docs commit.

The circuit breaker still keyed failures by bare URL after the client and tool
caches moved to a credential-aware key. Two configs pointing at one server with
different credentials therefore shared a circuit: one agent's revoked key would
trip discovery for the other, still-valid one. Keyed by the same cacheKey now,
with an isCircuitOpen(McpServerConfig) overload — the contract callers actually
want, since the key is a credential-aware implementation detail and asking by
URL gives the wrong answer.

Two tests drove real discovery and then asserted by URL; they ask by config now.
The rest seed and read the same string and are unaffected.

- A spec naming a path or query parameter "requestBody" made putIfAbsent skip
  the body variable, leaving the template referencing something undeclared —
  the empty-body bug again, for that one spec shape. The body variable is
  renamed on collision instead of dropped; the structural parameter keeps its
  name.
- withBearerPrefix trimmed only for its scheme check and returned the original,
  so leading or trailing whitespace — a newline above all — could reach an
  Authorization header. It returns the trimmed value.
- normalizeEndpointPath logged the raw configured path; sanitized, consistent
  with main's CWE-117 sweep.
- docs/hitl.md claimed a call is tested against source:name first. The
  endpoint-qualified form is tried before it. Corrected — the doc described the
  order I had written, not the one the code runs.

815 tests pass on a clean build.
Copilot AI review requested due to automatic review settings July 30, 2026 08:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGate.java:36

  • The class-level Javadoc still says patterns are matched against source:name first, but the implementation now tries the endpoint-qualified form first (source.method:path). This is an important behavioral detail for config authors, so the Javadoc should be updated to match the new precedence.
    public GateResult classify(List<ToolExecutionRequest> batch, Map<String, String> toolSources,
                               ToolApprovalsConfig cfg, Set<String> clearedCallIds) {
        return classify(batch, toolSources, Map.of(), cfg, clearedCallIds);
    }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java (1)

347-366: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the $ref test match its fixture.

The fixture defines #/components/schemas/Thing at Line 357, so the reference is resolvable. This test does not cover the “unresolved $ref” case described by its name and comment; either use a genuinely unresolved reference and assert the parser’s behavior, or rename the test to cover an empty resolved schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java` around
lines 347 - 366, Align parseAndBuild_wholeBodyVariableIsDeclared with its
fixture by either changing the schema reference to an actually unresolved $ref
and retaining assertions for the unresolved-reference behavior, or renaming the
test and comment to describe the empty resolved Thing schema while preserving
the corresponding expected behavior.
src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java (1)

407-445: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve nested body schema shape in the parameter description
src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java:407-445 only describes top-level properties. Nested objects, array item schemas, and integer vs. number constraints are lost, so the model can emit bodies that no longer match the OpenAPI schema. Recursively render nested shapes or reject unsupported schemas, and add nested object/array coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java` around lines
407 - 445, Extend describeBodySchema to recursively preserve nested object
properties, array item schemas, and numeric distinctions or constraints when
building the parameter description. Reuse the existing schema type and property
rendering flow, adding a focused recursive helper for nested schemas and
coverage for nested objects and arrays; do not silently flatten unsupported
shapes.
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java (1)

594-600: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trim Authorization values before returning them.

value.trim() is used only for matching; the qualified branch returns the original value, and the bare branch prefixes the untrimmed value. Leading or trailing whitespace therefore survives.

Proposed fix
 if (value == null || value.isBlank()) {
     return value;
 }
+value = value.trim();
-return value.trim().matches("(?i)(bearer|basic|token|apikey)\\s.+") ? value : "Bearer " + value;
+return value.matches("(?i)(bearer|basic|token|apikey)\\s.+") ? value : "Bearer " + value;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 594 - 600, Update withBearerPrefix to trim the input once and use
the trimmed value for both scheme detection and the returned result, ensuring
qualified Authorization values and bearer-prefixed secrets contain no leading or
trailing whitespace.
🧹 Nitpick comments (3)
src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java (1)

256-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import Pattern at the top level.

The new test references java.util.regex.Pattern inline at Line 256. Import Pattern and use it directly. As per coding guidelines, reference types and annotations through top-level imports rather than fully qualified names inline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java` at line
256, Update McpApiToolBuilderTest by adding a top-level import for
java.util.regex.Pattern, then change the matcher declaration to reference
Pattern directly instead of using its fully qualified name.

Source: Coding guidelines

src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java (1)

351-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the body-template Javadoc.

The preceding Javadoc still describes per-property variables and array fallback, but the implementation now always emits one whole-body variable; it is also no longer attached to buildBodyTemplate. Move and update it so the documented contract matches the code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java` around lines
351 - 370, Move the Javadoc from the BodyTemplate record to buildBodyTemplate
and revise it to document that the method always emits a single whole-body
variable, including schemas with properties or arrays. Remove references to
per-property variables and array fallback, and describe the resulting template
and variable contract accurately.
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java (1)

720-738: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Attach the circuitKey Javadoc to the correct overload.

The @param circuitKey block is separated from the declarations by another Javadoc block, so it is orphaned while isCircuitOpen(String) remains undocumented. Merge the blocks or move the parameter documentation directly above the string overload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 720 - 738, Move the existing circuitKey `@param` documentation so it
directly precedes the isCircuitOpen(String circuitKey) overload, merging it with
that method’s description as needed. Keep the isCircuitOpen(McpServerConfig
config) Javadoc focused on the config overload and ensure both declarations have
correctly attached documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 840-843: Synchronize caller-token restrictions across the
documented sites: retain the canonical complete wording in AGENTS.md lines
840-843; update docs/httpcalls.md lines 88-94 to reject ${caller:token} in query
parameters, request bodies, and paths; and expand the “Headers only” rule in
docs/security.md line 130 to include request bodies and paths.

In `@docs/mcp-server.md`:
- Around line 687-689: Update the documentation sentence near the signed-in user
HTTP call reference to replace “apicall headers” with consistent “API call
headers” terminology, leaving the remaining guarantees and link unchanged.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java`:
- Around line 288-307: Update buildApiCall to construct the effective parameter
set from both the surrounding PathItem and operation.getParameters() before
populating paramDescriptions and selecting body variable names. Ensure
path-level parameters participate in the existing collision/rename logic in the
body variable loop, while preserving operation-level precedence and behavior for
non-colliding names; add coverage for a path-level requestBody parameter
collision.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 254-255: Add Micrometer instrumentation to the credential-scoped
client/tool cache and circuit-breaker flows in McpToolProviderManager, using
MeterRegistry to track cache hits and misses, discovery outcomes and timing, and
requests skipped while the circuit is open. Keep metric tags low-cardinality and
derive them from non-sensitive identifiers such as server or outcome values;
never expose raw credentials.
- Around line 254-255: Update
McpToolProviderManagerCircuitBreakerTest.circuitOpen_skipsServer so
injectFailures(...) stores failures under the credential-aware key produced by
cacheKey(serverConfig), including the anonymous credential suffix, matching
discoverTools() lookup behavior.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java`:
- Around line 407-445: Extend describeBodySchema to recursively preserve nested
object properties, array item schemas, and numeric distinctions or constraints
when building the parameter description. Reuse the existing schema type and
property rendering flow, adding a focused recursive helper for nested schemas
and coverage for nested objects and arrays; do not silently flatten unsupported
shapes.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 594-600: Update withBearerPrefix to trim the input once and use
the trimmed value for both scheme detection and the returned result, ensuring
qualified Authorization values and bearer-prefixed secrets contain no leading or
trailing whitespace.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java`:
- Around line 347-366: Align parseAndBuild_wholeBodyVariableIsDeclared with its
fixture by either changing the schema reference to an actually unresolved $ref
and retaining assertions for the unresolved-reference behavior, or renaming the
test and comment to describe the empty resolved Thing schema while preserving
the corresponding expected behavior.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java`:
- Around line 351-370: Move the Javadoc from the BodyTemplate record to
buildBodyTemplate and revise it to document that the method always emits a
single whole-body variable, including schemas with properties or arrays. Remove
references to per-property variables and array fallback, and describe the
resulting template and variable contract accurately.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 720-738: Move the existing circuitKey `@param` documentation so it
directly precedes the isCircuitOpen(String circuitKey) overload, merging it with
that method’s description as needed. Keep the isCircuitOpen(McpServerConfig
config) Javadoc focused on the config overload and ensure both declarations have
correctly attached documentation.

In `@src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java`:
- Line 256: Update McpApiToolBuilderTest by adding a top-level import for
java.util.regex.Pattern, then change the matcher declaration to reference
Pattern directly instead of using its fully qualified name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 51a8ea9a-6343-4e5c-9d7d-bce87d66aebd

📥 Commits

Reviewing files that changed from the base of the PR and between e74880e and ec86304.

📒 Files selected for processing (12)
  • AGENTS.md
  • docs/hitl.md
  • docs/httpcalls.md
  • docs/mcp-server.md
  • docs/security.md
  • src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpApiToolBuilderTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerCircuitBreakerTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerDiscoveryTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java
  • docs/hitl.md
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java

Comment thread AGENTS.md
Comment thread docs/mcp-server.md Outdated
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpApiToolBuilder.java
Of eight unresolved review threads, four were already fixed and only looked
open — GitHub marks a thread outdated when its hunk moves, not when the finding
is addressed, so "unresolved and not outdated" is not the same as "still true".
Checked each against the code: toolCache already keys on the credential,
rejectUnsupportedReference is already called before callerBound is decided, the
illegal-character message already lists / { }, and describeBodySchema already
branches on the schema type.

The four that were real:

- Adding isCircuitOpen(McpServerConfig) left three stacked Javadoc blocks, so
  an @PARAM documented a parameter the following method does not have. Each
  overload has its own now.
- The new caches and the credential-scoped circuit breaker had no metrics, which
  AGENTS.md requires for a new feature. eddi.mcp.discovery counts cache_hit,
  success, failure and circuit_open. A test asserts no tag carries the URL or
  the credential — the URL is unbounded cardinality, and the credential is the
  thing cacheKey hashes precisely so it never becomes a map key.
- AGENTS.md said a token reference is rejected in query parameters, bodies and
  paths while httpcalls.md and security.md still mentioned only query
  parameters. The code rejects all three; the two narrower pages now say so.
- "apicall headers" reads as a typo in prose — "API call headers".

816 tests pass on a clean build.
Copilot AI review requested due to automatic review settings July 30, 2026 09:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java:2517

  • normalizeEndpointPath() collapses an absolute URL with an empty path (e.g. "https://host" or "https://host?x=1") to "". That makes endpoint-qualified approval keys like "post:" which can never be matched by patterns expecting a leading '/'. Treat an empty extracted path as "/" so root endpoints can still be gated by endpoint-qualified patterns.
        if (lower.startsWith("http://") || lower.startsWith("https://")) {
            try {
                String extracted = URI.create(path).getPath();
                path = extracted != null ? extracted : "";

src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java:627

  • withBearerPrefix() trims the value for matching but returns the original (potentially whitespace-padded) string when it already has a scheme. That can emit an invalid Authorization header like " Bearer …" and is also inconsistent with the prefixed branch. Safer to trim once and use the trimmed value for both branches.
        if (value == null || value.isBlank()) {
            return value;
        }
        // A scheme is one token followed by a space; anything else is a bare secret.
        return value.trim().matches("(?i)(bearer|basic|token|apikey)\\s.+") ? value : "Bearer " + value;

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java (1)

664-671: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when a caller-bound tool call has no caller.

Returning Map.of() still sends the MCP request without authorization. For ${caller:...} tool calls, throw before the transport sends the request; discovery can remain unauthenticated only when it is not caller-bound. Also update unboundToolCallSendsNothing to expect authentication failure instead of empty headers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`
around lines 664 - 671, The caller-bound MCP tool path must fail before
transport when callerIdentityContext.current() is null instead of returning
Map.of() and sending unauthenticated. Update the logic around caller-bound
detection and the no-caller branch in McpToolProviderManager to throw the
established authentication failure, while preserving unauthenticated discovery
for non-caller-bound calls; update unboundToolCallSendsNothing to assert
authentication failure rather than empty headers.
🧹 Nitpick comments (1)
src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java (1)

307-316: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use imports for Micrometer test types.

Import SimpleMeterRegistry and Tag at the top of the file instead of using fully qualified names inline.

As per coding guidelines, “Reference types with top-level imports rather than inline fully qualified names; remove unused imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java`
around lines 307 - 316, Update McpToolProviderManagerAdditionalTest to import
SimpleMeterRegistry and Tag at the top, then replace their fully qualified
inline references in the registry and tag-value collection code with the
imported types; remove any now-unused imports.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java`:
- Around line 310-313: Update the test around
McpToolProviderManagerAdditionalTest to override fetchToolsFromServer(...) with
a deterministic exception instead of invoking discovery against the unreachable
URL. Keep the test focused on asserting the failure metric and ensure no live
network call or timeout-dependent behavior remains.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java`:
- Around line 664-671: The caller-bound MCP tool path must fail before transport
when callerIdentityContext.current() is null instead of returning Map.of() and
sending unauthenticated. Update the logic around caller-bound detection and the
no-caller branch in McpToolProviderManager to throw the established
authentication failure, while preserving unauthenticated discovery for
non-caller-bound calls; update unboundToolCallSendsNothing to assert
authentication failure rather than empty headers.

---

Nitpick comments:
In
`@src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java`:
- Around line 307-316: Update McpToolProviderManagerAdditionalTest to import
SimpleMeterRegistry and Tag at the top, then replace their fully qualified
inline references in the registry and tag-value collection code with the
imported types; remove any now-unused imports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 040f4840-2e20-40be-8056-c35985db3c65

📥 Commits

Reviewing files that changed from the base of the PR and between ec86304 and 824ed5b.

📒 Files selected for processing (5)
  • docs/httpcalls.md
  • docs/mcp-server.md
  • docs/security.md
  • src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerAdditionalTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/httpcalls.md
  • docs/security.md
  • docs/mcp-server.md

… root path

The trim fix claimed in ec86304 never landed. The scripted edit did not match,
I did not check the result, and the commit message asserted a change the code
did not contain — so withBearerPrefix still returned the untrimmed value and a
padded config would emit " Bearer …". Applied properly this time and verified in
the file.

Two more from the same round:

- normalizeEndpointPath collapsed an absolute URL with no path
  ("https://host", "https://host?x=1") to the empty string, making the endpoint
  key "post:" — which no pattern expecting a leading slash can match, and an
  unmatched require-pattern is an ungated call. A root endpoint is "/".
- The metrics test I added drove real discovery against an unreachable host,
  so it depended on DNS and waited out the 30-second default timeout. It stubs
  fetchToolsFromServer on a spy, like the discovery tests already do.

816 tests pass on a clean build.
Copilot AI review requested due to automatic review settings July 30, 2026 10:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGate.java:36

  • The class-level Javadoc still says patterns are tested against "source:name" first, but classify()/firstMatch() now tests the endpoint-qualified form first (when present). This doc mismatch can mislead future changes to the precedence rules.
    public GateResult classify(List<ToolExecutionRequest> batch, Map<String, String> toolSources,
                               ToolApprovalsConfig cfg, Set<String> clearedCallIds) {
        return classify(batch, toolSources, Map.of(), cfg, clearedCallIds);
    }

Comment thread src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
Request.path defaults to "" and means the target server's root — ApiCallExecutor
sends the request to targetServerUrl unchanged. normalizeEndpointPath returned
that empty string as-is, so the endpoint key was "post:", which no pattern can
match, and a require-rule left the call ungated.

The previous commit fixed the neighbouring case (an absolute URL carrying no
path) and missed this one: an empty path never reaches the URL branch and falls
past the leading-slash guard untouched.

Mutation-checked: removing the guard fails the test. 816 tests pass on a clean
build.
Copilot AI review requested due to automatic review settings July 30, 2026 11:21
@ginccc

ginccc commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

Review thread disposition

Every unresolved thread checked against current code. GitHub marks a thread outdated only when its diff hunk moves, not when the finding is addressed — so several below read as open while already being fixed. Verified individually rather than by that flag.

Fixed in this round

Thread Status
AgentOrchestrator — empty configured path yields key post: Real, fixed in d7ea2550e. Request.path defaults to "" and means the server root; returning it unchanged made a root endpoint ungateable. Mutation-checked.

Already addressed in an earlier commit (verified in the current tree, not assumed)

Thread Where
toolCache keyed by URL only ec863045f — now toolCache.get(cacheKey(serverConfig))
validate() illegal-character message stale d763b327f — message lists / { }
callerBound misses the bare {caller:token} form d763b327frejectUnsupportedReference runs first
describeBodySchema says "single JSON object" for arrays d763b327f — branches on schema.getType()
Caller-token restriction inconsistent across docs 824ed5be6httpcalls.md and security.md now state query parameters, bodies and paths
withBearerPrefix returns untrimmed value 04a20ae9f — the fix claimed in ec863045f had silently failed to apply; corrected and verified in the file

Verification: 816 tests pass on a clean build; checkstyle clean. Each behavioural fix on this branch was mutation-checked by reverting it and confirming a test fails.

One caveat for the approver. The central control here — ToolApprovalGate — fails open: an unmatched pattern leaves the call allowed. Seven review rounds each found a variant of that shape, which is why validation now rejects any pattern that cannot match, checked by compiling the prefix and testing it against the endpoint keys the runtime actually emits rather than by enumerating known-bad forms.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

@aisabella-ai
aisabella-ai self-requested a review July 30, 2026 13:46
@ginccc
ginccc merged commit f832886 into main Jul 30, 2026
25 checks passed
@ginccc
ginccc deleted the feat/operator-write-foundation branch July 30, 2026 13:47
ginccc added a commit that referenced this pull request Jul 30, 2026
main moved 16 commits (#622 operator-write foundation, caller-bound MCP
credentials, HITL endpoint patterns), putting the PR into CONFLICTING — which
matters beyond tidiness: a conflicting PR has no computable merge ref, so no
workflow can run, and the Integration Tests failure GitHub still showed was a
stale pre-fix result.

One textual conflict (docs/changelog.md, a pure union of entries). Three files
auto-merged silently: McpToolProviderManager, AgentOrchestrator and
McpToolProviderManagerAdditionalTest. One of those auto-merges was wrong.

#622 made the MCP tool cache credential-scoped, not just the client cache:
toolCache is keyed on cacheKey(config) (url|sha-256 of apiKey, or
url|anonymous). The F12 TTL tests seeded it under the bare URL, so after the
merge the seeded entry no longer matched the lookup:

  - freshEntryIsServedFromCache failed loudly (expected <1> but was <0>)
  - staleEntryIsNotServed kept PASSING, vacuously — nothing was served because
    nothing matched the key, not because the entry was stale

The helper now derives the key by reflectively calling the production cacheKey,
the same idiom #622's own tests use, so a future change to the key shape carries
these tests with it instead of hollowing them out. Production was correct
throughout; only the test was stale. The fix is included in this merge commit so
the commit builds and tests clean, per AGENTS.md rule 6.

Verified the other two auto-merges rather than trusting them: AgentOrchestrator
was javadoc-only on our side; McpToolProviderManager's guard block interleaved
coherently (validateServerUrl + our validateTransport + #622's
validateCallerBoundKey, with our one-time deprecated-transport warning after
it), and our URL-keyed deprecatedTransportWarned set is correctly unaffected by
credential scoping — transport is a property of the server, not the credential.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants