Skip to content

feat(operator): per-endpoint approval friction, gate provisioning, and agent-readable docs - #625

Merged
ginccc merged 8 commits into
mainfrom
feat/operator-write-capability
Aug 1, 2026
Merged

feat(operator): per-endpoint approval friction, gate provisioning, and agent-readable docs#625
ginccc merged 8 commits into
mainfrom
feat/operator-write-capability

Conversation

@ginccc

@ginccc ginccc commented Aug 1, 2026

Copy link
Copy Markdown
Member

Follows #622, which granted no capability but made granting it possible. This is the second half of that: the backend can now install an approval gate, differentiate friction per endpoint, and let an agent read EDDI's own documentation. No write capability is granted here — the Manager's endpoint allow-list is untouched and still read-only.

Two repos, two PRs. This is the backend; the EDDI-Manager half (scope picker, approval surface, system prompt, the write grant itself) depends on this merging and deploying first.

Iteration 1 — per-endpoint approval friction

timeoutPolicy, approvalTimeout, pauseReason and pendingMessage were single scalars covering every gated tool, so "deploy an agent" and "create an agent" could not differ in how long a reviewer had or what the approval card said.

"rules": [
  { "match": "http.post:/agentstore/agents", "timeoutPolicy": "WAIT_INDEFINITELY",
    "pauseReason": "Creating a new agent — review the whole config" },
  { "match": "http.post:/administration/{environment}/deploy/{agentId}",
    "timeoutPolicy": "AUTO_REJECT", "approvalTimeout": "PT5M" }
]

A rule tunes friction; it never gates or ungates. That stays entirely in requireApproval/exempt. The gate allows an unmatched call, so it only survives by gating broadly and exempting narrowly — a rule able to ungate would let a config grant capability by adding an entry, which is the enumerate-upward failure the design exists to avoid. Asserted against the gate itself: a config whose rules name an exempt GET and give a required POST AUTO_APPROVE changes neither classification.

Two resolution decisions worth reviewing closely:

  • Most specific wins, per call — fewest wildcards, then longest pattern, so http.post:/agentstore/agents beats http.post:* regardless of JSON array order. Order-dependence would mean losing intended friction to a list edit.
  • Strictest wins, per batch — a model can emit several gated calls in one message and they pause together, under one timeout policy, so matched rules must reduce to one. Ranked by how much of the human's decision the policy takes on timeout (WAIT_INDEFINITELY > ABORT > AUTO_REJECT > AUTO_APPROVE > none). Taking the first match instead would have let a model turn "delete waits for a human" into "delete auto-rejects in five minutes" by pairing a delete with a deploy.

The governing rule is resolved once at gate time and persisted on the batch: PendingToolCallBatch keeps each gated call's name and source but no endpoint, so an endpoint-addressed rule could not be re-matched by the post-pause resolvers — and a rule resolving differently on the two sides of a pause is exactly the bug that field removes. ToolApprovalGate.addressesOf is extracted and shared so the gate and the rule resolver can never drift.

New metric eddi.hitl.rule.matched{match="<configured pattern>"} — deduplicated per pause, tagged with the config pattern only, never a URL, argument or user id.

Iteration 2 — provision the gate through setup-api (the blocker)

CreateApiAgentRequest had no HITL field and createApiAgent built a bare AgentConfiguration, so every agent the wizard has ever created has hitlConfig == null and an inert gate. Nothing could provision a gated agent through setup-api at all.

hitlConfig is now a request field, set at step 7 on v1 of the agent document — creating it with the agent rather than PUT-ing it afterwards matters, because an update writes version + 1 and leaves the ungated v1 reachable by a redeploy.

Validated before the first resource exists. AgentStore.create validates it too, but at step 7 — so a bad pattern previously surfaced only after the apicalls, parser, behaviour, LLM and workflow had been created, orphaning all five.

Deliberately not exposed on the MCP create_api_agent tool, which already takes a caller-chosen endpoint filter; letting the caller choose the gate too would make it a complete escape from any allow-list.

Also adds mcpServerUrls to the API-agent path so one agent can hold both spec-generated tools and an MCP server's — previously unreachable through the wizard.

Iteration 3 — EDDI's docs readable by an EDDI agent

An MCP resource is only usable by a client that asks for it, and EDDI's own MCP client never calls resources/read — it consumes tools. So eddi://docs/* made EDDI's documentation readable by a desktop MCP client and not by an agent running on EDDI, which is backwards for an agent whose job is to explain the platform.

DocsService is extracted from McpDocResources and served at GET /administration/docs and /administration/docs/{name}. McpDocResources becomes a thin delegate; its pre-existing test class is kept assertion-for-assertion as evidence no MCP client sees a different response.

Documents that the runtime doc set is smaller than the repo's: the image ships only top-level docs/*.md and removes four of them, so callers must read the index rather than assume a page exists.

updateResourceUri verified as the gate-immune re-point path

PUT /agentstore/agents/{id} and PUT /llmstore/llms/{id} must stay permanently unbound for a gated operator — the gate lives in those documents. That leaves editing an agent apparently impossible, since the re-point cascade needs document writes. PUT /{id}/updateResourceUri is the escape hatch, so it was checked rather than assumed: the caller cannot supply a hitlConfig (the body is a text/plain URI) and the implementation preserves the stored one by round-tripping the whole document. Now pinned by a test that captures the written config.

Both variants also computed substring(0, lastIndexOf("?")), which throws on a URI with no ?version= — turning malformed input into a 500 on the exact path an LLM has to walk to finish an edit. Now a 400.

Verification

  • Every suite touching this code passes; mvnw validate clean; clean rebuild after the rebase onto main.
  • Ten mutations applied and each confirmed to kill tests. Two survived and exposed real problems: a redundant traversal guard, and a duplicated security predicate — both consolidated rather than left as two copies of one check.
  • The full local suite is red out of the box (Testcontainers, loopback-socket binders, model endpoints). None of the failing classes intersects this diff, and every class touched here passes when run directly. CI is the source of truth for integration tests.

Review pass found three real things

  1. GET /administration/docs would have 403'd an admin. Written as @RolesAllowed("eddi-viewer") — but EDDI has no role hierarchy (@RolesAllowed and McpToolUtils.requireRole are both literal hasRole checks) and eddi-viewer appears in no other REST endpoint. The principal an operator runs as would have been refused by the one endpoint built for it.
  2. A javadoc was silently reassigned, leaving recordPauseCapGuard undocumented.
  3. A rules[].match string-identical to an exempt pattern is provably dead config and is now refused — exact equality only, since a broader rule may legitimately overlap an exemption.

Open decisions for the owner

  1. The gate lives in artifacts the operator could write. Keep the agent/LLM PUTs permanently unbound (the updateResourceUri cascade covers re-pointing), or add a deployment-level requireApproval that merges with and cannot be weakened by the agent document. The latter closes the class properly and is what would eventually make those two PUTs bindable.
  2. Least privilege. Writes run as the chatting user, so an operator used by an admin can do what that admin can. Worth deciding deliberately whether the operator should be usable by admins at all, or only by an eddi-editor service persona.

Docs: docs/hitl.md (rules), docs/mcp-server.md (REST docs surface, gate-not-on-MCP), docs/changelog.md.

Summary by CodeRabbit

  • New Features

    • Added per-tool HITL approval rules with configurable policies, timeouts, pause reasons, and pending messages.
    • Added MCP server support during API-agent setup.
    • Added role-protected REST access to list and read Markdown documentation.
    • Persisted effective approval rules for pending tool calls.
  • Bug Fixes

    • Invalid resource URIs now return clear HTTP 400 responses instead of errors.
  • Documentation

    • Expanded guidance for HITL rules, MCP setup, documentation access, and related behavior.

ginccc added 5 commits August 1, 2026 17:09
timeoutPolicy, approvalTimeout, pauseReason and pendingMessage were single
scalars for every gated tool, so "deploy an agent" and "create an agent" could
not differ in how long a reviewer had or what the approval card said.

Adds an optional `rules` list addressed by the same pattern language as
requireApproval (bare name, source:name, or source.method:path). A rule tunes
friction only -- whether a call is gated stays entirely in
requireApproval/exempt, because the gate allows an unmatched call and a rule
able to ungate would let a config grant capability by adding an entry.

Resolution: most specific wins per call (fewest wildcards, then longest
pattern, so list order cannot change the outcome); strictest wins per batch
(WAIT_INDEFINITELY > ABORT > AUTO_REJECT > AUTO_APPROVE > none), so bundling a
lenient call into a batch can never soften a stricter rule. Fields fall back to
the scalars individually.

The governing rule is resolved at gate time and persisted on
PendingToolCallBatch: the batch keeps names and sources but no endpoints, so an
endpoint-addressed rule could not be re-matched by the post-pause resolvers.
ToolApprovalGate.addressesOf is extracted and shared so the gate and the rule
resolver can never drift.

Adds the eddi.hitl.rule.matched counter, tagged by the configured pattern and
deduplicated per pause.
CreateApiAgentRequest had no HITL field and createApiAgent built a bare
AgentConfiguration, so every agent the wizard has ever created had
hitlConfig == null and an inert gate -- nothing could provision a gated agent
through setup-api at all.

hitlConfig is now a request field, set at step 7 on v1 of the agent document.
Creating it with the agent rather than PUTing it afterwards matters: an update
writes version + 1 and leaves the ungated v1 reachable by a redeploy.

Validated before the first resource exists. AgentStore.create validates it too,
but that runs at step 7, so a bad pattern previously surfaced only after the
apicalls, parser, behaviour, LLM and workflow had been created and left all
five orphaned.

Deliberately not exposed on the MCP create_api_agent tool, which already takes
a caller-chosen endpoint filter -- letting the caller choose the gate too would
make it a complete escape from any allow-list.

Also adds mcpServerUrls to the API-agent path so one agent can hold both the
tools generated from its OpenAPI spec and an MCP server's; the per-URL creation
loop is shared with setupAgent rather than duplicated.
An MCP resource is only usable by a client that asks for it, and EDDI own MCP
client never calls resources/read -- it consumes tools. So eddi://docs/* made
EDDI documentation readable by a desktop MCP client and not by an agent running
on EDDI, which is backwards for an agent whose job is to explain the platform.

Extracts DocsService from McpDocResources (filesystem access plus the
path-traversal guard) and adds GET /administration/docs and
/administration/docs/{name}, both eddi-viewer. McpDocResources becomes a thin
delegate; its pre-existing tests are kept assertion-for-assertion as evidence
that no MCP client sees a different response.

Mutation-checking the name shape-check (/, \, ..) killed nothing -- readDoc also
verifies the resolved path stays under the docs directory, which subsumes it.
The check is kept because it is what lets MCP answer "invalid name" rather than
"not found", but McpDocResources had restated the predicate to pick that
message. It is now one shared DocsService.isValidDocName. REST returns a bare
404 for both cases so a traversal string is never echoed back.

Documents that the runtime doc set is smaller than the repository: the image
ships only top-level docs/*.md and removes four of them.
…pin the gate-preservation property

PUT /{id}/updateResourceUri on the agent and workflow stores is the only
re-point path available to an approval-gated agent, because the full
PUT /agentstore/agents/{id} is permanently unbound (the gate lives in that
document). It is safe to bind only if it provably cannot drop the gate, so this
pins the property rather than assuming it: the caller cannot supply a
hitlConfig (the body is a text/plain URI) and the implementation preserves the
stored one by round-tripping the whole document. Asserted by capturing the
written AgentConfiguration -- gate intact, only the URI list changed.

Both variants also computed substring(0, lastIndexOf("?")), which throws
StringIndexOutOfBoundsException on a URI with no ?version= and turned malformed
caller input into a 500. On this path that failure mode is one an LLM walking
the re-point cascade will hit and has to act on, so it is now a 400.
GET /administration/docs would have refused an admin. It was written as
@RolesAllowed("eddi-viewer") -- the role the MCP surface uses -- but EDDI has no
role hierarchy: both JAX-RS @RolesAllowed and McpToolUtils.requireRole are
literal hasRole checks, and eddi-viewer appears in no other REST endpoint. An
eddi-admin principal, which is what an operator agent runs as, would have been
403d by the one endpoint built for it. The read tier is now enumerated like
every other REST resource here.

recordRuleMatches had been inserted directly above recordPauseCapGuard, leaving
two consecutive javadoc blocks -- the original doc detached from its method and
recordPauseCapGuard ended up undocumented. Method moved.

Refuses a rules[].match string-identical to an exempt pattern: an exempt call is
never gated, so no rule is ever resolved for it, making the rule dead config
that reads as if it did something. Only exact equality is refused -- a broader
rule may legitimately overlap an exemption while still covering gated calls.

Asserts that PendingToolCallBatch.effectiveRule round-trips through the snapshot
serializer. If it did not, a paused conversation would render the rule pending
message and the resume would recompute the scalar one, stranding the placeholder
in the resolved turn.
Copilot AI review requested due to automatic review settings August 1, 2026 15:13
@ginccc
ginccc requested a review from rolandpickl as a code owner August 1, 2026 15:13
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ginccc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f36c6093-a4b5-4cba-acdf-85d98a7b366d

📥 Commits

Reviewing files that changed from the base of the PR and between 843a13f and eee9ab5.

📒 Files selected for processing (10)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java
  • src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStore.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java
  • src/main/java/ai/labs/eddi/utils/RestUtilities.java
  • src/test/java/ai/labs/eddi/configs/agents/rest/RestAgentStoreExpandedTest.java
  • src/test/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStoreCrudTest.java
  • src/test/java/ai/labs/eddi/engine/setup/AgentSetupServiceTest.java
📝 Walkthrough

Walkthrough

This change adds per-tool HITL approval rules with validation, matching, precedence, persistence, and runtime handling. It extends API-agent setup with HITL and MCP configuration. It adds REST documentation access and validates malformed resource URIs.

Changes

Per-tool HITL approval rules

Layer / File(s) Summary
Approval-rule model and validation
src/main/java/ai/labs/eddi/configs/hitl/..., docs/hitl.md, src/test/java/ai/labs/eddi/configs/hitl/...
Adds configurable approval rules and validates patterns, conflicts, timeout values, and message limits.
Address matching and rule selection
src/main/java/ai/labs/eddi/engine/hitl/tools/..., src/test/java/ai/labs/eddi/engine/hitl/tools/...
Matches endpoint, source-qualified, and bare tool addresses. Selects specific rules per call and strict policies for batches.
Rule-aware pause persistence
src/main/java/ai/labs/eddi/engine/memory/model/PendingToolCallBatch.java, src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java, related tests
Persists matched and governing rules and records rule-aware pause metadata.
Timeout and message precedence
src/main/java/ai/labs/eddi/engine/internal/ConversationService.java, src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java, related tests
Uses governing rule values before tool-level and outer HITL settings.
HITL documentation
docs/hitl.md, docs/changelog.md
Documents rule configuration, matching, precedence, persistence, and timeout behavior.

API-agent HITL and MCP setup

Layer / File(s) Summary
API-agent request contract
src/main/java/ai/labs/eddi/engine/setup/CreateApiAgentRequest.java, src/main/java/ai/labs/eddi/engine/mcp/McpSetupTools.java
Adds HITL configuration and optional MCP server URLs to API-agent setup requests.
API-agent resource provisioning
src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java, docs/mcp-server.md
Validates HITL configuration before creation, applies it to the initial agent, and combines HTTP and MCP workflow resources.
Setup integration tests
src/test/java/ai/labs/eddi/engine/setup/AgentSetupServiceTest.java, src/test/java/ai/labs/eddi/engine/mcp/McpSetupToolsTest.java
Covers validation, no-HITL setup, HITL provisioning, and combined HTTP/MCP workflows.

Documentation access and URI validation

Layer / File(s) Summary
Documentation service and REST contract
src/main/java/ai/labs/eddi/engine/docs/DocsService.java, src/main/java/ai/labs/eddi/engine/api/IRestDocs.java, src/main/java/ai/labs/eddi/engine/rest/RestDocs.java, src/test/java/ai/labs/eddi/engine/rest/RestDocsTest.java
Adds validated filesystem access and authorized REST endpoints for listing and reading Markdown documentation.
MCP documentation delegation
src/main/java/ai/labs/eddi/engine/mcp/McpDocResources.java, src/test/java/ai/labs/eddi/engine/mcp/McpDocResourcesTest.java
Routes MCP documentation operations through DocsService.
Resource URI validation
src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java, src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStore.java, related tests
Returns HTTP 400 for resource URIs without a version query component.

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

Possibly related PRs

  • labsai/EDDI#585: Both changes extend the HITL tool-approval framework and its runtime and persistence flows.
  • labsai/EDDI#617: Both changes update HITL documentation and ConversationService.
  • labsai/EDDI#622: Both changes modify endpoint-aware matching in ToolApprovalGate.

Suggested reviewers: rolandpickl, copilot, aisabella-ai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% 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 clearly summarizes the main changes: per-endpoint approval rules, gate provisioning, and REST documentation access.
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-capability

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.

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 extends EDDI’s HITL tool-approval system with per-endpoint “friction” rules (messages + timeout behavior), makes the setup-api able to provision a HITL gate on the initial v1 agent document (plus optional MCP server URLs), and exposes EDDI’s markdown documentation to EDDI-hosted agents via a REST surface backed by a shared DocsService.

Changes:

  • Add toolApprovals.rules resolution (most-specific per call; strictest per batch) and persist the governing rule onto PendingToolCallBatch for deterministic post-pause behavior.
  • Enable setup-api (CreateApiAgentRequest) to accept hitlConfig and provision it on agent creation v1; also add mcpServerUrls support and share MCP-calls creation logic.
  • Extract docs filesystem logic into DocsService and expose it via GET /administration/docs (+ {name}), while keeping MCP resource behavior intact; harden updateResourceUri paths to return 400 on URIs missing ?version=.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java Resolves and records per-call rule matches, selects governing rule, persists it in pending batches, and uses it for pause reason rendering + metrics.
src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGate.java Extracts and centralizes tool-call addressing (addressesOf) used by both gating and rule matching.
src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRules.java New: matches toolApprovals.rules per call (most-specific) and reduces to a governing rule per batch (strictest).
src/main/java/ai/labs/eddi/engine/memory/model/PendingToolCallBatch.java Persists governing rule (effectiveRule) and per-call matched rule id (matchedRule).
src/main/java/ai/labs/eddi/engine/internal/ConversationService.java Applies effective timeout policy using the persisted governing rule, while preserving legacy behavior for older batches.
src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java Renders end-user pending message using governing rule override, falling back to effective tool-approval config scalars.
src/main/java/ai/labs/eddi/configs/hitl/model/ToolApprovalsConfig.java Adds rules and ApprovalRule model for per-tool friction overrides.
src/main/java/ai/labs/eddi/configs/hitl/HitlConfigValidation.java Adds save-time validation for toolApprovals.rules (dead/unmatchable patterns, duplicates, finite-policy-without-duration, etc.).
src/main/java/ai/labs/eddi/engine/setup/CreateApiAgentRequest.java Adds hitlConfig and mcpServerUrls fields to setup-api request payload.
src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java Validates hitlConfig up-front; provisions HITL gate on agent v1; adds MCP server URLs support for API-agent wizard using shared helper.
src/main/java/ai/labs/eddi/engine/mcp/McpSetupTools.java Extends MCP create_api_agent tool signature to accept mcpServerUrls (still deliberately not exposing hitlConfig).
src/main/java/ai/labs/eddi/engine/docs/DocsService.java New: shared filesystem-backed doc listing/reading with traversal protections.
src/main/java/ai/labs/eddi/engine/api/IRestDocs.java New: REST API contract for listing/reading docs, with explicit @RolesAllowed tier.
src/main/java/ai/labs/eddi/engine/rest/RestDocs.java New: REST implementation delegating to DocsService, returning 404 without reflecting attacker input.
src/main/java/ai/labs/eddi/engine/mcp/McpDocResources.java Refactors MCP resources surface to delegate to DocsService while preserving existing response shapes.
src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java Returns 400 (not 500) when updateResourceUri receives a URI lacking a query (?version=).
src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStore.java Same 400-guard for workflow updateResourceUri on malformed URIs.
docs/hitl.md Documents per-tool friction rules semantics, reduction, persistence, and validation constraints.
docs/mcp-server.md Documents REST docs surface and setup-api HITL provisioning behavior.
docs/changelog.md Adds changelog entry describing the new HITL friction rules, setup-api gate provisioning, docs REST surface, and updateResourceUri hardening.
src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorCoverageTest.java Adds coverage tests for pause reason precedence and persisted governing rule/matchedRule behavior in pending batches.
src/test/java/ai/labs/eddi/engine/internal/ConversationServiceToolTimeoutTest.java Adds tests asserting governing-rule timeout policy/duration precedence and inheritance behavior.
src/test/java/ai/labs/eddi/engine/runtime/internal/ConversationHitlTest.java Adds tests ensuring governing rule pendingMessage overrides scalar levels and falls back correctly.
src/test/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRulesTest.java New: unit tests for matching specificity, strictest-governs reduction, and the “rules never gate/ungate” invariant.
src/test/java/ai/labs/eddi/configs/hitl/ToolApprovalRulesValidationTest.java New: validation tests for toolApprovals.rules save-time constraints.
src/test/java/ai/labs/eddi/engine/memory/model/PendingToolCallBatchSnapshotTest.java Ensures effectiveRule + per-call matchedRule round-trip through snapshot serialization.
src/test/java/ai/labs/eddi/engine/setup/AgentSetupServiceTest.java Validates that invalid HITL configs are rejected before creating any resources; valid configs proceed past the guard.
src/test/java/ai/labs/eddi/engine/mcp/McpSetupToolsTest.java Verifies setup-api provisions HITL gate on v1 agent doc and that MCP URLs add an MCP step alongside httpcalls.
src/test/java/ai/labs/eddi/engine/mcp/McpDocResourcesTest.java Refactors tests to validate MCP doc responses unchanged after extracting DocsService.
src/test/java/ai/labs/eddi/engine/rest/RestDocsTest.java New: tests REST docs list/read behavior and traversal handling (404 without reflection), and missing-dir behavior.
src/test/java/ai/labs/eddi/configs/agents/rest/RestAgentStoreExpandedTest.java Asserts updateResourceUri preserves hitlConfig and rejects URIs without ?version=.
src/test/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStoreCrudTest.java Adds regression test: URI without ?version returns 400 and does not update.

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

Comment thread docs/mcp-server.md Outdated
Comment on lines +175 to +178
| Endpoint | Role | Returns |
| -------- | ---- | ------- |
| `GET /administration/docs` | `eddi-viewer` | JSON array of page names, without the `.md` suffix |
| `GET /administration/docs/{name}` | `eddi-viewer` | The page's markdown source as `text/plain`; `404` if absent |
Comment thread docs/changelog.md Outdated

**An MCP resource does not reach an EDDI agent.** A resource is only usable by a client that asks for it, and EDDI's own MCP client never calls `resources/read` — it consumes *tools*. So `eddi://docs/*` made EDDI's documentation readable by a desktop MCP client and not by an agent running on EDDI, which is exactly backwards for an agent whose job is to explain the platform.

`DocsService` is extracted from `McpDocResources` (filesystem access plus the path-traversal guard) and served over REST at `GET /administration/docs` and `GET /administration/docs/{name}`, both `eddi-viewer` — the docs are published documentation, so anyone who may look at the deployment may read them. `McpDocResources` becomes a thin delegate, and its pre-existing test class is kept assertion-for-assertion as the evidence that no MCP client sees a different response than before.
Copilot review on #625: both docs said the endpoints were eddi-viewer, which
was true of the first draft but not of the code after the review-pass fix. EDDI
has no role hierarchy, so eddi-viewer alone would refuse an eddi-admin; the
annotation enumerates the widest read tier and the docs now say so.
Copilot AI review requested due to automatic review settings August 1, 2026 15:19

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 32 out of 32 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStore.java:102

  • Same as the agent-store variant: this only checks for a ?, but not that the query contains version=. That can accept and persist non-versioned resource URIs (e.g. ...?foo=bar), which later breaks version-pinned workflow references.
        String resourceURIString = resourceURI.toString();
        int queryStart = resourceURIString.lastIndexOf('?');
        if (queryStart < 0) {
            // substring(0, -1) would throw and turn a caller's malformed input into a
            // 500 — same guard as the agent-store variant, which shares this shape.

src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java:149

  • The new guard only checks that the URI contains a ?, but the endpoint contract (and error message) requires ?version=. As written, a caller can send ...?foo=bar and still get a 200 update, leaving the agent pointing at a non-versioned resource URI.
        int queryStart = resourceURIString.lastIndexOf('?');
        if (queryStart < 0) {
            // substring(0, -1) would throw and turn a caller's malformed input into a
            // 500. This endpoint sits on the re-point cascade that an approval-gated
            // agent has to walk to finish an edit, so its failure mode is one an LLM

src/main/java/ai/labs/eddi/engine/docs/DocsService.java:112

  • name is attacker-controlled (REST path param / MCP arg) but is logged verbatim via LOGGER.warnf(...), which enables log forging (CWE-117) via encoded newlines/control chars. Either sanitize before logging (preferred) or avoid logging the raw value.
        if (!isValidDocName(name)) {
            LOGGER.warnf("Rejected doc name: %s", name);
            return null;

src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java:406

  • createMcpCallsResources returns null only when the raw string is blank/null; if it contains only commas/whitespace (e.g. ", ,"), the loop produces no locations and the method returns an empty list. That contradicts the method contract and forces callers to treat empty/non-empty lists equivalently.
        if (mcpServerUrls == null || mcpServerUrls.isBlank()) {
            return null;
        }
        var locations = new ArrayList<String>();
        for (String url : mcpServerUrls.split(",")) {

Copilot AI review requested due to automatic review settings August 1, 2026 15:24
@github-actions

github-actions Bot commented Aug 1, 2026

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

@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: 6

🤖 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 `@docs/mcp-server.md`:
- Around line 175-178: Update the REST documentation endpoint table to enumerate
the complete permitted read roles, adding eddi-admin alongside the existing
eddi-viewer entry for both GET /administration/docs and GET
/administration/docs/{name}.

In `@src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java`:
- Around line 145-155: Require a parsed, valid version parameter in both
resource update paths instead of treating any query delimiter as sufficient:
update RestAgentStore around resourceURIWithoutVersion and RestWorkflowStore’s
equivalent logic to reject missing or invalid version values with HTTP 400
before performing updates. Add ?other=2 regression cases in
RestAgentStoreExpandedTest and RestWorkflowStoreCrudTest, asserting HTTP 400 and
that no update occurs.

In `@src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRules.java`:
- Around line 150-164: Update ToolApprovalRules.compile to catch failures from
ToolApprovalPatterns.compile for each nonblank rule, skip invalid entries, and
log the unusable match so one bad pattern does not abort the live turn. Preserve
valid-rule compilation and the existing specificity ordering, including the 1024
multiplier invariant relative to ToolApprovalPatterns.MAX_LENGTH.

In `@src/main/java/ai/labs/eddi/engine/internal/ConversationService.java`:
- Around line 2435-2474: Update applyEffectiveToolTimeoutPolicy to compute
effectiveTimeout once before the policy-selection branches, preserving the
precedence ruleTimeout, toolApprovals approvalTimeout, then hitlConfig
approvalTimeout. Replace the approvalTimeout presence checks for both rule and
toolApprovals with blank-aware validation (non-null and non-blank), so
whitespace-only values fall back to the next duration.

In `@src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java`:
- Around line 759-762: Update the pending-message condition in Conversation’s
rule-selection logic to use the blank-aware validation used by
HitlConfigValidation.isNullOrBlank instead of isNullOrEmpty, so whitespace-only
rule.getPendingMessage() values fall through to the configuration or default
message.

In `@src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java`:
- Line 169: Validate and parse every non-blank URL from request.mcpServerUrls()
before the setup flow writes any resources, including before the existing
resource creation paths around createdResources and createMcpCallsResources.
Reuse McpCallsConfiguration.validate() for each URL and abort setup on any
invalid value so no resources are persisted. Add coverage for both an invalid
first URL and an invalid later URL.
🪄 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: ddf525b0-ed73-4b7d-b9ed-2efe6cd31202

📥 Commits

Reviewing files that changed from the base of the PR and between e20d510 and 01b798c.

📒 Files selected for processing (32)
  • docs/changelog.md
  • docs/hitl.md
  • docs/mcp-server.md
  • src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java
  • src/main/java/ai/labs/eddi/configs/hitl/HitlConfigValidation.java
  • src/main/java/ai/labs/eddi/configs/hitl/model/ToolApprovalsConfig.java
  • src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStore.java
  • src/main/java/ai/labs/eddi/engine/api/IRestDocs.java
  • src/main/java/ai/labs/eddi/engine/docs/DocsService.java
  • src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalGate.java
  • src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRules.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpDocResources.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpSetupTools.java
  • src/main/java/ai/labs/eddi/engine/memory/model/PendingToolCallBatch.java
  • src/main/java/ai/labs/eddi/engine/rest/RestDocs.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java
  • src/main/java/ai/labs/eddi/engine/setup/CreateApiAgentRequest.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
  • src/test/java/ai/labs/eddi/configs/agents/rest/RestAgentStoreExpandedTest.java
  • src/test/java/ai/labs/eddi/configs/hitl/ToolApprovalRulesValidationTest.java
  • src/test/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStoreCrudTest.java
  • src/test/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRulesTest.java
  • src/test/java/ai/labs/eddi/engine/internal/ConversationServiceToolTimeoutTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpDocResourcesTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpSetupToolsTest.java
  • src/test/java/ai/labs/eddi/engine/memory/model/PendingToolCallBatchSnapshotTest.java
  • src/test/java/ai/labs/eddi/engine/rest/RestDocsTest.java
  • src/test/java/ai/labs/eddi/engine/runtime/internal/ConversationHitlTest.java
  • src/test/java/ai/labs/eddi/engine/setup/AgentSetupServiceTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorCoverageTest.java

Comment thread docs/mcp-server.md Outdated
Comment thread src/main/java/ai/labs/eddi/configs/agents/rest/RestAgentStore.java Outdated
Comment on lines +150 to +164
private static List<CompiledRule> compile(ToolApprovalsConfig cfg) {
if (cfg == null || cfg.getRules() == null || cfg.getRules().isEmpty()) {
return List.of();
}
List<ApprovalRule> sorted = new ArrayList<>();
for (ApprovalRule rule : cfg.getRules()) {
// A blank match is refused at save time (HitlConfigValidation); skipping it
// here keeps a hand-edited or legacy document from failing a live turn.
if (rule != null && rule.getMatch() != null && !rule.getMatch().isBlank()) {
sorted.add(rule);
}
}
sorted.sort(Comparator.comparingInt(ToolApprovalRules::specificity).reversed());
return sorted.stream().map(r -> new CompiledRule(r, ToolApprovalPatterns.compile(r.getMatch()))).toList();
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check ToolApprovalPatterns.compile failure mode and the pattern length cap.
set -euo pipefail

fd -t f 'ToolApprovalPatterns.java' | xargs -r -I{} ast-grep outline {} --items all

# The compile method body: does it throw on an invalid pattern?
fd -t f 'ToolApprovalPatterns.java' | while IFS= read -r f; do
  ast-grep run --lang java --pattern 'static Pattern compile($$$) { $$$ }' "$f" || true
  rg -n -C4 'throw|IllegalArgument|256|MAX_LEN|maxLength' "$f"
done

# Does any caller already guard compile() against an exception?
rg -nP --type=java -C3 'ToolApprovalPatterns\.compile\s*\('

Repository: labsai/EDDI

Length of output: 2949


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ToolApprovalPatterns.java =="
sed -n '1,120p' src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalPatterns.java

echo
echo "== ToolApprovalRules.java relevant section =="
sed -n '130,185p' src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRules.java

echo
echo "== ToolApprovalRules.java call sites and logging =="
rg -n 'matchByCallId|compile(|LOGGER|Logging\.getLogger|Logger' src/main/java/ai/labs/eddi/engine/hitl/tools/ToolApprovalRules.java

Repository: labsai/EDDI

Length of output: 8179


Drop individually invalid rule patterns so the live turn can still pause.

compile(r.getMatch()) can throw from invalid regex syntax after compile() has already filtered only null and blank matches. This violates the stated behavior that invalid entries are dropped and can make a turn error out instead of pausing behind a bad rule. Wrap the per-rule compile, drop the entry, and log the unusable match.

Also preserve the wildcard-vs-length ranking invariant: the 1024 score multiplier only avoids trading off wildcard count and length while ToolApprovalPatterns.MAX_LENGTH stays below 1024.

🤖 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/hitl/tools/ToolApprovalRules.java` around
lines 150 - 164, Update ToolApprovalRules.compile to catch failures from
ToolApprovalPatterns.compile for each nonblank rule, skip invalid entries, and
log the unusable match so one bad pattern does not abort the live turn. Preserve
valid-rule compilation and the existing specificity ordering, including the 1024
multiplier invariant relative to ToolApprovalPatterns.MAX_LENGTH.

Comment thread src/main/java/ai/labs/eddi/engine/internal/ConversationService.java Outdated
Comment on lines +759 to +762
var rule = batch != null ? batch.getEffectiveRule() : null;
if (rule != null && !isNullOrEmpty(rule.getPendingMessage())) {
template = rule.getPendingMessage();
} else if (cfg != null && !isNullOrEmpty(cfg.getPendingMessage())) {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a blank-aware check for rule.getPendingMessage().

isNullOrEmpty only checks String.isEmpty(), not .isBlank(). A whitespace-only rule.getPendingMessage() is treated as "set" here and used verbatim instead of falling back to cfg.getPendingMessage() or DEFAULT_PENDING_MESSAGE. This mirrors the same not-blank-aware pattern in ConversationService.applyEffectiveToolTimeoutPolicy, where HitlConfigValidation.isNullOrBlank (which does check .isBlank()) is the semantic the save-time validator already relies on.

🛠️ Proposed fix
         var rule = batch != null ? batch.getEffectiveRule() : null;
-        if (rule != null && !isNullOrEmpty(rule.getPendingMessage())) {
+        if (rule != null && rule.getPendingMessage() != null && !rule.getPendingMessage().isBlank()) {
             template = rule.getPendingMessage();
-        } else if (cfg != null && !isNullOrEmpty(cfg.getPendingMessage())) {
+        } else if (cfg != null && cfg.getPendingMessage() != null && !cfg.getPendingMessage().isBlank()) {
             template = cfg.getPendingMessage();
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var rule = batch != null ? batch.getEffectiveRule() : null;
if (rule != null && !isNullOrEmpty(rule.getPendingMessage())) {
template = rule.getPendingMessage();
} else if (cfg != null && !isNullOrEmpty(cfg.getPendingMessage())) {
var rule = batch != null ? batch.getEffectiveRule() : null;
if (rule != null && rule.getPendingMessage() != null && !rule.getPendingMessage().isBlank()) {
template = rule.getPendingMessage();
} else if (cfg != null && cfg.getPendingMessage() != null && !cfg.getPendingMessage().isBlank()) {
template = cfg.getPendingMessage();
}
🤖 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/runtime/internal/Conversation.java` around
lines 759 - 762, Update the pending-message condition in Conversation’s
rule-selection logic to use the blank-aware validation used by
HitlConfigValidation.isNullOrBlank instead of isNullOrEmpty, so whitespace-only
rule.getPendingMessage() values fall through to the configuration or default
message.

Comment thread src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java

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 32 out of 32 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/main/java/ai/labs/eddi/engine/docs/DocsService.java:119

  • DocsService.readDoc() falls back to reading the raw name path when <name>.md is absent. That means a caller can fetch any regular file in the configured docs directory by passing its exact filename (e.g. not-markdown.txt), even though this surface is meant to serve only markdown docs. With the REST endpoint exposed to broad read roles, this becomes an unintended file disclosure risk if non-doc files ever land in that directory.

Consider only allowing the fallback path when the caller already supplied a .md suffix (to support requests like architecture.md), and otherwise returning 404.

        if (!Files.isRegularFile(docFile)) {
            // The name may already carry the .md suffix.
            docFile = docsDir.resolve(name).normalize();
            if (!docFile.startsWith(docsDir) || !Files.isRegularFile(docFile)) {
                return null;

@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.

Caution

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

⚠️ Outside diff range comments (1)
docs/changelog.md (1)

35-35: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Do not claim bounded metric cardinality from pause deduplication.

eddi.hitl.rule.matched uses match as a label value, and each distinct pattern creates a distinct time series. Old series are not evicted by the per-pause deduplication, and the Prometheus retention value is deployment-configurable, so this should not state cardinality as bounded by the current rules list.

🤖 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` at line 35, Update the `eddi.hitl.rule.matched`
documentation in the Metric entry to remove the claim that label cardinality is
bounded by the configured `rules` list. Retain the accurate descriptions of
per-pause deduplication and the `match` value’s source and sensitivity.
🤖 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.

Outside diff comments:
In `@docs/changelog.md`:
- Line 35: Update the `eddi.hitl.rule.matched` documentation in the Metric entry
to remove the claim that label cardinality is bounded by the configured `rules`
list. Retain the accurate descriptions of per-pause deduplication and the
`match` value’s source and sensitivity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f28cb479-1935-4965-9d47-979d8c01ad68

📥 Commits

Reviewing files that changed from the base of the PR and between 01b798c and 843a13f.

📒 Files selected for processing (5)
  • docs/changelog.md
  • docs/mcp-server.md
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
  • docs/mcp-server.md
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java

…wo silent degradations

PR review on #625.

updateResourceUri could UNPIN a reference. The versionless-URI guard tested only
for the presence of a "?", so ".../workflows/{id}?other=2" satisfied it, matched
the stored "?version=1" reference by path prefix, and replaced it with a
versionless one -- silently unpinning the workflow an agent resolves at runtime.
Both stores now go through RestUtilities.pathWithoutVersionQuery, which parses
the query and requires a non-negative integer version.

A whitespace-only approvalTimeout degraded a finite policy silently.
RuntimeUtilities.isNullOrEmpty is isEmpty-only, but HitlConfigValidation uses
isBlank when deciding whether a finite rule may inherit the enclosing duration --
so a blank value saved as absent and then resolved as present, won the chain,
threw inside Duration.parse, armed no schedule, and left the bookmark reporting a
finite policy that could never fire. Both resolution sites are blank-aware now.

The three identical duration ternaries in applyEffectiveToolTimeoutPolicy are
computed once: the timeout resolves down its own chain regardless of which branch
picks the policy, and three copies would drift.

MCP server URLs are swept before the first write, like hitlConfig already was --
McpCallsConfiguration.validate rejects a non-http(s) URL, so a bad second URL
aborted with the first one persisted, and on the API-agent path with the
apicalls, parser, behaviour and LLM resources persisted too.
Copilot AI review requested due to automatic review settings August 1, 2026 15:48

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 33 out of 33 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/main/java/ai/labs/eddi/engine/setup/AgentSetupService.java:401

  • There are two consecutive Javadoc blocks here; the first one (about creating McpCalls resources) is not attached to any declaration and will trip Javadoc/Checkstyle rules in many builds. Remove the stray block (or move it to the method it documents).
    /**
     * Creates one McpCalls resource per comma-separated server URL, recording each
     * location in {@code createdResources}. Returns null when no URLs were given,
     * which is what {@code createWorkflowConfig} expects for "no MCP step".
     * <p>

src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java:828

  • The rule-level pendingMessage is treated as blank-aware, but the scalar toolApprovals.pendingMessage still uses isNullOrEmpty (isEmpty-only). A whitespace-only scalar pendingMessage would be used verbatim and render as an empty user-facing bubble instead of falling back to the default.
        if (rule != null && rule.getPendingMessage() != null && !rule.getPendingMessage().isBlank()) {
            template = rule.getPendingMessage();
        } else if (cfg != null && !isNullOrEmpty(cfg.getPendingMessage())) {
            template = cfg.getPendingMessage();

@ginccc
ginccc requested a review from aisabella-ai August 1, 2026 17:04
@ginccc
ginccc merged commit 2d675bc into main Aug 1, 2026
25 checks passed
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