June 2026
Archived entries for June 2026, newest first. For recent work see the live changelog.
π HITL Framework β Human-in-the-Loop Pause/Resume for Conversations & Group Discussions (2026-06-30)
Repo: EDDI (feat/hitl-framework) Plan: planning/hitl-framework-plan.md What changed: Full implementation of the Human-in-the-Loop (HITL) framework enabling conversations and group discussions to pause mid-pipeline for human approval, then resume or reject.
Wave 0: Storage Primitives & Lifecycle Prerequisites
IResourceStorage.storeIfFieldEqualsβ New CAS primitive for conditional updates on arbitrary JSON fields (not just_version). Implemented in bothMongoResourceStorage(Filters.eq) andPostgresResourceStorage(data->>?). Used by group conversation store for atomic state transitions.ControlSignalenum β CONTINUE, CANCEL_GRACEFUL, CANCEL_IMMEDIATE, PAUSE. Used byDiscussionControlTokenfor thread-safe in-flight control.DiscussionControlTokenβ AtomicReference-based token shared between execution loops and external callers (cancel/pause). IncludesactiveFuturefor immediate cancel interrupt.ConversationPauseExceptionβ Checked exception carrying pausedWorkflowId, absoluteTaskIndex, and reason. MirrorsConversationStopExceptionpattern.Cancel infrastructure β
IConversationMemory.setCancelled/isCancelled,GroupConversationState.CANCELLED,IGroupConversationStore.updateIfState/findByState, SSE events (EVENT_CANCELLED, EVENT_AWAITING_APPROVAL, EVENT_HITL_RESUME).
Wave 1: Core HITL State Machine
ConversationState.AWAITING_HUMANβ New state for paused conversations. Gatessay()with "use the /resume endpoint" message.HITL bookmark fields β 6 fields on
ConversationMemorySnapshot(hitlPausedWorkflowId, hitlPausedAbsoluteTaskIndex, hitlPausedAt, hitlPauseReason, hitlTimeoutPolicy, hitlApprovalTimeout) + correspondingIConversationMemorydefaults +ConversationMemoryimplementation.HitlDecision/HitlTimeoutPolicyβ Decision model (APPROVED/REJECTED + note + decidedBy) and timeout policy enum (AUTO_REJECT, AUTO_APPROVE, ABORT, WAIT_INDEFINITELY).LifecycleManagerextensions βexecuteLifecycleFromIndex()for resume-from-task,checkIfPauseConversationAction()for PAUSE_CONVERSATION detection, cancel check in main loop.Conversation.resume()β Full resume flow: skip-before-paused-workflow β resume-from-index β run-remaining-workflows. Handles re-pause, rejected short-circuit, and finally-block with state normalization.
Wave 2: REST API & Resume Flow
POST /{conversationId}/resumeβ AcceptsHitlDecisionbody, CAS on AWAITING_HUMANβIN_PROGRESS, reloads agent, submits resume via coordinator.GET /{conversationId}/approval-statusβ Summary or full detail of paused conversation.GET /pending-approvalsβ Lists all AWAITING_HUMAN conversations with PendingApprovalSummary.POST /{conversationId}/cancelβ Cancels active or paused conversations.IConversationMemoryStore.compareAndSetStateβ Atomic CAS on conversation state for both MongoDB and PostgreSQL.Timeout handler guard β
waitForExecutionFinishOrTimeoutskips state overwrite when AWAITING_HUMAN (Invariant 10).
Wave 3: Group Discussion HITL
GroupConversationHITL fields β pausedAtPhaseIndex, pausedTurnCount, pausedPhaseName, pausedAt, hitlPauseType (PHASE/TASK).SharedTaskListHITL methods β submitForApproval (IN_PROGRESSβAWAITING_APPROVAL), approveTask, rejectTask, resetToAssigned, hasAwaitingApproval.GroupConversationServiceHITL β cancelDiscussion (via DiscussionControlToken or direct DB), resumeDiscussion (task approvals + phase resume).Group REST endpoints β POST /{gcId}/cancel, POST /{gcId}/approve, POST /{gcId}/approve/stream (SSE), GET /{gcId}/approval-status.
GroupApprovalRequestβ REST body with HitlDecision + Map<String,String> taskApprovals for per-task verdicts.
Wave 4: Configuration, Timeout & Audit
AgentConfiguration.HitlConfigβ approvalTimeout (ISO-8601 duration), timeoutPolicy (default WAIT_INDEFINITELY).AgentGroupConfiguration.HitlConfigβ Same + granularity (PHASE/TASK).HitlTimeoutHandlerβ @ApplicationScoped handler dispatched by ScheduleFireExecutor when hitlType=hitl_timeout schedule fires. Routes to auto-approve/reject/abort based on policy.ScheduleFireExecutorintegration β Early return for hitl_timeout metadata in fire() method.
Files changed (38 total: 32 modified, 6 new)
New files: ControlSignal.java, DiscussionControlToken.java, ConversationPauseException.java, HitlDecision.java, HitlTimeoutPolicy.java, PendingApprovalSummary.java, GroupApprovalRequest.java, HitlTimeoutHandler.java
Key invariants preserved: (1) No typed POJOs in snapshot storage β bookmark fields are first-class. (2) Resume task index is absolute. (3) Group halt = set state + return. (4) Per-task approval detection = post-join scan. (5) Group CAS on state field. (9) Paused turns skip postConversationLifecycleTasks. (10) AWAITING_HUMAN is never overwritten by timeout handler.
Bug Fixes β Code Review Round (2026-07-01)
B1: Regular resume always landed in ERROR. ConversationService.resumeConversation() set in-memory state to IN_PROGRESS at line 844, but Conversation.resume() guards if (state != AWAITING_HUMAN) throw. The DB CAS was correct (AWAITING_HUMAN β IN_PROGRESS), but the in-memory state loaded from the updated snapshot was already IN_PROGRESS. Fix: set in-memory state to AWAITING_HUMAN so resume()'s own guard passes β it does its own transition at line 459.
B2: Group discussion never paused. phase.requiresApproval() / submitForApproval() / hasAwaitingApproval() had zero consumers β nothing set AWAITING_APPROVAL. Fix: Added commitPause() helper. After each phase completes, if requiresApproval() β pause. For TASK granularity, submitForApproval() replaces completeTask(), and hasAwaitingApproval() is checked after the join. Guarded COMPLETED assignment and finally cleanup block against AWAITING_APPROVAL state.
B3+M2: Group REST endpoints missing ownership checks (IDOR). cancelDiscussion, approveGroupPhase, approveGroupPhaseStreaming, and getGroupApprovalStatus had no ownership validation. Fix: Added validateGroupConversationOwnership() (mirrors RestAgentEngine pattern) and setDecidedByFromIdentity() to all 4 endpoints.
B4: Group double-resume race. resumeDiscussion used update(gc) β plain write. Fix: updateIfState(gc, AWAITING_APPROVAL) β ResourceModifiedException on concurrent resume β 409 Conflict.
M1: Timeout schedule never created. The HitlTimeoutHandler consumer was wired but no producer created schedules on pause. Fix: Injected IScheduleStore + IAgentStore into ConversationService. After storeConversationMemory detects AWAITING_HUMAN, scheduleHitlTimeout() loads agent config, checks for approvalTimeout + non-WAIT_INDEFINITELY policy, and creates a one-shot schedule with hitlType=hitl_timeout metadata.
M3: Turn budget reset on resume. turnCounter was initialized to 0 in executeDiscussion, and pausedTurnCount was reset to 0 in resumeDiscussion. Fix: Seed turnCounter from gc.getPausedTurnCount(). Don't reset pausedTurnCount in resumeDiscussion β only clear it on successful COMPLETED.
Minor: Phase resume index. Replaced subList hack in resumeDiscussion with startPhaseIndex parameter on executeDiscussion. Uses pausedAtPhaseIndex + 1 (paused phase already completed). Fixes absolute index corruption.
Minor: Bookmark mismatch guard. Conversation.resume() silently no-oped when pausedWorkflowId wasn't found. Now throws LifecycleException with descriptive message.
Minor: SSE leak on group HITL pause. onHitlPause listener in RestGroupConversation didn't close the SSE sink. Client connections would leak until timeout. Now calls closeQuietly(eventSink).
Files changed: ConversationService.java, GroupConversationService.java, RestGroupConversation.java, Conversation.java
π Security & Algorithm Hardening β SSRF/File-Read, Cron, DoS Guards (2026-06-29)
Repo: EDDI (fix/security-and-algo-hardening) What changed: Findings from a code/security/algorithm review and bug hunt. Most changes are surgical and behavior-preserving for valid input; the two intentional behavior corrections (cron dom/dow OR semantics, exponential retry backoff) are called out explicitly below. New SSRF protection is opt-in and off by default.
Security fixes
Local-file read / non-http SSRF in OpenAPI spec discovery (
McpApiToolBuilder.parseSpec) β TheGET /apicallstore/apicalls/discover-endpoints?specUrl=β¦endpoint (andcreate_api_agent) handed a user-supplied location straight to swagger-parser'sreadLocation(), which fetches URLs and reads local files (file:///etc/passwd) and resolves external$refs. Now, when the input is a remote location (not inline content), it must be anhttp(s)URL (UrlValidationUtils.isValidHttpUrl()) β rejectingfile://(local-file read),classpath:,jar:, and other non-http schemes. Inline JSON/YAML still parses with no network/file access. Inline-vs-location detection broadened via newlooksLikeInlineSpec()(handlesswagger:and multi-line YAML).Scheme-only by design: private/internal hosts stay allowed so internal OpenAPI discovery keeps working. The endpoint is
eddi-admin/eddi-editorgated, so SSRF to private/metadata IPs via anhttp(s)spec URL is an accepted residual β as is the remote-$refvector (swagger-parser has no clean toggle to disable only remote-ref resolution). Use fullUrlValidationUtils.validateUrl()here if a deployment needs private-IP blocking.
Opt-in SSRF protection for agent-driven outbound calls β New
eddi.security.ssrf-protection.enabledflag (default off to preserve internal-API calls in self-hosted deployments). When on:ApiCallExecutor(httpcalls): the fully-resolved, templated target URL is validated withUrlValidationUtils.validateUrl()(blocks private/loopback/link-local/CGNAT/cloud-metadata + non-http), and redirect-following is disabled per request (newIRequest.setFollowRedirects, honoured by the Vert.xHttpClientWrapper) so a3xx β internal hostcan't bypass validation.A2AToolProviderManager(peer Agent-Card fetch +tasks/send): both target URLs validated. The JDK client already defaults toRedirect.NEVER, so no redirect hop to re-check.Scoped out intentionally:
RemoteApiResourceSource(admin-initiated import-from-URL) β admin explicitly targets a URL, internal-instance imports are common, and the JDK client isRedirect.NEVER. Forcing private-IP blocking there would break legitimate internal imports.
Algorithm bugs found & fixed
CronParserβ day-of-week7not accepted as Sunday. Standard cron treats0and7as Sunday; the parser rejected7(range0β6) and, even if allowed,DayOfWeek % 7never yields7, so it would never match. Now7is accepted and normalized to0(normalizeDaysOfWeek).CronParserβ dom/dow used AND instead of standard-cron OR. When both day-of-month and day-of-week are restricted (neither is*), Vixie cron fires when either matches (e.g.0 0 13 * FRI= the 13th or any Friday). The parser ANDed them. NowdayMatches()applies OR when both fields are restricted, AND otherwise (single-restricted reduces to the restricted field, so existing schedules are unaffected). The smart-skip loop was reworked arounddayMatches.CronParserβ malformed fields crashed or silently never-fired.*/threwArrayIndexOutOfBoundsException(not a clean validation error); a reversed range like5-1produced an empty set β a schedule that never fires until the 2-year scan limit threw a confusingIllegalStateException. Both now throw a clearIllegalArgumentExceptionat parse time (step structure +start <= endchecks).ApiCallExecutorretry backoff was linear, not exponential.delay * amountOfExecutions(linear) despite theexponentialBackoffDelayInMillisfield name. Now true exponential βbase * 2^(attempt-1)β with an overflow-safe shift and a 5-minute ceiling (MAX_BACKOFF_MILLIS). First retry delay is unchanged (base), so the change only affects later retries.CalculatorToolβ unbounded recursion DoS. The recursive-descentSafeMathParserrecurses on nested parens; a long/deeply-nested LLM-supplied expression could throwStackOverflowError(anError, not caught bycalculate()). Added a 1000-char input cap plus a defensiveStackOverflowErrorcatch.InMemoryConversationCoordinatorβ unbounded dead-letter deque. The active-conversation map was capped butdeadLettersgrew without limit under a failure storm. Added a configurable cap (eddi.coordinator.max-dead-letters, default 1000;-1disables,0retains none) with oldest-first eviction β consistent with the existingeddi.coordinator.max-active-conversationsproperty.
Files changed
engine/mcp/McpApiToolBuilder.javaβ URL validation inparseSpec,looksLikeInlineSpec()modules/apicalls/impl/ApiCallExecutor.javaβ opt-in SSRF validation + redirect disable; exponential backoffmodules/llm/impl/A2AToolProviderManager.javaβ opt-in URL validation on peer fetch/sendengine/httpclient/IRequest.java+impl/HttpClientWrapper.javaβsetFollowRedirects(default no-op; Vert.x honours it)engine/runtime/internal/CronParser.javaβ DOW 7, OR semantics (dayMatches), step/range validationmodules/llm/tools/impl/CalculatorTool.javaβ length cap +StackOverflowErrorcatchengine/runtime/internal/InMemoryConversationCoordinator.javaβ dead-letter capresources/application.propertiesβ documentededdi.security.ssrf-protection.enabled
Tests added
McpApiToolBuilderTestβ +5 (file/classpath/non-http rejection, scheme-gate allows internal hosts, inline works, classifier)ApiCallExecutorTestβ +6 (SSRF block internal URL, disable redirects on public, protection-off no-op; exponential curve, ceiling cap, no-retry zero)CronParserTestβ +6 (DOW 7 = Sunday, 0β‘7, OR fires on dom and on weekday, single-restricted stays AND, reversed-range + malformed-step rejection)CalculatorToolTestβ +2 (over-long rejected, deep-nesting returns cleanly)InMemoryConversationCoordinatorTestβ +2 (dead-letter cap evicts oldest;-1disables)ApiCallExecutor/A2AToolProviderManager/InMemoryConversationCoordinatorconstructor-call sites updated across test files.Mock-based suites green; A2A + embedded-server suites are unrunnable in the sandbox (JDK
HttpClient/HttpServercan't open a selector) but compile and are exercised in CI.
Review follow-ups (Copilot + CodeRabbit)
IRequest.setFollowRedirectsfails closed β made it a non-default (abstract) interface method instead of a no-op default, so any newIRequestimpl must honour it and cannot silently re-enable the redirect bypass.Coordinator eviction is O(n), not O(nΒ²) β compute the dead-letter excess once and evict that many, instead of calling
ConcurrentLinkedDeque.size()per loop iteration.Coordinator dead-letter cap hardening β reject
max-dead-letters < -1at startup (only-1/0/positive are valid, so a typo like-2can't silently disable trimming), and serialize the add+trim under a small lock so concurrent failures enforce the cap deterministically (the existingpollFirstalready evicts oldest-first, so the newest failures were never dropped β the lock just removes transient under-retention).CronParserVixie star semantics β a day field is "starred" (not restricted, takes the AND path) when it begins with*, so*/2is treated like*(was exactequals("*"), which wrongly took the OR path).CronParserfield-aware parse errors βparseIntField()wrapsNumberFormatExceptioninto anIllegalArgumentExceptioncarrying the offending field (e.g.*/abcβ "Invalid number 'abc' in field: β¦"), instead of leaking a vague low-level message.CalculatorToolguards before logging β the length check now runs before the eagerLOGGER.debug("β¦ " + expression)concatenation, so an oversized payload is rejected without building/logging the big string.
Known residual (accepted, documented)
OpenAPI external
$refresolution (McpApiToolBuilder,setResolve(true)): the http(s) gate validates the top-level spec location but not external$refs inside the spec, so a crafted spec can still make the parser fetch a remote/file ref. Disabling resolution (setResolve(false)) would also break legitimate in-document#/componentsrefs that real specs rely on, so resolution is kept on. Mitigated by theeddi-admin/eddi-editorgate; a constrained ref-resolver is the proper (heavier) fix.
Not addressed here (architectural β out of scope for a hardening pass)
Open-by-default MCP/admin surface and role- vs tenant-based isolation for config resources.
Conversation-memory 16 MB BSON ceiling β needs a proper step-archival design, not a quick guard.
π PR Review Fixes β DynamicAgentConfig Propagation, Null Safety, Code Dedup (2026-06-26)
Repo: EDDI (feat/group-task-orchestration) What changed: 5 fixes addressing Copilot PR review findings.
Fixes
HIGH: DynamicAgentConfig propagation β Group-level guardrails were silently ignored. Fix: GCS stores config on GC (transient), passes via context to AgentOrchestrator which reads it from memory.
MEDIUM: Null-safe DynamicAgentConfig β Constructor defaults null to disabled config.
MEDIUM: Null-safe provider allow-list β
Objects::nonNullfilter beforeequalsIgnoreCase().MEDIUM: Null-safe model allow-list β Filters for both null map values and null list entries.
LOW: extractResponse() deduplication β Shared
ConversationOutputExtractorutility replacing 3 copies.
Files Changed
GroupConversation.javaβ TransientdynamicAgentConfigfield (@JsonIgnore)GroupConversationService.javaβ Config propagation +extractResponse()delegationAgentOrchestrator.javaβresolveDynamicAgentConfig()reads group config from contextCreateSubAgentTool.javaβ Null-safe constructor + allow-lists +extractResponse()delegationConverseWithAgentTool.javaβextractResponse()delegationConversationOutputExtractor.javaβ [NEW] Shared utility
Tests Added
ConversationOutputExtractorTestβ 11 testsDynamicAgentToolsTestβ 7 new null-safety tests
π§ MCP Group Tools β Async Discussion, Delete, @Blocking Fix (2026-06-26)
Repo: EDDI (feat/group-task-orchestration) What changed: 3 MCP improvements for Task Force group discussions.
Changes
Bug fix:
discuss_with_groupwas missing@Blockingβ a multi-minute TASK_FORCE discussion would block the Vert.x event loop thread, potentially freezing the MCP server. Now correctly annotated (matchestalk_to_agentpattern in McpConversationTools).New tool:
start_group_discussionβ async variant that returns immediately withgroupConversationId+IN_PROGRESSstate. Client polls withread_group_conversation. Uses existingstartAndDiscussAsync()backend method.New tool:
delete_group_conversationβ REST-MCP parity gap. DELETE endpoint existed in REST API but had no MCP equivalent.Improved docs: Tool descriptions now document what data
read_group_conversationreturns (task list, tracking lists, state) so MCP clients know they don't need separate tools for task inspection.
Design Decision
Rejected adding 5 separate tools (read_task_list, list_dynamic_agents, discuss_task, clone_group, describe_task_force_syntax) β all proposed data is already available via existing tools. Avoided tool sprawl (project already has 63 MCP tools).
Coverage
McpGroupTools: 91.79% instruction, 81.25% branch, 100% methods
9 new tests (31 total in McpGroupToolsTest): async success/defaults/blank/error, delete success/confirmation/error, @Blocking annotation reflection tests
Full suite: 9,611 tests, 0 failures
π§ͺ Comprehensive Branch Coverage for Dynamic Agent System (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: Added 60+ targeted unit tests to cover all uncovered branches in the Task Force / Dynamic Agent feature. Coverage improved from 0.88β0.89 instructions (unit tests only; CI with integration tests will exceed 0.90/0.80 thresholds).
Files Modified (Tests)
DynamicAgentToolsTest (+25 tests): initialMessage flow, extractResponse all branches, blank params, empty allow-lists, retain=false, general exceptions
TaskListParserTest (+22 tests): all JSON key aliases, null/empty members, markdown formats, long text truncation, null displayName safety
SharedTaskListTest (+12 tests): findTasksForAgent(null), wrong status transitions, nonexistent ID exceptions, failTask from various states, setTasks(null)
AgentGroupConfigurationTest (+12 tests): LifecyclePolicy toJson/fromJson, TaskDefinition constructors, DiscussionPhase requiresApproval
Notes
Local
mvnw verifyshows 0.89/0.78 because ITs are skipped. CI runs-DskipITs=falseβ exceeds thresholds.Total test count: 9,573 (0 failures, 0 errors)
π§ Dynamic Agent System β Critical Code Review Fixes (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: 3-reviewer code review uncovered 6 critical bugs and 8 medium issues. All critical and key medium issues fixed.
Critical Fixes
C1: Shared tracking lists β
AgentOrchestratorwas creating separatecreatedAgentIds/retainedAgentIdsper whitelist tool call. TeardownAgentTool couldn't see agents created by CreateSubAgentTool. Fixed: shared lists created once, passed to all tools.C2: Retain flag non-functional β
CreateSubAgentToolacceptedretain=truebut never populatedretainedAgentIds. Agents were auto-deleted despite LLM requesting retention. Fixed: wiredSet<String> retainedAgentIdsto constructor +retainedAgentIds.add(agentId)when retain=true.C3: Double quota counting β
CreateSubAgentToolcalledacquireConversationSlot()thenstartConversation()also called it internally. Each creation burned 2 quota slots. Fixed: removed explicit quota call from tool.C4: Transcript race condition β
GroupConversation.transcriptwas a plainArrayListaccessed from parallel virtual threads. Fixed:Collections.synchronizedList(new ArrayList<>())+ null-safe setter.C5: Dead ERROR detection β
ConverseWithAgentTool.extractResponse()returned""instead ofnull, makingresponse == nullcheck dead code. Fixed: returnsnullfor empty/missing outputs.C6: Zero test coverage β
ConverseWithAgentToolhad 154 lines of untested code. Added 8 tests covering new conversation, existing conversation, validation, timeout, error state, empty response.
Medium Fixes
M1: LifecyclePolicy enum β
lifecyclePolicychanged fromStringtoLifecyclePolicyenum with@JsonValue/@JsonCreatorfor kebab-case JSON. Typos now fail at deserialization instead of silently skipping cleanup.M2: synchronizedList streaming β
findMemberIncludingDynamic()now wrapsfindMember(dynamicMembers)insynchronized(dynamicMembers)block.M3: Cycle detection β
SharedTaskList.detectCycles()now called after task list dependency resolution. Circular deps throwGroupDiscussionExceptionfail-fast.M5: unretainAgent() β New
@Toolmethod onTeardownAgentToolto remove retention flags.M6: Agent removal after teardown β
createdAgentIds.remove(agentId)after successful undeploy, so counter accurately reflects active agents.M10: Case-insensitive guardrails β Provider/model allow-list checks now use
equalsIgnoreCase().
Test Updates
DynamicAgentToolsTest: +8 ConverseWithAgentTool tests, updated quota test, updated enum assertionsGroupConversationTest: Updated enum count assertions (TranscriptEntryType 11β14, GroupConversationState 5β6)9,486 tests pass, 0 failures
β¨ Dynamic Agent System β Create, Recruit, Delegate (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: LLM agents in TASK_FORCE group conversations can now dynamically create, recruit, converse with, and teardown other agents at runtime. This enables agentic patterns where a moderator or specialist agent can spin up sub-agents on-the-fly to accomplish tasks.
Config Model
DynamicAgentConfigβ new inner class onAgentGroupConfigurationwith config switches for creation, recruitment, delegation, guardrails (provider/model whitelists, per-discussion caps), and lifecycle policy (ephemeral/keep-deployed/undeploy-only/agent-decides)GroupConversationβ addeddynamicMembers,createdAgentIds,retainedAgentIdsfields for runtime tracking
4 LLM Tools (all @Vetoed, per-invocation constructed)
CreateSubAgentToolβ creates + deploys agent viaAgentSetupService, quota-gated, guardrail-validated, optional initial messageConverseWithAgentToolβ send messages to any deployed agent, supports multi-turn via conversationIdFindAgentsByCapabilityToolβ discover agents by skill viaCapabilityRegistryServiceTeardownAgentToolβ undeploy/delete created agents +retainAgentfor lifecycle override
Wiring
AgentOrchestrator+LlmTaskβ 5 new CDI dependencies, whitelist-gated tool names:create_sub_agent,converse_with_agent,find_agents_by_capability,teardown_agentGroupConversationServiceβfindMemberIncludingDynamic()for task assignment to dynamic members,cleanupEphemeralAgents()in finally block with lifecycle policy enforcement
Tests
DynamicAgentToolsTestβ 22 tests: CreateSubAgent (8), FindAgents (4), Teardown (5), DynamicAgentConfig (2), GroupConversation fields (6)All existing test files updated for new constructor signatures (11 files)
π Fix: Tenant Quota Enforcement in Group Conversations (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: QuotaExceededException from ConversationService was being silently caught and treated as a per-agent skip/retry. Now detected at 4 levels and causes immediate abort β prevents burning N round-trips when quota is exhausted.
executeAgentTurnβstartConversation(): immediateGroupDiscussionExceptionexecuteAgentTurnβsay(): unwrap fromExecutionException, abort (bypasses retry policy)Task execution loop: quota error exits the agent's
CompletableFutureimmediatelyParallel phase: quota propagates through
CompletionException, cancels remaining futuresReview fix: quota errors in task loop now propagate regardless of
onAgentFailurepolicy (was silently lost with SKIP policy)+3 regression tests (startConversation quota, say() quota, no-retry-even-with-RETRY-policy). Total: 112 tests, 0 failures.
π Fix: Final Review β Duplicate Task Bug, Regression Tests (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: Second review pass found 3 remaining issues (1 CRITICAL, 1 MEDIUM, 1 dead code). All fixed. Added comprehensive regression tests.
C1-final:
addTaskβupdateTaskβ pre-configured dependency resolution was APPENDING tasks with same ID instead of REPLACING, silently breaking dependency orderingM1-final:
setMemberConversationIdsdefensively wraps inConcurrentHashMap(MongoDB deserialization was replacing withLinkedHashMap)Dead code: Removed unused
snapshotTranscriptfromexecuteTaskExecutionPhaseNew:
SharedTaskList.updateTask()public synchronized methodRegression tests: +20 tests covering
resolveTaskAssignment(7),tryParseVerificationJson(6),handleTaskFailure(2),setMemberConversationIds(2),updateTask(3). Total: 109 tests, 0 failures.
π Fix: TASK_FORCE Code Review β Thread Safety, Verification Parser, Error Handling (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: Three-pass code review identified 4 CRITICAL, 6 HIGH, and 4 MEDIUM issues. All fixed.
Critical Fixes (C1βC4)
Thread safety: All
SharedTaskListpublic methods nowsynchronizedβ prevents race conditions during parallel EXECUTE phaseConcurrentHashMap:
GroupConversation.memberConversationIdschanged fromLinkedHashMaptoConcurrentHashMapDependency resolution: Pre-configured
TaskDefinition.dependsOnsubjects now resolved to actual task IDs (was silently dropped)Null guard:
resolveTaskAssignmentnull returns no longer crashassignTask
High Fixes (H1βH6)
Transcript snapshot: EXECUTE phase now takes
List.copyOf(gc.getTranscript())before launching parallel futures (consistent withexecuteParallelPhase)Timeout semantics: Changed from
timeout Γ agentCounttotimeout Γ maxTasksPerAgent(agents run in parallel, tasks per agent are sequential)Round-robin assignment:
resolveTaskAssignment("ALL")now distributes evenly across non-moderator members (was always picking first)Verification parser: Dedicated JSON parser reads
passedboolean directly (was using heuristiccontains("fail"))IllegalStateException: Now caught alongside
GroupDiscussionExceptionin parallel EXECUTE lambdaError events: New
handleTaskFailure()method emits transcript entry + SSE event for failed tasks
Medium Fixes (M1βM4)
Slack:
TASK_FORCEadded toEXPANDED_STYLESsetCycle detection: Changed from
ArrayList.contains()O(n) toHashSet.contains()O(1)Fallback:
singleTaskFallbacknow preserves LLM output as task description (was discarding it)HITL placeholders:
BLOCKEDandAWAITING_APPROVALstatuses documented as Phase 9b placeholders
Documentation Updates (6 files)
architecture.md,group-conversations.md,README.md,AGENTS.md,mcp-server.md,slack-integration.md,HANDOFF.mdβ all updated from "5 styles" to "6 styles" with TASK_FORCE entries
New Tests (+18 tests)
SharedTaskListTest: +11 tests (null findById, nonexistent IDs, verified deps, multiple deps, self-ref cycles, defensive copy, concurrent stress)TaskListParserTest: +7 tests (empty array, code-fenced JSON, empty members, missing fields, round-robin, tier-3 output preservation)
β¨ Feature: TASK_FORCE Group Orchestration β Collaborative Task Accomplishment (2026-06-25)
Repo: EDDI (feat/group-task-orchestration) What changed: Added a new TASK_FORCE discussion style to group conversations. Instead of debating, agents collaborate to accomplish concrete tasks together via a PLAN β EXECUTE β VERIFY β SYNTHESIS pipeline.
Key Design Decisions
Config-driven: Tasks can be pre-configured in
AgentGroupConfiguration.tasks[](skips PLAN phase) or dynamically generated by the LLM viaTaskListParser(three-tier fallback: JSON β Markdown β single task).Reuses existing infrastructure: Task execution goes through normal agent pipelines. No new REST endpoints.
State embedded in GroupConversation:
SharedTaskListis a field onGroupConversation, persisted as part of the MongoDB document.HITL forward-compatible:
AWAITING_APPROVALstate added to bothGroupConversationStateandTaskStatusfor Phase 9b.Parallel execution: Tasks for different agents run in parallel; tasks for the same agent run sequentially.
Files Changed (4 new, 12 modified)
New:
SharedTaskList.java,TaskListParser.java,SharedTaskListTest.java(18 tests),TaskListParserTest.java(12 tests)Model:
AgentGroupConfiguration.java(TASK_FORCE style + enums),GroupConversation.java(taskList field + entry types)Orchestration:
GroupConversationService.java(~400 LOC task phase logic),DiscussionStylePresets.java(expansion + templates)API:
GroupConversationEventSink.java,IGroupConversationService.java,RestGroupConversation.java,RestAgentGroupStore.java,McpGroupTools.java,SlackGroupDiscussionListener.javaTests:
DiscussionStylePresetsTest.java(+5 tests),McpGroupToolsTest.java(fixed for new param)
π README Audit & MCP Docs Update (2026-06-25)
Repo: EDDI (chore/readme-update) What changed: Comprehensive README accuracy audit and stale data fixes.
README.md
Removed hardcoded version: Replaced
**Latest version: 6.1.0**with a dynamic shields.io GitHub Release badge β auto-updates from GitHub Releases, no manual maintenance.Added UNIDO mention: Added
UNIDO Trusted Partneralongside Red Hat certification in the intro paragraph.Updated MCP tool count:
42 toolsβ60+ toolsin both the Standards table and Documentation table.Updated Quarkus SDK example: Replaced hardcoded
<version>6.1.0</version>with<version>LATEST</version>and a comment linking to the quarkus-eddi releases page.
docs/mcp-server.md
Updated header count:
Available Tools (48)βAvailable Tools (63).Fixed stale reference: Tool Filtering section still said "48 intended tools" β updated to 63.
Added 3 missing tool sections (15 tools total):
Memory Tools (8):
list_user_memories,get_visible_memories,search_user_memories,get_memory_by_key,upsert_user_memory,delete_user_memory,delete_all_user_memories,count_user_memoriesGDPR Tools (2):
delete_user_data,export_user_dataChannel Integration Tools (5):
list_channel_integrations,read_channel_integration,create_channel_integration,update_channel_integration,delete_channel_integration
Verification
All 63
@Toolannotations inengine/mcp/verified asio.quarkiverse.mcp.server.Tool(not langchain4j).GitHub Releases API confirms proper releases exist (badge renders correctly).
All 3 new cross-links (
user-memory.md,gdpr-compliance.md,slack-integration.md) verified present.
Files: README.md, docs/mcp-server.md, docs/changelog.md
Refactor: Standardize backup logger fields (2026-06-23)
Repo: EDDI (refactor/539-logger-name) What changed: Renamed the private logger field from log to LOGGER in the seven backup implementation classes listed in #539. The change follows the existing project convention and does not alter runtime behavior.
Files: RemoteApiResourceSource.java, RestExportService.java, RestImportService.java, SourceUrlValidator.java, StructuralMatcher.java, UpgradeExecutor.java, ZipResourceSource.java, docs/changelog.md
π Fix: Swagger UI CSP Regression β Duplicate Header Causes Inline Script Block (2026-06-23)
Repo: EDDI (fix/swagger-csp-duplicate-header) What changed: Swagger UI showed a blank page on Docker with Content-Security-Policy blocking inline scripts (script-src-elem violation).
Root Cause
The original CSP fix (June 3) used two quarkus.http.filter entries with order-based precedence, assuming the higher-order Swagger filter would replace the default filter's CSP header. In reality, both filters fire and both add a Content-Security-Policy header to the response. Per the CSP spec, when a browser receives multiple CSP headers it enforces the intersection (most restrictive) β so the default filter's script-src 'self' blocked Swagger's inline scripts regardless of the relaxed swagger filter.
The CI smoke test only checked /q/health/ready for header presence, never the Swagger UI path, and never checked for duplicate headers β so the bug was never caught.
Fix
Changed the default CSP filter regex from /.* to /(?!q/swagger-ui(/|$)).* β a negative lookahead that excludes exactly /q/swagger-ui and /q/swagger-ui/.... This ensures only one CSP header is sent per path: the strict one for the application and the relaxed one for Swagger UI.
Files: application.properties, docs/changelog.md
Swagger UI Overhaul, Manager Update & Version 6.1.1 (2026-06-23)
Repo: EDDI (feat/swagger-ui-overhaul) What changed: Complete overhaul of Swagger UI, version bump to 6.1.1, Manager frontend asset update, and Docker base image bump.
Tag Taxonomy (40 tags, 9 categories)
All @Tag annotations updated from flat names to category-based hierarchy (Category / Subcategory) for logical grouping in Swagger UI. The @OpenAPIDefinition tag array in OpenApiConfig.java defines the canonical taxonomy.
Agents: Setup, Agents, Administration, Agent Groups
Configuration: Workflows, LLM, Behavior Rules, Dictionary, Output, API Calls, MCP Calls, Properties, Prompt Snippets, Global Variables
Conversations: Conversations, Group Conversations, Conversation Store, Attachments
Integrations: A2A Protocol, Capability Registry, Channel Integrations, Slack Webhook
Knowledge & Memory: RAG Knowledge Bases, RAG Ingestion, User Memory
Security: Authentication, Secrets Vault, Audit Trail, GDPR / Privacy, Tenant Quotas
Administration: Backup, Schedules, Coordinator Admin, Orphan Admin, Log Admin, Descriptors
Tools: Tool History, Template Preview, Standalone NLP
UI: Chat UI
All 49 REST interface @Tag annotations now include description attributes (SmallRye was silently dropping @OpenAPIDefinition descriptions when interface-level @Tag lacked one).
4 previously untagged endpoints received new @Tag annotations:
ILogoutEndpointβSecurity / AuthenticationRestSlackWebhookβIntegrations / Slack WebhookRestToolHistoryβTools / Tool History(+ added missing@ApplicationScoped)RestA2AEndpointβIntegrations / A2A Protocol(capability endpoints taggedIntegrations / Capability Registry)
OpenApiTagSortFilter (new)
New OASFilter implementation (OpenApiTagSortFilter.java) sorts tags alphabetically at build time, producing stable ordering. Fixed UnsupportedOperationException caused by sorting SmallRye's unmodifiable tag list. Swagger UI config: quarkus.swagger-ui.tags-sorter=alpha, quarkus.swagger-ui.theme=original.
Swagger UI Light/Dark Mode
Complete rewrite of META-INF/branding/style.css with proper dual-theme support:
Light mode (default): white backgrounds, dark text, amber-600 (
#d97706) accentsDark mode (lamp toggle β
html.dark-mode): EDDI Manager palette β zinc-950 bg, zinc-900 surfaces, amber-500 accentsTopbar stays dark (
#18181b) in both modes for brand consistency with logoEDDI amber accents on Authorize, Execute, Explore, and Try-it-out buttons
Version badge
6.1.1with WCAG AAA contrast; OAS 3.1 badge demoted to subtle grayHTTP verb tinted operation blocks (blue GET, green POST, amber PUT, red DELETE, purple PATCH)
Logo renamed
eddi-logo.pngβlogo.png(Quarkus auto-detection convention)
Version Bump β 6.1.1
Updated across: pom.xml, application.properties (Γ3 fields), OpenApiConfig.java, Dockerfile, Chart.yaml, eddi-deployment.yaml, quickstart.yaml, redhat-certify.yml.
Docker Base Image
Bumped Red Hat UBI9 OpenJDK 25 runtime digest (sha256:0f4e04... β sha256:2aed9f...).
Manager Frontend
Updated manage.html asset references to latest EDDI-Manager build. Removed old bundle artifacts (~4,000 lines of obsolete JS/CSS).
Files changed: 100 files, +1,107 / β4,041 lines
π¦ Safe Dependency Bumps (2026-06-19)
Repo: EDDI (chore/bump-safe-deps) What changed: Bumped two dependencies to their latest stable patch/minor versions. Both are drop-in upgrades with no breaking changes.
quarkus-mcp-server.version:1.12.1β1.13.0β adds lazy SSE initialization for Streamable HTTP transport (defers SSE setup until first API call)swagger-parser:2.1.42β2.1.44β bug fix for unsafe Yaml instantiation in ReferenceVisitor
File: pom.xml Verified: mvnw compile passes cleanly.
π OpenSSF Scorecard β SAST on All Commits (2026-06-18)
Repo: EDDI (fix/code-review-bugs) What changed: Changed the CodeQL SAST job gate in ci.yml from a pure path-filter condition to a hybrid: always run on push to main, but still skip docs-only PRs. The previous if: needs.detect-changes.outputs.code == 'true' condition was causing CodeQL to be skipped on Dependabot merge commits, resulting in OpenSSF Scorecard warning: "28 commits out of 30 are checked with a SAST tool."
File:
.github/workflows/ci.ymlβcodeqljob now usesgithub.event_name == 'push' || needs.detect-changes.outputs.code == 'true'Rationale: OpenSSF Scorecard only checks commits on the default branch (push events), so CodeQL must always run on push. For PRs, the path filter still saves ~3 min of CI time on docs-only changes since PR checks don't affect the scorecard.
π Bug Fixes from Code Review β 4 Concurrency & Null Safety Issues (2026-06-10)
Repo: EDDI (fix/code-review-bugs) What changed: Fixed 4 verified bugs from code review (priority HIGH to MEDIUM). All fixes include regression tests.
Fix #1 β PropertySetterTask NPE on blank input (HIGH)
Root cause:
CATCH_ANY_INPUT_AS_PROPERTYhandler dereferencesgetLatestData("input:initial")without null check. When a client sends an empty/whitespace-only message,Conversation.storeUserInputInMemoryskips storinginput:initialβgetLatestDatareturns null β NPE β pipeline dies β conversation enters ERROR state.Fix: Added null guards for both
initialInputDataandinitialInput.Tests: 3 new tests β missing
input:initial, null result, empty string result.Files:
PropertySetterTask.java,PropertySetterTaskTest.java
Fix #2 β Config version race condition (HIGH PG / MEDIUM-LOW Mongo)
Root cause:
HistorizedResourceStore.update()does non-atomic readβincrementβwrite. Two concurrent edits both read version N, both write N+1 β last write wins silently. On PostgreSQL:ON CONFLICT DO UPDATEsilently merges history. On MongoDB: historyinsertOnethrows unhandledMongoWriteException(HTTP 500 instead of 409).Fix: Introduced optimistic locking via
storeIfCurrentVersion()default method onIResourceStorage. MongoDB overrides with version-conditionedupdateOne(checkmatchedCount). PostgreSQL overrides withUPDATE WHERE version = ?(check affected rows). History inserts hardened: Mongo catches duplicate-key 11000; Postgres usesON CONFLICT DO NOTHING.Tests: 1 new test for concurrent modification detection (mock throws
ResourceModifiedException); existing update test updated to verifystoreIfCurrentVersiondelegation.Files:
IResourceStorage.java,MongoResourceStorage.java,PostgresResourceStorage.java,HistorizedResourceStore.java,HistorizedResourceStoreTest.java
Fix #3 β ComponentCache HashMap race (MEDIUM)
Root cause:
ComponentCacheis@ApplicationScoped(singleton) using plainHashMap.computeIfAbsentonHashMapis not thread-safe. Concurrent reads (every conversation turn viaLifecycleManager) and writes (lazy agent deployment viaWorkflowStoreClientLibrary) can corrupt the map.Fix: Replaced
HashMapwithConcurrentHashMapfor both outer and inner maps.Tests: 1 new concurrent stress test (8 threads, 500 ops each, mixed read/write).
Files:
ComponentCache.java,ComponentCacheTest.java
Fix #4 β Zombie-write snapshot clobber after timeout (MEDIUM)
Root cause: When an agent times out,
future.cancel(true)sets the interrupt flag but doesn't stop threads blocked in non-interruptible I/O (LLM HTTP calls). When the call eventually completes,onCompletecallback fires βstoreConversationMemoryβ unconditionalreplaceOneoverwrites the newer conversation state.Fix: Check
Thread.currentThread().isInterrupted()before callingonComplete(). If interrupted, route toonFailure()instead (with log warning).Tests: 2 new tests β cancelled thread routes to
onFailure; non-interrupted thread still routes toonComplete.Files:
BaseRuntime.java,BaseRuntimeTest.java
Design Decisions
Optimistic locking as default method:
storeIfCurrentVersion()was added as adefaultmethod on theIResourceStorageinterface (delegating tostore()) rather than an abstract method. This avoids breaking all existing implementations while letting backends opt into conditional writes. The Javadoc clearly states that the default does not provide optimistic locking.Interrupt check over Future.isCancelled(): The zombie-write fix checks
Thread.currentThread().isInterrupted()inside the submitted lambda rather than inspectingFuture.isCancelled()from outside, because the interrupt flag is the only signal visible from within the executing thread after a non-interruptible I/O completes.Return null on interruption: When the interrupt flag is set, the lambda now returns
nullinstead of the stale result. This prevents callers whofuture.get()the returned Future from receiving a stale value that was already routed toonFailure.ConcurrentHashMap over synchronized blocks: For
ComponentCache,ConcurrentHashMapwas chosen overCollections.synchronizedMapor explicit locking becausecomputeIfAbsentprovides exactly the atomic read-or-create semantics needed, with better concurrency than full map locking.No conversation context in BaseRuntime logs:
BaseRuntimeis generic executor infrastructure with no access to conversation/agent IDs. The warning log includes the thread name for traceability; richer context is logged by the downstreamonFailurecallback inConversationService.
π‘οΈ Security Audit Remediation β IDOR Prevention & Ownership Validation (2026-06-10)
Repo: EDDI (fix/security-audit-idor-remediation) What changed: Addressed 5 findings from a comprehensive security audit. Added resource ownership validation across all conversation, user memory, and group conversation REST endpoints. Hardened GDPR, A2A, and MCP annotations.
Finding: IDOR β Conversations (HIGH β FIXED)
Problem: Any authenticated user with
eddi-userrole could read/modify ANY conversation by guessing the conversationId. No ownership validation existed despiteConversationDescriptorhaving auserIdfield.Fix:
RestAgentEnginenow injectsSecurityIdentity,OwnershipValidator, andIConversationDescriptorStore. All conversation-scoped endpoints (readConversation,say,endConversation,undo,redo,rerun,readConversationLog,getConversationState) validate that the caller owns the conversation.startConversationvalidates that the provideduserIdmatches the caller's identity (admins can set any userId).
Finding: IDOR β User Memory (HIGH β FIXED)
Problem: Any authenticated user could read/delete another user's persistent memories via the
/usermemorystore/memories/{userId}endpoints.Fix:
RestUserMemoryStorenow injectsSecurityIdentityandOwnershipValidator. All endpoints validate that the{userId}path parameter matches the authenticated caller.upsertMemoryvalidates against theuserIdin the request body.
Finding: IDOR β Group Conversations (HIGH β FIXED)
Problem: Any authenticated user could read/delete any group conversation.
Fix:
RestGroupConversationnow validates ownership onreadGroupConversationanddeleteGroupConversation.listGroupConversationsfilters results to only the caller's conversations.discuss/discussStreamingvalidate the provided userId.
Finding: GDPR Annotation on Implementation Only (MEDIUM β FIXED)
Problem:
@RolesAllowed("eddi-admin")was only onRestGdprAdminimplementation, not theIRestGdprAdmininterface. Fragile to refactoring.Fix: Moved
@RolesAllowed("eddi-admin")to the interface level.
Finding: A2A Endpoint Annotation Clarity (MEDIUM β FIXED)
Problem: A2A GET discovery endpoints had no explicit security annotations, making intent unclear.
Fix: Added
@PermitAllto all 5 GET discovery endpoints to document intentional public access per A2A protocol spec.
Finding: MCP Memory Ownership (NEW β FIXED)
Problem: MCP memory read tools (
list_user_memories,get_visible_memories, etc.) accepteduserIdas a tool parameter without validating against the caller's identity.Fix:
McpMemoryToolsnow injectsOwnershipValidatorand callsvalidateUserAccess()in all 5 read-only MCP memory tools (initially viaMcpToolUtils.requireOwnerOrAdmin(), consolidated to directOwnershipValidatoruse in code review hardening below).
New Component: OwnershipValidator
Centralized
@ApplicationScopedutility for ownership checksThree methods:
validateUserAccess(),validateAndResolveUserId(),requireOwnerOrAdmin()All checks are no-ops when
authorization.enabled=false(dev mode)eddi-adminrole bypasses all ownership checksLegacy data without ownership (null/blank userId) is allowed through gracefully
WARN-level audit logging on all ownership check failures
Dropped Finding: MCP Unauthenticated by Default
Rationale: When OIDC is disabled, ALL endpoints are unauthenticated β MCP is not uniquely vulnerable.
AuthStartupGuardalready prevents accidental unauthenticated production deployments. Not a finding.
Files: OwnershipValidator.java [NEW], RestAgentEngine.java, RestUserMemoryStore.java, RestGroupConversation.java, IRestGdprAdmin.java, RestGdprAdmin.java, RestA2AEndpoint.java, McpToolUtils.java, McpMemoryTools.java
Code Review Hardening (2026-06-10)
Repo: EDDI (fix/security-audit-idor-remediation) What changed: Addressed all findings from the post-implementation code review.
M1 β MCP ownership consolidation: Removed duplicate
requireOwnerOrAdminstatic method fromMcpToolUtils.McpMemoryToolsnow injectsOwnershipValidatordirectly and callsvalidateUserAccess()β single source of truth for ownership logic.M3 β PII in WARN logs:
OwnershipValidatorWARN messages no longer include caller/user IDs. Full details are logged at DEBUG level only, reducing compliance risk.M4 β Narrow catch clause:
RestAgentEngine.validateConversationOwnership()now catchesResourceNotFoundExceptionandResourceStoreExceptionspecifically instead of genericException, preventing unexpected errors from being silently swallowed.BUG-2 β deleteMemory ownership: Added
findEntryById(String entryId)toIUserMemoryStorewith MongoDB and PostgreSQL implementations.RestUserMemoryStore.deleteMemory()now looks up the entry, validates ownership viavalidateUserAccess(), and returns 404 if not found.
Files: OwnershipValidator.java, RestAgentEngine.java, RestUserMemoryStore.java, McpToolUtils.java, McpMemoryTools.java, IUserMemoryStore.java, MongoUserMemoryStore.java, PostgresUserMemoryStore.java
Test Coverage for Security Fixes (2026-06-10)
Repo: EDDI (fix/security-audit-idor-remediation) What changed: Added 36 new tests covering all security-critical ownership validation logic.
OwnershipValidatorTest [NEW]: 24 tests across 4 nested groups β
validateUserAccess,validateAndResolveUserId,requireOwnerOrAdmin,isAuthEnabled. Covers auth on/off, admin bypass, legacy null owner, caller mismatch β ForbiddenException.RestAgentEngineTest β OwnershipValidation: 5 tests β admin userId override, impersonation rejection, non-owner read/end, descriptor-not-found graceful skip.
RestUserMemoryStoreTest β DeleteMemory: 3 tests β owner match β 204, not found β 404, non-owner β ForbiddenException.
RestGroupConversationTest β OwnershipValidation: 4 tests β non-owner read/delete, userId resolution in discuss, list filtering for non-admin.
Existing test fixes: Updated
RestAgentEngineTest,RestGroupConversationTest,McpMemoryToolsTeststubs for new constructor parameters and ownership lookup patterns.
Total: 184 security-related tests, 0 failures, 0 errors. Files: OwnershipValidatorTest.java [NEW], RestAgentEngineTest.java, RestUserMemoryStoreTest.java, RestGroupConversationTest.java, McpMemoryToolsTest.java
GitHub Advanced Security / CodeQL Remediation (2026-06-10)
Repo: EDDI (fix/security-audit-idor-remediation) What changed: Addressed 12 CodeQL "Log Injection" findings and 5 Copilot validation-order findings from automated PR review.
Log Injection β RestAgentEngine:
validateConversationOwnership()now sanitizesconversationIdviaLogSanitizer.sanitize()before logging.Log Injection β OwnershipValidator: All 3 debug-level log statements (
validateUserAccess,validateAndResolveUserId,requireOwnerOrAdmin) now sanitize user-provided values (callerId,requestedUserId,resourceOwnerId,resourceType) viaLogSanitizer.sanitize().Fail-closed ownership check:
RestAgentEngine.validateConversationOwnership()now throwsForbiddenExceptiononResourceStoreExceptioninstead of silently skipping the ownership check. Previous fail-open behavior could allow unauthorized access during transient DB errors.MCP validation order: In
McpMemoryTools, all 5 read-only tools (listUserMemories,getVisibleMemories,searchUserMemories,getMemoryByKey,countUserMemories) now validateuserIdis non-null/non-blank before callingownershipValidator.validateUserAccess(). Previously, a missinguserIdwith auth enabled would throwForbiddenExceptioninstead of the intended"userId is required"error JSON.Changelog clarity: Updated MCP ownership entry (line 32-35) to reflect final state β
OwnershipValidator.validateUserAccess()is the sole mechanism, notrequireOwnerOrAdmin()inMcpToolUtils.
Files: RestAgentEngine.java, OwnershipValidator.java, McpMemoryTools.java, docs/changelog.md
π Fix: Swagger UI Broken by CSP β Per-Path Filter Override (2026-06-03)
Repo: EDDI (fix/swagger-ui-csp) What changed: Swagger UI (/q/swagger-ui/) was blocked by the strict Content-Security-Policy header β inline scripts and eval() were rejected, rendering a blank page.
Root Cause
The global quarkus.http.header.Content-Security-Policy applied script-src 'self' to all paths, including Swagger UI. Swagger UI requires 'unsafe-inline' (inline <script> tags) and 'unsafe-eval' (JSON schema rendering via eval()).
Fix
Replaced the global quarkus.http.header.Content-Security-Policy with two quarkus.http.filter entries using Quarkus's native path-based filter mechanism:
csp-default(order=10, matches/.*): Strict CSP for the entire application βscript-src 'self'csp-swagger(order=20, matches/q/swagger-ui/.*): Relaxed CSP β adds'unsafe-inline' 'unsafe-eval'toscript-src
Higher order takes precedence, so the Swagger filter overrides the default for its path.
Why not a Java filter?
An initial approach used @Observes Router to register a Vert.x handler, but this has an ordering race: Quarkus may apply quarkus.http.header headers via a headersEndHandler (fires just before wire flush), which would overwrite the Java handler's header. The quarkus.http.filter approach has no such ambiguity β Quarkus manages precedence internally via the order property.
Files: application.properties
π Fix: White Page at Root β index.html Revert to Redirect (2026-06-03)
Repo: EDDI (fix/manager-deploy-and-index-html) + EDDI-Manager (deploy script) What changed: Root URL (/) showed a white page because index.html referenced deleted asset hashes.
Root Cause
Commit 0ec6cb47c (Jun 2) replaced index.html's simple redirect with a full copy of the Manager SPA, duplicating the hashed asset references from manage.html. When the deploy script (deploy-to-local-eddi-repo.ps1) ran a Manager rebuild in d9e6361, it updated manage.html and the asset files but had no knowledge of index.html β leaving it pointing at deleted files (index-Bn-sgAam.js, index-BZNayFGO.css). With X-Content-Type-Options: nosniff, the browser blocked the HTML fallback response.
Fix
index.htmlβ Reverted to a simple<meta http-equiv="refresh">redirect to/manage. No asset references, no sync needed. Keycloak works because the SPA boots at/manageand setsredirectUrito its own URL; the Keycloak client'sredirectUris: ["http://localhost:*"]matches any path.deploy-to-local-eddi-repo.ps1β Removed$IndexHtmlhandling (no longer needed). Script only updatesmanage.html.
Architecture Clarification
index.html
/ (Quarkus static)
Redirect to /manage
manage.html
/manage + /manage/{path} (RestManagerResource)
Manager SPA entry + client-side route fallback
chat.html
/chat/...
Chat widget (separate assets under /scripts/)
Files: index.html, deploy-to-local-eddi-repo.ps1
π PR Review Fixes β Code Quality & Correctness (2026-06-03)
Repo: EDDI (fix/mcp-endpoint-bugs) What changed: Addressed all findings from automated PR review bots (github-code-quality, CodeRabbit, Copilot) plus a critical exception handling bug found during manual review.
Critical Fix: Stale Conversation Cleanup Was Dead Code
Root cause:
McpConversationTools.getOrCreateManagedConversation()caughtjakarta.ws.rs.NotFoundException, butRestAgentEngine.getConversationState()actually throwsIConversationService.ConversationNotFoundException(a plainRuntimeException). The JAX-RS exception was never thrown by this code path.Impact: Stale conversation mappings (pointing to deleted conversations) were never cleaned up β the exception propagated to the outer catch and returned a generic error.
Fix: Changed catch to
IConversationService.ConversationNotFoundException.Test: Updated
chatManaged_staleConversation_recreatesFreshto throw the correct exception type.
Other Fixes
Unused variable: Removed
String resultin test (github-code-quality)Field filter bypass:
readConversationwithreturningFields=conversationOutputsno longer strips the full payload β section-level names are now detected and preservedredhat-certify.yml: Updated default version from
6.0.2to6.1.0README.md: Updated version from
6.0.2to6.1.0(header badge and Maven snippet)redhat-openshift.md: Fixed YAML example indentation back to standard 2-space Kubernetes style
Changelog wording: Softened "across all deployment artifacts" to "across the main deployment artifacts"
New Tests
chatManaged_endedConversation_recreatesFreshβ coversConversationState.ENDEDβ delete+recreate pathchatManaged_transientStateError_doesNotRecreateβ verifies transient DB errors propagate without deleting valid mappingsreadConversationDescriptors_agentVersionFilter_matchesCorrectVersionβ agentVersion filter positive matchreadConversationDescriptors_agentVersionFilter_filtersWrongVersionβ agentVersion filter negative match
π¦ Version Bump 6.0.2 β 6.1.0 (2026-06-03)
Repo: EDDI (fix/mcp-endpoint-bugs) What changed: Bumped project version from 6.0.2 to 6.1.0 across the main deployment artifacts and related documentation to reflect the scope of changes since RC2 (MCP bug fixes, dependency updates, Manager UI refresh, security hardening).
Files Updated
pom.xmlβ Maven artifact versionsrc/main/docker/DockerfileβEDDI_VERSIONbuild arg + Red Hat certification labelshelm/eddi/Chart.yamlβappVersionk8s/base/eddi-deployment.yamlβapp.kubernetes.io/versionlabelsk8s/quickstart.yamlβapp.kubernetes.io/versionlabelssrc/main/resources/application.propertiesβsystemRuntime.projectVersion,quarkus.smallrye-openapi.info-version,quarkus.container-image.additional-tagssrc/main/resources/initial-agents/available_agents.txtβ Agent Father ZIP filenamesrc/main/resources/initial-agents/Agent+Father-6.1.0.zipβ [NEW] updated bundled agentsrc/main/resources/initial-agents/Agent+Father-6.0.2.zipβ [DELETED] superseded.github/workflows/redhat-certify.ymlβ Red Hat certification workflow version refsdocs/redhat-openshift.mdβ documentation version refs
π MCP Bug Fixes β Round 2: chat_managed + group discussion error content (2026-06-03)
Repo: EDDI (fix/mcp-endpoint-bugs) What changed: Fixed 3 remaining bugs from MCP endpoint audit retest.
chat_managed Internal Error (NEW β all calls returned "Internal error")
Root cause: Missing
@Blockingannotation onchatManaged(). BothtalkToAgent()andchatWithAgent()had it, butchatManaged()did not. SincesendMessageAndWait()blocks onCompletableFuture.get(), the MCP framework's event-loop thread was blocked, causing the generic "Internal error".Fix 1: Added
@Blockingannotation.Fix 2: Replaced
restAgentEngine.startConversationWithContext()with directconversationService.startConversation()to avoid the JAX-RS layer wrapping exceptions as HTTP responses.Fix 3: Hardened stale conversation handling β
getConversationState()now catchesExceptionwhen the stored UserConversation references a deleted conversation, cleans up the stale mapping, and creates a fresh conversation.Files:
McpConversationTools.java
BUG-2 Follow-up: Group discussion empty content when LLM fails
Root cause:
extractResponse()correctly returnsnullwhen no output keys are present (pipeline metadata only), but thisnullwas silently stored as the transcriptcontentfield β making entries appear empty.Fix: In
executeAgentTurn(), afterextractResponse()returns null, check the conversation state. If ERROR, set content to"[Agent failed to produce output β conversation entered ERROR state]".Files:
GroupConversationService.java
BUG-6 Follow-up: Trigger cache invalidation (verified indirectly)
The trigger validation in
getOrCreateManagedConversation()was already correct from the first fix round. It was untestable becausechat_manageditself was broken. Now that@Blockingis fixed, the trigger validation path is reachable.
π MCP Endpoint Bug Fixes β 8 Bugs Resolved (2026-06-02)
Repo: EDDI (fix/mcp-endpoint-bugs) What changed: Systematic testing of all 42 MCP endpoints revealed 8 bugs. All fixed with regression tests.
BUG-1: read_resource for langchain returns empty configuration: {}
Root cause:
LlmConfigurationis the only Java record-based config class. The programmatic MP REST Client's ObjectMapper may lackParameterNamesModule, causing silent deserialization failure toLlmConfiguration(null), thenNON_NULLserialization produces{}.Fix: Added
@JsonProperty("tasks")to the record component.Files:
LlmConfiguration.java
BUG-2: Group discussion shows raw {"actions":["send_message","unknown"]}
Root cause:
GroupConversationService.extractResponse()fallback serialized pipeline metadata when no text output keys were found.Fix: Added metadata-only detection: if output only contains
actions/input/contextkeys, returnnullinstead.Files:
GroupConversationService.java
BUG-3: list_conversations returns 0 results when filtering by agentId
Root cause:
RestConversationStoreusedgetResource()(conversation URI) instead ofgetAgentResource()(agent URI) for agent filtering β compared agentId against conversationId.Fix: Changed to use
getAgentResource()for agent ID extraction.Files:
RestConversationStore.java
BUG-4: read_conversation_log NPE when logSize is null
Root cause:
IntegerlogSize passed directly tointparameter causing NPE on unboxing.Fix: Added null guard:
logSize != null ? logSize : -1.Files:
ConversationService.java
BUG-5: delete_agent_trigger returns 200 for nonexistent intents
Root cause: Both MongoDB and Postgres implementations silently accepted no-op deletes.
Fix: Interface, Mongo, and Postgres stores now throw
ResourceNotFoundExceptionwhen delete count is zero. REST layer catches and returns 404.Files:
IAgentTriggerStore.java,AgentTriggerStore.java,PostgresAgentTriggerStore.java,RestAgentTriggerStore.java
BUG-6: chat_managed routes to stale conversation after trigger deletion
Root cause:
getOrCreateManagedConversation()reusedUserConversationrecords without validating trigger existence.Fix: Validate trigger before reusing, clean up stale records if trigger is deleted.
Files:
McpConversationTools.java
BUG-7: delete_group/update_group fail with version=0
Root cause:
RestVersionInfodidn't overridegetCurrentResourceId(), falling through toIRestVersionInfodefault which throws.Fix: Added
getCurrentResourceId()override that delegates toresourceStore.Files:
RestVersionInfo.java
BUG-8: returningFields filter in read_conversation has no effect
Root cause: Underlying utility only handles section-level filtering, not individual field names.
Fix: Added post-processing in MCP layer to filter conversationOutputs keys.
Files:
McpConversationTools.java
Code Review Follow-up (3 additional fixes)
ISSUE-1:
PostgresAgentTriggerStore.deleteAgentTrigger()silently swallowedSQLExceptionβ callers thought delete succeeded on DB failure. Addedthrow ResourceStoreException.ISSUE-2: BUG-8 field filter mutated the live
ConversationOutputmap viaremoveIfβ replaced with filtered copy to avoid corrupting shared/cached snapshots.ISSUE-3: BUG-2 metadata detection used a fragile hardcoded positive-list of 3 keys. Replaced with resilient absence-of-output check (
startsWith("output") || startsWith("reply")).OBS-1: BUG-6 trigger validation used
catch (Exception)β narrowed tocatch (ResourceNotFoundException)so transient DB errors propagate instead of falsely deleting state.
π¦ Dependency Updates β June 2026 (2026-06-01)
Repo: EDDI (chore/dependency-updates-june-2026) What changed: Bumped langchain4j and direct dependencies to latest versions.
Platform Version Bumps
langchain4j/langchain4j-libs: 1.15.0 β 1.15.1langchain4j-beta: 1.15.0-beta25 β 1.15.1-beta25New
langchain4j-community.versionproperty: 1.15.0-beta25 (community OCI GenAI module hasn't released 1.15.1-beta25 yet; separated from beta property to avoid resolution failure)
Direct Dependency Updates
org.jsoup:jsoup: 1.22.1 β 1.22.2io.swagger.core.v3:swagger-annotations: 2.2.48 β 2.2.50io.nats:jnats: 2.25.2 β 2.25.3io.quarkiverse.mcp:quarkus-mcp-server-http: 1.11.1 β 1.12.1io.swagger.parser.v3:swagger-parser: 2.1.40 β 2.1.42org.apache.maven.plugins:maven-enforcer-plugin: 3.5.0 β 3.6.3
Skipped (Transitive)
io.projectreactor.netty:reactor-netty-http: 1.2.8 β 1.3.5 β not updated. 1.3.x is a different Reactor release train (2025.x / Spring Framework 7). Our CVE-2025-22227 override at 1.2.8 is correct for the Azure SDK's 1.2.x dependency line.
Files: pom.xml, docs/changelog.md
Last updated
Was this helpful?