# CLAUDE.md — working on grok-glance grok-glance is the web control plane for grok's `/rc` remote control. Read [ARCHITECTURE.md](ARCHITECTURE.md) first — it explains *why* the pieces are shaped the way they are. This file is the operational half: how to build it, how to test it, and the handful of things that will waste an afternoon if you learn them the hard way. The Rust half lives in the sibling `newgrok/` tree as patches `0003`/`0004`. **The two are one feature.** A change to the wire format is a change to both. --- ## Layout ``` cmd/glance/main.go serve | apikey add·list·rm | bootstrap | version cmd/fakeagent/main.go a fake grok, for UI work without rebuilding Rust internal/ acp/ JSON-RPC framing + the ACP subset glance needs ~280 lines hub/ agents, browsers, ring buffer, interaction table the live system auth/ TOTP, bootstrap gate, signed cookies state/ ~/.grok/glance/state.json (0600) httpapi/ chi router, both WS upgrades, embedded SPA web/src/ App.tsx status gate: Setup / Login / Console pages/ Setup, Login, Sessions, Session components/ Transcript, ToolCall, PermissionDialog, PromptBox, TurnStatus lib/ api.ts (REST), ws.ts (GlanceSocket), acp.ts (frames → transcript) ``` If you are looking for where a decision lives: | Question | File | |---|---| | What does the browser send/receive? | `internal/hub/browser.go` | | What happens when two people click Allow? | `internal/hub/agent.go` → `Answer`, `retract` | | Which methods does glance refuse? | `internal/hub/agent.go` → `handleFrame` | | Is this frame part of the transcript? | `internal/acp/jsonrpc.go` → `IsTranscript` | | Why is `/setup` a 404? | `internal/auth/auth.go` → `BootstrapToken` | | How does a frame become a bubble? | `web/src/lib/acp.ts` → `applyFrame` | --- ## Build and run ```sh make build # frontend, then binary → bin/glance make server # binary only, keeping the last frontend build (fast server loop) make web # frontend only make check # go vet + go test + tsc --noEmit make dev # Go server + Vite with hot reload → http://localhost:5173 ``` `//go:embed all:dist` resolves at compile time, so **`go build` ships whatever `make web` last produced**. If a UI change does not appear in `bin/glance`, that is why. Use `make dev` for frontend work: Vite proxies `/api` to the Go server, which is what keeps the `__Host-glance` cookie working — it is `SameSite=Strict` and would never survive a cross-origin request. Hitting the Go port directly on 7717 with the Vite UI will look like a mysterious auth failure. ### First run ```sh make build bin/glance serve --addr 127.0.0.1:7717 --insecure-cookie # → prints a bootstrap URL; open it, scan the QR, enter one code bin/glance apikey add dev # → prints glance_sk_… once, plus a [remote_control] block for ~/.grok/config.toml ``` `--insecure-cookie` drops the `Secure` attribute so a plain-HTTP localhost session works. It is refused on a non-loopback address, deliberately. In production put glance behind a TLS-terminating proxy and leave the flag off. Lost the authenticator? Delete `~/.grok/glance/state.json` and start over. That is the whole recovery story, on purpose — see ARCHITECTURE.md. **`~/.grok/glance/` also contains `secret.key` and `hook.secret` that belong to something else entirely. Do not touch them.** --- ## Testing without rebuilding grok `cmd/fakeagent` dials the agent socket exactly as the real bridge does and plays a scripted turn: streamed text, a thought, a plan, a tool call, and a **real** `session/request_permission` that waits for a real answer. ```sh bin/glance serve --insecure-cookie & make fakeagent KEY=glance_sk_… # or: go run ./cmd/fakeagent --key … ``` Then in the browser: prompt it, watch it stream, answer the permission. Useful flags: - `--terminal-after 3s` — the "terminal" answers the permission first, so you can watch the browser's card retract by itself. This is the path a browser alone cannot exercise. - `--speed 1s` — slow the stream down to catch layout problems mid-turn. - Send the prompt and hit **Stop**: the fake agent honours `session/cancel` and finishes the turn with `stopReason: cancelled`. Run several at once with different `--title` to test the session list. It emits `turn_completed` on the **xAI rail** (`x.ai/session_notification`) on purpose: if the turn stops showing as finished in the UI, rail mirroring has regressed. That is the most likely thing to break silently after an upstream sync. What fakeagent does *not* prove is that the real bridge speaks this dialect. Only the end-to-end checklist does. --- ## Where the ACP types come from There is no Go SDK for ACP. `internal/acp` is hand-written against [`agentclientprotocol/agent-client-protocol`](https://github.com/agentclientprotocol/agent-client-protocol) (`schema/v1/schema.json`), and it is thin on purpose — glance correlates ids, recognises a dozen methods, and passes payloads through to the browser as `json.RawMessage`. **Do not "finish" it by modelling every update variant.** The stable `session/update` rail is a small closed set; the xAI rail has ~60 grok-internal variants that drift with every upstream sync. Typed structs for those would be a large amount of code whose only effect is to turn an upstream rename into a parse error that kills a live connection. The contract is: - **The stable rail carries correctness.** Turn state comes from `update.sessionUpdate`, read by `classifyUpdate` in `internal/hub/agent.go` — one field, both rails. - **The xAI rail is presentation.** Unrecognised variants are stored, forwarded, and skipped by the renderer. Never an error. - **`_meta` is forwarded byte-for-byte.** `eventId`, `promptId`, `chunkId` and `isReplay` are how a viewer dedups and orders; rewriting the envelope would break replay. To regenerate anything, clone the spec repo and read `schema/v1/schema.json`. There is no codegen step and adding one would be a mistake at this size. ### Response shapes are pinned to grok's Rust types, not to the spec The three interaction replies are built in `web/src/components/PermissionDialog.tsx`, and their exact shapes were read off grok's source, not guessed: | Interaction | Reply | |---|---| | `session/request_permission` | `{"outcome":{"outcome":"selected","optionId":"…"}}`, or `{"outcome":{"outcome":"cancelled"}}` | | `x.ai/exit_plan_mode` | `{"outcome":"approved"}`, `{"outcome":"cancelled","feedback":"…"}`, `{"outcome":"abandoned"}` | | `x.ai/ask_user_question` | `{"outcome":"accepted","answers":{"":["