Compare commits
1
Commits
b586323fdb
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966133eeda |
@@ -53,8 +53,9 @@ git clone <this repo> ~/.grok/plugins/grok-glance
|
|||||||
```
|
```
|
||||||
|
|
||||||
That is the whole thing — no `npm install`, no build. Open `/plugins` in Grok Build and enable
|
That is the whole thing — no `npm install`, no build. Open `/plugins` in Grok Build and enable
|
||||||
**grok-glance**. On the next session start its `SessionStart` hook boots the daemon in the
|
**grok-glance**. The first hook to fire after that — your next prompt, or the first tool call —
|
||||||
background, and the dashboard is on `http://127.0.0.1:8791`.
|
starts the daemon in the background, and the dashboard is on `http://127.0.0.1:8791`. (Not the
|
||||||
|
`SessionStart` hook, which for a plugin never runs; see [Hook wiring](#hook-wiring).)
|
||||||
|
|
||||||
### …or from a marketplace, by URL
|
### …or from a marketplace, by URL
|
||||||
|
|
||||||
@@ -244,6 +245,7 @@ Everything lives in `~/.grok/glance` (mode 0700), or `$GLANCE_HOME` if you set i
|
|||||||
| `events.jsonl` | Append-only event log, one JSON object per line, rotated at 5 MB |
|
| `events.jsonl` | Append-only event log, one JSON object per line, rotated at 5 MB |
|
||||||
| `sessions.json` | The agent roster — label, badge, workspace, state, counts — so a restart comes back with the overview intact. Written debounced, flushed on shutdown; sessions older than 12 hours are dropped on load. |
|
| `sessions.json` | The agent roster — label, badge, workspace, state, counts — so a restart comes back with the overview intact. Written debounced, flushed on shutdown; sessions older than 12 hours are dropped on load. |
|
||||||
| `daemon.log` | Daemon stdout/stderr |
|
| `daemon.log` | Daemon stdout/stderr |
|
||||||
|
| `daemon.lock` | Held while a hook script is starting the daemon, so a burst of events starts one and not five. Created with `O_EXCL`, deleted on the way out, and ignored by anyone else once 15s stale. |
|
||||||
|
|
||||||
Three environment variables override `config.json`, which is mostly useful for testing a second
|
Three environment variables override `config.json`, which is mostly useful for testing a second
|
||||||
instance without touching your real one:
|
instance without touching your real one:
|
||||||
@@ -343,7 +345,8 @@ traverses the tunnel, and it can present the shared secret — hook traffic goes
|
|||||||
`http://127.0.0.1:8791` and never leaves the machine. So each observed event runs
|
`http://127.0.0.1:8791` and never leaves the machine. So each observed event runs
|
||||||
`bin/glance-record.mjs`, which costs a Node start (~40 ms) and POSTs one event. Two entries differ:
|
`bin/glance-record.mjs`, which costs a Node start (~40 ms) and POSTs one event. Two entries differ:
|
||||||
|
|
||||||
- `SessionStart` runs `bin/glance-up.mjs`, which is what boots the daemon.
|
- `SessionStart` runs `bin/glance-up.mjs`. It is the obvious hook to boot the daemon from, and it
|
||||||
|
never runs — see below.
|
||||||
- `PreToolUse` is wired **twice** — a recording entry for the timeline, and a second entry matching
|
- `PreToolUse` is wired **twice** — a recording entry for the timeline, and a second entry matching
|
||||||
only `^(Bash|Write|Edit|MultiEdit|NotebookEdit)$` that runs `bin/glance-approve.mjs`. PreToolUse
|
only `^(Bash|Write|Edit|MultiEdit|NotebookEdit)$` that runs `bin/glance-approve.mjs`. PreToolUse
|
||||||
is the only blocking event, and a command hook is the only documented way to return a deny
|
is the only blocking event, and a command hook is the only documented way to return a deny
|
||||||
@@ -354,6 +357,30 @@ traverses the tunnel, and it can present the shared secret — hook traffic goes
|
|||||||
The hook scripts use nothing but the Node standard library and always exit 0 unless they are
|
The hook scripts use nothing but the Node standard library and always exit 0 unless they are
|
||||||
deliberately denying — including when the daemon rejects their token.
|
deliberately denying — including when the daemon rejects their token.
|
||||||
|
|
||||||
|
### Why every recorder boots the daemon
|
||||||
|
|
||||||
|
A plugin gets no usable boot event, so `bin/glance-record.mjs` starts the daemon itself when it
|
||||||
|
finds it missing, and whichever event fires first wins.
|
||||||
|
|
||||||
|
`SessionStart` looks like the right answer and cannot work. Grok Build dispatches it from inside
|
||||||
|
session creation (`xai-grok-shell`, `agent_ops.rs` → `SessionCommand::DispatchSessionStartHook`),
|
||||||
|
and the dispatch resolves against the session's hook registry **as it stands at that moment**. That
|
||||||
|
registry comes from `discover_hooks()`, whose sources are the config layers and the global/project
|
||||||
|
settings files — `~/.grok/settings.json`, `<git_root>/.grok/hooks`, the vendor-compat paths. Plugin
|
||||||
|
directories are not among them. Plugin hooks are appended separately, under a `plugin/` prefix, by
|
||||||
|
`reload_hooks_impl` and `reload_plugins_impl` — both of which run later, in response to a plugin
|
||||||
|
action, a `/hooks reload`, or a folder-trust grant. So a plugin's `SessionStart` entry is always
|
||||||
|
registered after `SessionStart` has already been dispatched, and is never called. Every other event
|
||||||
|
this plugin subscribes to happens later in the session, once the plugin registry has landed, which
|
||||||
|
is why they all work.
|
||||||
|
|
||||||
|
The symptom, if you hit this from the other end: no `daemon.log` at all, nothing on `:8791`, and a
|
||||||
|
manual `glance up` working perfectly.
|
||||||
|
|
||||||
|
Asking costs one loopback request to `/healthz` per event, which is the steady state once the
|
||||||
|
daemon is up. The spawn path is taken once. A `daemon.lock` (`O_EXCL`, 15s staleness) keeps a burst
|
||||||
|
of concurrent events from starting five daemons and leaving four of them to die on `EADDRINUSE`.
|
||||||
|
|
||||||
## Deliberately omitted
|
## Deliberately omitted
|
||||||
|
|
||||||
Not oversights — decisions:
|
Not oversights — decisions:
|
||||||
|
|||||||
+5
-17
@@ -10,17 +10,16 @@
|
|||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import {
|
import {
|
||||||
PLUGIN_ROOT,
|
PLUGIN_ROOT,
|
||||||
|
SERVER_ENTRY,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
ensureDaemon,
|
||||||
glanceHome,
|
glanceHome,
|
||||||
isDaemonUp,
|
isDaemonUp,
|
||||||
readConfig,
|
readConfig,
|
||||||
sleep,
|
|
||||||
} from "./glance-lib.mjs";
|
} from "./glance-lib.mjs";
|
||||||
|
|
||||||
const SERVER_ENTRY = path.join(PLUGIN_ROOT, "dist", "server", "index.js");
|
|
||||||
const cfg = readConfig();
|
const cfg = readConfig();
|
||||||
const cmd = process.argv[2] ?? "status";
|
const cmd = process.argv[2] ?? "status";
|
||||||
const args = process.argv.slice(3);
|
const args = process.argv.slice(3);
|
||||||
@@ -87,21 +86,10 @@ function requireBuild() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureUp() {
|
async function ensureUp() {
|
||||||
if (await isDaemonUp(cfg)) return true;
|
|
||||||
requireBuild();
|
requireBuild();
|
||||||
const home = glanceHome();
|
// No hook timeout to fit inside here, so wait long enough that a slow cold start still counts.
|
||||||
fs.mkdirSync(home, { recursive: true });
|
if (await ensureDaemon(cfg, { waitMs: 8000, startedBy: "cli" })) return true;
|
||||||
const logFd = fs.openSync(path.join(home, "daemon.log"), "a");
|
console.error(`daemon did not come up; see ${path.join(glanceHome(), "daemon.log")}`);
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
export const PLUGIN_ROOT = path.resolve(fileURLToPath(import.meta.url), "../..");
|
export const PLUGIN_ROOT = path.resolve(fileURLToPath(import.meta.url), "../..");
|
||||||
|
|
||||||
|
export const SERVER_ENTRY = path.join(PLUGIN_ROOT, "dist", "server", "index.js");
|
||||||
|
|
||||||
export const DEFAULT_PORT = 8791;
|
export const DEFAULT_PORT = 8791;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,3 +143,107 @@ export async function isDaemonUp(cfg = readConfig(), timeoutMs = 400) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ daemon start */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long a spawn lock is believed before another hook takes it over. Long enough to cover a
|
||||||
|
* cold Node start and a bind, short enough that a script killed mid-spawn cannot wedge startup
|
||||||
|
* for the rest of the session.
|
||||||
|
*/
|
||||||
|
const SPAWN_LOCK_STALE_MS = 15_000;
|
||||||
|
|
||||||
|
/** Never hold the lock for less than this, so a fire-and-forget caller still covers the bind. */
|
||||||
|
const MIN_SPAWN_WAIT_MS = 600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim the right to spawn the daemon, so a burst of hooks does not start a race in which every
|
||||||
|
* loser dies on EADDRINUSE and litters daemon.log.
|
||||||
|
*
|
||||||
|
* `wx` is the whole mechanism: an atomic create-or-fail. A lock that is already there and still
|
||||||
|
* fresh means another script is mid-spawn, and we wait for its daemon rather than starting a
|
||||||
|
* second one. Taking over a *stale* lock is deliberately not atomic — two scripts could both
|
||||||
|
* decide it is stale and both spawn — because the consequence is only the EADDRINUSE we had
|
||||||
|
* before, and it takes a 15s-dead lock to get there at all.
|
||||||
|
*/
|
||||||
|
function acquireSpawnLock(home) {
|
||||||
|
const file = path.join(home, "daemon.lock");
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(file, String(process.pid), { flag: "wx", mode: 0o600 });
|
||||||
|
return file;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
if (Date.now() - fs.statSync(file).mtimeMs < SPAWN_LOCK_STALE_MS) return null;
|
||||||
|
fs.writeFileSync(file, String(process.pid), { mode: 0o600 });
|
||||||
|
return file;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilUp(cfg, budgetMs) {
|
||||||
|
const deadline = Date.now() + budgetMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await sleep(100);
|
||||||
|
const left = deadline - Date.now();
|
||||||
|
if (left <= 0) break;
|
||||||
|
if (await isDaemonUp(cfg, Math.min(300, Math.max(50, left)))) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the daemon is listening, starting it if it is not. Never throws.
|
||||||
|
*
|
||||||
|
* Every recording hook calls this, not just one designated boot hook, because Grok Build gives
|
||||||
|
* a plugin no usable boot event. `SessionStart` is dispatched from inside session creation
|
||||||
|
* (xai-grok-shell agent_ops.rs, `DispatchSessionStartHook`) and dispatch reads the session's
|
||||||
|
* hook registry as it stands at that moment. That registry is built by `discover_hooks()`,
|
||||||
|
* whose sources are the config layers and the global/project settings files — plugin hooks are
|
||||||
|
* not among them. They are appended later, with a `plugin/` prefix, only by `reload_hooks_impl`
|
||||||
|
* and `reload_plugins_impl`. So a plugin's `SessionStart` entry is registered strictly after
|
||||||
|
* `SessionStart` has already fired, and never runs. Every other event we subscribe to happens
|
||||||
|
* later in the session, once the plugin registry has landed — so whichever of them fires first
|
||||||
|
* is the one that has to boot us.
|
||||||
|
*
|
||||||
|
* The cost of asking is one loopback request to /healthz once the daemon is up, which is the
|
||||||
|
* steady state; the spawn path is taken once per machine boot.
|
||||||
|
*/
|
||||||
|
export async function ensureDaemon(cfg = readConfig(), options = {}) {
|
||||||
|
const { waitMs = 8000, startedBy = "hook", onMissingBuild } = options;
|
||||||
|
try {
|
||||||
|
if (await isDaemonUp(cfg)) return true;
|
||||||
|
if (!fs.existsSync(SERVER_ENTRY)) {
|
||||||
|
onMissingBuild?.(SERVER_ENTRY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const home = glanceHome();
|
||||||
|
fs.mkdirSync(home, { recursive: true });
|
||||||
|
|
||||||
|
const budget = Math.max(waitMs, MIN_SPAWN_WAIT_MS);
|
||||||
|
const lock = acquireSpawnLock(home);
|
||||||
|
// Someone else is already starting it: wait on theirs instead of racing it.
|
||||||
|
if (!lock) return await waitUntilUp(cfg, budget);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logFd = fs.openSync(path.join(home, "daemon.log"), "a");
|
||||||
|
const child = spawn(process.execPath, [SERVER_ENTRY], {
|
||||||
|
detached: true,
|
||||||
|
stdio: ["ignore", logFd, logFd],
|
||||||
|
env: { ...process.env, GLANCE_STARTED_BY: startedBy },
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
return await waitUntilUp(cfg, budget);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(lock);
|
||||||
|
} catch {
|
||||||
|
/* best effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// A dashboard that cannot start must still not be the reason a tool call fails.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-2
@@ -11,11 +11,16 @@
|
|||||||
* A command hook costs a Node start (~40ms) per event, and buys back the ability to send an
|
* A command hook costs a Node start (~40ms) per event, and buys back the ability to send an
|
||||||
* authentication header, which the http runner has no config surface for.
|
* authentication header, which the http runner has no config surface for.
|
||||||
*
|
*
|
||||||
|
* It also boots the daemon if nothing else has. That is not this hook being greedy: a plugin's
|
||||||
|
* `SessionStart` entry provably never runs (see `ensureDaemon` in glance-lib.mjs), so there is
|
||||||
|
* no single boot event to delegate to and whichever recorder fires first has to do it.
|
||||||
|
*
|
||||||
* Always exits 0. A dashboard must never be the reason a tool call fails.
|
* Always exits 0. A dashboard must never be the reason a tool call fails.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
ensureDaemon,
|
||||||
envEnvelope,
|
envEnvelope,
|
||||||
hookHeaders,
|
hookHeaders,
|
||||||
postJson,
|
postJson,
|
||||||
@@ -23,11 +28,23 @@ import {
|
|||||||
readStdinJson,
|
readStdinJson,
|
||||||
} from "./glance-lib.mjs";
|
} from "./glance-lib.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hooks/hooks.json gives this hook 5s. Everything below has to finish inside that with room to
|
||||||
|
* spare, because a killed script is a lost event either way — and losing one is fine, the next
|
||||||
|
* event is 40ms behind it.
|
||||||
|
*/
|
||||||
|
const START_BUDGET_MS = 1200;
|
||||||
|
const POST_BUDGET_MS = 1500;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = envEnvelope(await readStdinJson());
|
const payload = envEnvelope(await readStdinJson());
|
||||||
const cfg = readConfig();
|
const cfg = readConfig();
|
||||||
// Short: if the daemon is not listening this must fail fast, not hold up the tool call.
|
// Cheap when the daemon is already up, which is every call but the first of the session.
|
||||||
await postJson(`${baseUrl(cfg)}/hook/record`, payload, 2000, hookHeaders());
|
if (await ensureDaemon(cfg, { waitMs: START_BUDGET_MS, startedBy: "record-hook" })) {
|
||||||
|
// hookHeaders() is read after the daemon is up: on a first-ever run it is the daemon we
|
||||||
|
// just started that created hook.secret.
|
||||||
|
await postJson(`${baseUrl(cfg)}/hook/record`, payload, POST_BUDGET_MS, hookHeaders());
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Daemon down, no secret yet, malformed payload — all the same answer: carry on.
|
// Daemon down, no secret yet, malformed payload — all the same answer: carry on.
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-43
@@ -2,64 +2,44 @@
|
|||||||
/**
|
/**
|
||||||
* SessionStart hook: make sure the glance daemon is running, then record the event.
|
* SessionStart hook: make sure the glance daemon is running, then record the event.
|
||||||
*
|
*
|
||||||
* This is the only hook that spawns anything. It always exits 0 — a monitoring
|
* Note that as of Grok Build's current hook wiring this never actually runs: a plugin's
|
||||||
* dashboard must never be the reason a Grok Build session fails to start.
|
* `SessionStart` entry is registered after `SessionStart` has already been dispatched, so the
|
||||||
|
* event finds no plugin hooks to call. `ensureDaemon` in glance-lib.mjs has the details, and
|
||||||
|
* bin/glance-record.mjs is what really boots the daemon.
|
||||||
|
*
|
||||||
|
* It stays wired anyway. It costs nothing when it does not fire, it is the correct hook for the
|
||||||
|
* job on the day that ordering is fixed, and it keeps the answer to "what starts this thing"
|
||||||
|
* in the obvious place.
|
||||||
|
*
|
||||||
|
* It always exits 0 — a monitoring dashboard must never be the reason a session fails to start.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import {
|
import {
|
||||||
PLUGIN_ROOT,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
ensureDaemon,
|
||||||
envEnvelope,
|
envEnvelope,
|
||||||
glanceHome,
|
|
||||||
hookHeaders,
|
hookHeaders,
|
||||||
isDaemonUp,
|
|
||||||
postJson,
|
postJson,
|
||||||
readConfig,
|
readConfig,
|
||||||
readStdinJson,
|
readStdinJson,
|
||||||
sleep,
|
|
||||||
} from "./glance-lib.mjs";
|
} from "./glance-lib.mjs";
|
||||||
|
|
||||||
const SERVER_ENTRY = path.join(PLUGIN_ROOT, "dist", "server", "index.js");
|
|
||||||
|
|
||||||
async function ensureDaemon(cfg) {
|
|
||||||
if (await isDaemonUp(cfg)) return true;
|
|
||||||
|
|
||||||
if (!fs.existsSync(SERVER_ENTRY)) {
|
|
||||||
// dist/ ships with the plugin, so this means an incomplete checkout. Say so once, on
|
|
||||||
// stderr, where it is recorded but harmless — a hook must never fail a session.
|
|
||||||
process.stderr.write(
|
|
||||||
`[grok-glance] dist/ is missing - run \`npm install && npm run build\` in ${PLUGIN_ROOT}\n`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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],
|
|
||||||
env: { ...process.env, GLANCE_STARTED_BY: "hook" },
|
|
||||||
});
|
|
||||||
child.unref();
|
|
||||||
|
|
||||||
// Give it a moment to bind before the first http hook fires.
|
|
||||||
for (let i = 0; i < 40; i++) {
|
|
||||||
await sleep(200);
|
|
||||||
if (await isDaemonUp(cfg, 300)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = envEnvelope(await readStdinJson());
|
const payload = envEnvelope(await readStdinJson());
|
||||||
const cfg = readConfig();
|
const cfg = readConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const up = await ensureDaemon(cfg);
|
// hooks/hooks.json allows this hook 20s; leave most of it as headroom.
|
||||||
|
const up = await ensureDaemon(cfg, {
|
||||||
|
waitMs: 8000,
|
||||||
|
startedBy: "session-start-hook",
|
||||||
|
onMissingBuild: (entry) => {
|
||||||
|
// dist/ ships with the plugin, so this means an incomplete checkout. Say so once, on
|
||||||
|
// stderr, where it is recorded but harmless — a hook must never fail a session.
|
||||||
|
process.stderr.write(
|
||||||
|
`[grok-glance] ${entry} is missing - run \`npm install && npm run build\` in the plugin root\n`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
if (up) {
|
if (up) {
|
||||||
// hookHeaders() is read here, not at import time: on a first-ever run the daemon we just
|
// hookHeaders() is read here, not at import time: on a first-ever run the daemon we just
|
||||||
// spawned is what created hook.secret.
|
// spawned is what created hook.secret.
|
||||||
|
|||||||
+11
-3
@@ -16,9 +16,17 @@
|
|||||||
"derived from config.json - the scripts read that themselves - so this file is plain,",
|
"derived from config.json - the scripts read that themselves - so this file is plain,",
|
||||||
"committed, and edited by hand.",
|
"committed, and edited by hand.",
|
||||||
"",
|
"",
|
||||||
"SessionStart boots the daemon. PreToolUse is wired twice on purpose: one entry records",
|
"Every recorder boots the daemon if it is not already up, rather than one designated boot",
|
||||||
"every call for the timeline, and a second, narrowly matched entry runs the approval gate,",
|
"hook doing it. Grok Build gives a plugin no usable boot event: SessionStart is dispatched",
|
||||||
"because PreToolUse is the only blocking event and only a command hook can return a deny.",
|
"from inside session creation, against the hook registry as it stands at that moment, and",
|
||||||
|
"that registry holds only config-layer and settings-file hooks. Plugin hooks are appended",
|
||||||
|
"afterwards, under a 'plugin/' prefix, by reload_hooks_impl and reload_plugins_impl - so the",
|
||||||
|
"SessionStart entry below is registered strictly after SessionStart has already fired and",
|
||||||
|
"never runs. It is kept because it costs nothing and is the right hook once that is fixed.",
|
||||||
|
"",
|
||||||
|
"PreToolUse is wired twice on purpose: one entry records every call for the timeline, and a",
|
||||||
|
"second, narrowly matched entry runs the approval gate, because PreToolUse is the only",
|
||||||
|
"blocking event and only a command hook can return a deny.",
|
||||||
"",
|
"",
|
||||||
"The gate's 125s timeout is the ceiling for the whole approval round trip. The daemon caps",
|
"The gate's 125s timeout is the ceiling for the whole approval round trip. The daemon caps",
|
||||||
"approval.timeoutMs at 90s against it, so the script always outlives its own wait and gets",
|
"approval.timeoutMs at 90s against it, so the script always outlives its own wait and gets",
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ behind a WebAuthn passkey. It can also pause risky tool calls until someone taps
|
|||||||
One daemon covers every session on the machine, so several agents running at once all appear on the
|
One daemon covers every session on the machine, so several agents running at once all appear on the
|
||||||
same dashboard — no per-session setup.
|
same dashboard — no per-session setup.
|
||||||
|
|
||||||
The daemon is started automatically by the `SessionStart` hook. Everything below is done through
|
The daemon starts itself: every recording hook boots it if it is not already up, so the first
|
||||||
the `glance` CLI at `$GROK_PLUGIN_ROOT/bin/glance`.
|
prompt or tool call of a session brings it back. (Not the `SessionStart` hook — a plugin's
|
||||||
|
`SessionStart` entry is registered after the event has already fired, so it never runs. If someone
|
||||||
|
reports an empty `:8791` and no `daemon.log`, that is the reason, and any hook firing will fix it.)
|
||||||
|
|
||||||
|
Everything below is done through the `glance` CLI at `$GROK_PLUGIN_ROOT/bin/glance`.
|
||||||
|
|
||||||
## No build step
|
## No build step
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user