Skip to content

feat(events): emit structured events for engine to fan out to Slack#164

Open
jacsamell wants to merge 1 commit into
mainfrom
claude/engine-events
Open

feat(events): emit structured events for engine to fan out to Slack#164
jacsamell wants to merge 1 commit into
mainfrom
claude/engine-events

Conversation

@jacsamell
Copy link
Copy Markdown
Contributor

@jacsamell jacsamell commented May 18, 2026

Cube emits structured events; Engine handles Slack delivery. Splits responsibilities cleanly.

Two emit channels

1. PR-context events ride the review body (the common case)

Every cube prv review body now ends in a hidden HTML-comment footer:

<!-- agent-cube-events
{
  "schema": 1,
  "events": [
    {"type": "human_call", "severity": "must-decide", "question": "...", "options": ["...", "..."], "lenses": ["principal-engineer"], "evidence": "file:line", "request_id": "cube-decision-..."},
    {"type": "pr_ready_to_merge", "approvals": 5, "total_judges": 5, "request_id": "cube-ready-..."}
  ]
}
-->
  • Invisible in rendered GitHub UI
  • Engine subscribes to pull_request_review.submitted for reviews from the-agent-cube[bot]
  • Zero new cube-side ingress / auth / infrastructure
  • The PR is the durable event log

2. Non-PR events POST to $CUBE_ENGINE_WEBHOOK_URL (rare path: setup_needed, scheduled sweeps). Fire-and-forget + one retry. No-op when env-var unset.

Event types (schema v1)

  • human_call (severity: must-decide | worth-asking) — wired
  • pr_ready_to_merge — wired
  • pr_blocked — defined, detection path follow-up
  • setup_needed — defined, detection path follow-up

What Engine needs to do

  • Subscribe to GitHub pull_request_review.submitted from the-agent-cube[bot]
  • Parse the <!-- agent-cube-events ... --> marker
  • Fan out to Slack with whatever routing logic you want
  • For resolutions: post a <!-- agent-cube-resolution {...} --> comment, cube picks up on resume (via PR feat(auto): persist -p feedback to task spec so judges see it #156's -p persistence) — follow-up PR

Open to your preference on resolution pathway (GitHub PR comment vs .cube/resolutions/<request_id>.json on the cube host).

8 module smoke tests + 192 existing tests pass. Not admin-merging — architecture choice worth eyeballs.

🤖 Generated with Claude Code

Overview

This PR introduces structured event emission from Cube to delegate Slack delivery responsibilities to the Engine. It establishes a clear event-driven boundary between the two systems via two communication channels.

Key Changes

New module: engine_events.py (+258 lines)

  • Defines event schema versioning and four event types: HumanCallEvent, PrReadyToMergeEvent, PrBlockedEvent, SetupNeededEvent
  • Implements two transport channels:
    1. PR context events: Serialized as an invisible HTML comment footer (<!-- agent-cube-events { ... } -->) appended to review bodies
    2. Non-PR events: POSTed to $CUBE_ENGINE_WEBHOOK_URL with one retry; no-op if env var unset
  • Provides utilities for building/parsing event footers and translating panel review inputs into structured events

Updated: peer_review.py (+14 lines)

  • Appends the engine events footer to review summary bodies after auto-approve gate logic

Design

  • PR as durable event log: Engine subscribes to pull_request_review.submitted and extracts events from bot review footers—no new Cube infrastructure needed
  • Schema v1 events: Support must-decide and worth-asking severity levels for human calls, plus merge readiness and blocking states
  • Fire-and-forget reliability: Webhook delivery retries transient failures but respects 4xx errors

Test Coverage

8 new module smoke tests + 192 existing tests pass

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Walkthrough

This PR adds a new event system for communicating peer review outcomes and human decisions from Cube to Aetheron Engine. It introduces typed event dataclasses, two transport mechanisms (GitHub PR footer comments and webhook delivery), and integrates event creation into the peer review workflow.

Changes

Engine Events System for Cube→Aetheron Communication

Layer / File(s) Summary
Event schema and PR footer transport
python/cube/integrations/engine_events.py
Introduces EVENT_SCHEMA_VERSION constant, four typed event dataclasses (HumanCallEvent, PrReadyToMergeEvent, PrBlockedEvent, SetupNeededEvent), and EngineEvent union type. Provides build_events_footer() to serialise events as an invisible HTML comment, and parse_events_footer() to extract and validate raw event dictionaries with JSON parsing and shape validation.
Webhook delivery and event translation
python/cube/integrations/engine_events.py
Implements post_webhook_event() to deliver events via HTTP POST to CUBE_ENGINE_WEBHOOK_URL with schema version, single retry logic for transient failures (no retry for 4xx errors), and timeout handling. Provides events_for_panel_review() to convert panel review inputs (deduped human calls, gate outcome, approvals, judge count) into EngineEvent instances with severity normalisation.
Peer review summary integration
python/cube/commands/peer_review.py
Constructs engine_events via events_for_panel_review() using gate result, gate reasons, deduped human calls, approval counts, and judge counts, then appends the serialised events footer to the GitHub review summary body.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A whispered message in the PR review thread,
Events hiding in HTML comments, by Cube instead,
To the Engine they hop with webhook precision,
Human calls and gate verdicts, in structured transmission!
No mystery now—just facts, neat and tidy,
For orchestration to follow, swiftly and bridy. 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding structured event emission for engine fan-out to Slack, which is the primary purpose of the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/cube/integrations/engine_events.py`:
- Around line 130-153: The parser parse_events_footer currently returns any JSON
with an "events" list; update it to validate the footer schema version too:
retrieve payload.get("schema") and return [] unless it exactly equals the
expected footer schema constant (introduce or use a module-level constant like
_FOOTER_SCHEMA_VERSION), so only when schema is present and matches do you
proceed to retrieve "events" and filter dict entries; keep all other error paths
returning [] and continue using _FOOTER_OPEN/_FOOTER_CLOSE to locate the
payload.
- Around line 175-196: The code currently passes the value returned by
_webhook_url() directly to urllib.request.Request / urlopen; add an explicit
scheme allowlist by parsing the URL (e.g., via urllib.parse.urlparse) and
verifying parsed.scheme is either "http" or "https" before proceeding. If the
scheme is missing or not in {"http","https"}, return False (same behavior as
when url is falsy) or log and abort; perform this check right after calling
_webhook_url() and before building the payload/Request and before the urlopen
loop in send (or the function containing the for-attempt loop) so only
http/https requests are allowed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66a1f50c-f7f4-49cb-94e0-77d3254878b6

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd9e9e and 92addc5.

📒 Files selected for processing (2)
  • python/cube/commands/peer_review.py
  • python/cube/integrations/engine_events.py
📜 Review details
🧰 Additional context used
🪛 Ruff (0.15.12)
python/cube/integrations/engine_events.py

[error] 180-188: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 195-195: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🔇 Additional comments (2)
python/cube/commands/peer_review.py (1)

506-519: LGTM!

python/cube/integrations/engine_events.py (1)

44-127: LGTM!

Also applies to: 216-258

Comment on lines +130 to +153
def parse_events_footer(body: str) -> list[dict[str, Any]]:
"""Inverse of `build_events_footer`. Returns raw event dicts.

Used by tests and (optionally) by engine implementations that want to
consume cube's emitted events without duplicating the marker constants.

Returns an empty list when no footer is present or the JSON is malformed.
"""
start = body.find(_FOOTER_OPEN)
if start < 0:
return []
end = body.find(_FOOTER_CLOSE, start + len(_FOOTER_OPEN))
if end < 0:
return []
raw = body[start + len(_FOOTER_OPEN) : end].strip()
try:
payload = json.loads(raw)
except json.JSONDecodeError:
return []
events = payload.get("events")
if not isinstance(events, list):
return []
return [e for e in events if isinstance(e, dict)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate schema version in footer parser.

At Line 146 onwards, the parser accepts any JSON containing an events list, even when schema is missing or incompatible. That weakens the versioned contract and can produce silent misreads across schema upgrades.

Suggested fix
 def parse_events_footer(body: str) -> list[dict[str, Any]]:
@@
     try:
         payload = json.loads(raw)
     except json.JSONDecodeError:
         return []
+    if payload.get("schema") != EVENT_SCHEMA_VERSION:
+        return []
     events = payload.get("events")
     if not isinstance(events, list):
         return []
     return [e for e in events if isinstance(e, dict)]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cube/integrations/engine_events.py` around lines 130 - 153, The parser
parse_events_footer currently returns any JSON with an "events" list; update it
to validate the footer schema version too: retrieve payload.get("schema") and
return [] unless it exactly equals the expected footer schema constant
(introduce or use a module-level constant like _FOOTER_SCHEMA_VERSION), so only
when schema is present and matches do you proceed to retrieve "events" and
filter dict entries; keep all other error paths returning [] and continue using
_FOOTER_OPEN/_FOOTER_CLOSE to locate the payload.

Comment on lines +175 to +196
url = _webhook_url()
if not url:
return False

payload = json.dumps({"schema": EVENT_SCHEMA_VERSION, "event": asdict(event)}).encode()
req = urllib.request.Request(
url,
data=payload,
method="POST",
headers={
"Content-Type": "application/json",
"User-Agent": "agent-cube",
},
)

# Track last error for debugging; not surfaced upstream — engine is the
# source of truth for human-facing comms.
_last_err: Optional[Exception] = None
for attempt in range(2):
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return 200 <= resp.status < 300
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict webhook URL schemes to http/https before opening.

At Line 175 and Line 195, CUBE_ENGINE_WEBHOOK_URL is used directly with urlopen. Add an explicit scheme allowlist to avoid unexpected scheme handling (file:, custom handlers) and tighten outbound request safety.

Suggested fix
 import json
 import os
 import urllib.error
+import urllib.parse
 import urllib.request
@@
 def post_webhook_event(event: EngineEvent, *, timeout: int = 10) -> bool:
@@
     url = _webhook_url()
     if not url:
         return False
+    parsed = urllib.parse.urlparse(url)
+    if parsed.scheme not in {"http", "https"}:
+        return False
 
     payload = json.dumps({"schema": EVENT_SCHEMA_VERSION, "event": asdict(event)}).encode()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
url = _webhook_url()
if not url:
return False
payload = json.dumps({"schema": EVENT_SCHEMA_VERSION, "event": asdict(event)}).encode()
req = urllib.request.Request(
url,
data=payload,
method="POST",
headers={
"Content-Type": "application/json",
"User-Agent": "agent-cube",
},
)
# Track last error for debugging; not surfaced upstream — engine is the
# source of truth for human-facing comms.
_last_err: Optional[Exception] = None
for attempt in range(2):
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return 200 <= resp.status < 300
url = _webhook_url()
if not url:
return False
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in {"http", "https"}:
return False
payload = json.dumps({"schema": EVENT_SCHEMA_VERSION, "event": asdict(event)}).encode()
req = urllib.request.Request(
url,
data=payload,
method="POST",
headers={
"Content-Type": "application/json",
"User-Agent": "agent-cube",
},
)
# Track last error for debugging; not surfaced upstream — engine is the
# source of truth for human-facing comms.
_last_err: Optional[Exception] = None
for attempt in range(2):
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return 200 <= resp.status < 300
🧰 Tools
🪛 Ruff (0.15.12)

[error] 180-188: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 195-195: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cube/integrations/engine_events.py` around lines 175 - 196, The code
currently passes the value returned by _webhook_url() directly to
urllib.request.Request / urlopen; add an explicit scheme allowlist by parsing
the URL (e.g., via urllib.parse.urlparse) and verifying parsed.scheme is either
"http" or "https" before proceeding. If the scheme is missing or not in
{"http","https"}, return False (same behavior as when url is falsy) or log and
abort; perform this check right after calling _webhook_url() and before building
the payload/Request and before the urlopen loop in send (or the function
containing the for-attempt loop) so only http/https requests are allowed.

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