Skip to content

feat: AG-UI protocol adapter + agentic protocol surface & generative UI (#386)#387

Open
plauzy wants to merge 1 commit into
awslabs:mainfrom
plauzy:feat/agentic-protocols-generative-ui
Open

feat: AG-UI protocol adapter + agentic protocol surface & generative UI (#386)#387
plauzy wants to merge 1 commit into
awslabs:mainfrom
plauzy:feat/agentic-protocols-generative-ui

Conversation

@plauzy

@plauzy plauzy commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the AG-UI (Agent–User Interaction) Protocol L1 adapter proposed in #386, contributed as the working proof-of-concept that issue references, alongside the surrounding agentic-protocol surface and generative-UI capability it was built with.

Everything here is strictly additive and default-off: every new surface is gated by an env/config flag or is a new module that isn't imported unless enabled. With no flags set, cao-server behavior is byte-identical — no new endpoints respond and no new listeners bind. The existing localhost dashboard, MCP servers, and REST API are untouched.

📦 Note on scope / bundling

The changes in this PR are currently bundled together for comprehensiveness — the AG-UI L1 adapter (the #386 core) ships alongside the surrounding agentic-protocol surface (A2A / Agent Card / ACP), the generative-UI renderer, and the opt-in observability / orchestration / persistence modules it was developed with, so reviewers can see the full picture end-to-end.

This can absolutely be broken up into smaller, independently-reviewable PRs if the maintainers prefer — a natural split would be: (1) AG-UI L1 adapter + emit_ui (the #386 scope), (2) A2A JSON-RPC + Agent Card listener, (3) generative-UI renderer + PWA, (4) the observability / orchestration / cache extras. Just say the word and I'll decompose it in whatever order you'd like to review.

AG-UI adapter (the #386 core)

  • services/agui_stream.py — a pure, table-driven L1 adapter mapping CAO's normalized event vocabulary onto AG-UI typed SSE events, per the mapping table in [Feat] AG-UI Protocol support as a composable construct layer — one face over many CLI agents #386.
  • GET /agui/v1/stream — behind CAO_AGUI_ENABLED, emits STATE_SNAPSHOT on connect + RFC-6902 STATE_DELTA patches thereafter, with ?since= history replay and query-parameter JWT auth for browser EventSource clients.
  • Privacy boundary — message bodies never appear on the wire; asserted by redaction tests.
  • Covered by mapping / redaction / refusal tests (test/services/test_agui_stream_mapping.py).

This satisfies the read-only Phase 0–1 scope of #386 (AC1 default-off, AC2 L1 stream, AC4 privacy boundary), leaving the L2 constructs and bidirectional interrupts for follow-up phases.

Also included (all opt-in)

Protocols — A2A JSON-RPC + signed Agent Card / OAuth PRM on a default-off loopback listener (CAO_AGENT_CARD_ENABLED); an Agent Client Protocol server (cao-acp) for editor integrations.

Generative UI — agents author UI intents rendered uniformly across providers via a closed, safe allow-list of named components with JSON props (no HTML/script/eval); off-list components are refused at the adapter, the React component, and the replay artifact. React renderer in the L2 dashboard PWA (cao_pwa/), a browser-openable deterministic replay artifact, and a Playwright recording harness + workflow.

Observability / orchestration / persistence / policy / cache — OpenTelemetry GenAI telemetry + W3C traceparent; ASI integrity evaluator + kill switch; orchestration topologies (DAG, swarm, Polecat, topology router); WAL + replay; a Cedar-style policy queue; a 3-layer cache.

Providers, auth, frontends — a credentials-free mock_cli for CI; WebSocket auth on /terminals/{id}/ws (token via the cao.bearer.<jwt> subprotocol, 4401/4403 close codes, read-only viewers' input dropped) — default-off preserves the byte-identical no-auth localhost contract.

✅ Shift-left testing — proof it works

Genuine shift-left proof, captured locally (not a mock-up) and reproduced in CI:

  • Backend (pytest): 35 AG-UI adapter tests pass — mapping / redaction / refusal (28) + the /agui/v1/stream endpoint + the emit_ui producer.
  • Frontend (vitest): 18/18 cao_pwa unit/component tests pass, including 10 GenerativeUI tests and the 3-layer safety-refusal assertions; tsc clean; vite build within the bundle-size budget.
  • E2E (Playwright): the recording harness plays a deterministic replay of frames produced by the real services/agui_stream.to_agui_event adapter and asserts heterogeneous providers render uniform cards and the off-list iframe component is refused.
  • Coverage: targeted tests added in this PR (addressing the Codecov patch-coverage comment) bring a2a/store, a2a/types, agent_card/router, agent_card/signing, and providers/mock_cli to 100%, a2a/rpc to 98%, and agent_card/listener to 95% (previously 65–89%). All CI checks — including the coverage ratchet — pass.

Live playback — heterogeneous CLI agents (kiro_cli, claude_code, codex) each author UI intents that render uniformly on one surface; the AG-UI event stream is on the right; the off-list iframe component is REFUSED, never rendered:

Generative UI over heterogeneous CLI agents — live playback

Final composed surface (full-page screenshot from the Playwright run):

Generative UI — final composed surface

Full-motion video (WebM): https://raw.githubusercontent.com/plauzy/cli-agent-orchestrator/feat/agentic-protocols-generative-ui/docs/media/agui-generative-ui-demo.webm

Notes

Implements #386.

@codecov-commenter

codecov-commenter commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.34205% with 96 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@01472e2). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/cli_agent_orchestrator/api/main.py 57.22% 71 Missing ⚠️
src/cli_agent_orchestrator/a2a/stream.py 87.95% 10 Missing ⚠️
src/cli_agent_orchestrator/a2a/rpc.py 98.26% 2 Missing ⚠️
src/cli_agent_orchestrator/agent_card/listener.py 95.00% 2 Missing ⚠️
...i_agent_orchestrator/services/event_log_service.py 33.33% 2 Missing ⚠️
src/cli_agent_orchestrator/services/sse_bus.py 50.00% 2 Missing ⚠️
src/cli_agent_orchestrator/telemetry/otel.py 96.36% 2 Missing ⚠️
src/cli_agent_orchestrator/providers/manager.py 66.66% 1 Missing ⚠️
src/cli_agent_orchestrator/services/agui_stream.py 99.05% 1 Missing ⚠️
.../cli_agent_orchestrator/services/status_monitor.py 75.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #387   +/-   ##
=======================================
  Coverage        ?   87.66%           
=======================================
  Files           ?      132           
  Lines           ?    14882           
  Branches        ?        0           
=======================================
  Hits            ?    13047           
  Misses          ?     1835           
  Partials        ?        0           
Flag Coverage Δ
unittests 87.66% <90.34%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

An additive, opt-in feature set that broadens how CAO exposes and observes a
heterogeneous fleet of CLI agents. Every new surface is default-off (gated by
env/config) or a new module that isn't imported unless enabled; the existing
localhost dashboard, MCP servers, and API are untouched.

Protocols
- AG-UI streaming: GET /agui/v1/stream maps CAO's normalized event vocabulary
  to AG-UI typed events, with a STATE_SNAPSHOT on connect + RFC-6902 STATE_DELTA
  shared-state channel and ?since= replay, behind CAO_AGUI_ENABLED + scope auth.
  This is the L1 adapter proposed in awslabs#386 (services/agui_stream.py plus the
  mapping / redaction / refusal tests).
- A2A JSON-RPC + signed Agent Card / OAuth PRM on a dedicated, default-off
  loopback listener (CAO_AGENT_CARD_ENABLED); an Agent Client Protocol server
  (cao-acp) for editor integrations.

Generative UI
- Agents author UI intents rendered uniformly across providers via a closed,
  safe allow-list of named components with JSON props (no HTML/script/eval);
  off-list components are refused at the adapter, the React component, and the
  replay artifact. React renderer in the L2 dashboard PWA (cao_pwa/), a
  browser-openable deterministic replay artifact, and a Playwright recording
  harness + workflow.

Observability / orchestration / persistence / policy / cache (all opt-in)
- OpenTelemetry GenAI telemetry + W3C traceparent on events; ASI integrity
  evaluator + kill switch; orchestration topologies (DAG, swarm, Polecat,
  topology router); WAL + replay; a Cedar-style policy queue; a 3-layer cache.

Providers, auth, frontends
- Additional CLI providers plus a credentials-free mock_cli for CI.
- WebSocket auth on /terminals/{id}/ws: token via the cao.bearer.<jwt>
  subprotocol, 4401/4403 close codes, read-only viewers' input dropped.
  Default-off preserves the byte-identical no-auth localhost contract.

Testing
- Full unit scope green (pytest test/ --ignore=test/e2e -m "not e2e"); cao_pwa
  vitest incl. generative-UI + safety-refusal tests; tsc clean; vite build
  within the bundle-size budget; black/isort/mypy clean; WebSocket auth e2e
  scenarios pass; generative-UI screen recording produced in CI with an
  offline replay.

Implements the AG-UI L1 adapter proposed in
awslabs#386.

Co-authored-by: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
@plauzy

plauzy commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the Codecov patch-coverage comment: added targeted unit tests for the flagged modules. a2a/store, a2a/types, agent_card/router, agent_card/signing, and providers/mock_cli are now at 100%, a2a/rpc at 98%, and agent_card/listener at 95% (previously 65–89%). New tests: test/a2a/test_store_coverage.py, test/a2a/test_rpc_background_coverage.py, test/a2a/test_stream_coverage.py, test/agent_card/test_{router,signing,listener}_coverage.py, test/providers/test_mock_cli_coverage.py, test/mcp_server/test_emit_ui_coverage.py. Also rebased onto current main (was one commit behind). Shift-left proof (GIF + screenshot + test counts) is embedded in the PR description.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a large set of opt-in, default-off protocol surfaces and supporting infrastructure around CAO, including an AG-UI streaming surface + generative-UI producer path, a dedicated Agent Card/A2A listener, and OpenTelemetry GenAI instrumentation, with substantial new test coverage, examples, docs, and CI workflows.

Changes:

  • Add OpenTelemetry (traces + optional metrics) initialization gates, GenAI span helpers, and W3C trace-context propagation utilities, with a dedicated telemetry test suite.
  • Add a credentials-free mock_cli provider and fixture binary plus CI/e2e smoke plumbing to exercise orchestration without external provider credentials.
  • Add a standalone cao_pwa/ dashboard (Vite/React/Vitest/Playwright harness) and documentation/examples to support multi-instance AG-UI consumption and generative UI proof artifacts.

Reviewed changes

Copilot reviewed 150 out of 157 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/init.py Adds a tests/ package marker (separate from the existing test/ suite).
test/test_constants.py Extends default CORS allow-list coverage for the PWA dev server port.
test/telemetry/test_spans.py Validates GenAI span helper naming and attributes.
test/telemetry/test_otel_init.py Tests opt-in OTel SDK initialization/shutdown gating.
test/telemetry/test_context_propagation.py Tests traceparent inject/extract round-trips.
test/telemetry/conftest.py Shared session-scoped tracer provider + in-memory exporter fixtures.
test/telemetry/init.py Marks telemetry tests package.
test/services/test_workflow_spec_service.py Updates versioning wording in workflow spec service tests.
test/services/test_workflow_service.py Updates versioning wording in workflow run engine tests.
test/services/test_step_output_store.py Updates versioning wording in step output store tests.
test/services/test_settings_service.py Resets settings-service module cache to avoid order-dependent tests.
test/scripts/init.py Package marker for script-related tests (empty file in diff context).
test/providers/test_native_status_shared.py Adjusts provider native-status test docstring wording/scope.
test/providers/test_mock_cli_unit.py Adds unit tests for MockCliProvider behavior and fixture binary.
test/providers/test_mock_cli_coverage.py Adds branch coverage for async init + cleanup of MockCliProvider.
test/providers/fixtures/bin/mock_cli Adds deterministic mock CLI REPL fixture binary used in tests/CI.
test/providers/fixtures/antigravity_cli_tool_call.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_response.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_processing.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_print_mode.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_permission.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_interactive_clean.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_idle.txt Adds captured Antigravity CLI fixture output.
test/providers/fixtures/antigravity_cli_error.txt Adds captured Antigravity CLI fixture output.
test/models/test_workflow.py Updates workflow-model test wording and class naming.
test/mcp_server/test_workflow_tools.py Updates workflow MCP tool test wording (v3 terminology).
test/mcp_server/test_workflow_return.py Updates workflow return tool test wording (v2 terminology).
test/mcp_server/test_handoff_equivalence.py Updates wording for v1 equivalence test docstring.
test/mcp_server/test_emit_ui_coverage.py Adds coverage tests for the new emit_ui MCP tool behavior.
test/fixtures/init.py Introduces managed-subprocess fixture package for e2e/integration.
test/e2e/test_headless_ci.py Adds an e2e test exercising the headless CI runner example script.
test/e2e/helpers/ws.py Adds a WS helper supporting JWT via subprotocol for browser parity.
test/e2e/helpers/init.py Package marker for e2e helpers.
test/e2e/conftest.py Switches e2e suite to managed cao_server fixture and adds Gemini gating.
test/conftest.py Adds global test env defaults, PATH prep for mock_cli, and shared fixtures.
test/clients/test_workflow_index_migration.py Updates migration test wording to v2 terminology.
test/cli/commands/test_workflow.py Updates CLI workflow test wording to v3 terminology.
test/api/test_workflow_runs.py Updates API workflow run endpoint test wording to v3 terminology.
test/api/test_workflow_endpoints.py Updates API workflow endpoints test wording to v2 terminology.
test/api/test_default_off_listeners.py Adds regression tests for default-off dedicated Agent Card listener.
test/api/test_agui_stream_endpoint.py Adds tests for /agui/v1/stream auth + replay + snapshot/delta.
test/api/test_agui_emit_ui.py Adds tests for /agui/v1/emit_ui allow-list, sizing, disabled surface.
test/agent_card/test_signing.py Adds Ed25519 Agent Card signing/verifying tests.
test/agent_card/test_signing_coverage.py Adds branch coverage for signer verification failure paths.
test/agent_card/test_router.py Adds integration tests for .well-known Agent Card + JWKS endpoints.
test/agent_card/test_router_coverage.py Adds branch coverage for router initialization and OAuth PRM route.
test/agent_card/test_listener_coverage.py Adds coverage for building/starting/stopping the dedicated listener.
test/agent_card/test_builder.py Tests Agent Card builder defaults and metadata merging.
test/agent_card/init.py Package marker for Agent Card tests.
test/a2a/test_stream_coverage.py Adds A2A stream/router and event-bus backpressure coverage.
test/a2a/test_store_coverage.py Adds A2A store lifecycle coverage.
test/a2a/test_rpc_background_coverage.py Adds coverage for A2A background executor/type helpers.
test/a2a/init.py Package marker for A2A tests.
src/cli_agent_orchestrator/utils/logging.py Adds stderr WARNING+ stream handler alongside file logging.
src/cli_agent_orchestrator/telemetry/spans.py Adds context-manager span helpers for GenAI semconv usage.
src/cli_agent_orchestrator/telemetry/semconv.py Adds frozen GenAI semantic-convention attribute keys/constants.
src/cli_agent_orchestrator/telemetry/otel.py Adds opt-in OTel SDK initialization + bounded shutdown behavior.
src/cli_agent_orchestrator/telemetry/context.py Adds W3C trace-context inject/extract helpers.
src/cli_agent_orchestrator/telemetry/init.py Exposes public telemetry helpers via package surface.
src/cli_agent_orchestrator/skills/workflow-author/SKILL.md Updates “Bolt” wording to “version” in skill docs.
src/cli_agent_orchestrator/services/workflow_spec_service.py Updates module docstring wording to v2/v3 terminology.
src/cli_agent_orchestrator/services/workflow_service.py Updates module docstring wording to v3 terminology.
src/cli_agent_orchestrator/services/step_output_store.py Updates module docstring wording to v2/v3 terminology.
src/cli_agent_orchestrator/services/status_monitor.py Tightens typing around pyte screen storage and provider returns.
src/cli_agent_orchestrator/services/sse_bus.py Adds backward-compatible alias and a test reset helper.
src/cli_agent_orchestrator/services/event_log_service.py Adds a test reset helper for the event log singleton.
src/cli_agent_orchestrator/providers/mock_cli.py Adds deterministic mock_cli provider implementation for CI/tests.
src/cli_agent_orchestrator/providers/manager.py Registers mock_cli provider type in provider factory.
src/cli_agent_orchestrator/plugins/events.py Adds traceparent field to plugin event base type.
src/cli_agent_orchestrator/plugins/builtin/event_log_publisher.py Adds backward-compatible plugin class alias.
src/cli_agent_orchestrator/models/workflow.py Updates documentation wording around “Bolt” vs “version/grammar layer”.
src/cli_agent_orchestrator/models/workflow_runtime.py Updates documentation wording around “Bolt” vs “v*” terminology.
src/cli_agent_orchestrator/models/provider.py Adds MOCK_CLI provider type enum entry.
src/cli_agent_orchestrator/mcp_server/server.py Adds emit_ui MCP tool and updates wording in workflow tool doc.
src/cli_agent_orchestrator/ext_apps/static/topology.js Updates comments to remove commit-specific wording.
src/cli_agent_orchestrator/ext_apps/init.py Updates comment wording for topology widget export.
src/cli_agent_orchestrator/constants.py Adds OTel service name, agent-card listener constants, and CORS origins.
src/cli_agent_orchestrator/cli/commands/workflow.py Updates wording around v3 workflow verbs.
src/cli_agent_orchestrator/agent_card/router.py Adds .well-known Agent Card/JWKS/OAuth PRM router.
src/cli_agent_orchestrator/agent_card/listener.py Adds dedicated default-off listener server implementation.
src/cli_agent_orchestrator/agent_card/builder.py Adds Agent Card document builder with provider/tool metadata.
src/cli_agent_orchestrator/agent_card/init.py Exposes Agent Card public API surface.
src/cli_agent_orchestrator/a2a/store.py Adds in-memory A2A task store and protocol seam.
src/cli_agent_orchestrator/a2a/init.py Exposes A2A public API surface.
skills/workflow-author/SKILL.md Updates “Bolt” wording to “version” in root skills docs.
skills/plugin.json Adds skills plugin manifest metadata.
skills/cao-dev/SKILL.md Adds dev workflow/checklist skill doc.
pyproject.toml Adds deps for OTel/auth/multipart and fixes mypy python_version config.
examples/working-directory/repo_specialist.md Adds a working-directory constrained supervisor profile example.
examples/working-directory/README.md Documents working-directory feature and usage.
examples/tool-restrictions/reviewer_readonly.md Adds read-only reviewer profile example.
examples/tool-restrictions/README.md Documents tool restriction profiles and expectations.
examples/tool-restrictions/developer_sandboxed.md Adds dev profile with no shell execution example.
examples/headless-ci/run.sh Adds headless CI runner script for one agent session.
examples/headless-ci/README.md Documents headless CI runner usage and expected exit codes.
examples/headless-ci/ci_developer.md Adds CI-tuned developer agent profile.
examples/flow/README.md Adds scheduled flows example documentation.
examples/cross-provider/data_analyst_gemini_cli.md Adds cross-provider Gemini CLI data analyst profile example.
docs/pwa.md Adds standalone PWA documentation and deployment/auth notes.
docs/generative-ui-implementation-2026-07-04.md Adds generative UI design/safety/testing write-up.
CHANGELOG.md Documents newly added protocol surfaces and subsystems.
cao_pwa/vite.config.ts Adds Vite config including Vitest settings and dev server port.
cao_pwa/tsconfig.json Adds TS config for the PWA.
cao_pwa/src/types.ts Adds shared types for instance list and AG-UI event parsing.
cao_pwa/src/test/setup.ts Adds vitest setup (fake IndexedDB, dialog stubs, cleanup).
cao_pwa/src/test/instances.test.ts Adds IndexedDB CRUD tests for instance persistence.
cao_pwa/src/test/GenerativeUI.test.tsx Adds component allow-list safety tests for Generative UI renderer.
cao_pwa/src/test/api.test.ts Adds tests for AG-UI EventSource client URL building and dispatch.
cao_pwa/src/main.tsx Adds PWA entrypoint.
cao_pwa/src/instances.ts Adds IndexedDB persistence layer for multi-instance list.
cao_pwa/src/components/InstancePicker.tsx Adds UI for adding/removing instances.
cao_pwa/src/App.tsx Adds PWA app shell wiring instance picker + active instance tab.
cao_pwa/src/api.ts Adds AG-UI SSE client with explicit typed-event subscriptions + reconnect.
cao_pwa/playwright.config.ts Adds Playwright config for generative-ui recording harness.
cao_pwa/package.json Adds PWA package scripts and dependencies.
cao_pwa/index.html Adds PWA HTML entrypoint.
cao_pwa/e2e/generative-ui.spec.ts Adds Playwright e2e recording proof for generative UI and refusals.
cao_pwa/.gitignore Adds PWA-specific ignore rules for build/test artifacts.
.github/workflows/python-e2e.yml Adds mock_cli-based Python e2e smoke workflow.
.github/workflows/cao-pwa-generative-ui.yml Adds PWA build/test + Playwright recording artifact workflow.
.github/PULL_REQUEST_TEMPLATE.md Adds/updates PR template with test plan checklist.
.devcontainer/test-ci.sh Adds script to run CI-like checks in devcontainer.
.devcontainer/Dockerfile Adds devcontainer image aligned with Playwright + tmux + uv + bun.
.devcontainer/devcontainer.json Adds devcontainer configuration and bootstrap commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/__init__.py
@@ -0,0 +1 @@
# tests init
Comment thread pyproject.toml
Comment on lines 118 to 120
[tool.mypy]
python_version = "2.1.1"
python_version = "3.11"
strict = true
event_type: str = ""
timestamp: datetime = field(default_factory=_utc_now)
session_id: str | None = None
# W3C trace-context (RFC 9114) for the upstream span that produced this
Comment on lines +66 to +70
<span
role="button"
tabIndex={0}
className="cao-pwa-remove"
aria-label={`Remove instance ${inst.label}`}
Comment on lines +7 to +12
Requires:
- Running CAO server (``uv run cao-server``)
- tmux
- A working CLI provider on PATH and authenticated (defaults to kiro_cli)
- ``ci_developer`` agent profile installed
(``cao install examples/headless-ci/ci_developer.md``)
@haofeif

haofeif commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@anilkmr-a2z @fanhongy please have a look, this is the awesome feature that we have always been looking for

@fanhongy fanhongy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this — the AG-UI adapter is a genuinely good direction for CAO. It's extremelly helpful for user to operate CAO via the dashboard to see live stream and send approval.

My main ask is decomposition. At 157 files / ~16k insertions squashed into one commit, this is really 5–6 features (AG-UI stream + generative UI, A2A/Agent Card, the cao_pwa dashboard, OpenTelemetry, mock_cli/test-fixture overhaul, docs/examples). You already offered to split it — I'd like to take that offer. My suggestion: land AG-UI stream + emit_ui + PWA first (smallest trust delta, improves every existing user), and hold A2A until the auth gap below is closed.

the task to ``canceled`` if it isn't terminal yet; otherwise
returns ``TASK_ALREADY_TERMINAL``.

Authentication is enforced via the JWKS published at

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A2A JSON-RPC has no authentication, despite the docstring saying it does. This says "Authentication is enforced via the JWKS … once auth is enabled," but there's no auth wiring in either A2A router.

Reproduced with auth explicitly enabled and no credentials on the request:
is_auth_enabled -> True
task.send status: 200 # anonymous write accepted
stored task present -> True # and persisted

Since the changelog advertises CAO_AGENT_CARD_HOST=0.0.0.0 for external discoverability, this exposes an unauthenticated task.send/get/cancel write surface. Please either wire real token/scope checks on the listener

@fanhongy

fanhongy commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@CorrineTan would be good to take a look for your TUI task

@gutosantos82 gutosantos82 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR Review: #387 — AG-UI Protocol L1 adapter + agentic-protocol surface

Summary

This PR implements the AG-UI L1 SSE adapter proposed in #386 (services/agui_stream.py,
GET /agui/v1/stream, emit_ui) and bundles a large surrounding surface: A2A JSON-RPC +
signed Agent Card listener, OpenTelemetry, a standalone React PWA, generative-UI, a
mock_cli provider — and deletion of the q_cli and gemini_cli providers. The AG-UI
core itself is genuinely well-built: dynamic verification confirmed it is default-off
(404 with no flags, no listener bound), the message-body redaction holds by construction,
and the generative-UI allow-list is enforced at both the adapter and the producer endpoint.
However, the bundled A2A listener has a real auth bypass and an unbounded-store DoS, and
the PR is ~400 files — far beyond #386's PoC scope. Recommendation: Request changes
fix the A2A auth/DoS findings and split the PR before merge.

Blocking (must fix before merge)

  • [security] a2a/rpc.py:268, agent_card/listener.py:59-62, api/main.py:432-433 🆕 — The
    A2A JSON-RPC router (POST /a2a/v1/rpc: task.send/get/cancel) and the A2A stream/REST
    routes are mounted on the :9890 listener with no authentication dependency at all;
    build_a2a_router/build_stream_router/build_listener_app attach no
    require_any_scope/auth middleware, yet the docstrings (rpc.py:24-25, stream.py:21-22)
    falsely claim auth "is enforced via the JWKS." With the documented
    CAO_AGENT_CARD_HOST=0.0.0.0 opt-in (or a container bridge), any network peer can
    submit/cancel/read A2A tasks with zero credentials even when auth is configured — a
    full auth bypass on an external-facing surface (core a2a/ + api/main.py, highest-risk
    path). Fix: gate all A2A routes behind scopes (task.send/cancel → cao:write,
    task.get/stream → cao:read), or refuse to mount them when is_auth_enabled() and no
    enforcement is wired; correct the docstrings.
  • [security] a2a/store.py:38, a2a/rpc.py:129-152 🆕 — InMemoryTaskStore grows
    unbounded ("tasks live until explicitly deleted or process restart"). Combined with the
    unauthenticated, peer-controllable task.send above, a remote peer can drive unbounded
    memory growth → remote memory-exhaustion DoS on cao-server. Fix: cap store
    size / TTL-evict and require auth.
  • [scope] whole PR (~400 files / +16k lines) 🆕 — Bundles far more than #386's "AG-UI L1
    adapter + emit_ui" PoC: A2A transport, Agent Card/JWKS, OTel, native workflow spec, a
    standalone PWA, generative-UI, mock_cli, and deletion of two providers. The PR body
    itself offers to decompose it. A PR this size in core src/ invites exactly the kind of
    auth defect found above to slip through. Fix: split into the stack the author proposes —
    land the reviewed, verified AG-UI core (#386 scope) first; hold A2A until auth is wired.

Important (should fix)

  • [consistency] PR description ↔ implementation mismatch 🆕 — The body advertises
    "orchestration topologies (DAG/swarm/Polecat), WAL+replay, a Cedar-style policy queue, a
    3-layer cache." A grep of src/ for polecat|Cedar|policy_queue|PolicyQueue|WAL|swarm
    returns zero matches — these features do not exist in the code. The doc's "three
    layers" refers to the refusal path (Python/React/replay), not a cache. Either the features
    are missing or the description oversells; reconcile before merge.
  • [conventions/consistency] CHANGELOG.md:773-807 — missing "Removed" section 🆕 — The PR
    fully deletes providers/q_cli.py (Amazon Q) and providers/gemini_cli.py (816 lines),
    drops Q_CLI/GEMINI_CLI from ProviderType, and removes both manager.py branches.
    README/CODEBASE.md were updated, but CHANGELOG has only Added/Changed/Deprecated/Fixed.
    Removing two user-facing providers is a breaking change (SemVer/Keep-a-Changelog). Fix:
    add a ### Removed entry for q_cli + gemini_cli.
  • [consistency] docs/inbox-delivery.md — doc↔code drift, introduced this PR 🆕 — This PR
    renamed check_and_send_pending_messages()deliver_pending() (inbox_service.py:58) and
    updated CODEBASE.md, but the new content it added to inbox-delivery.md (Reconciliation
    Sweep / OpenCode Poller sections) still names the dead symbol 4×. Fix:
    s/check_and_send_pending_messages()/deliver_pending()/ in inbox-delivery.md.
  • [security] api/main.py:744, 2545 — JWT in query string leaks to access logs 🆕 —
    /agui/v1/stream?access_token=<JWT> puts the bearer in the URL, and the main app's
    uvicorn.run() does not set access_log=False (the agent_card listener does, at
    listener.py:113). The full path+query — including the JWT — lands in uvicorn/proxy/Referer
    logs, replayable until exp. Fix: disable/scrub access logging for this route, document
    short token TTLs, prefer a short-lived-ticket handshake over a raw JWT in the URL.
  • [security] api/main.py:781, 1894-1897 — token-parse exceptions not caught 🆕 —
    extract_scopes_from_token() raises on invalid/expired/malformed JWTs, but the SSE path
    catches nothing and _extract_ws_scopes only catches HTTPException, so a bad token
    surfaces as an opaque 500 / handshake error instead of a clean 401 / 4401. It fails
    closed (no data served — not a bypass), but muddies auth telemetry. Fix: catch broad
    ExceptionNone → 401 / 4401.
  • [conventions] Inclusive-language violations, introduced 🆕 — New comments/docs use
    banned terms: whitelist in services/audit_log.py, services/memory_scoring.py,
    services/memory_service.py, and event-enforcement comments; "Master switch" rows in
    docs/memory.md and docs/mcp-apps.md. Repo + org convention mandates allowlist/denylist/
    primary. No CI lint gate exists, so the build won't fail, but this is a hard convention.
    Fix: rename to allowlist / "primary switch". (Pre-existing (master*) git-branch strings
    in deleted fixtures are not in scope.)

Nits (optional)

  • [conventions] pyproject.toml:21049-21057 — OTel, authlib, pyjwt[crypto],
    python-multipart added as unconditional runtime deps though all their features are
    default-off; consider optional extras ([otel], [a2a]) to keep base install lean.
    (uv.lock is updated and deps are pinned — no hygiene red flag.)
  • [conventions] CI toolchain — the new cao-mcp-apps job runs npm install (ci.yml:361,427)
    while python-e2e uses npm ci; with committed lockfiles the convention is npm ci. Also
    bun (devcontainer) vs npm (CI) is a minor mix.
  • [consistency] docs/generative-ui-implementation-2026-07-04.md — reads as a dated
    impl/design log ("Created July 4 2026", "Status: Implemented") linked from CHANGELOG+README;
    dated logs drift — fold into docs/pwa.md or docs/design/ without the date.
  • [consistency] api/main.py:644-655_agui_enabled() also returns True when the
    pre-existing CAO_MCP_APPS_ENABLED is set (not only the new CAO_AGUI_ENABLED); the
    byte-identical-with-no-flags claim still holds, but tighten the "no new flags" wording.
  • [tests] test/api/test_agui_stream_endpoint.py:52-54 — the since= replay boundary is
    stubbed at the endpoint (_FakeLog.history ignores since), so main.py:823-825 wiring
    isn't exercised end-to-end; the filter logic itself is well-covered at the unit layer.
  • [tests] — no test for a malformed/expired bearer on the stream endpoint (only "no
    token" / "wrong scope" paths) — pairs with the uncaught-exception finding above.
  • [consistency] — README/CODEBASE prose reformatting beyond the feature adds noise to an
    already-huge PR.

Reviewer disagreement to resolve

  • .coverage-baseline.json committed — the consistency reviewer flagged it as a committed
    generated artifact that isn't gitignored and should be removed; the conventions reviewer
    judged it intentional ratchet-floor config (like a checked-in baseline), which is fine.
    Lean toward the conventions read (it's config the coverage ratchet reads), but confirm it's
    meant to be tracked and, if so, leave a one-line note in the file/docs so it isn't mistaken
    for a stray artifact.

Tests

Strong, genuinely meaningful shift-left — not coverage-gaming. The *_coverage.py files
assert real behavior/branches (transition guards, executor-exception → FAILED metadata,
drop-oldest back-pressure, tampered-signature → InvalidSignature). Security properties are
asserted the right way: redaction tests stringify the whole output and assert the secret is
absent; allow-list refusal is tested at both adapter and producer (400) layers. Malformed
JSON-RPC, auth 401/403, and RFC-6902 add/replace/remove/pointer-escaping are all covered.
Gaps are nits only (endpoint-level since= replay stubbed; no malformed-token test).

Verification

Ran in a clean Docker toolchain (uv sync succeeded; numpy built fine in-container). All
probes executed the PR's real code:

  • VERIFIED "35 AG-UI adapter tests pass" — mapping (28) + stream_endpoint (3) + emit_ui
    (4) = 35, 40 passed / 0 failed including default-off + mcp coverage tests.
  • VERIFIED "strictly additive & default-off, byte-identical with no flags" — with all
    flags unset: GET /agui/v1/stream404, POST /agui/v1/emit_ui404,
    agent_card_listenerNone (no :9890 bind).
  • VERIFIED "message bodies never appear on the wire" — fed SECRET-XYZ through both
    event shapes; output is metadata-only, secret absent. Redaction is by construction.
  • VERIFIED "off-list components REFUSED at the adapter" — iframe/script
    RAW+rejected_component; allow-listed components round-trip; also refused server-side at
    emit_ui (400). Defense-in-depth confirmed.
  • VERIFIED coverage — agui_stream at 99% (1 miss: non-dict props fallback).
  • NOT VERIFIED — live E2E through a running PWA + SSE reconnect (needs a live
    cao-server + browser). The SSE snapshot/delta/replay generator is covered by the passing
    endpoint test.

Note: the verifier confirmed the AG-UI surface is additive/default-off, but this is
distinct from the A2A listener auth findings above and the provider deletions — those
were not contradicted, they are simply outside what the runnable AG-UI probes covered.

Prior feedback (already raised — not restating)

  • No substantive maintainer critique to dedupe against. Existing human activity is the
    author's own coverage note and @haofeif tagging @anilkmr-a2z / @fanhongy to look ("the
    awesome feature we've always been looking for"). Enthusiasm noted; no findings overlap.

Verdict

Request changes — the AG-UI core (#386 scope) is solid, verified, and default-off, but the
bundled A2A listener ships an unauthenticated task API (auth bypass) and an unbounded task
store (remote DoS), both in core src/. Combined with description↔code mismatches and a
~400-file scope that far exceeds #386, this should be split: land the verified AG-UI core
first, and hold A2A until auth is wired and the store is bounded.

Correctness review did not return in time; not covered as a standalone angle (though
correctness-adjacent logic — RFC-6902 patching, SSE mapping, JSON-RPC handling, default-off
gating — was exercised by the verifier and tests reviewer).

@plauzy

plauzy commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks all — @gutosantos82 for the thorough multi-angle pass (the dynamic
verification of the AG-UI core is genuinely appreciated), @fanhongy for the
decomposition steer and for reproducing the A2A issue, and the Copilot reviewer
for the inline nits. Short version: I agree with the decomposition and I'm
splitting this PR
— landing the verified AG-UI core first and holding A2A
until auth is wired. I'm also correcting a few findings below that attach to
pre-existing repo state rather than this diff, so the record stays accurate.

Where we fully agree (fixing these)

  • A2A auth gap (blocking). Correct and fair catch. The A2A routers mount on
    the :9890 listener with no scope enforcement, and the rpc.py docstring
    claims JWKS protection that isn't wired — worse than silence. Fix (in the
    split-out A2A PR): per-method scope enforcement inside the JSON-RPC handler
    using the existing require_any_scope / is_auth_enabled infra
    (task.send/task.cancelcao:write; task.get/stream → cao:read),
    returning 401/403 with JSON-RPC error bodies; plus a fail-closed guard that
    refuses to mount the A2A routers on a non-loopback bind when auth is
    unavailable; plus truthful docstrings.
  • Unbounded task store. Adding a size cap + TTL eviction to
    InMemoryTaskStore (env-tunable), with task.send rejected when full of
    non-terminal tasks. One note for severity calibration: remote exploitability
    is conditional on the auth gap above plus the non-default
    CAO_AGENT_CARD_HOST=0.0.0.0 opt-in — fixing auth removes the remote vector
    and the bound is defense-in-depth. Both ship together in the A2A PR.
  • Token-parse exceptions → 500. Agreed. Wrapping the SSE and WS token paths
    (except Exception → 401 / close 4401) and adding the missing
    malformed/expired-bearer tests. It already fails closed (no data served), so
    this is auth-telemetry hygiene rather than a bypass — but worth doing.
  • Description ↔ code mismatch. You're right that
    polecat/Cedar/WAL/swarm/policy-queue/cache don't exist in the tree — they were
    never in this PR's code. That's overselling in the PR description, and the
    fix is prose: I'm rewriting the body to list exactly what the diff contains,
    nothing more. (The same rewrite also removes the body's provider-deletion
    claim — see the correction below.)
  • Copilot nits (all five). Removing the stray tests/__init__.py; setting
    mypy python_version = "3.10" to match requires-python (this PR had already
    fixed the corrupt "2.1.1" value on main); citing W3C Trace Context instead
    of RFC 9114 in plugins/events.py; fixing the nested-interactive-button a11y
    issue in InstancePicker.tsx; updating the stale test_headless_ci.py
    docstring.
  • Assorted: folding the dated generative-ui-implementation-2026-07-04.md
    into docs/pwa.md; moving the OTel deps under an [otel] optional extra
    (authlib/python-multipart leave with the A2A PR, under [a2a]); tightening
    the _agui_enabled() docs — the CAO_MCP_APPS_ENABLED interaction is
    intentional (the two surfaces share one in-process event source) but the
    wording will say so explicitly; de-stubbing the ?since= boundary in the
    endpoint test so the replay wiring is exercised end-to-end; stripping the
    unrelated README/CODEBASE reformatting.

Decomposition plan (taking @fanhongy up on the offer)

Two PRs, structured exactly along the verified/blocked boundary:

  1. PR-A — AG-UI core + generative UI + PWA + mock_cli + opt-in OTel
    (this PR, reshaped in place; ≈13k lines incl. the PWA and fixtures). This is
    the subset the review already verified: default-off (404s, no extra
    listener), metadata-only redaction by construction, allow-list refusal at
    both layers, 99% adapter coverage. Lands first, with the hardening items
    above.
  2. PR-B — A2A JSON-RPC + signed Agent Card listener (≈3.4k lines, 25
    files, new branch) — held until the auth gating, store bounds, docstring
    corrections, and the per-method 401/403 test matrix are in.

Corrections for the record (with evidence)

  • Scale: this PR is 157 changed files (+16,288 / −119), not ~400 —
    GitHub's Files tab and the Copilot review header ("150 of 157") both show it.
    Still too big, hence the split — but the scope argument should rest on the
    real number. (A few cited locations — ci.yml:361,427, CHANGELOG:773-807
    don't exist on this branch, so part of the pass may have run against a stale
    or stacked checkout.)
  • Provider "deletion": this PR deletes nothing — the diff has zero
    removed files (117 added / 40 modified / 0 deleted). q_cli and gemini_cli
    were removed by Remove Amazon Q CLI and Gemini CLI providers #353, which is already in this PR's merge base; main has
    no such providers today. So there's no ### Removed entry for this PR's
    CHANGELOG to carry — that entry belongs to Remove Amazon Q CLI and Gemini CLI providers #353's release notes, and I'm happy
    to backfill it there in a separate docs PR. My earlier PR body wrongly claimed
    the deletion too; that claim is being removed in the rewrite. What this PR
    does contain — and no review caught — is the inverse bug:
    it adds
    examples/cross-provider/data_analyst_gemini_cli.md pointing at the
    now-nonexistent gemini_cli provider. That example is dead configuration and
    I'm retargeting it to antigravity_cli (the documented successor, upstream
    since feat(antigravity): add Antigravity CLI (agy) provider #323) in PR-A.
  • Inclusive language: happy to honor the allowlist/denylist/primary
    convention, but the flagged occurrences are not introduced here —
    audit_log.py (AUDIT_EVENT_WHITELIST), memory_scoring.py,
    memory_service.py, docs/memory.md, docs/mcp-apps.md are all untouched by
    this diff and the terms exist on main today. I'll fix them in a small
    separate hygiene PR (together with the deliver_pending() rename references
    in docs/inbox-delivery.md, which likewise pre-date this PR) so they don't
    gate this stack.
  • npm ci vs npm install: the two workflows this PR adds both use
    npm ci; the npm install occurrences are in ci.yml, which this PR doesn't
    touch. Also happy to fix those in the hygiene PR.
  • JWT in the query string: agreed the token shouldn't land in access logs,
    and I'm scrubbing/disabling access logging on that route (the :9890 listener
    already sets access_log=False; the main app didn't) plus documenting short
    token TTLs. On the mechanism itself: browser EventSource cannot set an
    Authorization header, so a query-param credential (or short-lived ticket) is
    the standard SSE pattern — I've filed the single-use ticket handshake as a
    follow-up rather than removing query auth. This path is also only reachable
    when auth is enabled and the operator has opted out of the loopback default.
  • .coverage-baseline.json: intentional — it's the ratchet-floor config the
    coverage script reads (as your conventions reviewer concluded). It pre-dates
    this PR; I'll add a one-line header comment in the hygiene PR so it isn't
    mistaken for a stray artifact.

Why keep pushing on AG-UI

The core value is one standard surface over N heterogeneous CLI agents — Kiro
CLI, Claude Code, and Codex rendering uniformly in any stock AG-UI client with
zero custom adapter code — backed by real tmux process lifecycle, with a
metadata-only privacy boundary, and the entire protocol dependency bounded to a
single pinned, default-off module. Your own verification confirmed those
properties hold. Splitting the PR serves that goal: the verified core reaches
users now, and the A2A transport returns hardened.

I'll push the reshaped PR-A shortly and link PR-B here when it's ready for
review.

@anilkmr-a2z anilkmr-a2z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Solid work shipping the AG-UI L1 adapter and supporting protocol surface -- default-off design is the right call. Three must-fix findings (security boundary, unbounded state growth, missing task ID validation) and two nits below.

asyncio.create_task(
_run_task_in_background(task, executor, store, bus),
name=f"a2a-task-{task.id}",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

must-fix -- missing id validation in task.send allows peer-controlled key injection into the in-memory store

When the peer supplies an id field, it is accepted verbatim (task.id = str(data["id"]) in Task.from_dict). Because the store key is just self._tasks[task.id], a peer can overwrite any existing task by resubmitting with the same id -- effectively cancelling or replacing another peer's in-flight work. This is a state-integrity issue.

Fix: either (a) always generate a server-side UUID and ignore the peer-supplied id, or (b) check await store.get(task.id) and reject with INVALID_PARAMS if the id already exists (i.e., idempotent-create semantics, not upsert).

"""

def __init__(self) -> None:
self._tasks: dict[str, Task] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

must-fix -- unbounded in-memory store growth with no eviction

Every task.send call appends to self._tasks and nothing ever removes entries (tasks in terminal states are never reaped). Under sustained peer traffic this is an OOM vector. The main API's StepOutputStore already has the WORKFLOW_OUTPUT_STORE_MAX_ENTRIES eviction pattern -- apply the same LRU/TTL eviction here so terminal tasks are reaped after a configurable window (e.g., 15 min or 1000 entries).

version="1.0.0",
docs_url=None, # No interactive docs on the public listener.
redoc_url=None,
openapi_url=None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

must-fix -- A2A JSON-RPC endpoint mounted on the agent-card listener has no authentication enforcement

The agent-card listener deliberately skips TrustedHostMiddleware to be externally discoverable. When a2a_router is mounted alongside the card, POST /a2a/v1/rpc is exposed to the network with zero authentication -- any peer can submit arbitrary tasks or cancel existing ones. The PR description says "Authentication is enforced via the JWKS published at /.well-known/jwks.json once auth is enabled" (rpc.py docstring) but no middleware or dependency on this listener actually verifies a token before dispatching RPC methods.

Fix: add a FastAPI Depends() on the /a2a/v1/rpc route (or as a router-level dependency on a2a_router) that validates a Bearer token against the JWKS when auth is enabled, same as the main API's require_any_scope pattern. Without this, the loopback default mitigates risk but the operator who sets CAO_AGENT_CARD_HOST=0.0.0.0 gets an open unauthenticated RPC endpoint.

known = {"id", "state", "messages", "metadata", "artifacts", "created_at", "updated_at"}
extra = {k: v for k, v in data.items() if k not in known}
return cls(
id=str(data["id"]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit -- Task.from_dict raises unhandled KeyError when id is missing

str(data["id"]) will raise KeyError if the peer's payload omits id. Since _handle_task_send already generates an id when task.id is falsy, use data.get("id", "") instead of data["id"] so the fallback path works without the caller needing a try/except.

Comment thread pyproject.toml
"jsonschema>=4.25",
# Ported net-new subsystems (fork plauzy/cao):
# OpenTelemetry GenAI telemetry (telemetry/, observability/span_consumer).
"opentelemetry-api>=1.27.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit -- authlib>=1.7.1 added as a production dependency but only used in test fixtures

Grepping the diff, authlib is imported only in test/conftest.py, test/fixtures/jwks_server.py, and test/fixtures/jwt_factory.py. If it is not needed by any src/ module at runtime, it should be a dev dependency (under [dependency-groups] dev) rather than inflating the production install.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] AG-UI Protocol support as a composable construct layer — one face over many CLI agents

7 participants