diff --git a/docs/contracts/issues/17.contract.md b/docs/contracts/issues/17.contract.md index 9fb16ca..1a5329a 100644 --- a/docs/contracts/issues/17.contract.md +++ b/docs/contracts/issues/17.contract.md @@ -1,7 +1,7 @@ --- contract_version: "2.1" target_module: "ratatoskr.sessions + ratatoskr.provider (+ cli/tui/web trigger surfaces)" -scope: "Issue #17 v1 — make the canary chat client self-drive AND observe its own Bifrost provider. Two parts. (1) BIND: `create_session` gains an optional single-plane Bifrost binding (`BifrostBinding{endpoint_url, scope}`) authenticated with a DISTINCT consumer Heimdall key; Worldtree runs the handshake synchronously at POST /sessions, so handshake failure is a session-create failure (502), surfaced on the create path. A plane selector (`memory`→:8391 / `affect`→:8390) + the consumer key thread through CLI / TUI / web; bound-state is visible. (2) OBSERVE: a structured op-feed in the provider, instrumented at the DISPATCH/ASGI layer (where the JWT ctx / session_id lives — bifrost passes ctx to upsert_many but NOT to search/get/delete, so the existing store-method stdout shim cannot see session_id), emitting JSONL {session_id, plane, op, req_summary, resp_summary, status, ts}. OPERATOR DECISIONS LOCKED: single-plane-per-session for v1 (composite endpoint fronting both planes is PARKED — vNext); op-feed with session-level correlation for v1 (turn-correlated debug-pane UI is PARKED — needs turn_id, TBD). Provider store scope semantics MUST NOT change (AND-parity with bifrost's reference store is a hard constraint). Direct in-session TDD; live-smoke against personal Worldtree is the load-bearing acceptance gate." +scope: "Issue #17 v1 — make the canary chat client self-drive AND observe its own Bifrost provider. Two parts. (1) BIND: `create_session` gains an optional single-plane Bifrost binding (`BifrostBinding{endpoint_url, scope}`) authenticated with a DISTINCT consumer Heimdall key; Worldtree runs the handshake synchronously at POST /sessions, so handshake failure is a session-create failure (502), surfaced on the create path. A plane selector (`memory`→:8391 / `affect`→:8390; `combined`→:8392 added post-#17 — the #18 composite, the web default) + the consumer key thread through CLI / TUI / web; bound-state is visible. (2) OBSERVE: a structured op-feed in the provider, instrumented at the DISPATCH/ASGI layer (where the JWT ctx / session_id lives — bifrost passes ctx to upsert_many but NOT to search/get/delete, so the existing store-method stdout shim cannot see session_id), emitting JSONL {session_id, plane, op, req_summary, resp_summary, status, ts}. OPERATOR DECISIONS LOCKED: single-plane-per-session for v1 (composite endpoint fronting both planes was PARKED at #17 — later shipped as #18 and surfaced in the web bind as the `combined` plane); op-feed with session-level correlation for v1 (turn-correlated debug-pane UI is PARKED — needs turn_id, TBD). Provider store scope semantics MUST NOT change (AND-parity with bifrost's reference store is a hard constraint). Direct in-session TDD; live-smoke against personal Worldtree is the load-bearing acceptance gate." depends_on: - "httpx" - "ratatoskr.sessions" @@ -92,8 +92,8 @@ async def create_session( def endpoint_for_plane(plane: str, base_host: str) -> str: - """'memory'->:8391, 'affect'->:8390 → f'http://{base_host}:{port}'. The - Worldtree-visible base URL. See FN endpoint_for_plane.""" + """'memory'->:8391, 'affect'->:8390, 'combined'->:8392 (#18 composite) → + f'http://{base_host}:{port}'. The Worldtree-visible base URL. See FN endpoint_for_plane.""" ``` ```python @@ -218,12 +218,12 @@ STEPS: ```contract FN endpoint_for_plane(plane: str, base_host: str) -> str -BRIEF: Map a plane name to the Worldtree-visible provider base URL (memory->:8391, affect->:8390). +BRIEF: Map a plane name to the Worldtree-visible provider base URL (memory->:8391, affect->:8390, combined->:8392 — the #18 composite both-plane endpoint, surfaced post-#17). -PRE: [PRE-001 hard] plane in {"memory", "affect"} -- else ValueError -POST: [POST-001 return_value] returns f"http://{base_host}:{port}", port 8391 (memory) / 8390 (affect) -- assert +PRE: [PRE-001 hard] plane in {"memory", "affect", "combined"} -- else ValueError +POST: [POST-001 return_value] returns f"http://{base_host}:{port}", port 8391 (memory) / 8390 (affect) / 8392 (combined) -- assert STEPS: - 1. port = 8391 if plane == "memory" else 8390 + 1. port = {"memory": 8391, "affect": 8390, "combined": 8392}[plane] 2. return the Worldtree-VISIBLE base URL (not client loopback); HTTPS relaxation is allowlist-side, not a URL concern ``` diff --git a/pyproject.toml b/pyproject.toml index 070f191..b89c13e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.18.0" +version = "0.18.1" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" diff --git a/src/ratatoskr/sessions.py b/src/ratatoskr/sessions.py index 83c3945..0d626ec 100644 --- a/src/ratatoskr/sessions.py +++ b/src/ratatoskr/sessions.py @@ -227,19 +227,21 @@ async def list_sessions( def endpoint_for_plane(plane: str, base_host: str) -> str: """Map a provider plane name to its Worldtree-VISIBLE base URL. - Issue #17 dev helper: `memory` → :8391, `affect` → :8390. Returns the - Worldtree-visible base (e.g. `http://10.100.10.50:8391`), NOT the client's - loopback — Worldtree must reach the provider over the network. `http://` is - deliberate: the HTTPS relaxation is allowlist-side (Worldtree's - BIFROST_CLIENT_ALLOWED_HOSTS), not a URL concern. A production HTTPS endpoint - is supplied directly, bypassing this helper. + Issue #17 dev helper: `memory` → :8391, `affect` → :8390, `combined` → :8392 + (the #18 composite both-plane endpoint). Returns the Worldtree-visible base + (e.g. `http://10.100.10.50:8391`), NOT the client's loopback — Worldtree must + reach the provider over the network. `http://` is deliberate: the HTTPS + relaxation is allowlist-side (Worldtree's BIFROST_CLIENT_ALLOWED_HOSTS), not a + URL concern. A production HTTPS endpoint is supplied directly, bypassing this + helper. """ - if plane not in ("memory", "affect"): + ports = {"memory": 8391, "affect": 8390, "combined": 8392} + if plane not in ports: raise ValueError( - f"unknown plane: {plane!r} (expected 'memory' or 'affect')" + f"unknown plane: {plane!r} " + "(expected 'memory', 'affect', or 'combined')" ) - port = 8391 if plane == "memory" else 8390 - return f"http://{base_host}:{port}" + return f"http://{base_host}:{ports[plane]}" def _bifrost_error_from(resp: httpx.Response) -> str | None: diff --git a/src/ratatoskr/web/server.py b/src/ratatoskr/web/server.py index 3668d07..850d3ac 100644 --- a/src/ratatoskr/web/server.py +++ b/src/ratatoskr/web/server.py @@ -138,7 +138,7 @@ async def _create_session_endpoint(request: Request) -> JSONResponse: consumer_key = request.app.state.bifrost_consumer_key visible_host = request.app.state.bifrost_visible_host if bifrost_plane: - if bifrost_plane not in ("memory", "affect"): + if bifrost_plane not in ("memory", "affect", "combined"): return JSONResponse( {"error_code": "invalid_bifrost_plane"}, status_code=400 ) diff --git a/src/ratatoskr/web/static/index.html b/src/ratatoskr/web/static/index.html index 8c881ce..5be3a22 100644 --- a/src/ratatoskr/web/static/index.html +++ b/src/ratatoskr/web/static/index.html @@ -513,6 +513,7 @@ body {