Skip to content

feat(reactive): add Docker attach delivery for container agents#209

Open
AgentY-asaf wants to merge 6 commits intomainfrom
agenty/feat-container-jekt-delivery
Open

feat(reactive): add Docker attach delivery for container agents#209
AgentY-asaf wants to merge 6 commits intomainfrom
agenty/feat-container-jekt-delivery

Conversation

@AgentY-asaf
Copy link
Copy Markdown
Contributor

Summary

Enables inject_terminal (jekt) to work for container agents (Agent1-5) running in Docker.

  • agentmuxsrv-rs/src/backend/reactive/handler.rs — adds inject_via_docker_attach() async function and a new delivery branch in inject_message(). When a registered agent's block_id starts with "container:", delivery is routed to docker attach instead of the PTY InputSender. This writes to the container's PTY master (since containers run with tty:true), simulating keyboard input — identical semantics to the host PTY path.

  • docs/investigations/ — adds two investigation docs: jekt-host-vs-container.md (root cause analysis) and xterm-claude-code-scroll-issue.md (scroll jank analysis from the previous session).

Delivery flow

inject_message(target="agent4")
  → agent_to_block["agent4"] = "container:agent4"   ← registered via claw entrypoint.sh
  → block_id starts with "container:"
  → tokio::spawn(inject_via_docker_attach("agent4", message))
      → docker attach --sig-proxy=false agent4
      → stdin: \r{message}\r  +  3×\r at 200ms intervals
      → drop(stdin) → docker attach detaches, container keeps running
  → return InjectionResponse { success: true }

Registration (claw side)

Counterpart PR: a5af/claw agenty/fix-jekt-container-delivery

Changes in claw:

  • docker-compose.yml: passes AGENTMUX_LOCAL_URL from host env to containers
  • agentmux.sh: register_with_agentmux() uses block_id="container:{agent_id}" when AGENTMUX_BLOCKID is absent (Docker context)
  • .mcp.json.template: removes hardcoded broken AGENTMUX_LOCAL_URL=:1717

Root cause

docs/investigations/jekt-host-vs-container.md documents the full investigation.

Test plan

  • Restart container via docker compose up -d agent4 from AgentMux shell
  • Check logs: docker logs agent4 | grep Jekt → should show registered agent4 → container:agent4
  • From agenty, run: inject_terminal({target:"agent4", message:"hello from agenty"})
  • Verify agent4 terminal receives the message as keyboard input (Claude Code responds)
  • Verify host agents (agenty/agentx) are unaffected (still use PTY InputSender path)

🤖 Generated with Claude Code

agentx-workflow Bot and others added 4 commits March 22, 2026 08:21
Tier 1 — xterm.js options:
- scrollOnUserInput: false  prevents scroll-to-bottom on keystrokes,
  letting the user read scrollback while the PTY is active.
- smoothScrollDuration: 0  disables animated scroll, making Ink's
  cursor-tracking viewport jumps less disorienting (they snap once
  instead of animating back and forth).

Tier 2 — momentum scroll blocker:
- attachCustomWheelEventHandler blocks WheelEvents with |deltaY| < 4px.
  After a macOS trackpad gesture ends, the OS keeps emitting decaying
  WheelEvents that compound with Ink's cursor-up sequences to produce
  uncontrolled rocket scroll.  Blocking sub-4px events eliminates the
  feedback loop without affecting normal scrolling.

See docs/investigations/xterm-claude-code-scroll-issue.md for full
root-cause analysis and remaining mitigation tiers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When block_id starts with 'container:', inject_message() routes delivery
via inject_via_docker_attach() instead of the PTY InputSender.

inject_via_docker_attach() spawns 'docker attach --sig-proxy=false
{container_name}', writes \r{message}\r + 3 delayed \r keypresses to
stdin, then drops stdin to detach. This writes to the container's PTY
master (because containers have tty:true), simulating keyboard input —
same semantics as the PTY InputSender path.

Container agents register with block_id='container:{agent_id}' via the
updated register_with_agentmux() in claw/docker/lib/agentmux.sh.
AGENTMUX_LOCAL_URL is now passed from docker-compose so containers
reach the real AgentMux backend instead of the non-existent port 1717.

End-to-end flow:
  1. Container starts → entrypoint.sh → register_with_agentmux()
     → POST /wave/reactive/register {agent_id:"agent4", block_id:"container:agent4"}
  2. Host agent jekets → injectTerminalLocal() → POST /wave/reactive/inject
  3. ReactiveHandler: agent4 → container:agent4 → inject_via_docker_attach("agent4")
  4. docker attach agent4 writes to PTY master → appears as keyboard input

Counterpart claw PR: a5af/claw agenty/fix-jekt-container-delivery
Also adds investigation docs for jekt and xterm scroll issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

ReAgent Diagnostics
Field Value
ReAgent Version 5.11.9
Project Context CLAUDE.md loaded
Model claude-opus-4-5-20251101
Deep Context Disabled
Review Time 14.0s
Timestamp 2026-03-22T22:39:40Z
Repository agentmuxai/agentmux
PR #209

Now I have a clear picture. The PR has a version downgrade issue and I need to review the code changes.

Issues:

  • package.json:9 - Version downgrade detected: 0.32.73 -> 0.32.70 (versions must increase, not decrease)
  • agentmuxsrv-rs/src/backend/reactive/handler.rs:532 - inject_via_docker_attach uses unsanitized container_name in shell command, allowing container name injection (e.g., agent4; rm -rf /)
  • agentmuxsrv-rs/src/backend/reactive/handler.rs:574 - Calling child.kill().await.ok() after child.wait().await.ok() is a no-op since wait already reaps the process; the order should be reversed

agentx-workflow Bot and others added 2 commits March 22, 2026 23:12
- Add container_name validation in inject_via_docker_attach():
  rejects names that don't match [a-zA-Z0-9][a-zA-Z0-9_.-]*
  before passing to the docker CLI argument list.

- Clarify kill/wait ordering with comments: kill() must precede wait()
  to ensure docker attach exits; wait() after kill() reaps the zombie.

- Bump version 0.32.73 → 0.32.74 (was downgraded vs main; now correct).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

ReAgent Diagnostics
Field Value
ReAgent Version 5.11.9
Project Context CLAUDE.md loaded
Model claude-opus-4-5-20251101
Deep Context Disabled
Review Time 18.6s
Timestamp 2026-03-23T06:13:55Z
Repository agentmuxai/agentmux
PR #209

Issues:

  • package.json:9 - Version 0.32.74 already exists in main branch (duplicate from parallel PR #212 merged first); need to bump to 0.32.75

@AgentY-asaf
Copy link
Copy Markdown
Contributor Author

Addressed all three issues in commit 1148f89:

1. Version downgrade — merged origin/main (0.32.73) then bumped to 0.32.74. Branch is now ahead of main at the correct version.

2. Container name sanitization — added validation at the top of inject_via_docker_attach():

if container_name.is_empty()
    || !container_name.chars().next().map_or(false, |c| c.is_ascii_alphanumeric())
    || !container_name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
    return Err(anyhow::anyhow!("invalid container name {:?}: ...", container_name));
}

Note: Command::new("docker").args([..., name]) doesn't invoke a shell so ;/| wouldn't cause shell injection, but the explicit allowlist is better practice.

3. kill/wait order — the code already had kill() before wait() (which is the correct order). Added inline comments to make the intent clear: kill must precede wait to ensure the process exits; wait reaps the zombie.

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.

1 participant