Files
grok-glance/bin/glance
T
iceBear67andClaude Opus 5 d20eb9255c Drop the hook-template scheme and other dead weight
hooks/hooks.json was generated from a template by scripts/gen-hooks.mjs. Once every
hook became a command hook that reads hook.secret from $GLANCE_HOME itself, the
template had exactly two placeholders left: {{HOOK_TOKEN}}, which nothing had ever
substituted into anything, and {{APPROVAL_TIMEOUT_SECS}}. Generating a whole file to
compute one number is not a good trade, so the number is now fixed at 125s in the
committed hooks.json and the coupling is enforced in code instead: the daemon clamps
approval.timeoutMs to APPROVAL_MAX_WAIT_MS (90s), which keeps the script inside its
own hook timeout no matter what a hand-edited config.json says. Losing that clamp is
what would actually hurt — a killed script never runs its fail-open path.

Also removed:

- `glance sync-hooks`, `npm run build:hooks`, and hookSecret({create}). The daemon is
  the only thing that should ever mint the secret.
- The ?k= query-string carrier for the hook secret. It existed for hooks that cannot
  set headers; there are none, and a secret in a URL lands in logs and shell history.
- Snapshot.now and SessionView.startedAt, which were written on every snapshot and
  every persist and read by nobody.
- An unused crypto import.

Docs and the e2e suite follow. The suite's ~12 sync-hooks assertions become static
checks on the committed file, plus new ones that hooks.json, APPROVAL_HOOK_TIMEOUT_SECS
and APPROVAL_MAX_WAIT_MS still agree, and that ?k= is refused. 220 checks, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 07:04:50 +00:00

249 lines
7.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* grok-glance CLI.
*
* Privileged operations (minting enrollment codes, revoking devices) are authenticated with
* a local admin token read from $GLANCE_HOME/admin.token, not by "is this request from
* localhost". That distinction matters: `tailscale serve` proxies remote traffic to
* 127.0.0.1, so the daemon cannot tell a local caller from a tunnelled one by address alone.
*/
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import {
PLUGIN_ROOT,
baseUrl,
glanceHome,
isDaemonUp,
readConfig,
sleep,
} from "./glance-lib.mjs";
const SERVER_ENTRY = path.join(PLUGIN_ROOT, "dist", "server", "index.js");
const cfg = readConfig();
const cmd = process.argv[2] ?? "status";
const args = process.argv.slice(3);
function adminToken() {
try {
return fs.readFileSync(path.join(glanceHome(), "admin.token"), "utf8").trim();
} catch {
return null;
}
}
async function api(pathname, { method = "GET", body, admin = false } = {}) {
const headers = { "content-type": "application/json" };
if (admin) {
const token = adminToken();
if (!token) {
throw new Error("no admin token found - is the daemon running? try `glance up`");
}
headers["x-glance-admin"] = token;
}
const res = await fetch(`${baseUrl(cfg)}${pathname}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(10_000),
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = null;
}
if (!res.ok) throw new Error(data?.error ?? `${res.status} ${res.statusText}`);
return data;
}
/** "3 (2 working, 1 waiting on you)" — a raw count says nothing when you watch several agents. */
function sessionBreakdown(states) {
if (!states || typeof states !== "object") return "";
const order = [
["waiting", "waiting on you"],
["error", "error"],
["working", "working"],
["idle", "idle"],
["ended", "ended"],
];
const parts = order
.filter(([key]) => Number(states[key]) > 0)
.map(([key, label]) => `${states[key]} ${label}`);
return parts.length ? ` (${parts.join(", ")})` : "";
}
function requireBuild() {
if (!fs.existsSync(SERVER_ENTRY)) {
console.error(`grok-glance is not built yet.\n\n cd ${PLUGIN_ROOT}\n npm install && npm run build\n`);
process.exit(1);
}
}
async function ensureUp() {
if (await isDaemonUp(cfg)) return true;
requireBuild();
const home = glanceHome();
fs.mkdirSync(home, { recursive: true });
const logFd = fs.openSync(path.join(home, "daemon.log"), "a");
const child = spawn(process.execPath, [SERVER_ENTRY], {
detached: true,
stdio: ["ignore", logFd, logFd],
});
child.unref();
for (let i = 0; i < 40; i++) {
await sleep(200);
if (await isDaemonUp(cfg, 300)) return true;
}
console.error(`daemon did not come up; see ${path.join(home, "daemon.log")}`);
return false;
}
switch (cmd) {
case "serve":
case "start": {
requireBuild();
await import(SERVER_ENTRY);
break;
}
case "up": {
if (await ensureUp()) console.log(`grok-glance running on ${baseUrl(cfg)}`);
else process.exit(1);
break;
}
case "stop": {
if (!(await isDaemonUp(cfg))) {
console.log("not running");
break;
}
await api("/local/shutdown", { method: "POST", admin: true });
console.log("stopped");
break;
}
case "status": {
if (!(await isDaemonUp(cfg))) {
console.log(`grok-glance: not running (port ${cfg.port})`);
console.log("start it with: glance up");
break;
}
const s = await api("/local/status", { admin: true });
console.log(`grok-glance ${s.version} on ${baseUrl(cfg)}`);
console.log(` public origin : ${s.origin ?? "(not configured - see README)"}`);
console.log(` rp id : ${s.rpId ?? "(not configured)"}`);
console.log(` devices : ${s.devices}`);
console.log(` approval mode : ${s.approval.mode}`);
console.log(` watchers : ${s.watchers}`);
console.log(` sessions : ${s.sessions}${sessionBreakdown(s.sessionStates)}`);
console.log(` events kept : ${s.events}`);
if (s.hookAuthOk === false) {
console.log(
"\n ! hook auth mismatch: $GLANCE_HOME/hook.secret no longer matches what the daemon" +
"\n loaded, so events are being dropped. Restart it: glance stop && glance up",
);
}
if (s.devices === 0) console.log("\nNo device enrolled yet. Run: glance enroll");
break;
}
case "enroll": {
if (!(await ensureUp())) process.exit(1);
const out = await api("/local/enroll", { method: "POST", admin: true });
console.log("\n Open this on your phone:\n");
console.log(` ${out.url}\n`);
console.log(` Enrollment code: ${out.code}`);
console.log(` Valid for: ${Math.round(out.expiresInMs / 60000)} minutes (single use)\n`);
if (!out.originConfigured) {
console.log(" Note: no public origin configured yet, so the URL above is localhost.");
console.log(" Set one up first (see README), e.g.:\n");
console.log(" tailscale serve --bg 127.0.0.1:" + cfg.port);
console.log(" glance set-origin https://<your-box>.<tailnet>.ts.net\n");
}
break;
}
case "set-origin": {
const origin = args[0];
if (!origin) {
console.error("usage: glance set-origin https://your-box.tailnet.ts.net");
process.exit(1);
}
if (!(await ensureUp())) process.exit(1);
const out = await api("/local/origin", { method: "POST", admin: true, body: { origin } });
console.log(`origin : ${out.origin}`);
console.log(`rp id : ${out.rpId}`);
console.log("\nEnrolled devices are bound to the rp id. Changing it invalidates them.");
break;
}
case "devices": {
if (!(await isDaemonUp(cfg))) {
console.error("not running");
process.exit(1);
}
const out = await api("/local/devices", { admin: true });
if (!out.devices.length) {
console.log("no devices enrolled - run: glance enroll");
break;
}
for (const d of out.devices) {
console.log(`${d.id.slice(0, 16)}${d.label.padEnd(24)} added ${new Date(d.createdAt).toISOString().slice(0, 10)} last seen ${d.lastUsedAt ? new Date(d.lastUsedAt).toISOString().slice(0, 16).replace("T", " ") : "never"}`);
}
break;
}
case "revoke": {
if (!args[0]) {
console.error("usage: glance revoke <device-id-prefix>");
process.exit(1);
}
const out = await api("/local/devices/revoke", {
method: "POST",
admin: true,
body: { idPrefix: args[0] },
});
console.log(`revoked ${out.revoked} device(s)`);
break;
}
case "approval": {
const mode = args[0];
if (!["off", "risky", "all"].includes(mode)) {
console.error("usage: glance approval <off|risky|all>");
process.exit(1);
}
const out = await api("/local/approval", { method: "POST", admin: true, body: { mode } });
console.log(`approval mode: ${out.mode}`);
break;
}
case "logs": {
const file = path.join(glanceHome(), "daemon.log");
if (!fs.existsSync(file)) {
console.log("no log yet");
break;
}
process.stdout.write(fs.readFileSync(file, "utf8").split("\n").slice(-60).join("\n") + "\n");
break;
}
default:
console.log(`grok-glance - glance at Grok Build from your phone
glance up start the daemon in the background
glance serve run it in the foreground
glance stop stop it
glance status show what is running
glance enroll mint a one-time code to enrol a phone
glance set-origin <url> set the public https origin (and webauthn rp id)
glance devices list enrolled devices
glance revoke <id-prefix> revoke a device
glance approval <off|risky|all> remote approval policy
glance logs tail the daemon log
`);
}