Files
waggle-os/docs/redesign-warm-hive/pr4-recon/04-inline-in-chat.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

349 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PR4 Recon — Slice 4: Inline-in-Chat Install (Marketplace Variation B)
> **Scope:** Screen 09 Marketplace, **Variation B** — Waggle offers a *missing connector*
> mid-conversation with a vault-aware approval ("token goes to your vault"), then shows the
> connected follow-up. Map the chat step/block rendering, the approval/confirmation infra,
> and the vault-write path; identify the exact hook points a PR4 build would touch.
>
> **READ-ONLY recon.** No source modified. Citations are `file:line` against
> `feature/warm-hive-pr4` (working tree at recon time).
---
## 1. The design contract (what Variation B must do)
From `docs/design_handoff_waggle_app/SCREENS.md:199-207`:
- **Variation B (Inline in chat):** "the same picker surfaced mid-conversation — Waggle
offers a missing Salesforce connector with a vault-aware approval ('token goes to your
vault'), then shows the connected follow-up."
- **CRITICAL — shared install state ("sync"):** "one store powers the grid, the agent picks,
AND the inline card." Type-aware one-click flows with progress→done micro-states:
- **skill** Add→Adding…→Added (instant)
- **connector** Connect→Signing in…→Connected (~1.1s, **token→vault**)
- **MCP** Enable→Enabling…→Enabled
- Each fires a toast + updates the count bar. **Installing in any view reflects in all.**
PR4 BUILD-PLAN row (`docs/redesign-warm-hive/BUILD-PLAN.md:142`):
`PR4 | Marketplace + shared install store ("sync") (grid + agent-pick + inline-in-chat) | MarketplaceApp, new install store | 09`
So Variation B is **one of three consumers** of a single shared install store. This slice maps
the *chat-side* surface (how the agent raises the offer mid-turn, how the FE renders the inline
card, and how approval → connect → vault wires). The grid + store itself is the broader PR4 build.
---
## 2. End-to-end map of the chat turn (what exists today)
### 2.1 SSE event protocol (server → FE)
`POST /api/chat` (`packages/server/src/local/routes/chat.ts:351`) is the SSE endpoint. It hijacks
the reply (`chat.ts:462`) and writes events with `sendEvent(event, data)` (`chat.ts:475-477`):
```
event: <name>\ndata: <JSON>\n\n
```
Event names emitted today (the wire vocabulary the FE switches on):
`step`, `tool` / `tool_result` (auto_recall only), `token`, `gepa_choices`, `model_switch`,
`approval_required`, `done`, `error`. (Note: the agent loop itself emits `tool_start`/`tool_end`
for real tool calls — see the FE switch in §2.3 — via the agent-loop callbacks, not direct
`sendEvent` in chat.ts.)
The **approval handshake** is the load-bearing primitive for Variation B. It lives in a
per-request `pre:tool` hook registered at `chat.ts:909-1045`:
1. A tool reaches the gate; `needsConfirmationWithAutonomy(toolName, args, autonomyLevel)`
decides if it gates (`chat.ts:916`).
2. The hook enriches the request with **risk metadata**`install_capability` gets a
content-based `assessTrust()` (`chat.ts:960-982`), every other gated tool gets
`classifyGatedToolRisk()` (`chat.ts:989-999`). `description` comes from
`describeToolUse(toolName, input)` (`chat-helpers.ts:106`).
3. It emits `sendEvent('approval_required', { requestId, toolName, input, sourceWorkspaceId,
...trustMeta })` (`chat.ts:1005-1009`).
4. It **blocks** on `new Promise<boolean>` registered in `server.agentState.pendingApprovals`
keyed by `requestId` (`chat.ts:1020-1036`), with a 5-min auto-deny timeout.
5. The FE posts the decision to `POST /api/approval/:requestId`
(`packages/server/src/local/routes/approval.ts:10`), which calls `pending.resolve(approved)`
(`approval.ts:31`) → the hook returns `{ cancel: true }` on deny (`chat.ts:1041`) or lets the
tool run on approve.
This approval handshake is **exactly the mechanism Variation B needs** — a server-side pause
mid-turn that surfaces an FE card and resumes on the user's click.
### 2.2 Block model (the FE message content type)
`apps/web/src/lib/types.ts:484-532` — `ContentBlock` is a discriminated union of:
`TextContentBlock` | `StepContentBlock` | `ToolUseContentBlock` | `ModelSwitchContentBlock` |
`ErrorContentBlock`. There is **no approval/install block type** — approvals are kept *out* of
the block stream (see §2.4) and rendered as a separate singleton.
`StepContentBlock` (`types.ts:497-508`) already carries an **optional `provenance: { sources }`**
field (PR3.5) — the precedent for type-specific metadata riding on a step block.
### 2.3 SSE → block reduction (`useChat`)
`apps/web/src/hooks/useChat.ts:125-273` consumes the SSE stream and reduces events into
`blocks[]` on the last assistant message. The relevant cases:
- `step` → pushes a `StepContentBlock` (`useChat.ts:152-175`), copying `data.provenance.sources`.
- `tool_start` / `tool_end` → push/patch a `ToolUseContentBlock` (`useChat.ts:177-222`).
- `approval_request` / `approval_required` → `setPendingApproval(data as ApprovalRequest)` and
**`return msgs`** — i.e. deliberately does NOT add a block (`useChat.ts:258-263`).
- `approveAction(requestId, approved, { always })` → `adapter.respondApproval(...)` then
`setPendingApproval(null)` (`useChat.ts:317-333`).
So today there is **one** pending approval at a time, held in component state, not in the block
stream. The hook returns `{ messages, isLoading, sendMessage, clearHistory, pendingApproval,
approveAction }` (`useChat.ts:335`).
### 2.4 Block rendering
`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:53-104` walks `blocks[]`:
- consecutive `step` blocks are grouped into one collapsible **`ActivityStream`** card
(`BlockRenderer.tsx:30-51`) — the "magic" surface.
- `tool_use` blocks route to **`ArtifactBlock`** (completed file-writes — `ArtifactBlock.tsx:21-28`
is the routing predicate `isArtifactBlock`) or the generic `ToolUseBlock`.
- `text` / `model_switch` / `error` each have a renderer.
`ArtifactBlock.tsx` is the **closest existing precedent** for what an inline install card should be:
a tool-result that, when completed, renders as a rich actionable card (icon + name + button) instead
of a debug row. An inline-install card is the same pattern for a *connector/skill/MCP* tool result.
### 2.5 Approval rendering (the inline card that exists)
`ChatApp` renders the singleton approval **inside the message list** at the bottom:
`{pendingApproval && <ApprovalGate request={pendingApproval} onRespond={onApprove} />}`
(`ChatApp.tsx:1213-1215`).
**`ApprovalGate`** (`ChatApp.tsx:225-311`) is already an in-thread, warm-token card:
`--honey-wash` fill + attention border + `AlertTriangle` + a `RiskBadge` + `description` + a
mono `toolName inputSummary` line + `trustSource` + Approve / Always-allow / Not-now buttons +
a "Show details" JSON toggle. It is **generic** (driven by `ApprovalRequest`), not connector-aware.
There is also a **reusable warm primitive** `InlineApprovalCard`
(`apps/web/src/components/os/warm/InlineApprovalCard.tsx`, exported from `warm/index.ts:19`) — a
cleaner card that takes an `ApprovalRequest`, `title`, `onApprove`/`onDecline`/`onAlwaysAllow`,
and an `approveLabel`. Its **default title is literally "Approve before I leave your machine"** —
already in the vault-aware register Variation B wants. NOTE: `ApprovalGate` (the one actually
wired in ChatApp) and `InlineApprovalCard` are **two parallel implementations of the same idea**;
both share the risk vocabulary via `lib/risk-display.ts`. A build should pick one.
`ApprovalRequest` shape (`apps/web/src/lib/types.ts:410-430`): `{ requestId, toolName, description,
input, rawJson?, sourceWorkspaceId?, riskLevel?, approvalClass?, trustSource?, assessmentMode?,
explanation?, permissions? }`. All of these are already on the wire from `chat.ts:1005-1009`.
---
## 3. The install / connect / vault paths (what an "approve" must trigger)
There are **three different install backends today**, none of which is currently reachable as a
single agent-loop "offer a missing connector" tool:
### 3.1 Skill install — agent-loop reachable (the only one that is)
`packages/agent/src/skill-tools.ts:480-668` — the `install_capability` tool. It copies a curated
starter-skill `.md` into the skills dir (`skill-tools.ts:631-633`), runs a `SecurityGate` scan,
records install-audit rows, hot-reloads via `onSkillsChanged()`, and returns the skill content.
**No vault.** This is the "skill: Add→Adding…→Added (instant)" lane. It already flows through the
chat approval gate (it is in `ALWAYS_CONFIRM`, `confirmation.ts:19`) and gets the **richest** trust
metadata on the approval event (`chat.ts:960-982`).
### 3.2 Connector connect — HTTP route, NOT agent-loop reachable
`POST /api/connectors/:id/connect` (`packages/server/src/local/routes/connectors.ts:96-156`):
- validates the connector exists in `connectorRegistry`,
- requires `token` or `apiKey` in the body,
- **writes the token to vault**: `fastify.vault.setConnectorCredential(id, { type, value,
refreshToken, expiresAt, scopes })` (`connectors.ts:122-128`) — **this is the "token→vault"
step the design names**,
- re-initializes the connector (`connector.connect(fastify.vault)`, `connectors.ts:138`),
- records an install-audit row (`connectors.ts:145-153`).
FE adapter: `adapter.connectConnector(id, credentials)` → POSTs that route
(`apps/web/src/lib/adapter.ts:1971-1979`).
**There is no agent tool that calls this.** The agent can only *discover* connectors via
`find_connector` / `list_connector_categories` (`packages/agent/src/connector-search.ts:161-265`),
which return catalog JSON (name, installCmd, url) as text — the agent literally cannot connect one.
### 3.3 Connector OAuth — separate browser-redirect flow
`packages/server/src/local/routes/oauth.ts` — `GET /api/oauth/:provider/authorize` →
provider page → `GET /api/oauth/:provider/callback` stores tokens in vault under
`${provider}_oauth_token` (keyed by **provider**, not connector id — see
`connectors.ts:16-23` `OAUTH_PROVIDER_FOR_CONNECTOR`). Only 5 providers configured
(github/slack/google/notion/jira, `oauth.ts:26-62`) and each needs app `client_id`/`client_secret`
pre-seeded in vault. This is the heavyweight path; the design's "token goes to your vault" implies
the **lightweight token-paste** path of §3.2, not OAuth.
### 3.4 Connector tools become live on the NEXT turn
Important for "the connected follow-up": connector action tools are **dynamic**.
`connectorRegistry.generateTools()` generates `connector_<id>_<action>` tools **only for connected
connectors** (`packages/server/src/local/index.ts:1005-1006` comment + `1034`). The agent loop
rebuilds `effectiveTools` per request (`chat.ts:1048-1073` → `buildToolsForWorkspace`). So once a
connector is connected mid-conversation, its tools appear on the **next** user turn (or next loop
iteration if connect happens inside the same turn before tool-pool rebuild — but the rebuild is
per-`/api/chat` call, so realistically next turn). `describeToolUse` already formats
`connector_<id>_<action>` as "<action> via <id>" (`chat-helpers.ts:192-196`).
### 3.5 MCP enable — yet another backend
MCP servers install through the MCP Hub (`MCPHubApp.tsx`, security scan + scope + approval). Not
chat-loop reachable today. Out of the *critical* path for Variation B's "Salesforce connector"
example, but the shared store must cover the "MCP Enable→Enabling…→Enabled" lane.
---
## 4. The DELTA — what's missing vs the Screen-09 contract
| # | Contract requirement | Current state | Gap |
|---|---|---|---|
| D1 | Agent can **offer a missing connector mid-conversation** | Agent can only `find_connector` (returns catalog text); no tool connects one | **No `connect_capability`/`offer_connector` tool** that raises an approval whose approve-side writes a token to vault. Needs a new agent tool OR a server-side "offer" step. |
| D2 | Inline card is **vault-aware** ("token goes to your vault") | `ApprovalGate`/`InlineApprovalCard` are generic; show `toolName input` + risk | No connector-typed variant: no token-input field, no "encrypted in your local vault" copy, no Connect→Signing in…→Connected micro-states. |
| D3 | **Token→vault** on approve (~1.1s) | `POST /api/connectors/:id/connect` writes vault, but is reached only from MarketplaceApp/ConnectorsApp FE | The chat approve path resolves a `boolean` promise (`approval.ts:31`); it has **no channel to carry a token** nor to invoke `connectConnector`. The approval contract is boolean-only. |
| D4 | **Connected follow-up** shown in thread | Connector tools regenerate per request; agent can use them next turn | No explicit "connected" confirmation block; the follow-up is implicit. Needs a success block/toast + (optionally) auto-continue of the original ask. |
| D5 | **Shared install store ("sync")** — one store, all 3 views reflect | `MarketplaceApp` uses **local `useState`** (`installing`/`extensions`, `MarketplaceApp.tsx:108`, `:231`); `ConnectorsApp`/`MCPHubApp` each own their own state | **No shared store.** Installing in chat would not reflect in the grid or count bar. This is the central PR4 artifact ("new install store"). |
| D6 | Type-aware **micro-states** + toast + count-bar update | Skill install returns text; connector connect returns `{connected:true}`; toasts exist per-app (`useToast`) | No unified progress→done state machine keyed by kind; no count bar; no cross-view toast. |
| D7 | Approval can carry **kind** (skill/connector/MCP) | `approval_required` carries `toolName` + trust meta, but kind is inferred from toolName | A connector offer needs an explicit `kind: 'connector'` + connector `id`/`name`/`why` so the card renders type-aware. |
---
## 5. Exact integration points a PR4 build would touch
### Server (agent loop + routes)
- **New agent tool** (e.g. `offer_connector` / `connect_capability`) in a new file under
`packages/agent/src/` (sibling to `connector-search.ts`), registered into `baseTools`
(`packages/server/src/local/index.ts:759`). It should be **gated** (add to `ALWAYS_CONFIRM`,
`packages/agent/src/confirmation.ts:16-26`, or rely on `connector_` prefix patterns) so it
hits the chat approval hook.
- **`chat.ts:909-1045` pre:tool hook** — extend the trust-metadata branch (currently special-cases
`install_capability` at `chat.ts:960`) to emit a **connector-typed** `approval_required` payload
(`kind: 'connector'`, connector `id`/`name`, "why", and a flag that a token field is needed).
- **`describeToolUse`** (`chat-helpers.ts:106-201`) — add a case for the new tool so the card's
description line is specific ("Connect Salesforce — token stored in your local vault").
- **Approval contract widening** — `POST /api/approval/:requestId` (`approval.ts:10`) +
`pendingApprovals` resolve currently carry only `boolean`. To pass a pasted **token** from the
inline card to the connect step, EITHER:
(a) the card calls `adapter.connectConnector(id, { token })` directly (writing vault via
`connectors.ts:96`) and *then* approves the tool with a boolean (token never transits the
approval channel — cleanest, reuses existing vault route), OR
(b) widen the approval body to carry the token and have the new tool's `execute` call
`setConnectorCredential`. Option (a) is the lower-risk path and keeps the vault write on the
audited `/connect` route.
- **Vault write** stays `fastify.vault.setConnectorCredential(id, …)` (`connectors.ts:122`) — do
not build a parallel secret store (CLAUDE.md §7.1).
### FE (chat + store)
- **`useChat.ts`** — the `approval_required` case (`useChat.ts:258-263`) sets a singleton
`pendingApproval`. For Variation B the connector offer can stay on this channel (it IS an
approval), but the payload must carry `kind`/connector fields so the renderer can branch.
Alternatively introduce an **install/offer block type** in `ContentBlock` (`types.ts:484`) +
`BlockRenderer` (`BlockRenderer.tsx`) so the offer lives *in* the thread like `ArtifactBlock`,
surviving history reload — recommended for the "connected follow-up" persistence.
- **New `ConnectorOfferCard`** (or extend `InlineApprovalCard`, `warm/InlineApprovalCard.tsx`) —
type-aware card with a token field, "encrypted in your local vault" copy, and
Connect→Signing in…→Connected micro-states. Render it from `ChatApp` where `ApprovalGate` renders
today (`ChatApp.tsx:1213-1215`), branching on `pendingApproval.kind`.
- **Shared install store** (the PR4 centerpiece, `BUILD-PLAN.md:142` "new install store") — a
React context/zustand store keyed by capability id with `{ kind, state: idle|installing|done }`,
consumed by `MarketplaceApp` (replacing its local `useState` at `MarketplaceApp.tsx:108/231`),
`ConnectorsApp`, `MCPHubApp`, AND the chat offer card. The card's approve handler calls
`adapter.connectConnector` and updates the store → grid + count bar reflect instantly.
- **Adapter** — reuse `adapter.connectConnector(id, { token })` (`adapter.ts:1971`),
`adapter.respondApproval(requestId, true)` (`adapter.ts:1757`), `adapter.installMarketplacePackage`
(skills/packages), and the connector list `adapter.getConnectors()` (for the count bar).
### Recommended seam (lowest-risk wiring)
1. New gated agent tool `offer_connector(id, why)` → emits connector-typed `approval_required`
(no token in the tool args; the token is collected by the FE card).
2. FE renders `ConnectorOfferCard`; on Connect it (a) `adapter.connectConnector(id, { token })`
→ vault write on the audited route, (b) updates the shared install store (grid/count-bar sync),
(c) `adapter.respondApproval(requestId, true)` to release the agent.
3. The tool's `execute` returns "Connected — Salesforce tools now available"; the agent uses
`connector_<id>_<action>` tools on the **next** turn (already live after `generateTools()`).
4. A success block/toast renders the "connected follow-up".
---
## 6. Risks / sharp edges
- **Boolean-only approval channel.** The existing approval handshake resolves a `boolean`
(`approval.ts:31`). Threading a secret token through it would put a credential on the approval
wire — prefer the FE-calls-`/connect`-directly seam (Option 5a) so the token stays on the
dedicated vault route.
- **Two parallel inline-approval components** (`ApprovalGate` in `ChatApp.tsx:225` vs warm
`InlineApprovalCard`). Only `ApprovalGate` is wired. Building a third card risks a 3-way drift;
consolidate onto the warm primitive.
- **Approval is a singleton, out-of-band of blocks** (`useChat.ts:262` returns without pushing a
block). It does **not** survive history reload and there is only one at a time. If the offer must
persist in the transcript / show a permanent "connected" follow-up, it needs to become a real
`ContentBlock` (new type), which touches the block union, `useChat`, `BlockRenderer`, and history
serialization.
- **Connector tools are next-turn, not same-turn.** The "connected follow-up that actually uses the
connector" won't have the `connector_<id>_*` tool in-loop on the same turn the connect happened
(tool pool is built once per `/api/chat`, `chat.ts:1048`). A same-turn auto-continue would need an
explicit re-dispatch.
- **No shared store today** means a chat-side install silently diverges from the grid/count bar —
the single most load-bearing PR4 requirement ("installing in any view reflects in all"). The store
must land before any of the three surfaces is "done".
- **MCP + skill lanes differ from connector.** Skill install is agent-reachable + vault-free;
connector connect is vault-bound + HTTP-only; MCP is Hub-only. A unified "type-aware one-click"
card must dispatch to three different backends behind one store interface.
- **OAuth vs token-paste.** Some connectors (the Google family, jira, slack, github) are OAuth, not
token-paste (`oauth.ts:26-62`). The vault-aware token field only fits `bearer`/`apiKey` connectors;
OAuth connectors need the redirect flow, which cannot complete inside an inline card without a
popup/redirect. The card must branch on `connector.authType`.
---
## 7. Open questions for the founder/lead
1. **Approval channel for tokens** — keep the approval handshake boolean and have the FE call
`/connect` directly (audited vault route), or widen the approval body to carry the token?
(Recommend the former.)
2. **Offer as block vs singleton** — should the inline connector offer (and its "connected"
follow-up) be a persistent `ContentBlock` in the transcript, or stay the ephemeral singleton
approval? (Persistence implies a new block type + history serialization.)
3. **One inline card or two** — consolidate `ApprovalGate` and `InlineApprovalCard` into the
warm primitive before adding a connector variant?
4. **OAuth connectors** — for OAuth-only connectors (Google/Slack/etc.), does Variation B fall
back to "open the Connector Hub" or attempt an in-chat popup redirect? Token-paste only covers
`bearer`/`apiKey` connectors.
5. **Same-turn vs next-turn follow-up** — is "shows the connected follow-up" satisfied by a
success toast + the tool being available next turn, or must the agent auto-continue and use the
connector in the same turn (requires re-dispatch)?
6. **Shared store shape** — new dedicated store, or extend an existing provider? It must be the
single source the grid count bar, the agent-pick suggestion box, and this card all read/write.
7. **What raises the offer** — a new gated agent tool the model calls when it hits a capability
gap, or a server-side heuristic (e.g. tool-not-found → CapabilityRouter, `chat.ts:1080`) that
injects the offer? The `CapabilityRouter` already exists for tool-not-found handling and could
be the trigger.
---
## 8. Key files (quick index)
| Path | Role |
|---|---|
| `docs/design_handoff_waggle_app/SCREENS.md:189-207` | Screen-09 contract (Variation A/B + sync) |
| `packages/server/src/local/routes/chat.ts:909-1045` | pre:tool approval hook — emits `approval_required`, blocks on `pendingApprovals` |
| `packages/server/src/local/routes/chat.ts:475-477,1005-1009` | `sendEvent` + the approval payload shape |
| `packages/server/src/local/routes/approval.ts:10-35` | `POST /api/approval/:requestId` → resolves the boolean promise |
| `packages/server/src/local/routes/connectors.ts:96-156` | `POST /connect` — **token→vault** (`setConnectorCredential`) |
| `packages/server/src/local/routes/oauth.ts` | OAuth redirect flow (heavyweight, 5 providers) |
| `packages/agent/src/connector-search.ts:161-265` | `find_connector` / `list_connector_categories` (discovery only — no connect) |
| `packages/agent/src/skill-tools.ts:480-668` | `install_capability` — the only agent-loop install (skills, vault-free) |
| `packages/agent/src/confirmation.ts:16-26` | `ALWAYS_CONFIRM` gate set + `needsConfirmation` |
| `packages/server/src/local/index.ts:759,1005-1034` | tool-pool assembly; connector tools generated for **connected** connectors only |
| `packages/server/src/local/routes/chat-helpers.ts:106-201` | `describeToolUse` (approval description line) |
| `apps/web/src/hooks/useChat.ts:258-333` | FE: `approval_required` → singleton `pendingApproval`; `approveAction` |
| `apps/web/src/components/os/apps/ChatApp.tsx:225-311,1213-1215` | `ApprovalGate` inline card + its render slot |
| `apps/web/src/components/os/warm/InlineApprovalCard.tsx` | reusable warm inline-approval primitive ("Approve before I leave your machine") |
| `apps/web/src/components/os/apps/chat-blocks/ArtifactBlock.tsx` | precedent: tool-result → rich actionable in-thread card |
| `apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx` | block dispatch (where a new offer block would route) |
| `apps/web/src/lib/types.ts:410-430,484-532` | `ApprovalRequest` + `ContentBlock` union |
| `apps/web/src/lib/adapter.ts:1757,1971` | `respondApproval`, `connectConnector` |
| `apps/web/src/components/os/apps/MarketplaceApp.tsx:108,221-231` | grid install — **local `useState`** (no shared store) |