fix(node): mirror official Grok hooks

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 05:20:44 -04:00
1 parent 3ddce86c37
commit d84580e5a7
17 files changed
+1648 -843

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ This repository publishes the open-source server, console, integration catalog,
- Keep stable, legacy, and early-access contracts explicit, including their activation commands.
- Resolve stable Kimi hooks through `$KIMI_CODE_HOME/config.toml` with `~/.kimi-code/config.toml` as the default, and treat an empty override as the default. Activate `~/.kimi/config.toml` only when the legacy home exists, ignore executable lookup as activation evidence, and collapse equal stable and legacy paths to the stable contract. Register exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` rules with ten-second commands; stable Kimi supports its current sixteen-event schema and legacy kimi-cli supports its thirteen-event schema. Preserve the native snake_case payload, propagate exit code `2`, and keep audit delivery fail-open with owner-only error logs. Use encoded PowerShell commands on Windows and exact two-argument shell commands on Unix. Commit every detected config, generated runtime, runtime config, and private key through one rollback-capable transaction. Status requires strict runtime metadata, a canonical private key, exact identity, and non-empty physical scripts.
- Preserve Kimi TOML comments, array style, and unrelated formatting through syntax-preserving edits, then parse every selected document before writing any file.
- Write Grok Build integrations to a native user hook file under non-empty `GROK_HOME` or `~/.grok`; keep Claude Code and Cursor compatibility sources read-only.
- Resolve Grok Build user hooks through `$GROK_HOME/hooks/*.json` with `~/.grok/hooks/*.json` as the default, and treat an empty override as the default. Keep Claude Code, Cursor, project `.grok/hooks`, plugin, and `hooks-paths` sources read-only. Register exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups with ten-second command handlers, preserve the complete native camelCase payload, and emit Grok's documented deny JSON plus exit code `2` for frozen or revoked agents. Use encoded PowerShell commands on Windows and exact two-argument shell commands on Unix. Commit each SDK's Grok hook file, generated runtimes, runtime config, and private key through one rollback-capable transaction. Status requires the exact event triple, strict runtime metadata, a canonical private key, exact identity, and non-empty physical scripts.
- Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds.
- Validate Auggie matcher syntax during installation with Node.js `new RegExp`; keep status and uninstall independent from the JavaScript validator so recovery remains available offline.
- Write Cline file hooks to `$CLINE_DIR/hooks` with `~/.cline/hooks` as the default; keep Documents and workspace hook roots read-only. Use supported event filenames, preserve official hook input byte-for-byte, use `process.execPath` in Node and Go wrappers, and resolve Python runtimes from absolute shebangs. Translate guard exit code `2` into JSON stdout with `cancel: true`. Treat hook errors, timeouts, and invalid control JSON as fail-open.
+1 -1
View File
@@ -4,7 +4,7 @@
The provider-level contract represents the default stable runtime. `contract_variants` records opt-in or transitional runtimes with their activation command and release channel. Kiro CLI uses this field for its v2 embedded hooks and v3 standalone hooks.
Grok Build adapters write native user hook files under `$GROK_HOME/hooks` or `~/.grok/hooks`. Project `.grok/hooks` files require `/hooks-trust` or `--trust`; Claude Code and Cursor hook files are read-only compatibility sources for Elydora.
Grok Build adapters write exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups under `$GROK_HOME/hooks` or `~/.grok/hooks`. The native camelCase payload includes `toolUseId`, `toolInputTruncated`, and `toolResult`; explicit denials use JSON stdout plus exit code `2`, while hook crashes and timeouts fail open. Project `.grok/hooks` files require `/hooks-trust` or `--trust`; `hooks-paths`, plugin, Claude Code, and Cursor sources remain read-only for Elydora.
GitHub Copilot command `preToolUse` hooks deny on crashes and every non-zero exit. Command timeouts continue through the normal permission flow, so `timeout_failure_mode` records that explicit exception when it differs from `failure_mode`.
+17 -3
View File
@@ -214,10 +214,24 @@
"config_paths": [
"$GROK_HOME/hooks/*.json",
"~/.grok/hooks/*.json",
".grok/hooks/*.json"
"~/.grok/hooks-paths",
".grok/hooks/*.json",
".claude/settings.json",
".cursor/hooks.json"
],
"events": { "before_tool": "PreToolUse", "after_tool": "PostToolUse" },
"event_fields": { "name": "toolName", "input": "toolInput", "session": "sessionId" },
"events": {
"before_tool": "PreToolUse",
"after_tool": "PostToolUse",
"after_tool_failure": "PostToolUseFailure"
},
"event_fields": {
"name": "toolName",
"input": "toolInput",
"session": "sessionId",
"call_id": "toolUseId",
"output": "toolResult",
"error": "toolResult"
},
"blocking": { "mechanism": "exit_code_2", "failure_mode": "fail_open" },
"adapters": { "node": true, "python": true, "go": true },
"delivery_state": "available",
+1 -1
View File
@@ -63,7 +63,7 @@ Factory Droid installation preserves the active user source across `~/.factory/h
Kimi installation writes exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` rules to every detected runtime: Kimi Code's `$KIMI_CODE_HOME/config.toml` (default `~/.kimi-code/config.toml`) and the legacy Python CLI's `~/.kimi/config.toml`. A fresh installation targets stable Kimi Code; an existing legacy home activates the migration contract. Empty `KIMI_CODE_HOME` values use the default. The selected TOML files, generated runtimes, runtime config, and private key commit as one rollback-capable transaction. Managed commands preserve Kimi's native snake_case event JSON, propagate freeze and revocation through exit code `2`, and use encoded PowerShell on Windows. Run `/hooks` to inspect the global rules.
Grok Build installation writes native global hooks to `$GROK_HOME/hooks/elydora-audit.json` (default `~/.grok/hooks/elydora-audit.json`). Project hooks still follow Grok's `/hooks-trust` workflow; Elydora leaves project, Claude Code, and Cursor compatibility files unchanged.
Grok Build installation writes native global `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` hooks to `$GROK_HOME/hooks/elydora-audit.json` (default `~/.grok/hooks/elydora-audit.json`). The hook file, generated runtimes, runtime config, and private key commit as one rollback-capable transaction. Managed commands preserve Grok's native camelCase JSON, return `{"decision":"deny","reason":"..."}` with exit code `2` for frozen or revoked agents, and use encoded PowerShell on Windows. Project hooks follow Grok's `/hooks-trust` workflow; Elydora leaves project, plugin, `hooks-paths`, Claude Code, and Cursor compatibility sources unchanged. Run `grok inspect --json` to inspect the effective hook inventory.
Qwen Code installation writes user hooks to `$QWEN_HOME/settings.json` (default `~/.qwen/settings.json`). User-level `.qwen/.env` takes precedence over `~/.env` when it defines `QWEN_HOME`; explicit process environment values take highest precedence. Workspace settings remain unchanged. Run `/hooks` to review the definitions. `disableAllHooks` and `--safe-mode` suspend hook execution.
@@ -64,6 +64,7 @@ export async function prepareCursorInstallation(
guardOptions: {
failClosed: true,
successOutput: '{"permission":"allow"}\n',
denyProtocol: 'cursor',
},
auditOptions: {
failClosed: true,
+159
View File
@@ -0,0 +1,159 @@
import os from 'node:os';
import path from 'node:path';
interface ParsedArgument {
readonly value: string;
readonly next: number;
}
export interface GrokRuntimeReference {
readonly agentId: string;
readonly scriptPath: string;
}
export function sameGrokPath(left: string, right: string): boolean {
const normalizedLeft = path.resolve(left);
const normalizedRight = path.resolve(right);
return process.platform === 'win32'
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
: normalizedLeft === normalizedRight;
}
export function sameGrokAgentId(left: string, right: string): boolean {
return process.platform === 'win32'
? left.toLowerCase() === right.toLowerCase()
: left === right;
}
function quotePosix(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function quotePowerShell(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}
function windowsPowerShellPath(): string {
const configuredRoot = process.platform === 'win32' ? process.env.SystemRoot : undefined;
const systemRoot = configuredRoot
&& path.win32.isAbsolute(configuredRoot)
&& !/["%\r\n]/.test(configuredRoot)
? configuredRoot
: 'C:\\Windows';
return path.win32.join(
systemRoot,
'System32',
'WindowsPowerShell',
'v1.0',
'powershell.exe',
);
}
function windowsCommand(scriptPath: string): string {
const source = `& ${quotePowerShell(process.execPath)} ${quotePowerShell(scriptPath)}; exit $LASTEXITCODE`;
const encoded = Buffer.from(source, 'utf16le').toString('base64');
return `"${windowsPowerShellPath()}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
}
export function buildGrokCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Grok hook commands require absolute executable and script paths');
}
return process.platform === 'win32'
? windowsCommand(scriptPath)
: `${quotePosix(process.execPath)} ${quotePosix(scriptPath)}`;
}
function readPosixArgument(command: string, start: number): ParsedArgument | undefined {
if (command[start] !== "'") return undefined;
const apostrophe = `'"'"'`;
let value = '';
for (let index = start + 1; index < command.length;) {
if (command.startsWith(apostrophe, index)) {
value += "'";
index += apostrophe.length;
continue;
}
if (command[index] === "'") return { value, next: index + 1 };
value += command[index];
index += 1;
}
return undefined;
}
function parsePosixCommand(command: string): readonly [string, string] | undefined {
const executable = readPosixArgument(command, 0);
if (!executable || command[executable.next] !== ' ') return undefined;
const script = readPosixArgument(command, executable.next + 1);
if (!script || script.next !== command.length) return undefined;
return [executable.value, script.value];
}
function readPowerShellArgument(command: string, start: number): ParsedArgument | undefined {
if (command[start] !== "'") return undefined;
let value = '';
for (let index = start + 1; index < command.length; index += 1) {
if (command[index] !== "'") {
value += command[index];
continue;
}
if (command[index + 1] === "'") {
value += "'";
index += 1;
continue;
}
return { value, next: index + 1 };
}
return undefined;
}
function parsePowerShellSource(source: string): readonly [string, string] | undefined {
if (!source.startsWith('& ')) return undefined;
const executable = readPowerShellArgument(source, 2);
if (!executable || source[executable.next] !== ' ') return undefined;
const script = readPowerShellArgument(source, executable.next + 1);
if (!script || source.slice(script.next) !== '; exit $LASTEXITCODE') return undefined;
return [executable.value, script.value];
}
function parseWindowsCommand(command: string): readonly [string, string] | undefined {
const match = /^"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/]+={0,2})$/.exec(command);
if (!match
|| !path.win32.isAbsolute(match[1])
|| path.win32.basename(match[1]).toLowerCase() !== 'powershell.exe') return undefined;
const encoded = match[2];
const buffer = Buffer.from(encoded, 'base64');
if (buffer.length % 2 !== 0 || buffer.toString('base64') !== encoded) return undefined;
return parsePowerShellSource(buffer.toString('utf16le'));
}
function parseLegacyWindowsCommand(command: string): readonly [string, string] | undefined {
const match = /^"([^"\r\n]+)" "([^"\r\n]+)"$/.exec(command);
return match ? [match[1], match[2]] : undefined;
}
function isNodeExecutable(filePath: string): boolean {
const basename = path.basename(filePath).toLowerCase();
return basename === 'node' || basename === 'node.exe';
}
export function grokRuntimeReference(
command: string,
scriptName: string,
): GrokRuntimeReference | undefined {
const parsed = parsePosixCommand(command)
?? parseWindowsCommand(command)
?? parseLegacyWindowsCommand(command);
if (!parsed
|| !path.isAbsolute(parsed[0])
|| !path.isAbsolute(parsed[1])
|| !isNodeExecutable(parsed[0])
|| path.basename(parsed[1]) !== scriptName) return undefined;
const agentDirectory = path.dirname(parsed[1]);
if (!sameGrokPath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) {
return undefined;
}
const agentId = path.basename(agentDirectory);
if (!agentId || agentId === '.' || agentId === '..') return undefined;
return { agentId, scriptPath: parsed[1] };
}
+276
View File
@@ -0,0 +1,276 @@
import {
buildGrokCommand,
grokRuntimeReference,
sameGrokAgentId,
type GrokRuntimeReference,
} from './grok-command.js';
import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js';
export const AGENT_KEY = 'grok';
export const GUARD_SCRIPT = 'guard.js';
export const AUDIT_SCRIPT = 'hook.js';
export const HOOK_TIMEOUT_SECONDS = 10;
const MATCHER_REJECTING_EVENTS = new Set([
'SessionStart',
'SessionEnd',
'Stop',
'UserPromptSubmit',
]);
export type ManagedGrokEvent = 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure';
export interface GrokHandler extends JsonObject {
readonly type: 'command' | 'http';
readonly command?: string;
readonly url?: string;
readonly timeout?: number;
}
export interface GrokGroup extends JsonObject {
readonly matcher?: string;
readonly hooks: GrokHandler[];
}
export type GrokHooks = Record<string, GrokGroup[]>;
export interface GrokDocument {
readonly exists: boolean;
readonly filePath: string;
readonly root: JsonObject;
readonly hooks: GrokHooks;
readonly raw?: string;
}
export interface RenderedGrokDocument {
readonly document: GrokDocument;
readonly changed: boolean;
readonly next?: string;
}
export interface GrokRuntimeContract {
readonly agentId: string;
readonly guardPath: string;
readonly auditPath: string;
}
function validateTimeout(value: unknown, label: string): void {
if (value === undefined) return;
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new Error(`${label} timeout must be a non-negative integer`);
}
}
function validateHandler(
value: unknown,
event: string,
groupIndex: number,
handlerIndex: number,
): GrokHandler {
const label = `Grok user hooks handler hooks.${event}[${groupIndex}].hooks[${handlerIndex}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (value.type !== 'command' && value.type !== 'http') {
throw new Error(`${label} has unsupported type "${String(value.type)}"`);
}
if (value.type === 'command' && (typeof value.command !== 'string' || !value.command)) {
throw new Error(`${label} requires a non-empty command`);
}
if (value.type === 'http' && (typeof value.url !== 'string' || !value.url)) {
throw new Error(`${label} requires a non-empty url`);
}
validateTimeout(value.timeout, label);
if (value.env !== undefined) {
if (!isObject(value.env)
|| Object.values(value.env).some((item) => typeof item !== 'string')) {
throw new Error(`${label} env must map names to strings`);
}
}
return value as GrokHandler;
}
function validateGroup(value: unknown, event: string, groupIndex: number): GrokGroup {
const label = `Grok user hooks group hooks.${event}[${groupIndex}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (value.matcher !== undefined && typeof value.matcher !== 'string') {
throw new Error(`${label} matcher must be a string`);
}
if (value.matcher !== undefined && MATCHER_REJECTING_EVENTS.has(event)) {
throw new Error(`${label} cannot declare a matcher for ${event}`);
}
if (!Array.isArray(value.hooks)) throw new Error(`${label} must contain a hooks array`);
return {
...value,
hooks: value.hooks.map(
(handler, handlerIndex) => validateHandler(handler, event, groupIndex, handlerIndex),
),
} as GrokGroup;
}
function readHooks(value: unknown): GrokHooks {
if (value === undefined) return {};
if (!isObject(value)) throw new Error('Grok user hooks field "hooks" must be an object');
const hooks: GrokHooks = {};
for (const [event, candidate] of Object.entries(value)) {
if (!Array.isArray(candidate)) {
throw new Error(`Grok user hooks field "hooks.${event}" must be an array`);
}
hooks[event] = candidate.map((group, index) => validateGroup(group, event, index));
}
return hooks;
}
export function parseGrokDocument(filePath: string, raw: string): GrokDocument {
const root = parseStrictJsonObject(raw, `Grok user hooks at ${filePath}`);
return { exists: true, filePath, root, hooks: readHooks(root.hooks), raw };
}
export function createGrokDocument(filePath: string): GrokDocument {
return { exists: false, filePath, root: {}, hooks: {} };
}
export function buildGrokGroup(scriptPath: string): GrokGroup {
return {
hooks: [{
type: 'command',
command: buildGrokCommand(scriptPath),
timeout: HOOK_TIMEOUT_SECONDS,
}],
};
}
function exactManagedGroup(group: GrokGroup): boolean {
return Object.keys(group).length === 1 && Object.hasOwn(group, 'hooks');
}
function exactManagedHandler(handler: GrokHandler): boolean {
return Object.keys(handler).sort().join('|') === 'command|timeout|type'
&& handler.type === 'command'
&& handler.timeout === HOOK_TIMEOUT_SECONDS
&& typeof handler.command === 'string';
}
function managedReference(
handler: GrokHandler,
scriptName: string,
): GrokRuntimeReference | undefined {
if (!exactManagedHandler(handler)) return undefined;
return grokRuntimeReference(handler.command as string, scriptName);
}
function removeFromGroups(
groups: readonly GrokGroup[],
scriptName: string,
agentId?: string,
): GrokGroup[] {
const result: GrokGroup[] = [];
for (const group of groups) {
if (!exactManagedGroup(group)) {
result.push(group);
continue;
}
const handlers = group.hooks.filter((handler) => {
const reference = managedReference(handler, scriptName);
return !reference
|| (agentId !== undefined && !sameGrokAgentId(reference.agentId, agentId));
});
if (handlers.length > 0) result.push({ hooks: handlers });
}
return result;
}
function managedEvent(event: string): readonly [ManagedGrokEvent, string] | undefined {
if (event === 'PreToolUse') return ['PreToolUse', GUARD_SCRIPT];
if (event === 'PostToolUse') return ['PostToolUse', AUDIT_SCRIPT];
if (event === 'PostToolUseFailure') return ['PostToolUseFailure', AUDIT_SCRIPT];
return undefined;
}
export function removeManagedGrokHooks(hooks: GrokHooks, agentId?: string): GrokHooks {
const next: GrokHooks = Object.fromEntries(
Object.entries(hooks).map(([event, groups]) => [event, [...groups]]),
);
for (const [event, scriptName] of [
['PreToolUse', GUARD_SCRIPT],
['PostToolUse', AUDIT_SCRIPT],
['PostToolUseFailure', AUDIT_SCRIPT],
] as const) {
const groups = removeFromGroups(next[event] ?? [], scriptName, agentId);
if (groups.length > 0) next[event] = groups;
else delete next[event];
}
return next;
}
function entirelyManaged(document: GrokDocument): boolean {
if (!document.exists || !Object.keys(document.root).every((key) => key === 'hooks')) return false;
const events = Object.entries(document.hooks);
if (events.length === 0) return false;
let handlerCount = 0;
for (const [event, groups] of events) {
const contract = managedEvent(event);
if (!contract || groups.length === 0) return false;
for (const group of groups) {
if (!exactManagedGroup(group)
|| group.hooks.length === 0
|| group.hooks.some((handler) => !managedReference(handler, contract[1]))) return false;
handlerCount += group.hooks.length;
}
}
return handlerCount > 0;
}
export function renderGrokDocument(
document: GrokDocument,
hooks: GrokHooks,
): RenderedGrokDocument {
if (!document.exists && Object.keys(hooks).length === 0) {
return { document, changed: false };
}
if (Object.keys(hooks).length === 0 && entirelyManaged(document)) {
return { document, changed: true };
}
const root: JsonObject = { ...document.root };
if (Object.keys(hooks).length > 0) root.hooks = hooks;
else delete root.hooks;
const next = `${JSON.stringify(root, null, 2)}\n`;
return { document, changed: next !== document.raw, next };
}
function referencesForEvent(
groups: readonly GrokGroup[],
scriptName: string,
): Map<string, GrokRuntimeReference[]> {
const result = new Map<string, GrokRuntimeReference[]>();
for (const group of groups) {
if (!exactManagedGroup(group)) continue;
for (const handler of group.hooks) {
const reference = managedReference(handler, scriptName);
if (!reference) continue;
const key = process.platform === 'win32'
? reference.agentId.toLowerCase()
: reference.agentId;
const entries = result.get(key) ?? [];
entries.push(reference);
result.set(key, entries);
}
}
return result;
}
export function grokRuntimeContracts(hooks: GrokHooks): GrokRuntimeContract[] {
const guards = referencesForEvent(hooks.PreToolUse ?? [], GUARD_SCRIPT);
const successes = referencesForEvent(hooks.PostToolUse ?? [], AUDIT_SCRIPT);
const failures = referencesForEvent(hooks.PostToolUseFailure ?? [], AUDIT_SCRIPT);
const contracts: GrokRuntimeContract[] = [];
for (const [key, guard] of guards) {
const success = successes.get(key);
const failure = failures.get(key);
if (guard.length !== 1 || success?.length !== 1 || failure?.length !== 1) continue;
contracts.push({
agentId: guard[0].agentId,
guardPath: guard[0].scriptPath,
auditPath: success[0].scriptPath,
});
}
return contracts;
}
+116
View File
@@ -0,0 +1,116 @@
import path from 'node:path';
import type { InstallConfig } from './base.js';
import {
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
type RenderedGrokDocument,
} from './grok-contract.js';
import {
commitManagedInstallation,
preflightManagedInstallation,
prepareManagedInstallation,
type ManagedRuntimePaths,
type PreparedManagedInstallation,
type RenameFile,
} from './managed-installation.js';
import {
commitManagedTransaction,
prepareManagedFileChange,
type PreparedManagedTransaction,
} from './managed-transaction.js';
const DISPLAY_NAME = 'Grok Build';
const HOOKS_DIRECTORY_LABEL = 'Grok hooks directory';
const HOOKS_LABEL = 'Grok user hooks';
export type GrokRuntimePaths = ManagedRuntimePaths;
export type PreparedGrokInstallation = PreparedManagedInstallation;
export type { RenameFile };
export async function preflightGrokInstallation(
config: InstallConfig,
hooksPath: string,
): Promise<GrokRuntimePaths> {
return preflightManagedInstallation({
agentKey: AGENT_KEY,
hookLocations: [{ directoryLabel: HOOKS_DIRECTORY_LABEL, filePath: hooksPath }],
config,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
export async function prepareGrokInstallation(
config: InstallConfig,
rendered: RenderedGrokDocument,
): Promise<PreparedGrokInstallation> {
const hooksSource = rendered.next ?? rendered.document.raw;
if (hooksSource === undefined) throw new Error('Grok hook configuration is missing');
const prepared = await prepareManagedInstallation({
agentKey: AGENT_KEY,
displayName: DISPLAY_NAME,
hookSources: [{
directoryLabel: HOOKS_DIRECTORY_LABEL,
label: HOOKS_LABEL,
filePath: rendered.document.filePath,
expectedSource: rendered.document.raw,
source: hooksSource,
}],
config,
guardOptions: { denyProtocol: 'grok' },
auditOptions: { nativePayload: true },
}, GUARD_SCRIPT, AUDIT_SCRIPT);
const hooksDirectory = path.dirname(rendered.document.filePath);
return {
...prepared,
transaction: {
...prepared.transaction,
directories: [
{ path: path.dirname(hooksDirectory), label: 'Grok home directory' },
...prepared.transaction.directories,
],
},
};
}
export async function commitGrokInstallation(
prepared: PreparedGrokInstallation,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedInstallation(prepared, renameFile);
}
export async function prepareGrokUninstall(
rendered: RenderedGrokDocument,
): Promise<PreparedManagedTransaction> {
const change = rendered.changed
? await prepareManagedFileChange({
filePath: rendered.document.filePath,
label: HOOKS_LABEL,
next: rendered.next,
mode: 0o600,
expectedSource: rendered.document.raw,
verifyExpectedSource: true,
})
: undefined;
return {
displayName: DISPLAY_NAME,
operation: 'uninstall',
directories: rendered.changed
? [
{
path: path.dirname(path.dirname(rendered.document.filePath)),
label: 'Grok home directory',
},
{ path: path.dirname(rendered.document.filePath), label: HOOKS_DIRECTORY_LABEL },
]
: [],
changes: change ? [change] : [],
};
}
export async function commitGrokUninstall(
prepared: PreparedManagedTransaction,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedTransaction(prepared, renameFile);
}
+112
View File
@@ -0,0 +1,112 @@
import os from 'node:os';
import path from 'node:path';
import {
AGENT_KEY,
createGrokDocument,
parseGrokDocument,
type GrokDocument,
type GrokRuntimeContract,
} from './grok-contract.js';
import { sameGrokAgentId, sameGrokPath } from './grok-command.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
const CONFIG_FILE = 'elydora-audit.json';
export function grokConfigPath(): string {
const configured = process.env.GROK_HOME;
const grokHome = configured === undefined || configured === ''
? path.join(os.homedir(), '.grok')
: path.resolve(configured);
return path.join(grokHome, 'hooks', CONFIG_FILE);
}
export async function readGrokDocument(): Promise<GrokDocument> {
const filePath = grokConfigPath();
await inspectPhysicalDirectory(path.dirname(path.dirname(filePath)), 'Grok home directory');
await inspectPhysicalDirectory(path.dirname(filePath), 'Grok hooks directory');
const snapshot = await readPhysicalFile(filePath, 'Grok user hooks');
return snapshot
? parseGrokDocument(filePath, snapshot.contents)
: createGrokDocument(filePath);
}
function requireNonEmptyString(value: unknown, field: string, configPath: string): string {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`Elydora runtime config ${field} is invalid: ${configPath}`);
}
return value;
}
function validateRuntimeConfig(
config: JsonObject,
contract: GrokRuntimeContract,
configPath: string,
): void {
const supported = new Set(['org_id', 'agent_id', 'kid', 'base_url', 'token', 'agent_name']);
const extra = Object.keys(config).find((key) => !supported.has(key));
if (extra) throw new Error(`Elydora runtime config has unsupported field "${extra}": ${configPath}`);
requireNonEmptyString(config.org_id, 'org_id', configPath);
requireNonEmptyString(config.kid, 'kid', configPath);
const agentId = requireNonEmptyString(config.agent_id, 'agent_id', configPath);
if (!sameGrokAgentId(agentId, contract.agentId) || config.agent_name !== AGENT_KEY) {
throw new Error(`Elydora runtime identity does not match Grok hooks: ${configPath}`);
}
if (config.token !== undefined) requireNonEmptyString(config.token, 'token', configPath);
const rawBaseUrl = requireNonEmptyString(config.base_url, 'base_url', configPath);
let baseUrl: URL;
try {
baseUrl = new URL(rawBaseUrl);
} catch (error) {
throw new Error(`Elydora runtime config base_url is invalid: ${configPath}`, {
cause: error instanceof Error ? error : new Error(String(error)),
});
}
if (!['http:', 'https:'].includes(baseUrl.protocol)
|| !baseUrl.hostname
|| baseUrl.username
|| baseUrl.password
|| baseUrl.search
|| baseUrl.hash) {
throw new Error(`Elydora runtime config base_url is invalid: ${configPath}`);
}
}
function validatePrivateKey(contents: string, keyPath: string): void {
const bytes = Buffer.from(contents, 'base64url');
if (bytes.length !== 32 || bytes.toString('base64url') !== contents) {
throw new Error(`Elydora private key is invalid: ${keyPath}`);
}
}
async function runtimeContractExists(contract: GrokRuntimeContract): Promise<boolean> {
const runtimeRoot = path.join(os.homedir(), '.elydora');
const agentDirectory = path.dirname(contract.guardPath);
if (!sameGrokPath(path.dirname(agentDirectory), runtimeRoot)
|| !sameGrokPath(contract.auditPath, path.join(agentDirectory, 'hook.js'))) return false;
if (!await inspectPhysicalDirectory(runtimeRoot, 'Elydora runtime directory')) return false;
if (!await inspectPhysicalDirectory(agentDirectory, 'Elydora agent runtime directory')) return false;
const configPath = path.join(agentDirectory, 'config.json');
const keyPath = path.join(agentDirectory, 'private.key');
const [config, key, guard, audit] = await Promise.all([
readPhysicalFile(configPath, 'Elydora runtime config'),
readPhysicalFile(keyPath, 'Elydora private key'),
readPhysicalFile(contract.guardPath, 'Elydora guard runtime'),
readPhysicalFile(contract.auditPath, 'Elydora audit runtime'),
]);
if (!config || !key || !guard || !audit) return false;
const parsed = parseStrictJsonObject(config.contents, `Elydora runtime config at ${configPath}`);
validateRuntimeConfig(parsed, contract, configPath);
validatePrivateKey(key.contents, keyPath);
return guard.contents.length > 0 && audit.contents.length > 0;
}
export async function grokRuntimeFilesExist(
contracts: readonly GrokRuntimeContract[],
): Promise<boolean> {
for (const contract of contracts) {
if (await runtimeContractExists(contract)) return true;
}
return false;
}
+56 -431
View File
@@ -1,458 +1,83 @@
import { randomUUID } from 'node:crypto';
import fsp from 'node:fs/promises';
import type { FileHandle } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
import {
AGENT_KEY,
buildGrokGroup,
grokRuntimeContracts,
removeManagedGrokHooks,
renderGrokDocument,
type GrokHooks,
} from './grok-contract.js';
import {
commitGrokInstallation,
commitGrokUninstall,
preflightGrokInstallation,
prepareGrokInstallation,
prepareGrokUninstall,
} from './grok-installation.js';
import { grokRuntimeFilesExist, readGrokDocument } from './grok-io.js';
import { SUPPORTED_AGENTS } from './registry.js';
const AGENT_KEY = 'grok';
const GUARD_SCRIPT = 'guard.js';
const AUDIT_SCRIPT = 'hook.js';
const HOOK_TIMEOUT_SECONDS = 10;
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
type JsonObject = Record<string, unknown>;
interface GrokHandler extends JsonObject {
readonly type: 'command' | 'http';
readonly command?: string;
readonly url?: string;
readonly timeout?: number;
}
interface GrokGroup extends JsonObject {
readonly matcher?: string;
readonly hooks: GrokHandler[];
}
type GrokHooks = Record<string, GrokGroup[]>;
interface GrokDocument {
readonly exists: boolean;
readonly configPath: string;
readonly root: JsonObject;
readonly hooks: GrokHooks;
}
interface ParsedArgument {
readonly value: string;
readonly next: number;
}
interface RuntimeContract {
readonly agentId: string;
readonly guardPath: string;
readonly auditPath: string;
}
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function hasOwn(value: JsonObject, key: string): boolean {
return Object.prototype.hasOwnProperty.call(value, key);
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function asError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function hasErrorCode(error: unknown, code: string): boolean {
return isObject(error) && error.code === code;
}
function resolveConfigPath(): string {
const grokHome = process.env.GROK_HOME || path.join(os.homedir(), '.grok');
return path.join(grokHome, 'hooks', entry.configFile);
}
function quotePosix(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function quoteWindows(value: string): string {
return `"${value.replaceAll('"', '\\"')}"`;
}
function buildCommand(scriptPath: string): string {
const quote = process.platform === 'win32' ? quoteWindows : quotePosix;
return `${quote(process.execPath)} ${quote(scriptPath)}`;
}
function buildHandler(scriptPath: string): GrokHandler {
function installedHooks(
hooks: GrokHooks,
guardPath: string,
auditPath: string,
): GrokHooks {
const cleaned = removeManagedGrokHooks(hooks);
return {
type: 'command',
command: buildCommand(scriptPath),
timeout: HOOK_TIMEOUT_SECONDS,
...cleaned,
PreToolUse: [...(cleaned.PreToolUse ?? []), buildGrokGroup(guardPath)],
PostToolUse: [...(cleaned.PostToolUse ?? []), buildGrokGroup(auditPath)],
PostToolUseFailure: [
...(cleaned.PostToolUseFailure ?? []),
buildGrokGroup(auditPath),
],
};
}
function readWindowsArgument(command: string, start: number): ParsedArgument | undefined {
if (command[start] !== '"') return undefined;
let value = '';
for (let index = start + 1; index < command.length; index += 1) {
if (command[index] === '\\' && command[index + 1] === '"') {
value += '"';
index += 1;
continue;
}
if (command[index] === '"') return { value, next: index + 1 };
value += command[index];
}
return undefined;
}
const POSIX_APOSTROPHE = `'"'"'`;
function readPosixArgument(command: string, start: number): ParsedArgument | undefined {
if (command[start] !== "'") return undefined;
let value = '';
for (let index = start + 1; index < command.length;) {
if (command.startsWith(POSIX_APOSTROPHE, index)) {
value += "'";
index += POSIX_APOSTROPHE.length;
continue;
}
if (command[index] === "'") return { value, next: index + 1 };
value += command[index];
index += 1;
}
return undefined;
}
function parseGeneratedCommand(command: string): readonly [string, string] | undefined {
const readArgument = process.platform === 'win32' ? readWindowsArgument : readPosixArgument;
const executable = readArgument(command, 0);
if (!executable || command[executable.next] !== ' ') return undefined;
const script = readArgument(command, executable.next + 1);
if (!script || script.next !== command.length || !executable.value || !script.value) return undefined;
return [executable.value, script.value];
}
function samePath(left: string, right: string): boolean {
const normalizedLeft = path.resolve(left);
const normalizedRight = path.resolve(right);
return process.platform === 'win32'
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
: normalizedLeft === normalizedRight;
}
function sameAgentId(left: string, right: string): boolean {
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
}
function managedAgentId(handler: GrokHandler, scriptName: string): string | undefined {
if (handler.type !== 'command'
|| handler.timeout !== HOOK_TIMEOUT_SECONDS
|| typeof handler.command !== 'string') return undefined;
const parsed = parseGeneratedCommand(handler.command);
if (!parsed) return undefined;
const scriptPath = parsed[1];
if (path.basename(scriptPath) !== scriptName) return undefined;
const agentDirectory = path.dirname(scriptPath);
const runtimeRoot = path.join(os.homedir(), '.elydora');
if (!samePath(path.dirname(agentDirectory), runtimeRoot)) return undefined;
const agentId = path.basename(agentDirectory);
return agentId && agentId !== '.' && agentId !== '..' ? agentId : undefined;
}
function validateHandler(value: unknown, event: string, groupIndex: number, handlerIndex: number): GrokHandler {
const label = `Grok hooks config handler hooks.${event}[${groupIndex}].hooks[${handlerIndex}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (value.type !== 'command' && value.type !== 'http') {
throw new Error(`${label} has unsupported type "${String(value.type)}"`);
}
if (value.type === 'command' && (typeof value.command !== 'string' || value.command.length === 0)) {
throw new Error(`${label} requires a non-empty command`);
}
if (value.type === 'http' && (typeof value.url !== 'string' || value.url.length === 0)) {
throw new Error(`${label} requires a non-empty url`);
}
if (value.timeout !== undefined
&& (typeof value.timeout !== 'number' || !Number.isFinite(value.timeout) || value.timeout <= 0)) {
throw new Error(`${label} timeout must be a positive finite number`);
}
return value as GrokHandler;
}
function validateGroup(value: unknown, event: string, groupIndex: number): GrokGroup {
const label = `Grok hooks config group hooks.${event}[${groupIndex}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (value.matcher !== undefined && typeof value.matcher !== 'string') {
throw new Error(`${label} matcher must be a string`);
}
if (!Array.isArray(value.hooks)) throw new Error(`${label} must contain a hooks array`);
const handlers = value.hooks.map(
(handler, handlerIndex) => validateHandler(handler, event, groupIndex, handlerIndex),
);
return { ...value, hooks: handlers } as GrokGroup;
}
function readHooks(root: JsonObject): GrokHooks {
if (root.hooks === undefined) return {};
if (!isObject(root.hooks)) throw new Error('Grok hooks config field "hooks" must be an object');
const hooks: GrokHooks = {};
for (const [event, value] of Object.entries(root.hooks)) {
if (!Array.isArray(value)) {
throw new Error(`Grok hooks config field "hooks.${event}" must be an array`);
}
hooks[event] = value.map((group, index) => validateGroup(group, event, index));
}
return hooks;
}
async function readConfig(): Promise<GrokDocument> {
const configPath = resolveConfigPath();
let raw: string;
try {
raw = await fsp.readFile(configPath, 'utf-8');
} catch (error) {
if (hasErrorCode(error, 'ENOENT')) return { exists: false, configPath, root: {}, hooks: {} };
throw new Error(`Read Grok hooks config at ${configPath}: ${errorMessage(error)}`, {
cause: asError(error),
});
}
let root: unknown;
try {
root = JSON.parse(raw);
} catch (error) {
throw new Error(`Failed to parse Grok hooks config at ${configPath}: ${errorMessage(error)}`, {
cause: asError(error),
});
}
if (!isObject(root)) throw new Error(`Grok hooks config at ${configPath} must contain a JSON object`);
return { exists: true, configPath, root, hooks: readHooks(root) };
}
function removeManaged(
groups: GrokGroup[],
scriptName: string,
agentId?: string,
): { readonly groups: GrokGroup[]; readonly changed: boolean } {
let changed = false;
const result: GrokGroup[] = [];
for (const group of groups) {
if (hasOwn(group, 'matcher')) {
result.push(group);
continue;
}
let groupChanged = false;
const handlers = group.hooks.filter((handler) => {
const managedId = managedAgentId(handler, scriptName);
const remove = managedId !== undefined
&& (agentId === undefined || sameAgentId(managedId, agentId));
if (remove) {
changed = true;
groupChanged = true;
}
return !remove;
});
if (!groupChanged) result.push(group);
else if (handlers.length > 0) result.push({ ...group, hooks: handlers });
}
return { groups: result, changed };
}
function removeManagedHooks(hooks: GrokHooks, agentId?: string): {
readonly hooks: GrokHooks;
readonly changed: boolean;
} {
const next = { ...hooks };
let changed = false;
for (const [event, scriptName] of [
['PreToolUse', GUARD_SCRIPT],
['PostToolUse', AUDIT_SCRIPT],
] as const) {
const result = removeManaged(next[event] ?? [], scriptName, agentId);
if (!result.changed) continue;
changed = true;
if (result.groups.length > 0) next[event] = result.groups;
else delete next[event];
}
return { hooks: next, changed };
}
async function failWrite(
handle: FileHandle | undefined,
tempPath: string,
configPath: string,
cause: unknown,
): Promise<never> {
const errors = [asError(cause)];
if (handle) {
try {
await handle.close();
} catch (error) {
errors.push(asError(error));
}
}
try {
await fsp.unlink(tempPath);
} catch (error) {
if (!hasErrorCode(error, 'ENOENT')) errors.push(asError(error));
}
const message = `Write Grok hooks config at ${configPath}: ${errorMessage(cause)}`;
if (errors.length > 1) throw new AggregateError(errors, message);
throw new Error(message, { cause: errors[0] });
}
async function writeConfig(configPath: string, root: JsonObject): Promise<void> {
const directory = path.dirname(configPath);
await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
const tempPath = path.join(directory, `.${path.basename(configPath)}.${randomUUID()}.tmp`);
let handle: FileHandle | undefined;
try {
handle = await fsp.open(tempPath, 'wx', 0o600);
await handle.writeFile(JSON.stringify(root, null, 2) + '\n', 'utf-8');
await handle.sync();
await handle.close();
handle = undefined;
await fsp.rename(tempPath, configPath);
} catch (error) {
await failWrite(handle, tempPath, configPath, error);
}
}
async function removeConfig(configPath: string): Promise<void> {
try {
await fsp.unlink(configPath);
} catch (error) {
if (hasErrorCode(error, 'ENOENT')) return;
throw new Error(`Remove Grok hooks config at ${configPath}: ${errorMessage(error)}`, {
cause: asError(error),
});
}
}
async function regularFileExists(filePath: string, label: string): Promise<boolean> {
try {
return (await fsp.stat(filePath)).isFile();
} catch (error) {
if (hasErrorCode(error, 'ENOENT') || hasErrorCode(error, 'ENOTDIR')) return false;
throw new Error(`Read ${label} at ${filePath}: ${errorMessage(error)}`, { cause: asError(error) });
}
}
async function requireRuntime(filePath: string, label: string): Promise<void> {
if (!filePath) throw new Error(`${label} path is required`);
if (!await regularFileExists(filePath, label)) throw new Error(`${label} is missing: ${filePath}`);
}
async function readJsonObject(filePath: string, label: string): Promise<JsonObject | undefined> {
let raw: string;
try {
raw = await fsp.readFile(filePath, 'utf-8');
} catch (error) {
if (hasErrorCode(error, 'ENOENT')) return undefined;
throw new Error(`Read ${label} at ${filePath}: ${errorMessage(error)}`, { cause: asError(error) });
}
let value: unknown;
try {
value = JSON.parse(raw);
} catch (error) {
throw new Error(`Failed to parse ${label} at ${filePath}: ${errorMessage(error)}`, {
cause: asError(error),
});
}
if (!isObject(value)) throw new Error(`${label} at ${filePath} must contain a JSON object`);
return value;
}
function managedIds(groups: GrokGroup[], scriptName: string): Set<string> {
const ids = new Set<string>();
for (const group of groups) {
if (hasOwn(group, 'matcher')) continue;
for (const handler of group.hooks) {
const agentId = managedAgentId(handler, scriptName);
if (agentId) ids.add(agentId);
}
}
return ids;
}
function runtimeContracts(hooks: GrokHooks): RuntimeContract[] {
const guards = managedIds(hooks.PreToolUse ?? [], GUARD_SCRIPT);
const audits = managedIds(hooks.PostToolUse ?? [], AUDIT_SCRIPT);
const root = path.join(os.homedir(), '.elydora');
return [...guards]
.filter((agentId) => audits.has(agentId))
.map((agentId) => ({
agentId,
guardPath: path.join(root, agentId, GUARD_SCRIPT),
auditPath: path.join(root, agentId, AUDIT_SCRIPT),
}));
}
async function runtimeScriptsExist(contracts: RuntimeContract[]): Promise<boolean> {
const root = path.join(os.homedir(), '.elydora');
let entries: Array<{ isDirectory(): boolean; name: string }>;
try {
entries = await fsp.readdir(root, { withFileTypes: true });
} catch (error) {
if (hasErrorCode(error, 'ENOENT')) return false;
throw new Error(`Read Elydora runtime directory at ${root}: ${errorMessage(error)}`, {
cause: asError(error),
});
}
for (const contract of contracts) {
if (!entries.some((item) => item.isDirectory() && sameAgentId(item.name, contract.agentId))) continue;
const runtimeConfigPath = path.join(root, contract.agentId, 'config.json');
const runtimeConfig = await readJsonObject(runtimeConfigPath, 'Elydora runtime config');
if (!runtimeConfig || runtimeConfig.agent_name !== AGENT_KEY) continue;
const [guardExists, auditExists] = await Promise.all([
regularFileExists(contract.guardPath, 'Elydora guard runtime'),
regularFileExists(contract.auditPath, 'Elydora audit runtime'),
]);
if (guardExists && auditExists) return true;
}
return false;
}
export const grokPlugin: AgentPlugin = {
managesRuntime: true,
async preflightInstall(config: InstallConfig): Promise<void> {
const document = await readGrokDocument();
await preflightGrokInstallation(config, document.filePath);
},
async install(config: InstallConfig): Promise<void> {
if (!config.agentId) throw new Error('agentId is required');
const document = await readConfig();
await requireRuntime(config.guardScriptPath, 'Elydora guard runtime');
await requireRuntime(config.hookScriptPath, 'Elydora audit runtime');
const cleaned = removeManagedHooks(document.hooks).hooks;
const hooks: GrokHooks = {
...cleaned,
PreToolUse: [...(cleaned.PreToolUse ?? []), { hooks: [buildHandler(config.guardScriptPath)] }],
PostToolUse: [...(cleaned.PostToolUse ?? []), { hooks: [buildHandler(config.hookScriptPath)] }],
};
await writeConfig(document.configPath, { ...document.root, hooks });
console.log(' Grok Build: global PreToolUse and PostToolUse hooks installed.');
const document = await readGrokDocument();
const paths = await preflightGrokInstallation(config, document.filePath);
const rendered = renderGrokDocument(
document,
installedHooks(document.hooks, paths.guardPath, paths.auditPath),
);
await commitGrokInstallation(await prepareGrokInstallation(config, rendered));
console.log(
' Grok Build: global PreToolUse, PostToolUse, and PostToolUseFailure hooks installed.',
);
},
async uninstall(agentId?: string): Promise<void> {
const document = await readConfig();
if (!document.exists) return;
const result = removeManagedHooks(document.hooks, agentId);
if (!result.changed) return;
const root = { ...document.root };
if (Object.keys(result.hooks).length > 0) root.hooks = result.hooks;
else delete root.hooks;
if (Object.keys(root).length === 0) await removeConfig(document.configPath);
else await writeConfig(document.configPath, root);
const document = await readGrokDocument();
const rendered = renderGrokDocument(
document,
removeManagedGrokHooks(document.hooks, agentId),
);
await commitGrokUninstall(await prepareGrokUninstall(rendered));
},
async status(): Promise<PluginStatus> {
const document = await readConfig();
const contracts = runtimeContracts(document.hooks);
const document = await readGrokDocument();
const contracts = grokRuntimeContracts(document.hooks);
const hookConfigured = contracts.length > 0;
const hookScriptExists = hookConfigured ? await runtimeScriptsExist(contracts) : false;
const hookScriptExists = hookConfigured && await grokRuntimeFilesExist(contracts);
return {
installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY,
displayName: entry.name,
hookConfigured,
hookScriptExists,
configPath: document.configPath,
configPath: document.filePath,
};
},
};
+6 -1
View File
@@ -3,6 +3,7 @@ import { PROTECTED_RUNTIME_READER } from './runtime-reader-template.js';
export interface GuardScriptOptions {
readonly failClosed?: boolean;
readonly successOutput?: string;
readonly denyProtocol?: 'cursor' | 'grok';
}
export function generateGuardScript(
@@ -12,6 +13,7 @@ export function generateGuardScript(
): string {
const failClosed = options.failClosed ?? false;
const successOutput = options.successOutput ?? '';
const denyProtocol = options.denyProtocol ?? '';
return `#!/usr/bin/env node
'use strict';
@@ -25,6 +27,7 @@ const AGENT_NAME = ${JSON.stringify(agentName)};
const AGENT_ID = ${JSON.stringify(agentId)};
const SUCCESS_OUTPUT = ${JSON.stringify(successOutput)};
const FAIL_CLOSED = ${JSON.stringify(failClosed)};
const DENY_PROTOCOL = ${JSON.stringify(denyProtocol)};
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
@@ -42,12 +45,14 @@ function validateStatus(value, source) {
function blockAgent(status) {
const state = status === 'frozen' ? 'frozen' : 'revoked';
const message = 'Agent "' + AGENT_NAME + '" is ' + state + ' in Elydora.';
if (SUCCESS_OUTPUT) {
if (DENY_PROTOCOL === 'cursor') {
process.stdout.write(JSON.stringify({
permission: 'deny',
userMessage: message,
agentMessage: message + ' Tool execution is blocked.',
}) + '\\n');
} else if (DENY_PROTOCOL === 'grok') {
process.stdout.write(JSON.stringify({ decision: 'deny', reason: message }) + '\\n');
}
process.stderr.write('[Elydora guard] ' + message + ' Tool execution blocked.\\n');
process.exitCode = 2;
@@ -0,0 +1,212 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export const VALID_PRIVATE_KEY = Buffer.alloc(32).toString('base64url');
export const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/grok.js')).href;
export const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
export const contractModuleUrl = pathToFileURL(path.resolve('dist/plugins/grok-contract.js')).href;
export const ioModuleUrl = pathToFileURL(path.resolve('dist/plugins/grok-io.js')).href;
export const installationModuleUrl = pathToFileURL(
path.resolve('dist/plugins/grok-installation.js'),
).href;
export const cliPath = path.resolve('dist/cli.js');
export function runNode(args, env, cwd, input = '', unset = []) {
return new Promise((resolve, reject) => {
const childEnv = { ...process.env, ...env };
for (const key of unset) delete childEnv[key];
const child = spawn(process.execPath, args, {
cwd,
env: childEnv,
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
export function runGrokHook(command, input, fixture, environment = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, {
shell: true,
cwd: fixture.projectDir,
env: {
...process.env,
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
GROK_HOME: fixture.grokHomeOverride ?? '',
...environment,
},
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
export function installConfig(fixture, overrides = {}) {
return {
agentName: 'grok',
orgId: 'org-1',
agentId: 'agent-1',
privateKey: VALID_PRIVATE_KEY,
kid: 'kid-1',
token: 'token-1',
baseUrl: fixture.baseUrl,
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
...overrides,
};
}
export async function runPlugin(fixture, method, argument, environment = {}) {
const source = `
import { grokPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await grokPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
const env = {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
...environment,
};
const unset = [];
if (fixture.grokHomeOverride === undefined) unset.push('GROK_HOME');
else env.GROK_HOME = fixture.grokHomeOverride;
return runNode(
['--input-type=module', '--eval', source],
env,
fixture.projectDir,
'',
unset,
);
}
async function writeOptional(filePath, contents) {
if (contents === undefined) return;
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, contents, { encoding: 'utf-8', mode: 0o600 });
}
export async function createFixture({
baseUrl = 'http://127.0.0.1:9',
config,
explicitGrokHome = true,
} = {}) {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-grok-'));
const homeDir = path.join(rootDir, "home with spaces and 'quote %GROK_HOOK_EVENT%");
const projectDir = path.join(rootDir, 'project with spaces');
const grokHome = explicitGrokHome
? path.join(homeDir, 'custom grok')
: path.join(homeDir, '.grok');
const configPath = path.join(grokHome, 'hooks', 'elydora-audit.json');
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(projectDir, { recursive: true });
await writeOptional(configPath, config);
const fixture = {
agentDir,
baseUrl,
configPath,
grokHome,
grokHomeOverride: explicitGrokHome ? grokHome : undefined,
guardScriptPath,
homeDir,
hookScriptPath,
projectDir,
rootDir,
install(overrides = {}) {
return runPlugin(this, 'install', installConfig(this, overrides));
},
async close() {
await rm(rootDir, { recursive: true, force: true });
},
};
return fixture;
}
export async function readGrokConfig(filePath) {
const raw = await readFile(filePath, 'utf-8');
return { raw, config: JSON.parse(raw) };
}
export function managedHandler(config, event) {
for (const group of [...(config.hooks?.[event] ?? [])].reverse()) {
if (Object.keys(group).length !== 1 || !Array.isArray(group.hooks)) continue;
const handler = group.hooks.findLast((candidate) => (
Object.keys(candidate).sort().join('|') === 'command|timeout|type'
&& candidate.type === 'command'
&& candidate.timeout === 10
));
if (handler) return handler;
}
return undefined;
}
export function assertManagedHandler(handler) {
assert(handler);
assert.deepEqual(Object.keys(handler).sort(), ['command', 'timeout', 'type']);
assert.equal(handler.type, 'command');
assert.equal(handler.timeout, 10);
if (process.platform === 'win32') {
assert.match(handler.command, /^"[^"\r\n]+powershell\.exe" .* -EncodedCommand /i);
} else {
assert.match(handler.command, /^'[^']*node[^']*' /i);
}
}
export function legacyCommand(scriptPath) {
if (process.platform === 'win32') return `"${process.execPath}" "${scriptPath}"`;
const quote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
return `${quote(process.execPath)} ${quote(scriptPath)}`;
}
export async function startApiServer({ status = 'active', operationStatus = 201 } = {}) {
const requests = [];
const server = http.createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8');
requests.push({ headers: request.headers, method: request.method, url: request.url, raw });
if (request.method === 'GET') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ agent: { status } }));
return;
}
response.writeHead(operationStatus, { 'Content-Type': 'application/json' });
response.end(operationStatus >= 200 && operationStatus < 300
? JSON.stringify({ operation: { accepted: true } })
: JSON.stringify({ error: { code: 'UPSTREAM_FAILURE', message: 'failed' } }));
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
close: () => new Promise((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
}),
};
}
+201 -405
View File
@@ -1,146 +1,33 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import {
assertManagedHandler,
cliPath,
createFixture,
legacyCommand,
managedHandler,
readGrokConfig,
registryModuleUrl,
runNode,
runPlugin,
} from '../test-support/grok-test-helpers.mjs';
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/grok.js')).href;
const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
const cliPath = path.resolve('dist/cli.js');
function runNode(args, env, input = '', unset = [], cwd) {
return new Promise((resolve, reject) => {
const childEnv = { ...process.env, ...env };
for (const key of unset) delete childEnv[key];
const child = spawn(process.execPath, args, {
cwd,
env: childEnv,
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
function runCommand(command, input = '') {
return new Promise((resolve, reject) => {
const child = spawn(command, { shell: true, stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
function quotePosix(value) {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function quoteWindows(value) {
return `"${value.replaceAll('"', '\\"')}"`;
}
function buildCommand(scriptPath) {
const quote = process.platform === 'win32' ? quoteWindows : quotePosix;
return `${quote(process.execPath)} ${quote(scriptPath)}`;
}
async function runPlugin(fixture, method, argument) {
const script = `
import { grokPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await grokPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
const env = {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
};
if (fixture.explicitGrokHome) env.GROK_HOME = fixture.grokHome;
return runNode(
['--input-type=module', '--eval', script],
env,
'',
fixture.explicitGrokHome ? [] : ['GROK_HOME'],
fixture.homeDir,
function assertManagedTriple(config) {
assertManagedHandler(managedHandler(config, 'PreToolUse'));
assertManagedHandler(managedHandler(config, 'PostToolUse'));
assertManagedHandler(managedHandler(config, 'PostToolUseFailure'));
assert.equal(
managedHandler(config, 'PostToolUse').command,
managedHandler(config, 'PostToolUseFailure').command,
);
}
async function createFixture({
config,
explicitGrokHome = true,
guardSource,
hookSource,
} = {}) {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-grok-'));
const grokHome = explicitGrokHome ? path.join(homeDir, 'custom-grok') : path.join(homeDir, '.grok');
const configPath = path.join(grokHome, 'hooks', 'elydora-audit.json');
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(agentDir, { recursive: true });
await writeFile(
guardScriptPath,
guardSource ?? "process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n",
);
await writeFile(hookScriptPath, hookSource ?? 'process.exit(0);\n');
await writeFile(path.join(agentDir, 'config.json'), JSON.stringify({
agent_id: 'agent-1',
agent_name: 'grok',
}));
if (config !== undefined) {
await mkdir(path.dirname(configPath), { recursive: true });
await writeFile(configPath, typeof config === 'string' ? config : JSON.stringify(config, null, 2));
}
const fixture = {
agentDir,
configPath,
explicitGrokHome,
grokHome,
guardScriptPath,
homeDir,
hookScriptPath,
async close() {
await rm(homeDir, { recursive: true, force: true });
},
};
fixture.installResult = await runPlugin(fixture, 'install', {
agentName: 'grok',
agentId: 'agent-1',
baseUrl: 'https://api.elydora.com',
guardScriptPath,
hookScriptPath,
});
return fixture;
}
function managedHandler(config, event, scriptName) {
for (const group of config.hooks?.[event] ?? []) {
if (Object.hasOwn(group, 'matcher')) continue;
const handler = group.hooks.find((candidate) => candidate.command?.includes(scriptName));
if (handler) return handler;
}
return undefined;
}
function assertStrictHandler(handler) {
assert.deepEqual(Object.keys(handler).sort(), ['command', 'timeout', 'type']);
assert.equal(handler.type, 'command');
assert.equal(handler.timeout, 10);
}
test('Grok Build is registered in the SDK and CLI', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('grok'), {
@@ -148,71 +35,63 @@ test('Grok Build is registered in the SDK and CLI', async () => {
configDir: '~/.grok/hooks',
configFile: 'elydora-audit.json',
});
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-grok-cli-'));
const fixture = await createFixture();
try {
const result = await runNode(['--no-warnings', cliPath, 'status'], {
HOME: homeDir,
USERPROFILE: homeDir,
GROK_HOME: path.join(homeDir, 'custom-grok'),
}, '', [], homeDir);
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
GROK_HOME: fixture.grokHome,
}, fixture.projectDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /Grok Build \(grok\)/);
} finally {
await rm(homeDir, { recursive: true, force: true });
}
});
test('Grok install preserves native user config and is idempotent', async () => {
const existing = {
schemaVersion: 1,
hooks: {
SessionStart: [{
matcher: 'startup',
hooks: [{ type: 'http', url: 'https://example.test/hook', timeout: 5, headers: { x: 'keep' } }],
label: 'keep group metadata',
}],
PreToolUse: [{
matcher: 'Bash',
hooks: [{ type: 'command', command: 'existing-command', timeout: 5 }],
}],
},
};
const fixture = await createFixture({ config: existing });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
assert.match(fixture.installResult.stdout, /global PreToolUse and PostToolUse hooks installed/i);
const second = await runPlugin(fixture, 'install', {
agentName: 'grok',
agentId: 'agent-1',
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
});
assert.equal(second.code, 0, second.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
assert.equal(config.schemaVersion, 1);
assert.deepEqual(config.hooks.SessionStart, existing.hooks.SessionStart);
assert.deepEqual(config.hooks.PreToolUse[0], existing.hooks.PreToolUse[0]);
assert.equal(config.hooks.PreToolUse.length, 2);
assert.equal(config.hooks.PostToolUse.length, 1);
assert.deepEqual(Object.keys(config.hooks.PreToolUse[1]), ['hooks']);
assertStrictHandler(managedHandler(config, 'PreToolUse', 'guard.js'));
assertStrictHandler(managedHandler(config, 'PostToolUse', 'hook.js'));
await assert.rejects(readFile(path.join(fixture.homeDir, '.grok', 'hooks', 'elydora-audit.json')), {
code: 'ENOENT',
});
await assert.rejects(readFile(path.join(fixture.homeDir, '.claude', 'settings.json')), { code: 'ENOENT' });
await assert.rejects(readFile(path.join(fixture.homeDir, '.cursor', 'hooks.json')), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Grok treats an empty home override as the official default', async () => {
test('Grok installs an exact managed triple and preserves valid user hooks', async () => {
const existing = {
schemaVersion: 1,
hooks: {
SessionStart: [{
hooks: [{ type: 'http', url: 'https://example.test/hook', timeout: 0 }],
label: 'keep group metadata',
}],
PreToolUse: [{
matcher: 'Bash|run_terminal_command',
hooks: [{ type: 'command', command: 'existing-command', timeout: 5 }],
}],
},
};
const source = `${JSON.stringify(existing, null, 2)}\n`;
const fixture = await createFixture({ config: source });
try {
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /PostToolUseFailure hooks installed/i);
const firstDocument = await readGrokConfig(fixture.configPath);
assert.equal(firstDocument.config.schemaVersion, 1);
assert.deepEqual(firstDocument.config.hooks.SessionStart, existing.hooks.SessionStart);
assert.deepEqual(firstDocument.config.hooks.PreToolUse[0], existing.hooks.PreToolUse[0]);
assert.equal(firstDocument.config.hooks.PreToolUse.length, 2);
assertManagedTriple(firstDocument.config);
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
assert.equal(await readFile(fixture.configPath, 'utf-8'), firstDocument.raw);
await assertMissing(path.join(fixture.homeDir, '.grok', 'hooks', 'elydora-audit.json'));
await assertMissing(path.join(fixture.homeDir, '.claude', 'settings.json'));
await assertMissing(path.join(fixture.homeDir, '.cursor', 'hooks.json'));
} finally {
await fixture.close();
}
});
test('empty GROK_HOME resolves to the documented default', async () => {
const fixture = await createFixture({ explicitGrokHome: false });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
fixture.explicitGrokHome = true;
fixture.grokHome = '';
assert.equal((await fixture.install()).code, 0);
fixture.grokHomeOverride = '';
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
@@ -221,240 +100,157 @@ test('Grok treats an empty home override as the official default', async () => {
}
});
test('Grok commands block freezes and forward the official payload byte-for-byte', async () => {
const capturePath = path.join(os.tmpdir(), `elydora-grok-event-${process.pid}-${Date.now()}.json`);
const hookSource = `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => fs.writeFileSync(${JSON.stringify(capturePath)}, Buffer.concat(chunks)));
`;
const fixture = await createFixture({ hookSource });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
const guard = managedHandler(config, 'PreToolUse', 'guard.js');
const audit = managedHandler(config, 'PostToolUse', 'hook.js');
const prePayload = JSON.stringify({
hookEventName: 'PreToolUse',
sessionId: 'session-1',
cwd: fixture.homeDir,
workspaceRoot: fixture.homeDir,
toolName: 'Bash',
toolInput: { command: 'echo test' },
});
const guardResult = await runCommand(guard.command, prePayload);
assert.equal(guardResult.code, 2);
assert.match(guardResult.stderr, /Agent is frozen by Elydora/);
const postPayload = JSON.stringify({
hookEventName: 'PostToolUse',
sessionId: 'session-1',
cwd: fixture.homeDir,
workspaceRoot: fixture.homeDir,
toolName: 'Bash',
toolInput: { command: 'echo test' },
toolResult: { output: 'test' },
});
const auditResult = await runCommand(audit.command, postPayload);
assert.equal(auditResult.code, 0, auditResult.stderr);
assert.equal(await readFile(capturePath, 'utf-8'), postPayload);
} finally {
await fixture.close();
await rm(capturePath, { force: true });
}
});
test('Grok status requires a complete hook pair and both runtime files', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
let status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
delete config.hooks.PostToolUse;
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
await runPlugin(fixture, 'install', {
agentName: 'grok',
agentId: 'agent-1',
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
});
await rm(fixture.guardScriptPath);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
} finally {
await fixture.close();
}
});
test('Grok uninstall removes exact ownership and preserves mixed user handlers', async () => {
const fixture = await createFixture({ config: { owner: 'user', hooks: { Notification: [] } } });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
config.hooks.PreToolUse.at(-1).hooks.push({ type: 'command', command: 'user-command', timeout: 10 });
config.hooks.PreToolUse.push({ hooks: [{
type: 'command',
command: buildCommand(path.join(fixture.agentDir, 'guard.js.backup')),
timeout: 10,
}] });
config.hooks.PreToolUse.push({ hooks: [{
type: 'command',
command: buildCommand(path.join(fixture.homeDir, '.elydora', 'agent-10', 'guard.js')),
timeout: 10,
}] });
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
const uninstallId = process.platform === 'win32' ? 'AGENT-1' : 'agent-1';
const result = await runPlugin(fixture, 'uninstall', uninstallId);
assert.equal(result.code, 0, result.stderr);
const remaining = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
assert.equal(remaining.owner, 'user');
assert.equal(remaining.hooks.PreToolUse.length, 3);
assert.equal(remaining.hooks.PreToolUse[0].hooks[0].command, 'user-command');
assert(remaining.hooks.PreToolUse.some((group) => group.hooks[0].command.includes('guard.js.backup')));
assert(remaining.hooks.PreToolUse.some((group) => group.hooks[0].command.includes('agent-10')));
assert.equal(remaining.hooks.PostToolUse, undefined);
assert.deepEqual(remaining.hooks.Notification, []);
} finally {
await fixture.close();
}
});
test('Grok install replaces stale Elydora handlers for every agent', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
for (const [event, scriptName] of [['PreToolUse', 'guard.js'], ['PostToolUse', 'hook.js']]) {
config.hooks[event].push({ hooks: [{
type: 'command',
command: buildCommand(path.join(fixture.homeDir, '.elydora', 'agent-old', scriptName)),
timeout: 10,
}] });
}
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
const result = await runPlugin(fixture, 'install', {
agentName: 'grok',
agentId: 'agent-1',
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
});
assert.equal(result.code, 0, result.stderr);
const current = await readFile(fixture.configPath, 'utf-8');
assert.doesNotMatch(current, /agent-old/);
assert.equal(JSON.parse(current).hooks.PreToolUse.length, 1);
assert.equal(JSON.parse(current).hooks.PostToolUse.length, 1);
} finally {
await fixture.close();
}
});
test('Grok uninstall preserves an untouched empty native group', async () => {
const fixture = await createFixture({ config: { owner: 'user' } });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
config.hooks.PreToolUse.unshift({ hooks: [], label: 'keep empty group' });
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
const result = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
const remaining = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
assert.deepEqual(remaining.hooks.PreToolUse, [{ hooks: [], label: 'keep empty group' }]);
assert.equal(remaining.hooks.PostToolUse, undefined);
} finally {
await fixture.close();
}
});
test('Grok uninstall removes a config owned entirely by Elydora', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const result = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
await assert.rejects(readFile(fixture.configPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Grok preserves malformed JSON for recovery', async () => {
test('Grok parses the hook file before creating runtime files', async () => {
const fixture = await createFixture({ config: '{ malformed' });
try {
assert.equal(fixture.installResult.code, 1);
assert.match(fixture.installResult.stderr, /parse Grok hooks config/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), '{ malformed');
} finally {
await fixture.close();
}
});
test('Grok rejects invalid native hook shapes before writing', async () => {
const cases = [
{ hooks: null },
{ hooks: { PreToolUse: null } },
{ hooks: { PreToolUse: [null] } },
{ hooks: { PreToolUse: [{ matcher: 1, hooks: [] }] } },
{ hooks: { PreToolUse: [{ hooks: null }] } },
{ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: '' }] }] } },
{ hooks: { PreToolUse: [{ hooks: [{ type: 'file', command: 'x' }] }] } },
{ hooks: { PreToolUse: [{ hooks: [{ type: 'http', url: '' }] }] } },
{ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: 0 }] }] } },
];
for (const existing of cases) {
const fixture = await createFixture({ config: existing });
try {
assert.equal(fixture.installResult.code, 1);
assert.deepEqual(JSON.parse(await readFile(fixture.configPath, 'utf-8')), existing);
} finally {
await fixture.close();
}
}
});
test('Grok install rejects missing runtimes before creating config', async () => {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-grok-missing-runtime-'));
const fixture = {
explicitGrokHome: true,
grokHome: path.join(homeDir, 'custom-grok'),
homeDir,
};
const configPath = path.join(fixture.grokHome, 'hooks', 'elydora-audit.json');
try {
const result = await runPlugin(fixture, 'install', {
agentName: 'grok',
agentId: 'agent-1',
guardScriptPath: path.join(homeDir, '.elydora', 'agent-1', 'guard.js'),
hookScriptPath: path.join(homeDir, '.elydora', 'agent-1', 'hook.js'),
});
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /runtime is missing/i);
await assert.rejects(readFile(configPath), { code: 'ENOENT' });
assert.match(result.stderr, /parse Grok user hooks/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), '{ malformed');
await assertMissing(fixture.agentDir);
} finally {
await rm(homeDir, { recursive: true, force: true });
await fixture.close();
}
});
test('Grok status surfaces malformed referenced runtime metadata', async () => {
test('Grok rejects inactive native hook shapes before writes', async (t) => {
const cases = [
['duplicate field', '{"hooks":{},"hooks":{}}', /duplicate field "hooks"/i],
['hooks shape', JSON.stringify({ hooks: null }), /field "hooks" must be an object/i],
['event shape', JSON.stringify({ hooks: { PreToolUse: null } }), /must be an array/i],
['group shape', JSON.stringify({ hooks: { PreToolUse: [null] } }), /group.*must be an object/i],
['matcher shape', JSON.stringify({ hooks: { PreToolUse: [{ matcher: 1, hooks: [] }] } }), /matcher must be a string/i],
['lifecycle matcher', JSON.stringify({ hooks: { SessionStart: [{ matcher: 'x', hooks: [] }] } }), /cannot declare a matcher/i],
['handler shape', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [null] }] } }), /handler.*must be an object/i],
['handler type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'file' }] }] } }), /unsupported type/i],
['command value', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: '' }] }] } }), /non-empty command/i],
['http value', JSON.stringify({ hooks: { PostToolUse: [{ hooks: [{ type: 'http', url: '' }] }] } }), /non-empty url/i],
['negative timeout', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: -1 }] }] } }), /non-negative integer/i],
['fraction timeout', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: 1.5 }] }] } }), /non-negative integer/i],
['environment shape', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', env: { A: 1 } }] }] } }), /env must map names to strings/i],
];
for (const [name, config, pattern] of cases) {
await t.test(name, async () => {
const fixture = await createFixture({ config });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, pattern);
assert.equal(await readFile(fixture.configPath, 'utf-8'), config);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
}
});
test('Grok migrates exact legacy commands and preserves ownership lookalikes', async () => {
const fixture = await createFixture();
const lookalike = `${legacyCommand(fixture.guardScriptPath)} --inspect`;
const legacy = {
hooks: {
PreToolUse: [
{ hooks: [{ type: 'command', command: legacyCommand(fixture.guardScriptPath), timeout: 10 }] },
{ hooks: [{ type: 'command', command: lookalike, timeout: 10 }] },
],
PostToolUse: [{ hooks: [{
type: 'command',
command: legacyCommand(fixture.hookScriptPath),
timeout: 10,
}] }],
},
};
try {
await mkdir(path.dirname(fixture.configPath), { recursive: true });
await writeFile(fixture.configPath, JSON.stringify(legacy, null, 2));
const install = await fixture.install();
assert.equal(install.code, 0, install.stderr);
let { config } = await readGrokConfig(fixture.configPath);
assertManagedTriple(config);
assert(config.hooks.PreToolUse.some((group) => group.hooks[0].command === lookalike));
const managedGroup = config.hooks.PreToolUse.at(-1);
managedGroup.hooks.push({ type: 'command', command: 'user-command', timeout: 10 });
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
const uninstall = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(uninstall.code, 0, uninstall.stderr);
config = (await readGrokConfig(fixture.configPath)).config;
assert.deepEqual(config.hooks.PreToolUse.flatMap((group) => group.hooks), [
{ type: 'command', command: lookalike, timeout: 10 },
{ type: 'command', command: 'user-command', timeout: 10 },
]);
assert.equal(config.hooks.PostToolUse, undefined);
assert.equal(config.hooks.PostToolUseFailure, undefined);
} finally {
await fixture.close();
}
});
test('Grok uninstall preserves user config and removes a fully managed file', async () => {
const userSource = `${JSON.stringify({ owner: 'user', hooks: { Notification: [] } }, null, 2)}\n`;
const userFixture = await createFixture({ config: userSource });
try {
assert.equal((await userFixture.install()).code, 0);
const result = await runPlugin(userFixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
assert.deepEqual((await readGrokConfig(userFixture.configPath)).config, {
owner: 'user',
hooks: { Notification: [] },
});
} finally {
await userFixture.close();
}
const managedFixture = await createFixture();
try {
assert.equal((await managedFixture.install()).code, 0);
const result = await runPlugin(managedFixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
await assertMissing(managedFixture.configPath);
} finally {
await managedFixture.close();
}
});
test('Grok status requires one complete triple, exact identity, and private key', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
await writeFile(path.join(fixture.agentDir, 'config.json'), '{ malformed');
const status = await runPlugin(fixture, 'status', null);
assert.equal((await fixture.install()).code, 0);
let status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
let { config } = await readGrokConfig(fixture.configPath);
delete config.hooks.PostToolUseFailure;
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
assert.equal((await fixture.install()).code, 0);
config = (await readGrokConfig(fixture.configPath)).config;
config.hooks.PostToolUseFailure.push(config.hooks.PostToolUseFailure.at(-1));
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
assert.equal((await fixture.install()).code, 0);
await writeFile(path.join(fixture.agentDir, 'private.key'), 'invalid');
status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /parse Elydora runtime config/i);
assert.match(status.stderr, /private key is invalid/i);
} finally {
await fixture.close();
}
});
test('Grok atomic writes leave no temporary files', async () => {
test('Grok installation leaves no transaction files', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
assert.equal((await readdir(path.dirname(fixture.configPath))).some((name) => name.endsWith('.tmp')), false);
assert.equal((await fixture.install()).code, 0);
for (const directory of [fixture.agentDir, path.dirname(fixture.configPath)]) {
const names = await readdir(directory);
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
} finally {
await fixture.close();
}
+139
View File
@@ -0,0 +1,139 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
createFixture,
managedHandler,
readGrokConfig,
runGrokHook,
startApiServer,
} from '../test-support/grok-test-helpers.mjs';
const prePayload = {
hookEventName: 'pre_tool_use',
sessionId: 'session-1',
cwd: 'C:/project',
workspaceRoot: 'C:/project',
toolName: 'run_terminal_command',
toolInput: { command: 'npm test' },
toolUseId: 'tool-use-1',
toolInputTruncated: false,
timestamp: '2026-07-19T12:00:00Z',
};
test('Grok runtimes enforce active state and preserve success and failure payloads', async () => {
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
const install = await fixture.install();
assert.equal(install.code, 0, install.stderr);
const { config } = await readGrokConfig(fixture.configPath);
const guard = managedHandler(config, 'PreToolUse');
const successAudit = managedHandler(config, 'PostToolUse');
const failureAudit = managedHandler(config, 'PostToolUseFailure');
const guardResult = await runGrokHook(
guard.command,
JSON.stringify(prePayload),
fixture,
{ GROK_HOOK_EVENT: 'injected-command-fragment' },
);
assert.equal(guardResult.code, 0, guardResult.stderr);
assert.equal(guardResult.stdout, '');
const successPayload = {
...prePayload,
hookEventName: 'post_tool_use',
toolResult: { output: 'tests passed' },
toolResultTruncated: false,
durationMs: 125,
};
const success = await runGrokHook(
successAudit.command,
JSON.stringify(successPayload),
fixture,
);
assert.equal(success.code, 0, success.stderr);
const failurePayload = {
...prePayload,
hookEventName: 'post_tool_use_failure',
toolResult: { error: 'command failed', exitCode: 1 },
toolResultTruncated: false,
durationMs: 40,
};
const failure = await runGrokHook(
failureAudit.command,
JSON.stringify(failurePayload),
fixture,
);
assert.equal(failure.code, 0, failure.stderr);
const operations = api.requests
.filter((request) => request.method === 'POST')
.map((request) => JSON.parse(request.raw));
assert.equal(operations.length, 2);
assert.deepEqual(operations[0].payload, successPayload);
assert.deepEqual(operations[1].payload, failurePayload);
assert.equal(operations[0].subject.session_id, 'session-1');
assert.equal(operations[1].action.tool, 'run_terminal_command');
} finally {
await fixture.close();
await api.close();
}
});
test('Grok guard emits official deny JSON and exit code 2 for frozen and revoked agents', async (t) => {
for (const status of ['frozen', 'revoked']) {
await t.test(status, async () => {
const api = await startApiServer({ status });
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { config } = await readGrokConfig(fixture.configPath);
const result = await runGrokHook(
managedHandler(config, 'PreToolUse').command,
JSON.stringify(prePayload),
fixture,
);
assert.equal(result.code, 2);
assert.match(result.stderr, new RegExp(status, 'i'));
const decision = JSON.parse(result.stdout);
assert.deepEqual(Object.keys(decision).sort(), ['decision', 'reason']);
assert.equal(decision.decision, 'deny');
assert.match(decision.reason, new RegExp(status, 'i'));
} finally {
await fixture.close();
await api.close();
}
});
}
});
test('Grok audit runtime records malformed input and API failures', async () => {
const api = await startApiServer({ operationStatus: 503 });
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { config } = await readGrokConfig(fixture.configPath);
const command = managedHandler(config, 'PostToolUse').command;
const malformed = await runGrokHook(command, '{ malformed', fixture);
assert.equal(malformed.code, 0);
assert.equal(malformed.stderr, '');
const upstream = await runGrokHook(command, JSON.stringify({
...prePayload,
hookEventName: 'post_tool_use',
toolResult: { output: 'test' },
}), fixture);
assert.equal(upstream.code, 0);
assert.equal(upstream.stderr, '');
const log = await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8');
assert.match(log, /invalid JSON/i);
assert.match(log, /HTTP 503/i);
} finally {
await fixture.close();
await api.close();
}
});
+168
View File
@@ -0,0 +1,168 @@
import assert from 'node:assert/strict';
import {
lstat,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
createFixture,
installConfig,
runPlugin,
} from '../test-support/grok-test-helpers.mjs';
async function createLink(t, target, linkPath, type) {
try {
await symlink(target, linkPath, type);
return true;
} catch (error) {
if (error?.code === 'EPERM') {
t.skip(`symbolic links unavailable: ${error.message}`);
return false;
}
throw error;
}
}
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
test('Grok rejects linked hook files before creating runtimes', async (t) => {
const fixture = await createFixture();
try {
const target = path.join(fixture.rootDir, 'hooks-target.json');
const source = '{"owner":"protected"}\n';
await mkdir(path.dirname(fixture.configPath), { recursive: true });
await writeFile(target, source, 'utf-8');
if (!await createLink(t, target, fixture.configPath, 'file')) return;
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /not a physical file/i);
assert.equal(await readFile(target, 'utf-8'), source);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
test('Grok rejects linked home, hooks, and runtime directories before writes', async (t) => {
for (const kind of ['home', 'hooks', 'runtime']) {
await t.test(kind, async (subtest) => {
const fixture = await createFixture();
try {
const target = path.join(fixture.rootDir, `${kind}-target`);
await mkdir(target, { recursive: true });
let linkPath;
if (kind === 'home') {
linkPath = fixture.grokHome;
await mkdir(path.join(target, 'hooks'), { recursive: true });
await mkdir(path.dirname(linkPath), { recursive: true });
} else if (kind === 'hooks') {
linkPath = path.dirname(fixture.configPath);
await mkdir(fixture.grokHome, { recursive: true });
} else {
linkPath = path.join(fixture.homeDir, '.elydora');
await mkdir(path.dirname(linkPath), { recursive: true });
}
if (!await createLink(subtest, target, linkPath, 'junction')) return;
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /physical directory/i);
const untouched = kind === 'runtime' ? fixture.configPath : fixture.agentDir;
await assertMissing(untouched);
} finally {
await fixture.close();
}
});
}
});
test('Grok status rejects a linked private key', async (t) => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const keyPath = path.join(fixture.agentDir, 'private.key');
const target = path.join(fixture.rootDir, 'private-key-target');
const source = await readFile(keyPath, 'utf-8');
await writeFile(target, source, 'utf-8');
await rm(keyPath);
if (!await createLink(t, target, keyPath, 'file')) return;
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /not a physical file/i);
assert.equal(await readFile(target, 'utf-8'), source);
} finally {
await fixture.close();
}
});
test('Grok rejects orphaned and mismatched runtime identity before hook writes', async () => {
for (const kind of ['orphaned', 'mismatched']) {
const fixture = await createFixture();
try {
await mkdir(fixture.agentDir, { recursive: true });
if (kind === 'orphaned') {
await writeFile(fixture.guardScriptPath, 'orphaned guard\n');
} else {
await writeFile(path.join(fixture.agentDir, 'config.json'), JSON.stringify({
agent_name: 'grok',
agent_id: 'another-agent',
}));
}
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(
result.stderr,
kind === 'orphaned' ? /identity cannot be verified/i : /identity does not match/i,
);
await assertMissing(fixture.configPath);
} finally {
await fixture.close();
}
}
});
test('Grok validates managed runtime inputs before creating hook files', async (t) => {
const fixture = await createFixture();
try {
for (const [name, overrides, pattern] of [
['agent name', { agentName: 'codex' }, /requires agentName grok/i],
['private key', { privateKey: 'invalid' }, /canonical 32-byte/i],
['API origin', { baseUrl: 'https://api.elydora.com/path?token=secret' }, /query parameters/i],
[
'runtime path',
{ guardScriptPath: path.join(fixture.homeDir, 'outside', 'guard.js') },
/managed agent directory/i,
],
]) {
await t.test(name, async () => {
const result = await runPlugin(fixture, 'install', installConfig(fixture, overrides));
assert.equal(result.code, 1);
assert.match(result.stderr, pattern);
await assertMissing(fixture.configPath);
await assertMissing(fixture.agentDir);
});
}
} finally {
await fixture.close();
}
});
test('Grok status surfaces malformed referenced runtime metadata', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
await writeFile(path.join(fixture.agentDir, 'config.json'), '{ malformed');
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /parse Elydora runtime config/i);
} finally {
await fixture.close();
}
});
+175
View File
@@ -0,0 +1,175 @@
import assert from 'node:assert/strict';
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
contractModuleUrl,
createFixture,
installationModuleUrl,
installConfig,
ioModuleUrl,
runNode,
} from '../test-support/grok-test-helpers.mjs';
const runtimeNames = ['guard.js', 'config.json', 'private.key', 'hook.js'];
async function snapshotFixture(fixture) {
const paths = [
...runtimeNames.map((name) => path.join(fixture.agentDir, name)),
fixture.configPath,
];
return new Map(await Promise.all(paths.map(async (filePath) => [
filePath,
await readFile(filePath, 'utf-8'),
])));
}
async function assertSnapshot(snapshot) {
for (const [filePath, contents] of snapshot) {
assert.equal(await readFile(filePath, 'utf-8'), contents, filePath);
}
}
async function assertNoTransactionFiles(fixture) {
for (const directory of [fixture.agentDir, path.dirname(fixture.configPath)]) {
const names = await readdir(directory);
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
}
function runFixtureSource(fixture, source, environment = {}) {
return runNode(
['--input-type=module', '--eval', source],
{
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
GROK_HOME: fixture.grokHome,
...environment,
},
fixture.projectDir,
);
}
const prepareInstallSource = `
import {
buildGrokGroup,
removeManagedGrokHooks,
renderGrokDocument,
} from ${JSON.stringify(contractModuleUrl)};
import { readGrokDocument } from ${JSON.stringify(ioModuleUrl)};
import {
preflightGrokInstallation,
prepareGrokInstallation,
} from ${JSON.stringify(installationModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_INSTALL_CONFIG);
const document = await readGrokDocument();
const paths = await preflightGrokInstallation(config, document.filePath);
const cleaned = removeManagedGrokHooks(document.hooks);
const hooks = {
...cleaned,
PreToolUse: [...(cleaned.PreToolUse ?? []), buildGrokGroup(paths.guardPath)],
PostToolUse: [...(cleaned.PostToolUse ?? []), buildGrokGroup(paths.auditPath)],
PostToolUseFailure: [
...(cleaned.PostToolUseFailure ?? []),
buildGrokGroup(paths.auditPath),
],
};
const prepared = await prepareGrokInstallation(
config,
renderGrokDocument(document, hooks),
);
`;
test('Grok restores its hook config and four runtime files after the final commit fails', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotFixture(fixture);
const source = `${prepareInstallSource}
import { rename } from 'node:fs/promises';
import { commitGrokInstallation } from ${JSON.stringify(installationModuleUrl)};
let calls = 0;
await commitGrokInstallation(prepared, async (from, to) => {
calls += 1;
if (calls === 5) throw new Error('injected Grok config failure');
await rename(from, to);
});
`;
const result = await runFixtureSource(fixture, source, {
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture, {
orgId: 'org-updated',
token: 'token-updated',
})),
});
assert.equal(result.code, 1);
assert.match(result.stderr, /injected Grok config failure/i);
await assertSnapshot(before);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Grok rejects concurrent config changes before committing runtime updates', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotFixture(fixture);
const concurrent = '{"owner":"concurrent"}\n';
const source = `${prepareInstallSource}
import { writeFile } from 'node:fs/promises';
import { commitGrokInstallation } from ${JSON.stringify(installationModuleUrl)};
await writeFile(document.filePath, process.env.ELYDORA_CONCURRENT, 'utf-8');
await commitGrokInstallation(prepared);
`;
const result = await runFixtureSource(fixture, source, {
ELYDORA_CONCURRENT: concurrent,
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture, { orgId: 'updated' })),
});
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during Grok Build installation/i);
for (const [filePath, contents] of before) {
if (filePath !== fixture.configPath) {
assert.equal(await readFile(filePath, 'utf-8'), contents, filePath);
}
}
assert.equal(await readFile(fixture.configPath, 'utf-8'), concurrent);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Grok uninstall keeps the original hook config when commit fails', async () => {
const fixture = await createFixture({ config: '{"owner":"user"}\n' });
try {
assert.equal((await fixture.install()).code, 0);
const before = await readFile(fixture.configPath, 'utf-8');
const source = `
import {
removeManagedGrokHooks,
renderGrokDocument,
} from ${JSON.stringify(contractModuleUrl)};
import { readGrokDocument } from ${JSON.stringify(ioModuleUrl)};
import {
commitGrokUninstall,
prepareGrokUninstall,
} from ${JSON.stringify(installationModuleUrl)};
const document = await readGrokDocument();
const prepared = await prepareGrokUninstall(renderGrokDocument(
document,
removeManagedGrokHooks(document.hooks, 'agent-1'),
));
await commitGrokUninstall(prepared, async () => {
throw new Error('injected Grok uninstall failure');
});
`;
const result = await runFixtureSource(fixture, source);
assert.equal(result.code, 1);
assert.match(result.stderr, /injected Grok uninstall failure/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), before);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
@@ -248,15 +248,22 @@ test('high-drift providers retain their verified hook contracts', async () => {
name: 'toolName',
input: 'toolInput',
session: 'sessionId',
call_id: 'toolUseId',
output: 'toolResult',
error: 'toolResult',
});
assert.deepEqual(providers.get('grok').config_paths, [
'$GROK_HOME/hooks/*.json',
'~/.grok/hooks/*.json',
'~/.grok/hooks-paths',
'.grok/hooks/*.json',
'.claude/settings.json',
'.cursor/hooks.json',
]);
assert.deepEqual(providers.get('grok').events, {
before_tool: 'PreToolUse',
after_tool: 'PostToolUse',
after_tool_failure: 'PostToolUseFailure',
});
assert.deepEqual(providers.get('grok').blocking, {
mechanism: 'exit_code_2',