/hook/record and /hook/approve accepted anything that reached the port. That is not "loopback only": `tailscale serve` proxies tailnet traffic to 127.0.0.1, so anyone who could reach the tunnel could forge timeline events and answer approval prompts. Both endpoints now require a 32-byte secret from $GLANCE_HOME/hook.secret (0600, created once, never rotated so nothing in flight is 403'd mid-session), compared in constant time before the body is read, as an x-glance-hook header or a ?k= parameter. Requests carrying x-forwarded-* are refused outright: a local hook process never sends them and a tunnelled caller always does. The check applies to /hook/* only, so the dashboard is unaffected. While wiring that up: the 13 passive `type: "http"` hooks could never have worked. Grok Build's http runner rejects every scheme but https, then resolves the host and blocks private/link-local/CGNAT addresses (validate_hook_url + is_blocked_ip), so neither loopback-over-http nor *.ts.net (100.64/10) can be a hook target - and it sends no header but Content-Type, so such a hook could not authenticate anyway. They were failing validation silently on every event. All of them are now command hooks running bin/glance-record.mjs, which costs a Node start and can present the secret. hooks.json is generated from hooks/hooks.template.json by scripts/gen-hooks.mjs (npm run build, glance sync-hooks). It creates the secret, derives the approval hook's timeout from approval.timeoutMs instead of hand-copying 125, and refuses to write a hook that cannot fire: bad type, non-positive timeout, non-https http URL, missing bin/ script, or a leftover placeholder. A template that embeds the token makes the output 0600 with a warning. Fail-open is unchanged: a missing, stale or rejected secret degrades to "no telemetry", and glance-approve.mjs still allows on every error path. glance status warns when the on-disk secret no longer matches the daemon's. Validated with the e2e suite (190 checks, including no-token/wrong-token/ same-length-token 403s, ?k= acceptance, x-forwarded-* refusal, and the recorder's fail-open paths) and a clean npm run build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.9 KiB
Markdown
87 lines
3.9 KiB
Markdown
---
|
|
name: glance
|
|
description: Set up, inspect, or control grok-glance — the passkey-guarded phone dashboard for this Grok Build session. Use when the user asks to watch a session from their phone, enrol a device, expose the dashboard over Tailscale, or turn remote approve/deny on or off.
|
|
---
|
|
|
|
# grok-glance
|
|
|
|
A local daemon plus web dashboard that shows what Grok Build is doing, readable from a phone
|
|
behind a WebAuthn passkey. It can also pause risky tool calls until someone taps approve.
|
|
|
|
The daemon is started automatically by the `SessionStart` hook. Everything below is done through
|
|
the `glance` CLI at `$GROK_PLUGIN_ROOT/bin/glance`.
|
|
|
|
## First check whether it is even built
|
|
|
|
The plugin ships as TypeScript and must be built once:
|
|
|
|
```sh
|
|
cd "$GROK_PLUGIN_ROOT" && npm install && npm run build
|
|
```
|
|
|
|
`glance status` prints a "not built" error with this same instruction if it is missing. Do not
|
|
attempt to skip the build — the daemon entry point is `dist/server/index.js`.
|
|
|
|
## The commands
|
|
|
|
```sh
|
|
glance status # is it running, which origin, how many devices
|
|
glance up # start the daemon in the background
|
|
glance stop # stop it
|
|
glance logs # last 60 lines of the daemon log
|
|
glance enroll # mint a one-time code + URL for a new phone
|
|
glance set-origin <https-url> # set the public origin and WebAuthn RP ID
|
|
glance devices # list enrolled devices
|
|
glance revoke <id-prefix> # revoke one
|
|
glance approval <off|risky|all> # remote approve/deny policy
|
|
glance sync-hooks # regenerate hooks/hooks.json from hooks/hooks.template.json
|
|
```
|
|
|
|
## Getting it onto a phone
|
|
|
|
The dashboard listens on `127.0.0.1` only. Passkeys need a real hostname with valid TLS — a bare
|
|
IP can never be a WebAuthn RP ID — so the supported path is Tailscale Serve:
|
|
|
|
```sh
|
|
tailscale serve --bg 127.0.0.1:8791
|
|
tailscale serve status # read the https://<box>.<tailnet>.ts.net URL
|
|
glance set-origin https://<box>.<tailnet>.ts.net
|
|
glance enroll
|
|
```
|
|
|
|
Then open the printed URL on the phone, type the code, and create the passkey. The phone must be
|
|
on the same tailnet.
|
|
|
|
Changing the origin changes the RP ID, which invalidates existing passkeys. Say so before running
|
|
`set-origin` on a working setup.
|
|
|
|
## Remote approve/deny
|
|
|
|
`glance approval risky` makes `Bash`, `Write`, `Edit`, `MultiEdit` and `NotebookEdit` calls pause
|
|
and wait for a tap on the phone. Defaults that matter:
|
|
|
|
- Nothing waits unless a phone is actually watching the dashboard (`requireWatcher`).
|
|
- If nobody answers within 90s the call is **allowed**, not denied. Flip that on the phone's
|
|
settings panel if you want the opposite.
|
|
- Every failure path is fail-open: daemon down, timeout, bad JSON, a rejected hook secret — the tool
|
|
call proceeds. This is a convenience gate, not a security boundary.
|
|
|
|
`glance approval off` (the default) means Grok Build never blocks on the phone.
|
|
|
|
## When something does not work
|
|
|
|
- **"not running"** → `glance up`, then `glance logs`.
|
|
- **Passkey prompt fails with a security error** → the phone is on a hostname the RP ID does not
|
|
cover. Compare `glance status`'s `rp id` with the hostname in the phone's address bar.
|
|
- **Dashboard loads but shows nothing** → hooks are not firing. `glance status` warns if the hook
|
|
secret in `$GLANCE_HOME/hook.secret` no longer matches the one the daemon loaded (events are being
|
|
dropped with a 403); `glance stop && glance up` fixes that. Otherwise check that
|
|
`hooks/hooks.json` exists and is registered — it is generated, so `glance sync-hooks` rebuilds it
|
|
from the template.
|
|
- **Page says "run npm install && npm run build"** → the web bundle is missing; build it.
|
|
|
|
## What it deliberately does not do
|
|
|
|
Read-only plus approve/deny. It cannot send prompts, edit files, run tools, or resume a session.
|
|
Do not tell the user otherwise.
|