Environment details
' +
+ "" +
+ escapeHtml(env) +
+ "
"
+ }
+ return html
+ }
+
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&")
@@ -70,7 +97,7 @@
switch (kind) {
case "user_feedback":
case "user_feedback_diff":
- return { role: "user", label: "You", icon: "\u{1F464}", body: md(m.text) }
+ return { role: "user", label: "You", icon: "\u{1F464}", body: userContentHtml(m.text) }
case "text":
if (!m.text && !(m.images && m.images.length)) return null
@@ -78,7 +105,7 @@
role: "assistant",
label: "Assistant",
icon: "\u{1F916}",
- body: md(m.text) + images(m),
+ body: userContentHtml(m.text) + images(m),
fold: true,
activity: "Respondingβ¦",
}
diff --git a/self-hosted-cloudapi/tests/test_back_channel_host.py b/self-hosted-cloudapi/tests/test_back_channel_host.py
new file mode 100644
index 000000000..57607f168
--- /dev/null
+++ b/self-hosted-cloudapi/tests/test_back_channel_host.py
@@ -0,0 +1,101 @@
+"""Back-channel Host header behaviour.
+
+Authentik routes to its OAuth/OIDC endpoints by HTTP Host header and 404s on an
+invalid host (e.g. the compose service name `auth_server`, whose underscore is an
+invalid RFC-1123 hostname). The api therefore presents the public front-channel
+host (host of AUTHENTIK_BASE_URL) on every server-to-server call. These tests
+lock that in so the OAuth callback can't silently regress to a 502.
+"""
+
+import pytest
+
+import config.auth as auth_cfg
+from config.auth import get_back_channel_host_header
+from config.settings import settings
+import src.auth.authentik as authentik
+
+
+def test_host_header_is_front_channel_when_internal_url_set(monkeypatch):
+ monkeypatch.setattr(settings, "authentik_base_url", "https://auth.tumblecode.dev")
+ monkeypatch.setattr(settings, "authentik_internal_url", "http://auth_server:9000")
+
+ host = get_back_channel_host_header()
+
+ assert host == "auth.tumblecode.dev"
+ assert "_" not in host # the bug: underscore hosts get 404'd by Authentik
+
+
+def test_host_header_keeps_port_for_dev_stack(monkeypatch):
+ monkeypatch.setattr(settings, "authentik_base_url", "http://localhost:9000")
+ monkeypatch.setattr(settings, "authentik_internal_url", "http://auth_server:9000")
+
+ assert get_back_channel_host_header() == "localhost:9000"
+
+
+def test_host_header_none_for_single_host(monkeypatch):
+ # No internal URL β front == back channel β httpx's default Host is correct.
+ monkeypatch.setattr(settings, "authentik_internal_url", None)
+
+ assert get_back_channel_host_header() is None
+
+
+class _FakeResp:
+ def __init__(self, data):
+ self._data = data
+
+ def raise_for_status(self):
+ return None
+
+ def json(self):
+ return self._data
+
+
+class _CapturingClient:
+ """Stand-in for httpx.AsyncClient that records the headers it was called with."""
+
+ last_headers: dict = {}
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, *exc):
+ return False
+
+ async def post(self, url, data=None, headers=None):
+ _CapturingClient.last_headers = headers or {}
+ return _FakeResp({"access_token": "fake"})
+
+ async def get(self, url, headers=None):
+ _CapturingClient.last_headers = headers or {}
+ return _FakeResp({"sub": "fake"})
+
+
+@pytest.fixture
+def capture_httpx(monkeypatch):
+ monkeypatch.setattr(authentik.httpx, "AsyncClient", _CapturingClient)
+ monkeypatch.setattr(settings, "authentik_base_url", "https://auth.tumblecode.dev")
+ monkeypatch.setattr(settings, "authentik_internal_url", "http://auth_server:9000")
+ return _CapturingClient
+
+
+async def test_token_exchange_sends_brand_host(capture_httpx):
+ await authentik.exchange_code_for_tokens("code", "verifier")
+
+ headers = capture_httpx.last_headers
+ assert headers["Host"] == "auth.tumblecode.dev"
+ # Existing content-type header is preserved alongside the injected Host.
+ assert headers["Content-Type"] == "application/x-www-form-urlencoded"
+
+
+async def test_userinfo_sends_brand_host(capture_httpx):
+ await authentik.get_userinfo("access-token")
+
+ headers = capture_httpx.last_headers
+ assert headers["Host"] == "auth.tumblecode.dev"
+ assert headers["Authorization"] == "Bearer access-token"
+
+
+async def test_discovery_sends_brand_host(capture_httpx):
+ await authentik.get_openid_configuration()
+
+ assert capture_httpx.last_headers["Host"] == "auth.tumblecode.dev"
diff --git a/self-hosted-cloudapi/tests/test_bridge.py b/self-hosted-cloudapi/tests/test_bridge.py
index 008f438b2..5b7188a13 100644
--- a/self-hosted-cloudapi/tests/test_bridge.py
+++ b/self-hosted-cloudapi/tests/test_bridge.py
@@ -328,6 +328,39 @@ async def test_task_event_stamps_workspace_path_from_registered_instance(
assert task.workspace_path == ws
+async def test_task_event_prefers_event_workspace_path_over_registry(
+ patch_session_factory, db_session, session_factory, stub_emit
+):
+ """When several windows share one cloud account the registry holds only the
+ most recently registered window's path. The originating window stamps its own
+ worktree root on the event, which must take precedence so the task is
+ attributed to the project it actually ran in."""
+ await _seed_user(db_session, "owner")
+
+ # Registry points at a *different* window (the last to register).
+ registry.attach("ext_owner", "extension", "owner")
+ registry.register_extension(
+ "ext_owner", "owner", {"workspacePath": "/home/krzych/Projekty/septicoBackend"}
+ )
+
+ event_ws = "/home/krzych/Projekty/lids-uniform-api"
+ await sio_module.on_task_event(
+ "ext_owner",
+ {
+ "taskId": "task-evt-ws",
+ "type": EVT_MESSAGE,
+ "workspacePath": event_ws,
+ "message": {"ts": 1, "type": "say", "say": "text", "text": "hi"},
+ },
+ )
+
+ async with session_factory() as s:
+ task = (
+ await s.execute(select(Task).where(Task.id == "task-evt-ws"))
+ ).scalar_one()
+ assert task.workspace_path == event_ws
+
+
async def test_task_event_backfills_workspace_path_on_legacy_null_task(
patch_session_factory, db_session, session_factory, stub_emit
):
diff --git a/self-hosted-cloudapi/tests/test_web_and_share.py b/self-hosted-cloudapi/tests/test_web_and_share.py
index f5a188b2c..d8808fa5f 100644
--- a/self-hosted-cloudapi/tests/test_web_and_share.py
+++ b/self-hosted-cloudapi/tests/test_web_and_share.py
@@ -307,6 +307,47 @@ async def test_app_lists_owned_tasks(client, db_session, session_factory):
assert "Build me a feature" in resp.text
+async def test_title_strips_environment_details_wrapper(client, db_session, session_factory):
+ """A first turn in Roo Code's API-prompt form (typed text wrapped in
+