Skip to content

feat: add auto-event emission to CapiscioMiddleware#33

Merged
beonde merged 3 commits intomainfrom
feature/middleware-auto-events
Feb 27, 2026
Merged

feat: add auto-event emission to CapiscioMiddleware#33
beonde merged 3 commits intomainfrom
feature/middleware-auto-events

Conversation

@beonde
Copy link
Copy Markdown
Member

@beonde beonde commented Feb 24, 2026

Summary

Adds opt-in automatic event emission to the FastAPI/Starlette middleware, closing the gap where all observability events had to be manually emitted.

Problem

The middleware handled badge verification but emitted zero events. Developers had to manually instrument every request lifecycle event, which meant most deployments had no observability into request patterns, verification outcomes, or latency.

Solution

When an EventEmitter is provided to CapiscioMiddleware, it automatically emits standardized events at key middleware lifecycle points:

Event Type When Key Fields
request.received Every inbound request method, path
verification.success Badge verified successfully method, path, caller_did, duration_ms
verification.failed Badge missing or invalid method, path, reason, duration_ms
request.completed After response sent method, path, status_code, duration_ms, caller_did

Design Principles

  • Fully opt-in: No emitter parameter = no events. Zero breaking changes.
  • Safe: All event emission is wrapped in try/except — emitter errors never break request handling.
  • Standardized: Consistent field names across all event types for easy querying.
  • Excluded paths respected: Paths in exclude_paths emit nothing (no noise from health checks).

Usage

from capiscio_sdk.events import EventEmitter
from capiscio_sdk.integrations.fastapi import CapiscioMiddleware

emitter = EventEmitter(agent_id="...", api_key="...", registry_url="...")
app.add_middleware(CapiscioMiddleware, guard=guard, emitter=emitter)
# That's it — events flow automatically

New Event Type Constants

Added to capiscio_sdk/events.py:

  • EVENT_REQUEST_RECEIVED
  • EVENT_REQUEST_COMPLETED
  • EVENT_REQUEST_FAILED
  • EVENT_VERIFICATION_SUCCESS
  • EVENT_VERIFICATION_FAILED

Tests

7 new tests in TestAutoEvents covering:

  • Successful request lifecycle (received → success → completed)
  • Blocked request (received → failed, no completed)
  • Log mode verification failure (all events emitted, request proceeds)
  • Backward compatibility (no emitter = no events)
  • Excluded paths (no events emitted)
  • Emitter error resilience (exceptions don't break requests)

All 50 unit tests pass (22 fastapi + 28 events).

Add opt-in automatic event emission to the FastAPI/Starlette middleware.
When an EventEmitter is provided, the middleware automatically emits:
- request.received: on every inbound request
- verification.success/failed: after badge verification
- request.completed: after response with status_code and duration_ms

Design:
- Fully opt-in: no emitter = no events (backward compatible)
- Safe: emitter errors never break request handling
- Standardized fields: method, path, caller_did, duration_ms, status_code
Copilot AI review requested due to automatic review settings February 24, 2026 19:26
@github-actions
Copy link
Copy Markdown

✅ Documentation validation passed!

Unified docs will be deployed from capiscio-docs repo.

@github-actions
Copy link
Copy Markdown

✅ All checks passed! Ready for review.

@github-actions
Copy link
Copy Markdown

✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds opt-in automatic observability event emission to the FastAPI/Starlette CapiscioMiddleware so request lifecycle and verification outcomes can be tracked without manual instrumentation.

Changes:

  • Extend CapiscioMiddleware to accept an optional EventEmitter and emit standardized middleware lifecycle events.
  • Add new middleware-related event type constants to EventEmitter.
  • Add unit tests validating auto-event emission across success/failure/excluded-path scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
capiscio_sdk/integrations/fastapi.py Adds emitter support and emits request.received, verification.*, and request.completed events around middleware execution.
capiscio_sdk/events.py Introduces middleware auto-event type constants on EventEmitter.
tests/unit/test_fastapi_integration.py Adds TestAutoEvents coverage for auto-emission behavior using a mocked EventEmitter.

Comment on lines +493 to +503
def test_auto_events_on_missing_badge_block(self):
"""Emitter receives request.received and verification.failed when badge missing."""
guard = MagicMock()
guard.agent_id = "test-agent"

emitter = MagicMock(spec=EventEmitter)

app = self._make_app(guard, emitter)
client = TestClient(app)

response = client.post("/tasks/send", json={"msg": "hi"})
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Auto-events are tested for missing-badge in block mode and for verification failure in log mode, but there’s no test covering missing-badge behavior in fail_mode="log"/"monitor" with an emitter. Add a test to lock in whether verification.failed should be emitted (per PR description) while still allowing the request.

Copilot uses AI. Check for mistakes.
import json
import pytest
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, patch, call
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

call is imported from unittest.mock but never used in this test module; this will be flagged by ruff (F401) and should be removed (or actually used if you intended to assert call ordering).

Suggested change
from unittest.mock import MagicMock, patch, call
from unittest.mock import MagicMock, patch

Copilot uses AI. Check for mistakes.
Comment on lines 100 to +104
if self.fail_mode in ("log", "monitor"):
logger.warning(f"Missing X-Capiscio-Badge header for {request.url.path} ({self.fail_mode} mode)")
request.state.agent = None
request.state.agent_id = None
return await call_next(request)
response = await call_next(request)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

In the missing-badge branch for fail_mode = "log"/"monitor", the middleware currently emits request.received and request.completed but does not emit verification.failed. This conflicts with the PR description that verification.failed should be emitted when the badge is missing; consider emitting it here as well (without blocking).

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +112
self._auto_emit(EventEmitter.EVENT_VERIFICATION_FAILED, {
"method": request.method,
"path": path,
"reason": "missing_badge",
})
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The verification.failed auto-event emitted for a blocked missing-badge request does not include duration_ms, but the PR description lists duration_ms as a key field for verification failures. Consider including a duration (e.g., from request_start).

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +33
the middleware automatically emits request.received, request.completed,
verification.success, and verification.failed events.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Docstring says the middleware "automatically emits request.completed" when an emitter is provided, but blocked requests (401/403 paths) return before _auto_emit_completed is called. Update the docstring to clarify when request.completed is (and is not) emitted, or emit it for blocked responses too.

Suggested change
the middleware automatically emits request.received, request.completed,
verification.success, and verification.failed events.
the middleware automatically emits request.received, verification.success,
and verification.failed events, and emits request.completed for requests
that reach the downstream ASGI app (blocked 4xx responses may not emit
request.completed).

Copilot uses AI. Check for mistakes.
# Middleware auto-event types (emitted automatically by CapiscioMiddleware)
EVENT_REQUEST_RECEIVED = "request.received"
EVENT_REQUEST_COMPLETED = "request.completed"
EVENT_REQUEST_FAILED = "request.failed"
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

EVENT_REQUEST_FAILED is introduced but not referenced anywhere in the repository. If it’s not part of the current auto-event contract, consider removing it to avoid confusing API consumers, or wire it into the middleware and add coverage.

Suggested change
EVENT_REQUEST_FAILED = "request.failed"

Copilot uses AI. Check for mistakes.
- CHANGELOG.md: document middleware auto-events feature
- README.md: add middleware observability section
- docs/guides/configuration.md: add auto-events configuration guide
- docs/api-reference.md: add EventEmitter and event constants
@github-actions
Copy link
Copy Markdown

✅ Documentation validation passed!

Unified docs will be deployed from capiscio-docs repo.

@github-actions
Copy link
Copy Markdown

✅ All checks passed! Ready for review.

@github-actions
Copy link
Copy Markdown

✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

- Remove unused 'call' import from test module
- Remove dead EVENT_REQUEST_FAILED constant (not wired into middleware)
- Emit verification.failed for missing badge in log/monitor mode
- Add duration_ms to blocked missing-badge verification.failed event
- Fix docstring to clarify request.completed not emitted for blocked responses
- Add test for missing-badge in log mode with emitter
Copilot AI review requested due to automatic review settings February 27, 2026 19:33
@github-actions
Copy link
Copy Markdown

✅ Documentation validation passed!

Unified docs will be deployed from capiscio-docs repo.

@github-actions
Copy link
Copy Markdown

✅ All checks passed! Ready for review.

@github-actions
Copy link
Copy Markdown

✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Comment on lines +189 to +195
def _auto_emit(self, event_type: str, data: Dict[str, Any]) -> None:
"""Emit an auto-event if emitter is configured."""
if self._emitter:
try:
self._emitter.emit(event_type, data)
except Exception:
logger.debug(f"Failed to emit auto-event {event_type}", exc_info=True)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

_auto_emit() calls EventEmitter.emit() synchronously from the async middleware. EventEmitter.emit() can trigger a flush, which performs a blocking httpx.Client.post() and can therefore block the event loop and add latency to requests. Consider offloading emission/flush to a background thread/queue (or using an async emitter) so middleware request handling isn’t impacted by network I/O.

Copilot uses AI. Check for mistakes.

# Middleware auto-event types (emitted automatically by CapiscioMiddleware)
EVENT_REQUEST_RECEIVED = "request.received"
EVENT_REQUEST_COMPLETED = "request.completed"
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

Docs/PR metadata refer to a EVENT_REQUEST_FAILED constant, but the code only defines EVENT_REQUEST_RECEIVED, EVENT_REQUEST_COMPLETED, EVENT_VERIFICATION_SUCCESS, and EVENT_VERIFICATION_FAILED. Either add the missing constant (and ensure it’s actually emitted where appropriate) or remove references to it from docs/changelog to avoid advertising a nonexistent API surface.

Suggested change
EVENT_REQUEST_COMPLETED = "request.completed"
EVENT_REQUEST_COMPLETED = "request.completed"
EVENT_REQUEST_FAILED = "request.failed"

Copilot uses AI. Check for mistakes.
emitter = EventEmitter(
agent_id="your-agent-id",
api_key="sk_live_...",
registry_url="https://registry.capisc.io"
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The example constructs EventEmitter with registry_url=..., but EventEmitter.__init__ expects server_url (there’s no registry_url kwarg). This snippet will raise TypeError if copied. Update the parameter name (or add a backwards-compatible alias in EventEmitter).

Suggested change
registry_url="https://registry.capisc.io"
server_url="https://registry.capisc.io"

Copilot uses AI. Check for mistakes.
Comment on lines +706 to +718
If you're using `CapiscIO.connect()`, the agent identity already has an emitter:

```python
from capiscio_sdk import CapiscIO
from capiscio_sdk.integrations.fastapi import CapiscioMiddleware

agent = CapiscIO.connect(api_key="sk_live_...")

app.add_middleware(
CapiscioMiddleware,
guard=guard,
emitter=agent.emitter # Use the agent's emitter
)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

This section says CapiscIO.connect() returns an object with agent.emitter, but AgentIdentity currently only exposes emit(...) and keeps the emitter as a private _emitter field. Either add a public emitter property/attribute on AgentIdentity, or update the docs to show the supported pattern (e.g., construct an EventEmitter from agent.server_url/agent.api_key/agent.agent_id, or just call agent.emit(...)).

Copilot uses AI. Check for mistakes.
from capiscio_sdk.events import EventEmitter
from capiscio_sdk.integrations.fastapi import CapiscioMiddleware

emitter = EventEmitter(agent_id="...", api_key="...", registry_url="...")
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The README example uses EventEmitter(..., registry_url="..."), but the initializer parameter is server_url. As written, this snippet will raise TypeError when run; please update the argument name (or support registry_url as an alias).

Suggested change
emitter = EventEmitter(agent_id="...", api_key="...", registry_url="...")
emitter = EventEmitter(agent_id="...", api_key="...", server_url="...")

Copilot uses AI. Check for mistakes.
- Standardized fields: `method`, `path`, `caller_did`, `duration_ms`, `status_code`
- Safe by design: emitter errors never break request handling
- Excluded paths emit no events
- New event type constants: `EVENT_REQUEST_RECEIVED`, `EVENT_REQUEST_COMPLETED`, `EVENT_REQUEST_FAILED`, `EVENT_VERIFICATION_SUCCESS`, `EVENT_VERIFICATION_FAILED`
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The changelog lists EVENT_REQUEST_FAILED as a new constant, but it isn’t defined in capiscio_sdk.events (and no corresponding middleware event type appears to be emitted). Please either add/emit this event type or remove it from the changelog entry to keep release notes accurate.

Suggested change
- New event type constants: `EVENT_REQUEST_RECEIVED`, `EVENT_REQUEST_COMPLETED`, `EVENT_REQUEST_FAILED`, `EVENT_VERIFICATION_SUCCESS`, `EVENT_VERIFICATION_FAILED`
- New event type constants: `EVENT_REQUEST_RECEIVED`, `EVENT_REQUEST_COMPLETED`, `EVENT_VERIFICATION_SUCCESS`, `EVENT_VERIFICATION_FAILED`

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +219
- EVENT_TASK_STARTED
- EVENT_TASK_COMPLETED
- EVENT_TASK_FAILED
- EVENT_TOOL_CALL
- EVENT_TOOL_RESULT
- EVENT_REQUEST_RECEIVED
- EVENT_REQUEST_COMPLETED
- EVENT_REQUEST_FAILED
- EVENT_VERIFICATION_SUCCESS
- EVENT_VERIFICATION_FAILED
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

docs/api-reference.md lists EVENT_TASK_STARTED, EVENT_TOOL_CALL, EVENT_REQUEST_FAILED, etc. as module members of capiscio_sdk.events, but these are currently class attributes on EventEmitter (and EVENT_REQUEST_FAILED isn’t defined at all). This will produce incorrect API docs (and may break doc builds depending on mkdocstrings settings). Consider documenting only EventEmitter here, or exporting module-level constants (e.g., EVENT_TASK_STARTED = EventEmitter.EVENT_TASK_STARTED, etc.) and ensuring all listed symbols exist.

Suggested change
- EVENT_TASK_STARTED
- EVENT_TASK_COMPLETED
- EVENT_TASK_FAILED
- EVENT_TOOL_CALL
- EVENT_TOOL_RESULT
- EVENT_REQUEST_RECEIVED
- EVENT_REQUEST_COMPLETED
- EVENT_REQUEST_FAILED
- EVENT_VERIFICATION_SUCCESS
- EVENT_VERIFICATION_FAILED

Copilot uses AI. Check for mistakes.
@beonde beonde merged commit 1cd2e92 into main Feb 27, 2026
17 checks passed
@beonde beonde deleted the feature/middleware-auto-events branch February 27, 2026 21:00
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