Skip to content

Agent: browser (--web) and editor (--acp) front ends on one shared session - #467

Merged
bernardladenthin merged 5 commits into
mainfrom
ccr-d629238e-hohc22
Sep 29, 2026
Merged

bernardladenthin merged 5 commits into
mainfrom
ccr-d629238e-hohc22

Conversation

@bernardladenthin

Copy link
Copy Markdown
Owner

Summary

  • One session, three front ends. Everything in llama-atmosphere-agent that is not presentation moved into a front-end-independent AgentSession: history, slash commands, /compact, /loop, /retry, transcript, approval mode, cancellation and running a turn. A front end implements SessionFrontend. TurnRecorder records each turn and forwards it to that front end's renderer, so the history note, /calls and the transcript are the same whichever front end showed the turn. ModeGatedApprovalStrategy reads the session's mode on every call, so switching modes in any front end applies to the next call.
  • --web: the agent runs in a browser through Atmosphere's prebuilt AI console on embedded Jetty 12 (no Spring). It listens on 127.0.0.1 only. Access needs the token from the printed address, which is exchanged for an HttpOnly, SameSite=Strict cookie. Requests with a non-loopback Host or a foreign Origin are refused. Approvals appear as Approve/Deny cards, and /stop cancels a running turn. The banner explains the SSH/PuTTY tunnel.
  • --acp: the Agent Client Protocol on stdin/stdout, for JetBrains IDEs and Zed (VS Code needs an ACP extension). Each editor session gets its own workspace, the editor's cwd. Answers stream; tool calls appear as cards with kind and file location; approvals use session/request_permission (allow / allow all → auto / reject). Modes appear as ACP session modes, slash commands as available_commands_update, and session/cancel stops a turn.
  • Two fixes, both with tests that failed before:
    • run_command output came back as [output unavailable: NullPointerException], because the live-output sink captured the console before it existed.
    • /clear left the pending tool note and the /retry message behind.

Commits:

Commit Content
d9d1027 The two fixes
c976163 AgentSession extraction
efc5550 --web
bfab8ed --acp
dc550f6 Docs and release-jar smoke

Dependencies:

  • Jetty 12.1.13 (ee10 servlet and Jakarta WebSocket).
  • atmosphere-spring-boot-starter, excluded transitively. The assembly unpacks only its META-INF/resources/atmosphere/console/**.
  • ACP Java SDK 0.18.0 (acp-core, acp-json-jackson2).

The agent release jar grows from ~7 MB to ~14 MB. Bytecode stays within 65.

Test plan

  • Affected unit / integration tests pass locally: mvn verify in llama-atmosphere-agent passes, 277 tests with 63 skipped (JLine screen tests without the patched JLine, and model-backed tests). New test classes:
    • AgentSessionTest (13)
    • WebServerTest (11): real Jetty and Atmosphere over a WebSocket speaking the atmosphere.js protocol, covering streaming, approvals, /stop and the token guard
    • AcpServerTest (9): plain JSON-RPC as an editor sends it, covering handshake, streaming, tool cards, allow / reject / cancelled dialog / allow-all, set_mode, cancel and attached resources
    • TurnRecorderTest (4)
    • ModeGatedApprovalStrategyTest (3)
    • 2 new tests in AgentOptionsTest
  • --web checked by hand in headless Chromium: token login, approval card, result, and 401 without a token.
  • New smoke steps in .github/smoke-agent-jar.sh run locally against a real release-jar pair built here (agent jar next to a freshly built core fat jar):
    • --web passes: 401 without token, 302 plus cookie, console page with a real CSP nonce.
    • --acp passes handshake, modes, streamed answer, command list, JSON-only stdout and clean exit. Its read_file round fails locally only because the local model (SmolLM2-135M) makes no tool calls. CI uses the Qwen2.5-1.5B tool model that step 4 already relies on.
  • CI is green on this branch
  • Docs updated: agent README (front ends, security, IDE configuration, limitations), root README, CLAUDE.md.

Related issues / PRs

—

Checklist

  • I have read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • My commits follow Conventional Commits
  • No security-sensitive changes: --web exposes a shell-capable agent over HTTP. It is loopback-only with token auth, cookie exchange and Host/Origin checks by default; --web-host 0.0.0.0 prints a warning.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FtfgoazykGcQSCR3TYBmTQ


Generated by Claude Code

- The run_command live-output sink captured the console before it was
  created, so the first output line threw a NullPointerException inside
  the reader and every command's result became "[output unavailable]".
  The sink now looks the console up when a line arrives.
- /clear left the REPL's tool note and last message in place: the next
  request still carried the note of a forgotten turn, and /retry repeated
  a message from before the clear.

Both are pinned by new LocalAgentTest cases that were red before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtfgoazykGcQSCR3TYBmTQ
The whole conversation state and every slash command lived as locals in
LocalAgent.run(), so only the console could drive the agent. It now lives
in AgentSession, and a front end is a small SessionFrontend port: where a
line goes, where a turn's events go, how to wait for a turn, and who
answers approvals and questions.

- AgentSession: history, approval mode, transcript, tool-call log, token
  figures, tool note, retry, compaction, /load, /save, /loop; one request
  at a time; cancel() from any thread.
- TurnRecorder records a turn and forwards every event to the front
  end's renderer; ConsoleRenderer is the console half, ConsoleSession
  keeps the old name for recorder + console renderer.
- ModeGatedApprovalStrategy: auto approves, manual asks the front end of
  the running request, nobody to ask means no.
- TaskLoop runs its steps through a step function; ModelEndpoint owns
  --model/--base-url; Prompts owns the prompt resources.
- LocalAgent keeps only the entry point and the two consoles.
- A message typed twice on purpose is now in the record twice (a retry
  is still noted once).

Tests: AgentSessionTest drives the session through a recording front end
(commands, approval per mode, live command output, cancel from another
thread, retry, clear, /loop with and without anybody to ask);
TurnRecorderTest and ModeGatedApprovalStrategyTest cover the two new
building blocks. All 255 tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtfgoazykGcQSCR3TYBmTQ
The same AgentSession, served in a browser: Atmosphere's prebuilt AI
Console on an embedded Jetty 12, no Spring. Streaming, tool cards,
approvals through the console's Approve/Deny buttons (Atmosphere's own
/__approval protocol, resolved against the connection's registry), all
slash commands, and /stop.

- WebServer: Jetty + AtmosphereServlet with an explicit annotation map
  (no classpath scan, fat-jar safe). Atmosphere reads that map only when
  it scans a package, and skips its classpath scan whenever JUnit is on
  the classpath, so packages=all and scanClassPath=false make tests and
  production take the same path.
- WebAccessGuard in front of everything, the WebSocket upgrade included:
  a per-start token exchanged for an HttpOnly SameSite=Strict cookie,
  same-origin check, loopback Host check against DNS rebinding.
  Binds 127.0.0.1 by default; the banner prints the SSH/PuTTY tunnel.
- WebAgentEndpoint: @aiendpoint with timeout -1 (the default would cut a
  request after two minutes); one shared session per process, a new
  message replaces a running one.
- WebFrontend: the SessionFrontend for one Atmosphere connection.
- WebConsole serves the console pages from the starter jar (CSP nonce
  filled in) plus /api/console/info; the release jar packs only those
  pages from the starter, none of its Spring classes.
- AgentOptions: --web, --web-port (8787), --web-host, --web-token, --acp.

Tests: WebServerTest (11) drives the real server over HTTP and a
WebSocket speaking atmosphere.js's protocol: no token/wrong token/no
cookie refused, WebSocket upgrade refused without the token, token to
cookie exchange, foreign Origin refused, console page and info served,
streaming, commands, Approve runs the command, Deny keeps it from
running, /stop ends a slow request. Also verified by hand in headless
Chromium against the console.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtfgoazykGcQSCR3TYBmTQ
The agent speaks ACP (JSON-RPC over stdin/stdout) so JetBrains IDEs and
Zed can drive it natively, and VS Code through an ACP extension. One ACP
session is one AgentSession whose workspace is the directory the editor
names, so the tools stay confined to the open project.

- AcpServer: initialize, session/new (approval modes as ACP session
  modes, slash commands announced as available_commands_update),
  session/prompt, session/set_mode, session/cancel. stdout is the
  protocol; anything else printed lands on stderr.
- AcpFrontend: streamed text as agent_message_chunk, each tool call as a
  tool_call with kind, arguments and the file it touches, then a
  tool_call_update with the result; running command output is pushed to
  the card. Approvals go through session/request_permission (allow /
  allow always -> auto mode / reject); a cancelled or missing answer is
  a no.
- SlashCommands carry a description and argument hint for the editor.
- AcpServerTest drives the server with plain JSON-RPC as an editor
  sends it, over the real session and the real OpenAiCompatServer with
  a scripted engine: handshake, streaming, tool calls in cwd, allow,
  reject, cancelled dialog, allow-always, set_mode, cancel, attached
  resources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtfgoazykGcQSCR3TYBmTQ
…e release jar

- smoke-agent-jar.sh: two more steps on the release-jar pair. --web on
  port 0: the console is 401 without the token, the token link answers
  302 with the session cookie, the console page is served with it and
  carries a real CSP nonce. --acp through smoke/agent_acp_smoke.py, a
  standard-library Python editor: handshake, modes, a streamed answer,
  the announced commands, a read_file tool card that brings a marker
  back, only JSON-RPC on stdout, a clean exit when stdin closes.
- Agent README: the three front ends on one session, --web (security,
  SSH/PuTTY tunnel), --acp (JetBrains acp.json, Zed, VS Code), options,
  what is verified where, updated limitations.
- Root README: short pointer to --web and --acp.
- CLAUDE.md: AgentSession / SessionFrontend / TurnRecorder architecture,
  the mode-gated approval wrapper, the web front end (including the
  Atmosphere classpath-scanning trap under JUnit and the WebSocket test
  client race), the ACP mapping, the console pages unpacked from the
  starter jar, and the two step-0 fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtfgoazykGcQSCR3TYBmTQ
@bernardladenthin
bernardladenthin merged commit a42665e into main Sep 29, 2026
9 of 14 checks passed
@bernardladenthin
bernardladenthin deleted the ccr-d629238e-hohc22 branch September 29, 2026 08:00
@sonarqubecloud

Copy link
Copy Markdown

This branch had an error being deployed

1 failed deployment
startgate — dc550f61 Deployed Sep 29, 2026 by bernardladenthin via Start gate (abort window) #1067
maven-central — dc550f61 Deployed Sep 29, 2026 by bernardladenthin via Verify GPG signing key (no secrets printed) #1067
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.

2 participants