For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

  1. IResourceStorage.storeIfFieldEquals β€” New CAS primitive for conditional updates on arbitrary JSON fields (not just _version). Implemented in both MongoResourceStorage (Filters.eq) and PostgresResourceStorage (data->>?). Used by group conversation store for atomic state transitions.

  2. ControlSignal enum β€” CONTINUE, CANCEL_GRACEFUL, CANCEL_IMMEDIATE, PAUSE. Used by DiscussionControlToken for thread-safe in-flight control.

  3. DiscussionControlToken β€” AtomicReference-based token shared between execution loops and external callers (cancel/pause). Includes activeFuture for immediate cancel interrupt.

  4. ConversationPauseException β€” Checked exception carrying pausedWorkflowId, absoluteTaskIndex, and reason. Mirrors ConversationStopException pattern.

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

  1. ConversationState.AWAITING_HUMAN β€” New state for paused conversations. Gates say() with "use the /resume endpoint" message.

  2. HITL bookmark fields β€” 6 fields on ConversationMemorySnapshot (hitlPausedWorkflowId, hitlPausedAbsoluteTaskIndex, hitlPausedAt, hitlPauseReason, hitlTimeoutPolicy, hitlApprovalTimeout) + corresponding IConversationMemory defaults + ConversationMemory implementation.

  3. HitlDecision / HitlTimeoutPolicy β€” Decision model (APPROVED/REJECTED + note + decidedBy) and timeout policy enum (AUTO_REJECT, AUTO_APPROVE, ABORT, WAIT_INDEFINITELY).

  4. LifecycleManager extensions β€” executeLifecycleFromIndex() for resume-from-task, checkIfPauseConversationAction() for PAUSE_CONVERSATION detection, cancel check in main loop.

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

  1. POST /{conversationId}/resume — Accepts HitlDecision body, CAS on AWAITING_HUMAN→IN_PROGRESS, reloads agent, submits resume via coordinator.

  2. GET /{conversationId}/approval-status β€” Summary or full detail of paused conversation.

  3. GET /pending-approvals β€” Lists all AWAITING_HUMAN conversations with PendingApprovalSummary.

  4. POST /{conversationId}/cancel β€” Cancels active or paused conversations.

  5. IConversationMemoryStore.compareAndSetState β€” Atomic CAS on conversation state for both MongoDB and PostgreSQL.

  6. Timeout handler guard β€” waitForExecutionFinishOrTimeout skips state overwrite when AWAITING_HUMAN (Invariant 10).

Wave 3: Group Discussion HITL

  1. GroupConversation HITL fields β€” pausedAtPhaseIndex, pausedTurnCount, pausedPhaseName, pausedAt, hitlPauseType (PHASE/TASK).

  2. SharedTaskList HITL methods — submitForApproval (IN_PROGRESS→AWAITING_APPROVAL), approveTask, rejectTask, resetToAssigned, hasAwaitingApproval.

  3. GroupConversationService HITL β€” cancelDiscussion (via DiscussionControlToken or direct DB), resumeDiscussion (task approvals + phase resume).

  4. Group REST endpoints β€” POST /{gcId}/cancel, POST /{gcId}/approve, POST /{gcId}/approve/stream (SSE), GET /{gcId}/approval-status.

  5. GroupApprovalRequest β€” REST body with HitlDecision + Map<String,String> taskApprovals for per-task verdicts.

Wave 4: Configuration, Timeout & Audit

  1. AgentConfiguration.HitlConfig β€” approvalTimeout (ISO-8601 duration), timeoutPolicy (default WAIT_INDEFINITELY).

  2. AgentGroupConfiguration.HitlConfig β€” Same + granularity (PHASE/TASK).

  3. HitlTimeoutHandler β€” @ApplicationScoped handler dispatched by ScheduleFireExecutor when hitlType=hitl_timeout schedule fires. Routes to auto-approve/reject/abort based on policy.

  4. ScheduleFireExecutor integration β€” 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

  1. Local-file read / non-http SSRF in OpenAPI spec discovery (McpApiToolBuilder.parseSpec) β€” The GET /apicallstore/apicalls/discover-endpoints?specUrl=… endpoint (and create_api_agent) handed a user-supplied location straight to swagger-parser's readLocation(), 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 an http(s) URL (UrlValidationUtils.isValidHttpUrl()) β€” rejecting file:// (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 new looksLikeInlineSpec() (handles swagger: 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-editor gated, so SSRF to private/metadata IPs via an http(s) spec URL is an accepted residual β€” as is the remote-$ref vector (swagger-parser has no clean toggle to disable only remote-ref resolution). Use full UrlValidationUtils.validateUrl() here if a deployment needs private-IP blocking.

  2. Opt-in SSRF protection for agent-driven outbound calls β€” New eddi.security.ssrf-protection.enabled flag (default off to preserve internal-API calls in self-hosted deployments). When on:

    • ApiCallExecutor (httpcalls): the fully-resolved, templated target URL is validated with UrlValidationUtils.validateUrl() (blocks private/loopback/link-local/CGNAT/cloud-metadata + non-http), and redirect-following is disabled per request (new IRequest.setFollowRedirects, honoured by the Vert.x HttpClientWrapper) so a 3xx β†’ internal host can't bypass validation.

    • A2AToolProviderManager (peer Agent-Card fetch + tasks/send): both target URLs validated. The JDK client already defaults to Redirect.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 is Redirect.NEVER. Forcing private-IP blocking there would break legitimate internal imports.

Algorithm bugs found & fixed

  1. CronParser β€” day-of-week 7 not accepted as Sunday. Standard cron treats 0 and 7 as Sunday; the parser rejected 7 (range 0–6) and, even if allowed, DayOfWeek % 7 never yields 7, so it would never match. Now 7 is accepted and normalized to 0 (normalizeDaysOfWeek).

  2. 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. Now dayMatches() 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 around dayMatches.

  3. CronParser β€” malformed fields crashed or silently never-fired. */ threw ArrayIndexOutOfBoundsException (not a clean validation error); a reversed range like 5-1 produced an empty set β†’ a schedule that never fires until the 2-year scan limit threw a confusing IllegalStateException. Both now throw a clear IllegalArgumentException at parse time (step structure + start <= end checks).

  4. ApiCallExecutor retry backoff was linear, not exponential. delay * amountOfExecutions (linear) despite the exponentialBackoffDelayInMillis field 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.

  5. CalculatorTool β€” unbounded recursion DoS. The recursive-descent SafeMathParser recurses on nested parens; a long/deeply-nested LLM-supplied expression could throw StackOverflowError (an Error, not caught by calculate()). Added a 1000-char input cap plus a defensive StackOverflowError catch.

  6. InMemoryConversationCoordinator β€” unbounded dead-letter deque. The active-conversation map was capped but deadLetters grew without limit under a failure storm. Added a configurable cap (eddi.coordinator.max-dead-letters, default 1000; -1 disables, 0 retains none) with oldest-first eviction β€” consistent with the existing eddi.coordinator.max-active-conversations property.

Files changed

  • engine/mcp/McpApiToolBuilder.java β€” URL validation in parseSpec, looksLikeInlineSpec()

  • modules/apicalls/impl/ApiCallExecutor.java β€” opt-in SSRF validation + redirect disable; exponential backoff

  • modules/llm/impl/A2AToolProviderManager.java β€” opt-in URL validation on peer fetch/send

  • engine/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 validation

  • modules/llm/tools/impl/CalculatorTool.java β€” length cap + StackOverflowError catch

  • engine/runtime/internal/InMemoryConversationCoordinator.java β€” dead-letter cap

  • resources/application.properties β€” documented eddi.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; -1 disables)

  • ApiCallExecutor/A2AToolProviderManager/InMemoryConversationCoordinator constructor-call sites updated across test files.

  • Mock-based suites green; A2A + embedded-server suites are unrunnable in the sandbox (JDK HttpClient/HttpServer can't open a selector) but compile and are exercised in CI.

Review follow-ups (Copilot + CodeRabbit)

  • IRequest.setFollowRedirects fails closed β€” made it a non-default (abstract) interface method instead of a no-op default, so any new IRequest impl 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 < -1 at startup (only -1/0/positive are valid, so a typo like -2 can't silently disable trimming), and serialize the add+trim under a small lock so concurrent failures enforce the cap deterministically (the existing pollFirst already evicts oldest-first, so the newest failures were never dropped β€” the lock just removes transient under-retention).

  • CronParser Vixie star semantics β€” a day field is "starred" (not restricted, takes the AND path) when it begins with *, so */2 is treated like * (was exact equals("*"), which wrongly took the OR path).

  • CronParser field-aware parse errors β€” parseIntField() wraps NumberFormatException into an IllegalArgumentException carrying the offending field (e.g. */abc β†’ "Invalid number 'abc' in field: …"), instead of leaking a vague low-level message.

  • CalculatorTool guards before logging β€” the length check now runs before the eager LOGGER.debug("… " + expression) concatenation, so an oversized payload is rejected without building/logging the big string.

Known residual (accepted, documented)

  • OpenAPI external $ref resolution (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 #/components refs that real specs rely on, so resolution is kept on. Mitigated by the eddi-admin/eddi-editor gate; 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

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

  2. MEDIUM: Null-safe DynamicAgentConfig β€” Constructor defaults null to disabled config.

  3. MEDIUM: Null-safe provider allow-list β€” Objects::nonNull filter before equalsIgnoreCase().

  4. MEDIUM: Null-safe model allow-list β€” Filters for both null map values and null list entries.

  5. LOW: extractResponse() deduplication β€” Shared ConversationOutputExtractor utility replacing 3 copies.

Files Changed

  • GroupConversation.java β€” Transient dynamicAgentConfig field (@JsonIgnore)

  • GroupConversationService.java β€” Config propagation + extractResponse() delegation

  • AgentOrchestrator.java β€” resolveDynamicAgentConfig() reads group config from context

  • CreateSubAgentTool.java β€” Null-safe constructor + allow-lists + extractResponse() delegation

  • ConverseWithAgentTool.java β€” extractResponse() delegation

  • ConversationOutputExtractor.java β€” [NEW] Shared utility

Tests Added

  • ConversationOutputExtractorTest β€” 11 tests

  • DynamicAgentToolsTest β€” 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_group was missing @Blocking β€” a multi-minute TASK_FORCE discussion would block the Vert.x event loop thread, potentially freezing the MCP server. Now correctly annotated (matches talk_to_agent pattern in McpConversationTools).

  • New tool: start_group_discussion β€” async variant that returns immediately with groupConversationId + IN_PROGRESS state. Client polls with read_group_conversation. Uses existing startAndDiscussAsync() 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_conversation returns (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 verify shows 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 β€” AgentOrchestrator was creating separate createdAgentIds/retainedAgentIds per 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 β€” CreateSubAgentTool accepted retain=true but never populated retainedAgentIds. Agents were auto-deleted despite LLM requesting retention. Fixed: wired Set<String> retainedAgentIds to constructor + retainedAgentIds.add(agentId) when retain=true.

  • C3: Double quota counting β€” CreateSubAgentTool called acquireConversationSlot() then startConversation() also called it internally. Each creation burned 2 quota slots. Fixed: removed explicit quota call from tool.

  • C4: Transcript race condition β€” GroupConversation.transcript was a plain ArrayList accessed from parallel virtual threads. Fixed: Collections.synchronizedList(new ArrayList<>()) + null-safe setter.

  • C5: Dead ERROR detection β€” ConverseWithAgentTool.extractResponse() returned "" instead of null, making response == null check dead code. Fixed: returns null for empty/missing outputs.

  • C6: Zero test coverage β€” ConverseWithAgentTool had 154 lines of untested code. Added 8 tests covering new conversation, existing conversation, validation, timeout, error state, empty response.

Medium Fixes

  • M1: LifecyclePolicy enum β€” lifecyclePolicy changed from String to LifecyclePolicy enum with @JsonValue/@JsonCreator for kebab-case JSON. Typos now fail at deserialization instead of silently skipping cleanup.

  • M2: synchronizedList streaming β€” findMemberIncludingDynamic() now wraps findMember(dynamicMembers) in synchronized(dynamicMembers) block.

  • M3: Cycle detection β€” SharedTaskList.detectCycles() now called after task list dependency resolution. Circular deps throw GroupDiscussionException fail-fast.

  • M5: unretainAgent() β€” New @Tool method on TeardownAgentTool to 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 assertions

  • GroupConversationTest: 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 on AgentGroupConfiguration with config switches for creation, recruitment, delegation, guardrails (provider/model whitelists, per-discussion caps), and lifecycle policy (ephemeral/keep-deployed/undeploy-only/agent-decides)

  • GroupConversation β€” added dynamicMembers, createdAgentIds, retainedAgentIds fields for runtime tracking

4 LLM Tools (all @Vetoed, per-invocation constructed)

  • CreateSubAgentTool β€” creates + deploys agent via AgentSetupService, quota-gated, guardrail-validated, optional initial message

  • ConverseWithAgentTool β€” send messages to any deployed agent, supports multi-turn via conversationId

  • FindAgentsByCapabilityTool β€” discover agents by skill via CapabilityRegistryService

  • TeardownAgentTool β€” undeploy/delete created agents + retainAgent for lifecycle override

Wiring

  • AgentOrchestrator + LlmTask β€” 5 new CDI dependencies, whitelist-gated tool names: create_sub_agent, converse_with_agent, find_agents_by_capability, teardown_agent

  • GroupConversationService β€” 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(): immediate GroupDiscussionException

  • executeAgentTurn β†’ say(): unwrap from ExecutionException, abort (bypasses retry policy)

  • Task execution loop: quota error exits the agent's CompletableFuture immediately

  • Parallel phase: quota propagates through CompletionException, cancels remaining futures

  • Review fix: quota errors in task loop now propagate regardless of onAgentFailure policy (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 ordering

  • M1-final: setMemberConversationIds defensively wraps in ConcurrentHashMap (MongoDB deserialization was replacing with LinkedHashMap)

  • Dead code: Removed unused snapshotTranscript from executeTaskExecutionPhase

  • New: SharedTaskList.updateTask() public synchronized method

  • Regression 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 SharedTaskList public methods now synchronized β€” prevents race conditions during parallel EXECUTE phase

  • ConcurrentHashMap: GroupConversation.memberConversationIds changed from LinkedHashMap to ConcurrentHashMap

  • Dependency resolution: Pre-configured TaskDefinition.dependsOn subjects now resolved to actual task IDs (was silently dropped)

  • Null guard: resolveTaskAssignment null returns no longer crash assignTask

High Fixes (H1–H6)

  • Transcript snapshot: EXECUTE phase now takes List.copyOf(gc.getTranscript()) before launching parallel futures (consistent with executeParallelPhase)

  • Timeout semantics: Changed from timeout Γ— agentCount to timeout Γ— 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 passed boolean directly (was using heuristic contains("fail"))

  • IllegalStateException: Now caught alongside GroupDiscussionException in parallel EXECUTE lambda

  • Error events: New handleTaskFailure() method emits transcript entry + SSE event for failed tasks

Medium Fixes (M1–M4)

  • Slack: TASK_FORCE added to EXPANDED_STYLES set

  • Cycle detection: Changed from ArrayList.contains() O(n) to HashSet.contains() O(1)

  • Fallback: singleTaskFallback now preserves LLM output as task description (was discarding it)

  • HITL placeholders: BLOCKED and AWAITING_APPROVAL statuses 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

  1. Config-driven: Tasks can be pre-configured in AgentGroupConfiguration.tasks[] (skips PLAN phase) or dynamically generated by the LLM via TaskListParser (three-tier fallback: JSON β†’ Markdown β†’ single task).

  2. Reuses existing infrastructure: Task execution goes through normal agent pipelines. No new REST endpoints.

  3. State embedded in GroupConversation: SharedTaskList is a field on GroupConversation, persisted as part of the MongoDB document.

  4. HITL forward-compatible: AWAITING_APPROVAL state added to both GroupConversationState and TaskStatus for Phase 9b.

  5. 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.java

  • Tests: 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 Partner alongside Red Hat certification in the intro paragraph.

  • Updated MCP tool count: 42 tools β†’ 60+ tools in 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_memories

    • GDPR Tools (2): delete_user_data, export_user_data

    • Channel Integration Tools (5): list_channel_integrations, read_channel_integration, create_channel_integration, update_channel_integration, delete_channel_integration

Verification

  • All 63 @Tool annotations in engine/mcp/ verified as io.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 / Authentication

  • RestSlackWebhook β†’ Integrations / Slack Webhook

  • RestToolHistory β†’ Tools / Tool History (+ added missing @ApplicationScoped)

  • RestA2AEndpoint β†’ Integrations / A2A Protocol (capability endpoints tagged Integrations / 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) accents

  • Dark mode (lamp toggle β†’ html.dark-mode): EDDI Manager palette β€” zinc-950 bg, zinc-900 surfaces, amber-500 accents

  • Topbar stays dark (#18181b) in both modes for brand consistency with logo

  • EDDI amber accents on Authorize, Execute, Explore, and Try-it-out buttons

  • Version badge 6.1.1 with WCAG AAA contrast; OAS 3.1 badge demoted to subtle gray

  • HTTP 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 β€” codeql job now uses github.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_PROPERTY handler dereferences getLatestData("input:initial") without null check. When a client sends an empty/whitespace-only message, Conversation.storeUserInputInMemory skips storing input:initial β†’ getLatestData returns null β†’ NPE β†’ pipeline dies β†’ conversation enters ERROR state.

  • Fix: Added null guards for both initialInputData and initialInput.

  • 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 UPDATE silently merges history. On MongoDB: history insertOne throws unhandled MongoWriteException (HTTP 500 instead of 409).

  • Fix: Introduced optimistic locking via storeIfCurrentVersion() default method on IResourceStorage. MongoDB overrides with version-conditioned updateOne (check matchedCount). PostgreSQL overrides with UPDATE WHERE version = ? (check affected rows). History inserts hardened: Mongo catches duplicate-key 11000; Postgres uses ON CONFLICT DO NOTHING.

  • Tests: 1 new test for concurrent modification detection (mock throws ResourceModifiedException); existing update test updated to verify storeIfCurrentVersion delegation.

  • Files: IResourceStorage.java, MongoResourceStorage.java, PostgresResourceStorage.java, HistorizedResourceStore.java, HistorizedResourceStoreTest.java

Fix #3 β€” ComponentCache HashMap race (MEDIUM)

  • Root cause: ComponentCache is @ApplicationScoped (singleton) using plain HashMap. computeIfAbsent on HashMap is not thread-safe. Concurrent reads (every conversation turn via LifecycleManager) and writes (lazy agent deployment via WorkflowStoreClientLibrary) can corrupt the map.

  • Fix: Replaced HashMap with ConcurrentHashMap for 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, onComplete callback fires β†’ storeConversationMemory β†’ unconditional replaceOne overwrites the newer conversation state.

  • Fix: Check Thread.currentThread().isInterrupted() before calling onComplete(). If interrupted, route to onFailure() instead (with log warning).

  • Tests: 2 new tests β€” cancelled thread routes to onFailure; non-interrupted thread still routes to onComplete.

  • Files: BaseRuntime.java, BaseRuntimeTest.java

Design Decisions

  • Optimistic locking as default method: storeIfCurrentVersion() was added as a default method on the IResourceStorage interface (delegating to store()) 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 inspecting Future.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 null instead of the stale result. This prevents callers who future.get() the returned Future from receiving a stale value that was already routed to onFailure.

  • ConcurrentHashMap over synchronized blocks: For ComponentCache, ConcurrentHashMap was chosen over Collections.synchronizedMap or explicit locking because computeIfAbsent provides exactly the atomic read-or-create semantics needed, with better concurrency than full map locking.

  • No conversation context in BaseRuntime logs: BaseRuntime is 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 downstream onFailure callback in ConversationService.


πŸ›‘οΈ 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-user role could read/modify ANY conversation by guessing the conversationId. No ownership validation existed despite ConversationDescriptor having a userId field.

  • Fix: RestAgentEngine now injects SecurityIdentity, OwnershipValidator, and IConversationDescriptorStore. All conversation-scoped endpoints (readConversation, say, endConversation, undo, redo, rerun, readConversationLog, getConversationState) validate that the caller owns the conversation. startConversation validates that the provided userId matches 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: RestUserMemoryStore now injects SecurityIdentity and OwnershipValidator. All endpoints validate that the {userId} path parameter matches the authenticated caller. upsertMemory validates against the userId in the request body.

Finding: IDOR β€” Group Conversations (HIGH β†’ FIXED)

  • Problem: Any authenticated user could read/delete any group conversation.

  • Fix: RestGroupConversation now validates ownership on readGroupConversation and deleteGroupConversation. listGroupConversations filters results to only the caller's conversations. discuss/discussStreaming validate the provided userId.

Finding: GDPR Annotation on Implementation Only (MEDIUM β†’ FIXED)

  • Problem: @RolesAllowed("eddi-admin") was only on RestGdprAdmin implementation, not the IRestGdprAdmin interface. 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 @PermitAll to 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.) accepted userId as a tool parameter without validating against the caller's identity.

  • Fix: McpMemoryTools now injects OwnershipValidator and calls validateUserAccess() in all 5 read-only MCP memory tools (initially via McpToolUtils.requireOwnerOrAdmin(), consolidated to direct OwnershipValidator use in code review hardening below).

New Component: OwnershipValidator

  • Centralized @ApplicationScoped utility for ownership checks

  • Three methods: validateUserAccess(), validateAndResolveUserId(), requireOwnerOrAdmin()

  • All checks are no-ops when authorization.enabled=false (dev mode)

  • eddi-admin role bypasses all ownership checks

  • Legacy 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. AuthStartupGuard already 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 requireOwnerOrAdmin static method from McpToolUtils. McpMemoryTools now injects OwnershipValidator directly and calls validateUserAccess() β€” single source of truth for ownership logic.

  • M3 β€” PII in WARN logs: OwnershipValidator WARN 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 catches ResourceNotFoundException and ResourceStoreException specifically instead of generic Exception, preventing unexpected errors from being silently swallowed.

  • BUG-2 β€” deleteMemory ownership: Added findEntryById(String entryId) to IUserMemoryStore with MongoDB and PostgreSQL implementations. RestUserMemoryStore.deleteMemory() now looks up the entry, validates ownership via validateUserAccess(), 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, McpMemoryToolsTest stubs 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 sanitizes conversationId via LogSanitizer.sanitize() before logging.

  • Log Injection β€” OwnershipValidator: All 3 debug-level log statements (validateUserAccess, validateAndResolveUserId, requireOwnerOrAdmin) now sanitize user-provided values (callerId, requestedUserId, resourceOwnerId, resourceType) via LogSanitizer.sanitize().

  • Fail-closed ownership check: RestAgentEngine.validateConversationOwnership() now throws ForbiddenException on ResourceStoreException instead 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 validate userId is non-null/non-blank before calling ownershipValidator.validateUserAccess(). Previously, a missing userId with auth enabled would throw ForbiddenException instead 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, not requireOwnerOrAdmin() in McpToolUtils.

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' to script-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 /manage and sets redirectUri to its own URL; the Keycloak client's redirectUris: ["http://localhost:*"] matches any path.

  • deploy-to-local-eddi-repo.ps1 β€” Removed $IndexHtml handling (no longer needed). Script only updates manage.html.

Architecture Clarification

File
Served at
Purpose

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() caught jakarta.ws.rs.NotFoundException, but RestAgentEngine.getConversationState() actually throws IConversationService.ConversationNotFoundException (a plain RuntimeException). 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_recreatesFresh to throw the correct exception type.

Other Fixes

  • Unused variable: Removed String result in test (github-code-quality)

  • Field filter bypass: readConversation with returningFields=conversationOutputs no longer strips the full payload β€” section-level names are now detected and preserved

  • redhat-certify.yml: Updated default version from 6.0.2 to 6.1.0

  • README.md: Updated version from 6.0.2 to 6.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 β€” covers ConversationState.ENDED β†’ delete+recreate path

  • chatManaged_transientStateError_doesNotRecreate β€” verifies transient DB errors propagate without deleting valid mappings

  • readConversationDescriptors_agentVersionFilter_matchesCorrectVersion β€” agentVersion filter positive match

  • readConversationDescriptors_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 version

  • src/main/docker/Dockerfile β€” EDDI_VERSION build arg + Red Hat certification labels

  • helm/eddi/Chart.yaml β€” appVersion

  • k8s/base/eddi-deployment.yaml β€” app.kubernetes.io/version labels

  • k8s/quickstart.yaml β€” app.kubernetes.io/version labels

  • src/main/resources/application.properties β€” systemRuntime.projectVersion, quarkus.smallrye-openapi.info-version, quarkus.container-image.additional-tags

  • src/main/resources/initial-agents/available_agents.txt β€” Agent Father ZIP filename

  • src/main/resources/initial-agents/Agent+Father-6.1.0.zip β€” [NEW] updated bundled agent

  • src/main/resources/initial-agents/Agent+Father-6.0.2.zip β€” [DELETED] superseded

  • .github/workflows/redhat-certify.yml β€” Red Hat certification workflow version refs

  • docs/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 @Blocking annotation on chatManaged(). Both talkToAgent() and chatWithAgent() had it, but chatManaged() did not. Since sendMessageAndWait() blocks on CompletableFuture.get(), the MCP framework's event-loop thread was blocked, causing the generic "Internal error".

  • Fix 1: Added @Blocking annotation.

  • Fix 2: Replaced restAgentEngine.startConversationWithContext() with direct conversationService.startConversation() to avoid the JAX-RS layer wrapping exceptions as HTTP responses.

  • Fix 3: Hardened stale conversation handling β€” getConversationState() now catches Exception when 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 returns null when no output keys are present (pipeline metadata only), but this null was silently stored as the transcript content field β€” making entries appear empty.

  • Fix: In executeAgentTurn(), after extractResponse() 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 because chat_managed itself was broken. Now that @Blocking is 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: LlmConfiguration is the only Java record-based config class. The programmatic MP REST Client's ObjectMapper may lack ParameterNamesModule, causing silent deserialization failure to LlmConfiguration(null), then NON_NULL serialization 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/context keys, return null instead.

  • Files: GroupConversationService.java

BUG-3: list_conversations returns 0 results when filtering by agentId

  • Root cause: RestConversationStore used getResource() (conversation URI) instead of getAgentResource() (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: Integer logSize passed directly to int parameter 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 ResourceNotFoundException when 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() reused UserConversation records 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: RestVersionInfo didn't override getCurrentResourceId(), falling through to IRestVersionInfo default which throws.

  • Fix: Added getCurrentResourceId() override that delegates to resourceStore.

  • 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 swallowed SQLException β€” callers thought delete succeeded on DB failure. Added throw ResourceStoreException.

  • ISSUE-2: BUG-8 field filter mutated the live ConversationOutput map via removeIf β€” 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 to catch (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.1

  • langchain4j-beta: 1.15.0-beta25 β†’ 1.15.1-beta25

  • New langchain4j-community.version property: 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.2

  • io.swagger.core.v3:swagger-annotations: 2.2.48 β†’ 2.2.50

  • io.nats:jnats: 2.25.2 β†’ 2.25.3

  • io.quarkiverse.mcp:quarkus-mcp-server-http: 1.11.1 β†’ 1.12.1

  • io.swagger.parser.v3:swagger-parser: 2.1.40 β†’ 2.1.42

  • org.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?