feat(bub): capture long-running agent events - #1230
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in Bub-side “capture layer” that records long-running agent trajectory events (user prompt, LLM results, tool results) as bounded PowerContext Content Sources, periodically checkpointing them through the Memory pipeline and redacting sensitive values at the client boundary.
Changes:
- Switch Bub integration configuration to Bub’s Pydantic settings (
PowerContextSettings) and wire capture/checkpoint options into the plugin. - Capture and flush Bub events as Content Sources with JSONL optional capture logging and secret redaction.
- Add an e2e regression test for credential redaction and update integration dependencies/docs.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| integrations/bub/src/powercontext_bub/plugin.py | Implements Pydantic-backed settings, event capture/checkpointing, and redaction utilities. |
| integrations/bub/src/powercontext_bub/init.py | Exposes PowerContextSettings alongside the plugin. |
| integrations/bub/README.md | Documents opt-in capture behavior and new configuration options. |
| integrations/bub/pyproject.toml | Adds pydantic-settings dependency for Bub settings integration. |
| e2e/bub/uv.lock | Locks the new dependency for the Bub e2e environment. |
| e2e/bub/tests/test_bub_capture.py | Adds regression coverage for credential redaction in captured tool results. |
Suppressed comments (1)
integrations/bub/src/powercontext_bub/plugin.py:393
- Same
isinstance(value, list | tuple)issue here: it will raiseTypeErrorwhen walking Codex auth JSON arrays, which can break secret redaction (and therefore capture) at runtime. Use(list, tuple)instead.
def _sensitive_values(value: Any, *, sensitive: bool = False) -> set[str]:
if isinstance(value, dict):
secrets: set[str] = set()
for key, item in value.items():
secrets.update(_sensitive_values(item, sensitive=sensitive or _is_sensitive_key(str(key))))
return secrets
if isinstance(value, list | tuple):
secrets = set()
for item in value:
secrets.update(_sensitive_values(item, sensitive=sensitive))
return secrets
if sensitive and isinstance(value, str) and len(value) >= 8:
return {value}
return set()
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| timeout=float(os.getenv("POWERCONTEXT_BUB_TIMEOUT", "10")), | ||
| max_bytes=max_bytes, | ||
| ) | ||
| base_url: HttpUrl = HttpUrl("http://127.0.0.1:8000") |
| def _sanitize(value: Any) -> Any: | ||
| if isinstance(value, dict): | ||
| return { | ||
| str(key): "[REDACTED]" if _is_sensitive_key(str(key)) else _sanitize(item) for key, item in value.items() | ||
| } | ||
| if isinstance(value, list | tuple): | ||
| return [_sanitize(item) for item in value] | ||
| return value |
| def _codex_auth_secrets() -> set[str]: | ||
| codex_home = Path(os.getenv("CODEX_HOME", str(Path.home() / ".codex"))).expanduser() | ||
| try: | ||
| auth = json.loads((codex_home / "auth.json").read_text(encoding="utf-8")) | ||
| except (OSError, json.JSONDecodeError): | ||
| return set() | ||
| return _sensitive_values(auth) |
| assert len(captured_requests) == 1 | ||
| request = captured_requests[0] | ||
| assert request.metadata["event"] == "tool_result" | ||
| assert sensitive_value not in request.content | ||
| assert "[REDACTED]" in request.content |
| def load_state(self, message: Any, session_id: str) -> TurnState: | ||
| del message, session_id | ||
| return { | ||
| STATE_KEY: { | ||
| "base_url": self.settings.base_url, | ||
| "scope_id": self.settings.scope_id, | ||
| "base_url": self.base_url, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
integrations/bub/src/powercontext_bub/plugin.py:70
load_statediscards thesession_idBub provides, but event capture later falls back tostate.get("session_id", "unknown"). If Bub doesn’t injectsession_idintostate, captured events will all be tagged with "unknown" (andsource_iduniqueness degrades across sessions). Persist thesession_idin the returned state so capture metadata is stable without callers patching state manually.
def load_state(self, message: Any, session_id: str) -> TurnState:
del message, session_id
return {
STATE_KEY: {
"base_url": self.base_url,
integrations/bub/src/powercontext_bub/plugin.py:155
save_stateperforms only a single flush. Sinceflush_memoryis explicitly bounded, one call may not advancecurrent_cursorto the latest capturedposition, leaving some captured Sources unprocessed when the session ends. Consider retrying flush until the cursor reaches the captured high-watermark (or no further progress is possible), with a small upper bound to avoid infinite loops.
if not self.settings.capture_events:
return
async with self._capture_lock:
await self._flush_captured_sources(state, final=True)
e2e/bub/tests/test_bub_capture.py:58
- This test sets
BUB_API_KEYto the samesensitive_valuethat is passed in tool arguments and echoed inToolCallResult.result, so it can still pass even if key-based redaction ofargumentsregresses (because env-value redaction would remove the sentinel from the serialized content). Use a different env value and avoid embedding the argument sentinel inresultso the test specifically verifies argument-key redaction at the client boundary.
settings = PowerContextSettings(
base_url="http://127.0.0.1:8000",
scope_id="test:scope",
capture_events=True,
capture_checkpoint_every=100,
| def _sanitize(value: Any) -> Any: | ||
| if isinstance(value, dict): | ||
| return { | ||
| str(key): "[REDACTED]" if _is_sensitive_key(str(key)) else _sanitize(item) for key, item in value.items() | ||
| } | ||
| if isinstance(value, list | tuple): | ||
| return [_sanitize(item) for item in value] | ||
| return value |
Stack
Merge in this order. This PR depends on #1229.
Which issue or RFC does this PR close?
Implements the Bub capture layer of #1229.
Rationale for this change
Long-running acceptance workloads need durable evidence without persisting credentials or adding harness-specific provider configuration.
What changes are included in this PR?
Are there any user-facing changes?
Capture is opt-in. Existing Bub behavior and configuration remain unchanged.
How was this change tested?
make harness-syncmake harness-checkAI usage statement
OpenAI Codex (GPT-5) assisted with implementation and tests. The author is responsible for the submitted changes.