Skip to content

fix(llm): dynamicAgentConfig guardrails failed open on any turn that did not re-inject them - #655

Closed
ginccc wants to merge 2 commits into
mainfrom
fix/dynamic-agent-context-boundary
Closed

fix(llm): dynamicAgentConfig guardrails failed open on any turn that did not re-inject them#655
ginccc wants to merge 2 commits into
mainfrom
fix/dynamic-agent-context-boundary

Conversation

@ginccc

@ginccc ginccc commented Aug 9, 2026

Copy link
Copy Markdown
Member

A systematic sweep of the Context serialization boundary: for every context the system injects, what type does it hold (a) on the injecting turn, (b) after a store reload, (c) after crash recovery, (d) on another node?

Nine context keys. Eight are strings, lists or maps and survive unchanged. dynamicAgentConfig is the only typed POJO the system injects — and it is the one that gates the tools that deploy real agents to production.

key injected as after reload verdict
groupId, groupConversationId, groupDepth, delegationDepth String String OK
groupTranscript, schedule, attachment_N List / Map List / Map OK
mcp, slack, lang, channelIntent String String OK
dynamicAgentConfig POJO Map broken

1. The resolver failed open in two independent ways

MemberTurnExecutor injects the group's guardrails on every member turn, and deliberately injects an explicitly disabled config when the group configured none. Its comment says exactly why: a member turn that finds no config falls back to the STANDALONE default, which is fully permissive — creation, recruitment and delegation all on.

resolveDynamicAgentConfig defeated that defense twice:

  1. Current step only. Any turn that re-enters without a fresh injection — an HITL tool-approval resume, crash recovery, the group follow-up path — saw nothing.
  2. Typed-object assumption. ConversationMemoryStore and PostgresConversationMemoryStore both rebuild a stored context as new Context(type, map.get("value")). After any reload the value is a Map, so the instanceof DynamicAgentConfig failed even when the key was present.

Both paths landed on the permissive default.

This was the only sibling resolver that did not already handle the resume case — ContextualToolsProvider#resolveGroupIds and resolveDelegationDepth both fall back to earlier steps, with comments explaining that a resumed turn re-enters without the original context map. Fixed the same way, plus whole-object Jackson coercion of the map form (not field-by-field — a guardrail added later must not silently read back as its permissive Java default).

2. Unresolvable guardrails on a group turn now fail CLOSED

A conversation that demonstrably belongs to a group but whose config cannot be resolved returns a disabled config instead of the permissive default — the discipline GroupTaskToolsProvider and ArtifactToolsProvider already state.

The group probe requires a context entry that actually carries a value, not merely a key. "The key exists" is too weak a claim to strip a standalone agent of tools its designer explicitly whitelisted.

3. GroupLifecycleOps.followUp never injected the guardrails at all

It injects groupTranscript / groupId / groupConversationId but not dynamicAgentConfig — so the defense MemberTurnExecutor documents was bypassed on the entire follow-up path, with no crash or resume needed. Now injected identically.

4. Two disagreeing defaults, and a Javadoc that was not true

ToolAssemblyContext's compact constructor normalizes a null config to a disabled one. DynamicAgentToolsProvider used a permissive one. And contribute() ignored ctx.dynamicAgentConfig() entirely, re-resolving from memory — despite AgentOrchestrator#toolAssemblyContext documenting that the value is resolved once "so two providers resolving it independently could [not] disagree". The inconsistency the Javadoc warns about was already in the code. The provider now consumes the resolved value.

5. Withheld group tools are logged

GroupTaskToolsProvider / ArtifactToolsProvider returned an empty contribution silently. Withholding is correct for a forged or finished discussion id — but it is also what happens if the discussion runs on another node, and that failure looked like "the model just lost its tools" with nothing to diagnose from.

Verified, not changed: the node-affinity invariant

LiveDiscussionRegistry's correctness rests on "a member turn always runs in-process". Re-checked against the implementation rather than the changelog note: NatsConversationCoordinator.publishAndExecute publishes only conversationId.getBytes() as an ordering marker and then runs the callable through runtime.submitCallable locally. No JetStream consumer deserializes or executes callables, and the payload could not carry one. A member turn cannot be routed off-node. Invariant holds.

Testing

377 tests across DynamicAgentToolsProvider / AgentOrchestrator / GroupLifecycleOps / MemberTurnExecutor stay green. +7 new in DynamicAgentConfigContextBoundaryTest, one nest per observation point (a)–(d).

Summary by CodeRabbit

  • Bug Fixes

    • Improved dynamic agent configuration handling across resumed conversations and group follow-ups.
    • Preserved configuration settings when conversations are restored from saved data.
    • Added safer fallback behavior when group configurations are missing or invalid.
    • Ensured earlier conversation context is checked when current configuration is unavailable.
  • Documentation

    • Added changelog details covering configuration handling, tool access safeguards, and verification results.

…did not re-inject them

dynamicAgentConfig is the only typed POJO the system injects into conversation
context, and it gates the tools that deploy real agents to production.
resolveDynamicAgentConfig defeated MemberTurnExecutor's deliberate defense twice:

- it read the CURRENT step only, so an HITL resume, crash recovery or the group
  follow-up path saw no config at all;
- it required a live DynamicAgentConfig, but conversation memory rebuilds a
  stored context as new Context(type, map), so after any reload the instanceof
  failed even when the key was present.

Both landed on the fully permissive standalone default. Now: earlier steps are
consulted (the shape every sibling resolver already uses for the resume case),
the map form is coerced whole-object, and a group turn with unresolvable
guardrails fails CLOSED. GroupLifecycleOps.followUp never injected the config at
all — fixed. contribute() now consumes the once-resolved ctx.dynamicAgentConfig()
instead of re-resolving, making AgentOrchestrator's Javadoc true. Withheld group
tools are logged so an off-node discussion is diagnosable.

+7 tests, one nest per observation point: injecting turn, after store reload,
no fresh injection, genuinely standalone.
@ginccc
ginccc requested a review from rolandpickl as a code owner August 9, 2026 13:27
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

Dependency Review

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

Scanned Files

None

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f43cf97-cf25-416e-b989-fb8bb96dbf0a

📥 Commits

Reviewing files that changed from the base of the PR and between d5294a6 and 50f8790.

📒 Files selected for processing (6)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/ArtifactToolsProvider.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/DynamicAgentToolsProvider.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/GroupTaskToolsProvider.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/DynamicAgentConfigContextBoundaryTest.java

📝 Walkthrough

Walkthrough

The change resolves dynamic-agent configuration from current and earlier context steps, restores persisted maps, fails closed for unresolved group configuration, injects configuration into group follow-ups, logs withheld tools, and adds boundary tests.

Changes

Dynamic agent context handling

Layer / File(s) Summary
Configuration resolution and tool assembly
src/main/java/ai/labs/eddi/modules/llm/impl/DynamicAgentToolsProvider.java, src/test/java/ai/labs/eddi/modules/llm/impl/DynamicAgentConfigContextBoundaryTest.java, docs/changelog.md
DynamicAgentToolsProvider resolves live and persisted configurations from current or earlier steps. Group contexts use a disabled configuration when resolution fails. Standalone contexts retain permissive defaults. Tests cover serialization, resumed turns, group failures, and standalone conversations.
Group follow-up configuration injection
src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java
Group follow-up calls now include the configured dynamic-agent settings or an explicitly disabled configuration.
Withheld group tool diagnostics
src/main/java/ai/labs/eddi/modules/llm/impl/ArtifactToolsProvider.java, src/main/java/ai/labs/eddi/modules/llm/impl/GroupTaskToolsProvider.java
Artifact and group task providers log sanitized identifiers when live discussion membership is absent.

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

Sequence Diagram(s)

sequenceDiagram
  participant ConversationMemory
  participant DynamicAgentToolsProvider
  participant GroupLifecycleOps
  participant DynamicAgentConfig
  ConversationMemory->>DynamicAgentToolsProvider: Read current and earlier context
  DynamicAgentToolsProvider->>DynamicAgentConfig: Restore or resolve configuration
  DynamicAgentConfig-->>DynamicAgentToolsProvider: Return configuration or disabled group default
  GroupLifecycleOps->>DynamicAgentToolsProvider: Invoke follow-up with dynamicAgentConfig
Loading

Possibly related PRs

  • labsai/EDDI#626: Both changes modify dynamic-agent configuration handling and group follow-up context injection.
  • labsai/EDDI#649: Both changes modify persisted configuration handling and group fail-closed guardrails.

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main fix: preventing dynamicAgentConfig guardrails from failing open when they are not re-injected.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dynamic-agent-context-boundary

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

❤️ Share

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

CodeQL log-injection alerts 490-491. Both values are caller-supplied — the
group conversation id arrives as a context variable and the conversation id
from memory — which is exactly why these lines exist, so they must be
sanitized before they reach the log.
@aisabella-ai
aisabella-ai self-requested a review August 10, 2026 17:35
@ginccc

ginccc commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Superseded — closing without merging.

While this branch was in review, main moved on: #648 (agent-lifecycle-and-group-deadlines), #649 (dynamic-agent-guardrails), #650 (cadence-claim-expiry) and #651 (deployment-wait-machinery) landed and address the same findings, in places more thoroughly than this PR did. Merging it now would duplicate or regress those.

I re-checked every finding in this PR against current main rather than assuming. What was already fixed there is dropped; what was genuinely still missing has been rebuilt on top of current main in:

No work is lost; the review threads here remain readable for the reasoning.

@ginccc ginccc closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants