feat(#2): consume GET /sessions/{id}/tools — Tools-pane inventory hydrate

v1 coverage-audit Tier-2 quick win. The owner-scoped tool-inventory
endpoint (#183) had no caller; wire it into the TUI Tools pane.

- sessions.py: get_session_tools (GET /sessions/{id}/tools) — owner-
  scoped (consumer key, no admin scope), 200 -> parsed dict verbatim,
  non-200 -> SessionApiFailed. Mirrors get_persona_state / get_me.
- tui.py: _format_tool_inventory helper + _hydrate_session_tools
  best-effort worker (mirrors _hydrate_persona), wired unconditionally
  in on_mount. Writes the merged {agent_id, builtin_tools,
  bifrost_tools} inventory the LLM saw at turn-fire into the Tools
  pane + audits; never crashes on failure.
- Covers the design-brief 5 "Tools widget" via the reachable owner
  endpoint (the admin variant stays a gap only for cross-user debug).
- Contract #2 amended (FN) + validated. TDD: 3 wrapper tests + 1
  format-helper unit + 2 hydrate integration tests. Coverage: REST
  10/40. Suite 544 green; touched code ruff-clean.
This commit is contained in:
vh
2026-06-30 22:50:12 -07:00
parent 0205b81319
commit e62208d8e3
9 changed files with 229 additions and 13 deletions
+29
View File
@@ -251,3 +251,32 @@ TESTS:
happy [happy]: 200 {ephemeral_templates:{echo:{...}}} → dict returned verbatim
non_200_raises [error]: 500 → SessionApiFailed(status=500)
```
## Amendment 2026-07-01 — session tool introspection (v1 coverage-audit)
Owner-scoped tool-inventory read (spec #183, `GET /sessions/{id}/tools`),
surfaced in the TUI Tools pane on session-attach. Same shape as the other
introspection wrappers: GET, 200 → parsed dict verbatim, non-200 →
`SessionApiFailed`. Reachable with the consumer key (no admin scope), unlike the
admin variant `GET /admin/sessions/{id}/tools`.
```contract
FN get_session_tools(client: httpx.AsyncClient, session_id: str) -> dict[str, Any]
BRIEF: GET /sessions/{session_id}/tools — owner-scoped merged tool inventory (spec #183) the LLM saw at turn-fire: {agent_id, builtin_tools: [...], bifrost_tools: [{name, description, parameters}, ...]}. Owner gate (ctx.user_id == session.user_id); cross-owner → 404 session_not_found (existence-hiding), revoked → 401 auth_revoked. Parsed dict verbatim; any non-200 → SessionApiFailed.
PRE: [PRE-001 hard] client is not None -- assert client is not None
PRE: [PRE-002 hard] session_id is non-empty str -- assert session_id and isinstance(session_id, str)
POST: [POST-001 return_value] on 200 returns resp.json() unmodified -- assert result == resp.json()
ERROR_ROUTING:
HTTP non-200 (incl. 404 session_not_found cross-owner/unknown, 401 auth_revoked):
local_handling: raise SessionApiFailed(status=resp.status_code, body=resp.content)
flow_control: abort
state_recovery: none
STEPS:
1. [setup, prescriptive] assert PRE-001, PRE-002
2. [sequential, prescriptive] resp = await client.get(f"/sessions/{session_id}/tools")
3. [branch, prescriptive] IF resp.status_code == 200: RETURN resp.json(); ELSE RAISE SessionApiFailed
TESTS:
happy [happy,tracer]: 200 {agent_id, builtin_tools:[], bifrost_tools:[{name,...}]} → dict verbatim
cross_owner_404 [error]: 404 session_not_found → SessionApiFailed(status=404)
empty_session_id [adversarial]: "" → AssertionError; no HTTP issued
```
+13 -11
View File
@@ -48,7 +48,7 @@ resolved (§ Surface 1, scope-resolution table).
| Surface | Points | ✅ covered-live | ⬜ gap (in-scope) | 🚫 excluded-by-design |
|---|---|---|---|---|
| REST (OpenAPI 2.2.0, path groups) | 40 | 9 | 9 | 22 |
| REST (OpenAPI 2.2.0, path groups) | 40 | 10 | 8 | 22 |
| SSE events | 11 | 11 | 0 | 0 |
| Bifrost provider planes | 8 verbs | 8 | 0 | (10 gated verbs deferred) |
@@ -77,6 +77,7 @@ sub-gap).
| `DELETE /agents/{id}` | ✅ | `tier3.py:242` → `_run_delete` | Tier-3 hard-delete |
| `GET /me` | ✅ | `sessions.py:411` `get_me` → `cli.py` `--whoami` | identity/whoami probe; 401→SessionApiFailed |
| `GET /capabilities` | ✅ | `sessions.py:428` `get_capabilities` → `cli.py` `--whoami` | Echo ephemeral-template discovery |
| `GET /sessions/{id}/tools` | ✅ | `sessions.py:411` `get_session_tools` → `tui.py` `_hydrate_session_tools` | owner-scoped tool inventory in the TUI Tools pane (#183) |
**Sub-gaps inside ✅ path groups** (the method we use is live; a sibling method
on the same path is an unwired frontier item — see frontier Tier 1):
@@ -103,7 +104,6 @@ on the same path is an unwired frontier item — see frontier Tier 1):
| Endpoint | Status | Why in-scope |
|---|---|---|
| `GET /sessions/{id}/tools` | ⬜ | Tier-3 owner-scoped tool introspection (#183) — reachable with the **consumer key** (no admin scope), the unblocked tool-introspection path |
| `POST /sessions/{id}/persona_state` (write) | ⬜ | affect-injection is debug-relevant; pairs with our provider affect plane |
| `POST /characters` · `DELETE /characters/{id}` · `GET /characters/{id}/state` · `GET /models/available-for-characters` | ⬜ | transient-characters (Echo) is a session-creation **routing path** a debug client should be able to drive a turn through |
@@ -201,21 +201,23 @@ starts exercising them.
## Convergence frontier (the v1 to-do)
**Tier 1 — debug-observability core**, in dependency order:
**Tier 1 — debug-observability core:**
1. **Session picker + SSE-resume** — wrappers exist (`list_sessions`,
`reconnect_turn`), need presenter wiring only. **Cheapest; unblocked.**
2. **`GET /capabilities` + `GET /me`** — cheap debug primitives. Unblocked.
3. **BifrostState + Tools widgets** (`GET /admin/sessions/{id}/{bifrost,tools}`)
— design-brief'd v1, unbuilt. Admin-key-gated reads.
1. ✅ **DONE** — Session picker (`v0.18.7`) + SSE-resume (`v0.18.5`/`.6`).
2. ✅ **DONE** — `GET /capabilities` + `GET /me` (`v0.18.8`, `--whoami`).
3. **BifrostState + Tools widgets** (`GET /admin/sessions/{id}/bifrost` — the
admin session-bifrost inspection) — design-brief'd v1, unbuilt. Admin-key-gated.
*(The Tools half is effectively covered by the owner-scoped `GET /sessions/{id}/tools`
inventory, item 5 — the admin `/admin/sessions/{id}/tools` remains a gap only for
cross-user operator debug.)*
4. **#11 — AdminEvents pane** — **blocked** on an `admin.events.read` scope grant
(infra-ops). The single externally-blocked item; everything else can ship
without it.
**Tier 2 — rounds out coverage** (lower priority):
**Tier 2 — rounds out coverage:**
5. **`GET /sessions/{id}/tools`** — owner-scoped tool introspection; consumer-key
reachable (no admin scope), so unblocked.
5. ✅ **DONE** — `GET /sessions/{id}/tools` (`v0.18.9`, owner-scoped tool inventory
in the TUI Tools pane).
6. **Transient-characters routing** (4 endpoints) + **`POST /sessions/{id}/persona_state`**.
---