Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions e2e/bub/tests/test_bub_capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import annotations

import asyncio
from pathlib import Path
from types import SimpleNamespace
from typing import Any

from bub.hooks.interception import ToolCall, ToolCallResult
from powercontext_bub import plugin as plugin_module
from powercontext_bub.plugin import PowerContextPlugin, PowerContextSettings


def test_tool_capture_redacts_credentials_before_crossing_the_client_boundary(
monkeypatch,
tmp_path: Path,
) -> None:
sensitive_value = "provider-secret-sentinel"
captured_requests: list[Any] = []

class RecordingClient:
def __init__(self, base_url: str, *, timeout: float) -> None:
del base_url, timeout

async def __aenter__(self) -> RecordingClient:
return self

async def __aexit__(self, *exc_info: object) -> None:
del exc_info

async def capture_content_source(self, request: Any) -> SimpleNamespace:
captured_requests.append(request)
return SimpleNamespace(position=1)

settings = PowerContextSettings(
base_url="http://127.0.0.1:8000",
scope_id="test:scope",
capture_events=True,
capture_checkpoint_every=100,
)
monkeypatch.setenv("BUB_API_KEY", sensitive_value)
monkeypatch.setattr(plugin_module, "ensure_config", lambda _: settings)
monkeypatch.setattr(plugin_module, "PowerContextClient", RecordingClient)
plugin = PowerContextPlugin(SimpleNamespace(workspace=tmp_path))
state = plugin.load_state(message=None, session_id="session-1")
state["session_id"] = "session-1"

asyncio.run(
plugin.after_tool_call(
ToolCall(run_id="run-1", tool="provider.request", arguments={"api_key": sensitive_value}),
ToolCallResult(
run_id="run-1",
tool="provider.request",
arguments={"api_key": sensitive_value},
result=f"response contained {sensitive_value}",
),
state,
)
)

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
Comment on lines +60 to +64
2 changes: 2 additions & 0 deletions e2e/bub/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 26 additions & 2 deletions integrations/bub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,41 @@ This package connects Bub to a running PowerContext Server through the public Py
- `powercontext.context` prepares bounded context for a question.

Before each model call, the plugin also prepares relevant context and adds it as host-supplied historical evidence.
The plugin does not persist Bub conversation history. A new Bub session can observe an earlier session only through the
configured PowerContext scope.
Automatic trajectory capture is opt-in. When enabled, the plugin captures the initial task and completed LLM and tool
events as bounded Content Sources. It periodically flushes those Sources through the Memory pipeline so later model
steps in the same Bub run can recall earlier findings. Provider-hidden reasoning is never available to the hook and is
not captured.

## Configuration

The plugin uses Bub's Pydantic settings extension. Configuration can live in the `powercontext` section of Bub's
configuration file:

```yaml
powercontext:
base_url: http://127.0.0.1:8000
scope_id: project:example
capture_events: true
capture_checkpoint_every: 5
```

Environment variables use the `POWERCONTEXT_BUB_` prefix and take precedence over file values. Values are parsed and
validated by Pydantic before the plugin starts.

| Variable | Default | Purpose |
| --- | --- | --- |
| `POWERCONTEXT_BUB_BASE_URL` | `http://127.0.0.1:8000` | PowerContext Server URL |
| `POWERCONTEXT_BUB_SCOPE_ID` | workspace-derived | Durable scope shared by Bub sessions |
| `POWERCONTEXT_BUB_TIMEOUT` | `10` | Client timeout in seconds |
| `POWERCONTEXT_BUB_MAX_BYTES` | `8000` | Maximum prepared-context size |
| `POWERCONTEXT_BUB_CAPTURE_EVENTS` | `false` | Capture completed Bub events as Content Sources |
| `POWERCONTEXT_BUB_CAPTURE_CHECKPOINT_EVERY` | `5` | Flush Memory after this many captured events |
| `POWERCONTEXT_BUB_CAPTURE_MAX_BYTES` | `8192` | Maximum UTF-8 bytes stored for one captured event |
| `POWERCONTEXT_BUB_CAPTURE_LOG` | unset | Optional JSONL evidence path; records metadata but not event content |

Captured tool arguments redact values under credential-like keys. Known credential environment values are also
removed from serialized event content. Keep the PowerContext scope and optional capture log protected because normal
tool output can still contain sensitive project data.

Install the package together with PowerContext and Bub:

Expand Down
1 change: 1 addition & 0 deletions integrations/bub/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ requires-python = ">=3.12,<4.0"
dependencies = [
"bub>=0.4.0,<0.5.0",
"powercontext[client]>=0.0.1",
"pydantic-settings>=2.7,<3",
]

[project.entry-points."bub"]
Expand Down
4 changes: 2 additions & 2 deletions integrations/bub/src/powercontext_bub/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Bub integration for PowerContext."""

from powercontext_bub import tools as _tools # noqa: F401
from powercontext_bub.plugin import PowerContextPlugin
from powercontext_bub.plugin import PowerContextPlugin, PowerContextSettings

__all__ = ["PowerContextPlugin"]
__all__ = ["PowerContextPlugin", "PowerContextSettings"]
Loading