Files
new-grok-glance/cmd/fakeagent/main.go
T
iceBear67andClaude Opus 5 051efe8fec grok-glance: web control plane for grok's /rc remote control
A single Go binary that grok dials out to over a WebSocket, and a HeroUI
web UI for driving the session it is attached to.

The roles are inverted relative to the terminal: over the /rc link grok
is the ACP Agent and glance is the Client. That makes glance a stock ACP
client and the web Stop button a real session/cancel rather than a
bespoke control message.

Both notification rails are mirrored. The stable session/update rail
carries correctness; x.ai/session_notification is presentation only and
degrades rather than erroring, because its ~60 variants are grok
internal and drift with every upstream sync. _meta is forwarded
byte for byte so viewers can dedup and order.

Permissions race: the terminal and any browser may answer, first
responder wins, and the loser's UI retracts by itself. All three
interaction methods go through that path, not just permissions.

Auth is TOTP only, with no accounts to have. A bootstrap token printed
at first start gates /setup, which is a 404 without it; state lives in
one 0600 JSON file and history in an in-memory ring, so there is no
database and no recovery story beyond deleting the file.

ARCHITECTURE.md covers the topology and the limits of that auth model;
CLAUDE.md covers building, the fakeagent loop, and the end-to-end
checklist that unit tests cannot replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 10:07:33 +00:00

435 lines
12 KiB
Go

// Command fakeagent impersonates grok's `/rc` bridge so the glance server and
// web UI can be developed without rebuilding grok.
//
// Rebuilding the Rust side is a multi-minute cargo run through a patch tree,
// which is a poor inner loop for "does this tool card wrap correctly". This
// dials the agent socket exactly as the real bridge does, answers `initialize`
// with session metadata, and runs a scripted turn on every prompt: streamed
// text on both notification rails, a thought, a plan, a tool call, and a real
// `session/request_permission` that waits for a genuine answer.
//
// glance apikey add dev # prints glance_sk_...
// go run ./cmd/fakeagent --key glance_sk_...
//
// It is a development aid, not a test fixture -- the tests in internal/hub have
// their own in-process fake. What this adds is a browser you can click.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/coder/websocket"
"github.com/user/grok-glance/internal/acp"
)
func main() {
url := flag.String("url", "ws://127.0.0.1:7717/api/acp/agent", "glance agent socket")
key := flag.String("key", os.Getenv("GLANCE_API_KEY"), "API key from `glance apikey add` (or $GLANCE_API_KEY)")
title := flag.String("title", "fake session", "session title shown in the UI")
model := flag.String("model", "grok-4-fake", "model name shown in the UI")
cwd := flag.String("cwd", mustCwd(), "working directory shown in the UI")
// Simulates the terminal answering a permission first, which is the one
// path a browser alone cannot exercise: the card must retract by itself.
terminalAfter := flag.Duration("terminal-after", 0, "answer permissions from the `terminal` after this delay (0 = never)")
speed := flag.Duration("speed", 220*time.Millisecond, "delay between streamed chunks")
flag.Parse()
if *key == "" {
fmt.Fprintln(os.Stderr, "fakeagent: --key is required (run `glance apikey add dev`)")
os.Exit(2)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
a := &agent{
out: make(chan acp.Frame, 64),
pending: make(map[string]chan acp.Frame),
meta: acp.SessionMeta{
SessionID: "fake-session-1",
CWD: *cwd,
Title: *title,
Model: *model,
Hostname: hostname(),
Version: "fakeagent",
},
terminalAfter: *terminalAfter,
speed: *speed,
}
if err := a.run(ctx, *url, *key); err != nil && !errors.Is(err, context.Canceled) {
log.Fatalf("fakeagent: %v", err)
}
}
type agent struct {
conn *websocket.Conn
out chan acp.Frame
mu sync.Mutex
nextID uint64
pending map[string]chan acp.Frame // our requests, awaiting glance's answer
cancel context.CancelFunc // cancels the turn in flight, if any
meta acp.SessionMeta
terminalAfter time.Duration
speed time.Duration
}
func (a *agent) run(ctx context.Context, url, key string) error {
conn, resp, err := websocket.Dial(ctx, url, &websocket.DialOptions{
HTTPHeader: http.Header{"Authorization": {"Bearer " + key}},
})
if err != nil {
if resp != nil && resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("glance rejected the API key (%s)", resp.Status)
}
return err
}
defer conn.CloseNow()
conn.SetReadLimit(8 << 20)
a.conn = conn
log.Printf("connected to %s as %q", url, a.meta.Label())
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go a.writeLoop(ctx)
for {
typ, data, err := conn.Read(ctx)
if err != nil {
return err
}
if typ != websocket.MessageText {
continue
}
var frame acp.Frame
if err := json.Unmarshal(data, &frame); err != nil {
log.Printf("undecodable frame: %v", err)
continue
}
a.handle(ctx, frame)
}
}
func (a *agent) writeLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case frame := <-a.out:
data, err := json.Marshal(frame)
if err != nil {
log.Printf("unencodable frame: %v", err)
continue
}
if err := a.conn.Write(ctx, websocket.MessageText, data); err != nil {
return
}
}
}
}
func (a *agent) handle(ctx context.Context, frame acp.Frame) {
switch frame.Kind() {
case acp.KindResponse:
a.mu.Lock()
ch := a.pending[string(frame.ID)]
delete(a.pending, string(frame.ID))
a.mu.Unlock()
if ch != nil {
ch <- frame
}
return
case acp.KindNotification:
if frame.Method == acp.MethodRCViewers {
var p acp.ViewersParams
_ = json.Unmarshal(frame.Params, &p)
log.Printf("viewers: %d", p.Count)
}
return
}
switch frame.Method {
case acp.MethodInitialize:
a.reply(frame.ID, acp.InitializeResult{
ProtocolVersion: 1,
Meta: &acp.InitializeMeta{
Session: a.meta,
RemoteControl: &acp.RemoteControlStatus{ReplayBuffer: 2048},
},
})
case acp.MethodSessionList:
a.reply(frame.ID, map[string]any{"sessions": []acp.SessionMeta{a.meta}})
case acp.MethodSessionPrompt:
var p acp.PromptParams
_ = json.Unmarshal(frame.Params, &p)
log.Printf("prompt: %q", p.Text)
go a.turn(ctx, frame.ID, p.Text)
case acp.MethodSessionCancel:
a.mu.Lock()
cancel := a.cancel
a.mu.Unlock()
if cancel != nil {
log.Print("cancelled by glance")
cancel()
}
a.reply(frame.ID, map[string]any{})
default:
a.send(acp.NewErrorResponse(frame.ID, acp.CodeMethodNotFound, "fakeagent: "+frame.Method))
}
}
// turn plays a scripted turn. The response to the prompt request is sent last,
// which is what the real bridge does: `session/prompt` does not return until the
// turn is over.
func (a *agent) turn(parent context.Context, promptID json.RawMessage, text string) {
ctx, cancel := context.WithCancel(parent)
a.mu.Lock()
if a.cancel != nil {
a.cancel() // a second prompt supersedes the turn in flight
}
a.cancel = cancel
a.mu.Unlock()
defer func() {
cancel()
a.mu.Lock()
a.cancel = nil
a.mu.Unlock()
}()
stopped := func() bool {
select {
case <-ctx.Done():
return true
case <-time.After(a.speed):
return false
}
}
a.update("user_message_chunk", map[string]any{"content": textBlock(text)})
a.update("agent_thought_chunk", map[string]any{"content": textBlock("Considering how to answer that.")})
if stopped() {
a.finish(promptID, "cancelled")
return
}
a.update("plan", map[string]any{"entries": []map[string]any{
{"content": "Read the file", "status": "in_progress", "priority": "high"},
{"content": "Report back", "status": "pending", "priority": "medium"},
}})
for _, chunk := range []string{"Sure — ", "let me look at ", "that file.\n\n"} {
if stopped() {
a.finish(promptID, "cancelled")
return
}
a.update("agent_message_chunk", map[string]any{"content": textBlock(chunk)})
}
const toolCallID = "call-1"
a.update("tool_call", map[string]any{
"toolCallId": toolCallID,
"title": "Read src/main.rs",
"kind": "read",
"status": "pending",
"rawInput": map[string]any{"path": "src/main.rs"},
})
granted, err := a.requestPermission(ctx, toolCallID)
if err != nil {
a.update("tool_call_update", map[string]any{"toolCallId": toolCallID, "status": "failed"})
a.finish(promptID, "cancelled")
return
}
if !granted {
a.update("tool_call_update", map[string]any{"toolCallId": toolCallID, "status": "failed"})
a.update("agent_message_chunk", map[string]any{"content": textBlock("Understood, leaving it alone.")})
a.turnCompleted()
a.finish(promptID, "end_turn")
return
}
a.update("tool_call_update", map[string]any{
"toolCallId": toolCallID,
"status": "completed",
"content": []map[string]any{
{"type": "content", "content": textBlock("fn main() {\n println!(\"hello\");\n}\n")},
},
})
if stopped() {
a.finish(promptID, "cancelled")
return
}
a.update("plan", map[string]any{"entries": []map[string]any{
{"content": "Read the file", "status": "completed", "priority": "high"},
{"content": "Report back", "status": "completed", "priority": "medium"},
}})
a.update("agent_message_chunk", map[string]any{"content": textBlock("It prints `hello`. Nothing else in there.")})
a.turnCompleted()
a.finish(promptID, "end_turn")
}
// requestPermission raises a real interaction and waits for a real answer, so
// the browser's dialog is exercised end to end rather than mocked.
func (a *agent) requestPermission(ctx context.Context, toolCallID string) (bool, error) {
id, reply := a.request(acp.MethodRequestPermission, map[string]any{
"sessionId": a.meta.SessionID,
"toolCall": map[string]any{
"toolCallId": toolCallID,
"title": "Read src/main.rs",
"kind": "read",
},
"options": []map[string]any{
{"optionId": "allow", "name": "Allow", "kind": "allow_once"},
{"optionId": "reject", "name": "Reject", "kind": "reject_once"},
},
})
// The terminal racing the browser for the same answer.
var terminal <-chan time.Time
if a.terminalAfter > 0 {
t := time.NewTimer(a.terminalAfter)
defer t.Stop()
terminal = t.C
}
select {
case <-ctx.Done():
return false, ctx.Err()
case <-terminal:
log.Print("terminal answered first; retracting the browser's dialog")
a.notify(acp.MethodRCInteractionCancelled, acp.InteractionCancelledParams{ID: id, ToolCallID: toolCallID})
a.mu.Lock()
delete(a.pending, fmt.Sprint(id))
a.mu.Unlock()
return true, nil
case frame := <-reply:
if frame.Error != nil {
// A decline: glance is not answering, so in the real bridge the
// terminal's dialog stays up. Here, the terminal shrugs and allows.
log.Printf("declined by glance (%s); falling back to the terminal", frame.Error.Message)
return true, nil
}
var out struct {
Outcome struct {
Outcome string `json:"outcome"`
OptionID string `json:"optionId"`
} `json:"outcome"`
}
_ = json.Unmarshal(frame.Result, &out)
log.Printf("answered: %s/%s", out.Outcome.Outcome, out.Outcome.OptionID)
return out.Outcome.OptionID != "reject" && out.Outcome.Outcome != "cancelled", nil
}
}
// turnCompleted is emitted on the xAI rail on purpose: if it shows up in the UI,
// both rails are being mirrored, which is the thing most likely to silently
// regress.
func (a *agent) turnCompleted() {
a.notify(acp.MethodXAINotification, map[string]any{
"sessionId": a.meta.SessionID,
"update": map[string]any{"sessionUpdate": "turn_completed", "stopReason": "end_turn"},
})
}
func (a *agent) finish(promptID json.RawMessage, stopReason string) {
a.reply(promptID, map[string]any{"stopReason": stopReason})
}
func (a *agent) update(kind string, fields map[string]any) {
update := map[string]any{"sessionUpdate": kind}
for k, v := range fields {
update[k] = v
}
a.notify(acp.MethodSessionUpdate, map[string]any{
"sessionId": a.meta.SessionID,
"update": update,
})
}
func (a *agent) notify(method string, params any) {
frame, err := acp.NewNotification(method, params)
if err != nil {
log.Printf("notify %s: %v", method, err)
return
}
a.send(frame)
}
func (a *agent) request(method string, params any) (uint64, <-chan acp.Frame) {
a.mu.Lock()
a.nextID++
id := a.nextID
ch := make(chan acp.Frame, 1)
a.pending[fmt.Sprint(id)] = ch
a.mu.Unlock()
frame, err := acp.NewRequest(id, method, params)
if err != nil {
log.Printf("request %s: %v", method, err)
return id, ch
}
a.send(frame)
return id, ch
}
func (a *agent) reply(id json.RawMessage, result any) {
frame, err := acp.NewResponse(id, result)
if err != nil {
log.Printf("reply: %v", err)
return
}
a.send(frame)
}
func (a *agent) send(frame acp.Frame) {
select {
case a.out <- frame:
default:
log.Print("send queue full, dropping frame")
}
}
func textBlock(text string) map[string]any {
return map[string]any{"type": "text", "text": text}
}
func mustCwd() string {
dir, err := os.Getwd()
if err != nil {
return "/"
}
return dir
}
func hostname() string {
name, err := os.Hostname()
if err != nil {
return "localhost"
}
return name
}