fix(letta): harden official hook integration

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 18:05:48 -04:00
1 parent 69a75153f5
commit 53b08ec4a7
13 files changed
+1985 -146

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
import os from 'node:os';
import path from 'node:path';
interface ParsedArgument {
readonly value: string;
readonly next: number;
}
export interface LettaRuntimeReference {
readonly agentId: string;
readonly executablePath?: string;
readonly scriptPath: string;
}
export function sameLettaPath(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 sameLettaAgentId(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("'", "''")}'`;
}
export function buildLettaCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Letta Code hook commands require absolute executable and script paths');
}
if (process.platform === 'win32') {
return `& ${quotePowerShell(process.execPath)} ${quotePowerShell(scriptPath)}; exit $LASTEXITCODE`;
}
return `${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 parsePowerShellCommand(command: string): readonly [string, string] | undefined {
if (!command.startsWith('& ')) return undefined;
const executable = readPowerShellArgument(command, 2);
if (!executable || command[executable.next] !== ' ') return undefined;
const script = readPowerShellArgument(command, executable.next + 1);
if (!script || command.slice(script.next) !== '; exit $LASTEXITCODE') return undefined;
return [executable.value, script.value];
}
function legacyScriptPath(command: string): string | undefined {
if (!command.startsWith('node "') || !command.endsWith('"')) return undefined;
const scriptPath = command.slice(6, -1);
return scriptPath.includes('"') ? undefined : scriptPath;
}
function isNodeExecutable(filePath: string): boolean {
const basename = path.basename(filePath).toLowerCase();
return basename === 'node' || basename === 'node.exe';
}
function runtimeReference(
scriptPath: string,
scriptName: string,
executablePath?: string,
): LettaRuntimeReference | undefined {
if (!path.isAbsolute(scriptPath) || path.basename(scriptPath) !== scriptName) return undefined;
const agentDirectory = path.dirname(scriptPath);
if (!sameLettaPath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) {
return undefined;
}
const agentId = path.basename(agentDirectory);
if (!agentId || agentId === '.' || agentId === '..') return undefined;
return { agentId, executablePath, scriptPath };
}
export function lettaRuntimeReference(
command: string,
scriptName: string,
): LettaRuntimeReference | undefined {
const parsed = process.platform === 'win32'
? parsePowerShellCommand(command)
: parsePosixCommand(command);
if (parsed
&& path.isAbsolute(parsed[0])
&& isNodeExecutable(parsed[0])) {
return runtimeReference(parsed[1], scriptName, parsed[0]);
}
return undefined;
}
export function lettaLegacyRuntimeReference(
command: string,
scriptName: string,
): LettaRuntimeReference | undefined {
const legacy = legacyScriptPath(command);
return legacy ? runtimeReference(legacy, scriptName) : undefined;
}
+183
View File
@@ -0,0 +1,183 @@
import {
applyEdits,
modify,
type FormattingOptions,
type JSONPath,
} from 'jsonc-parser';
import type { FileSnapshot } from './managed-files.js';
import {
MANAGED_EVENTS,
managedLettaRemovals,
readLettaHooks,
type LettaGroup,
type LettaHooks,
type ManagedLettaEvent,
} from './letta-contract.js';
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
export type LettaDocumentKind = 'global' | 'project' | 'project-local';
export interface LettaDocument {
readonly kind: LettaDocumentKind;
readonly filePath: string;
readonly exists: boolean;
readonly raw: string;
readonly snapshot?: FileSnapshot;
readonly root: JsonObject;
readonly hooks: LettaHooks;
readonly hasHooksContainer: boolean;
}
export interface RenderedLettaDocument {
readonly document: LettaDocument;
readonly changed: boolean;
readonly next?: string;
}
interface DocumentOptions {
readonly kind: LettaDocumentKind;
readonly exists: boolean;
readonly filePath: string;
readonly raw: string;
readonly snapshot?: FileSnapshot;
}
export function lettaSourceLabel(kind: LettaDocumentKind): string {
switch (kind) {
case 'project': return 'Letta Code project settings';
case 'project-local': return 'Letta Code project-local settings';
default: return 'Letta Code global settings';
}
}
export function parseLettaDocument(options: DocumentOptions): LettaDocument {
const label = `${lettaSourceLabel(options.kind)} at ${options.filePath}`;
const root = parseStrictJsonObject(options.raw, label);
return {
...options,
root,
hooks: readLettaHooks(root.hooks),
hasHooksContainer: Object.hasOwn(root, 'hooks'),
};
}
export function createLettaDocument(
kind: LettaDocumentKind,
filePath: string,
): LettaDocument {
return parseLettaDocument({
kind,
exists: false,
filePath,
raw: '{}\n',
});
}
function formatting(raw: string): FormattingOptions {
const indentation = /\r?\n([ \t]+)\S/.exec(raw)?.[1];
const insertSpaces = !indentation?.includes('\t');
return {
eol: raw.includes('\r\n') ? '\r\n' : '\n',
insertSpaces,
tabSize: insertSpaces ? Math.max(1, indentation?.length ?? 2) : 1,
};
}
function change(
raw: string,
jsonPath: JSONPath,
value: unknown,
isArrayInsertion = false,
): string {
return applyEdits(raw, modify(raw, jsonPath, value, {
formattingOptions: formatting(raw),
isArrayInsertion,
}));
}
function currentDocument(document: LettaDocument, raw: string): LettaDocument {
return parseLettaDocument({
kind: document.kind,
exists: document.exists,
filePath: document.filePath,
raw,
snapshot: document.snapshot,
});
}
function groupsForEvent(hooks: LettaHooks, event: ManagedLettaEvent): LettaGroup[] {
const groups = hooks[event];
return Array.isArray(groups) ? groups : [];
}
function removeManagedEntries(
document: LettaDocument,
initial: string,
agentId?: string,
): string {
let raw = initial;
const removals = managedLettaRemovals(document.hooks, agentId);
for (const event of MANAGED_EVENTS) {
const eventRemovals = removals
.filter((removal) => removal.event === event)
.sort((left, right) => right.groupIndex - left.groupIndex);
for (const removal of eventRemovals) {
const groupPath: JSONPath = ['hooks', event, removal.groupIndex];
if (removal.removeGroup) {
raw = change(raw, groupPath, undefined);
continue;
}
for (const handlerIndex of [...removal.handlerIndexes].sort(
(left, right) => right - left,
)) {
raw = change(raw, [...groupPath, 'hooks', handlerIndex], undefined);
}
}
if (eventRemovals.length > 0) {
const current = currentDocument(document, raw);
if (groupsForEvent(current.hooks, event).length === 0) {
raw = change(raw, ['hooks', event], undefined);
}
}
}
const current = currentDocument(document, raw);
if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) {
raw = change(raw, ['hooks'], undefined);
}
return raw;
}
function appendGroup(
document: LettaDocument,
raw: string,
event: ManagedLettaEvent,
group: LettaGroup,
): string {
const current = currentDocument(document, raw);
const groups = groupsForEvent(current.hooks, event);
if (groups.length > 0) {
return change(raw, ['hooks', event, groups.length], group, true);
}
return change(raw, ['hooks', event], [group]);
}
export function renderLettaDocument(
document: LettaDocument,
agentId: string | undefined,
additions: ReadonlyMap<ManagedLettaEvent, LettaGroup>,
): RenderedLettaDocument {
let raw = removeManagedEntries(document, document.raw, agentId);
for (const event of MANAGED_EVENTS) {
const group = additions.get(event);
if (group) raw = appendGroup(document, raw, event, group);
}
const current = currentDocument(document, raw);
if (additions.size === 0 && Object.keys(current.root).length === 0) {
return { document, changed: true, next: undefined };
}
return { document, changed: raw !== document.raw, next: raw };
}
export function lettaDocumentLabel(document: LettaDocument): string {
return lettaSourceLabel(document.kind);
}
+281
View File
@@ -0,0 +1,281 @@
import {
buildLettaCommand,
lettaLegacyRuntimeReference,
lettaRuntimeReference,
sameLettaAgentId,
sameLettaPath,
type LettaRuntimeReference,
} from './letta-command.js';
import { isObject, type JsonObject } from './strict-json.js';
export const AGENT_KEY = 'letta';
export const GUARD_SCRIPT = 'guard.js';
export const AUDIT_SCRIPT = 'hook.js';
export const HOOK_TIMEOUT_MS = 10_000;
export const LETTA_AUDIT_OPTIONS = {
nativePayload: true,
failClosed: true,
} as const;
export const MANAGED_EVENTS = [
'PreToolUse',
'PostToolUse',
'PostToolUseFailure',
] as const;
const TOOL_EVENTS = new Set([
'PreToolUse',
'PostToolUse',
'PostToolUseFailure',
'PermissionRequest',
]);
const SIMPLE_EVENTS = new Set([
'UserPromptSubmit',
'Notification',
'Stop',
'SubagentStop',
'PreCompact',
'SessionStart',
'SessionEnd',
]);
const KNOWN_EVENTS = new Set([...TOOL_EVENTS, ...SIMPLE_EVENTS]);
export type ManagedLettaEvent = (typeof MANAGED_EVENTS)[number];
export interface LettaHandler extends JsonObject {
readonly type: 'command' | 'prompt';
readonly command?: string;
readonly prompt?: string;
readonly model?: string;
readonly timeout?: number;
readonly quiet?: boolean;
}
export interface LettaGroup extends JsonObject {
readonly matcher?: string;
readonly hooks: LettaHandler[];
}
export interface LettaHooks extends JsonObject {
readonly disabled?: boolean;
readonly [event: string]: LettaGroup[] | boolean | undefined;
}
export interface LettaRuntimeContract {
readonly agentId: string;
readonly guardPath: string;
readonly auditPath: string;
}
export interface ManagedLettaRemoval {
readonly event: ManagedLettaEvent;
readonly groupIndex: number;
readonly handlerIndexes: readonly number[];
readonly removeGroup: boolean;
}
function validateTimeout(value: unknown, label: string): void {
if (value === undefined) return;
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
throw new Error(`${label} timeout must be a non-negative finite number`);
}
}
function validateHandler(value: unknown, label: string): LettaHandler {
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (value.type !== 'command' && value.type !== 'prompt') {
throw new Error(`${label} has unsupported type "${String(value.type)}"`);
}
validateTimeout(value.timeout, label);
if (value.quiet !== undefined && typeof value.quiet !== 'boolean') {
throw new Error(`${label} quiet must be a boolean`);
}
if (value.type === 'command') {
if (typeof value.command !== 'string' || value.command.length === 0) {
throw new Error(`${label} requires a non-empty command`);
}
} else {
if (typeof value.prompt !== 'string' || value.prompt.length === 0) {
throw new Error(`${label} requires a non-empty prompt`);
}
if (value.model !== undefined && typeof value.model !== 'string') {
throw new Error(`${label} model must be a string`);
}
}
return value as LettaHandler;
}
function validateGroup(
value: unknown,
event: string,
index: number,
): LettaGroup {
const label = `Letta Code settings group hooks.${event}[${index}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (TOOL_EVENTS.has(event)) {
if (typeof value.matcher !== 'string') {
throw new Error(`${label} matcher must be a string`);
}
} else if (value.matcher !== undefined) {
throw new Error(`${label} matcher is unsupported 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, `${label}.hooks[${handlerIndex}]`)
)),
} as LettaGroup;
}
export function readLettaHooks(value: unknown): LettaHooks {
if (value === undefined) return {};
if (!isObject(value)) throw new Error('Letta Code settings field "hooks" must be an object');
if (value.disabled !== undefined && typeof value.disabled !== 'boolean') {
throw new Error('Letta Code settings field "hooks.disabled" must be a boolean');
}
const hooks: JsonObject = {};
for (const [event, groups] of Object.entries(value)) {
if (event === 'disabled') {
hooks.disabled = groups;
continue;
}
if (!KNOWN_EVENTS.has(event)) {
hooks[event] = groups;
continue;
}
if (!Array.isArray(groups)) {
throw new Error(`Letta Code settings field "hooks.${event}" must be an array`);
}
hooks[event] = groups.map((group, index) => validateGroup(group, event, index));
}
return hooks as LettaHooks;
}
export function buildLettaGroup(scriptPath: string): LettaGroup {
return {
matcher: '*',
hooks: [{
type: 'command',
command: buildLettaCommand(scriptPath),
timeout: HOOK_TIMEOUT_MS,
}],
};
}
function exactGroup(group: LettaGroup): boolean {
return Object.keys(group).sort().join('|') === 'hooks|matcher'
&& group.matcher === '*';
}
function exactCurrentReference(
handler: LettaHandler,
scriptName: string,
): LettaRuntimeReference | undefined {
return Object.keys(handler).sort().join('|') === 'command|timeout|type'
&& handler.type === 'command'
&& handler.timeout === HOOK_TIMEOUT_MS
&& typeof handler.command === 'string'
? lettaRuntimeReference(handler.command, scriptName)
: undefined;
}
function exactLegacyReference(
handler: LettaHandler,
scriptName: string,
): LettaRuntimeReference | undefined {
return Object.keys(handler).sort().join('|') === 'command|type'
&& handler.type === 'command'
&& typeof handler.command === 'string'
? lettaLegacyRuntimeReference(handler.command, scriptName)
: undefined;
}
function managedReference(
handler: LettaHandler,
scriptName: string,
): LettaRuntimeReference | undefined {
return exactCurrentReference(handler, scriptName)
?? exactLegacyReference(handler, scriptName);
}
const EVENT_CONTRACTS = [
['PreToolUse', GUARD_SCRIPT],
['PostToolUse', AUDIT_SCRIPT],
['PostToolUseFailure', AUDIT_SCRIPT],
] as const;
function eventGroups(hooks: LettaHooks, event: ManagedLettaEvent): LettaGroup[] {
const value = hooks[event];
return Array.isArray(value) ? value : [];
}
export function managedLettaRemovals(
hooks: LettaHooks,
agentId?: string,
): ManagedLettaRemoval[] {
const removals: ManagedLettaRemoval[] = [];
for (const [event, scriptName] of EVENT_CONTRACTS) {
eventGroups(hooks, event).forEach((group, groupIndex) => {
const handlerIndexes = group.hooks.flatMap((handler, handlerIndex) => {
const reference = managedReference(handler, scriptName);
return reference && (agentId === undefined || sameLettaAgentId(reference.agentId, agentId))
? [handlerIndex]
: [];
});
if (handlerIndexes.length > 0) {
removals.push({
event,
groupIndex,
handlerIndexes,
removeGroup: exactGroup(group) && handlerIndexes.length === group.hooks.length,
});
}
});
}
return removals;
}
function referencesForEvent(
groups: readonly LettaGroup[],
scriptName: string,
): Map<string, LettaRuntimeReference[]> {
const references = new Map<string, LettaRuntimeReference[]>();
for (const group of groups) {
if (!exactGroup(group)) continue;
for (const handler of group.hooks) {
const reference = exactCurrentReference(handler, scriptName);
if (!reference
|| !reference.executablePath
|| !sameLettaPath(reference.executablePath, process.execPath)) continue;
const key = process.platform === 'win32'
? reference.agentId.toLowerCase()
: reference.agentId;
references.set(key, [...(references.get(key) ?? []), reference]);
}
}
return references;
}
export function lettaRuntimeContracts(hooks: LettaHooks): LettaRuntimeContract[] {
const guards = referencesForEvent(eventGroups(hooks, 'PreToolUse'), GUARD_SCRIPT);
const posts = referencesForEvent(eventGroups(hooks, 'PostToolUse'), AUDIT_SCRIPT);
const failures = referencesForEvent(
eventGroups(hooks, 'PostToolUseFailure'),
AUDIT_SCRIPT,
);
const contracts: LettaRuntimeContract[] = [];
for (const [key, guard] of guards) {
const post = posts.get(key);
const failure = failures.get(key);
if (guard.length !== 1 || post?.length !== 1 || failure?.length !== 1) continue;
if (!sameLettaPath(post[0].scriptPath, failure[0].scriptPath)) continue;
contracts.push({
agentId: guard[0].agentId,
guardPath: guard[0].scriptPath,
auditPath: post[0].scriptPath,
});
}
return contracts;
}
+134
View File
@@ -0,0 +1,134 @@
import path from 'node:path';
import type { InstallConfig } from './base.js';
import {
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
LETTA_AUDIT_OPTIONS,
} from './letta-contract.js';
import { sameLettaPath } from './letta-command.js';
import {
lettaDocumentLabel,
type RenderedLettaDocument,
} from './letta-config.js';
import {
commitManagedInstallation,
preflightManagedInstallation,
prepareManagedInstallation,
type ManagedRuntimePaths,
type PreparedManagedInstallation,
type RenameFile,
} from './managed-installation.js';
import {
commitManagedTransaction,
prepareManagedFileChange,
type PreparedManagedTransaction,
} from './managed-transaction.js';
import {
requireLettaHooksEnabled,
type LettaSources,
} from './letta-sources.js';
const DISPLAY_NAME = 'Letta Code';
const SETTINGS_DIRECTORY_LABEL = 'Letta Code global configuration directory';
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
export type LettaRuntimePaths = ManagedRuntimePaths;
export type PreparedLettaInstallation = PreparedManagedInstallation;
export type { RenameFile };
export async function preflightLettaInstallation(
config: InstallConfig,
sources: LettaSources,
): Promise<LettaRuntimePaths> {
requireLettaHooksEnabled(sources);
return preflightManagedInstallation({
agentKey: AGENT_KEY,
hookLocations: [{
directoryLabel: SETTINGS_DIRECTORY_LABEL,
filePath: sources.global.filePath,
}],
config,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
function readOnlyPreconditions(sources: LettaSources, changedPath?: string) {
return sources.preconditions.filter((condition) => (
changedPath === undefined || !sameLettaPath(condition.filePath, changedPath)
));
}
export async function prepareLettaInstallation(
config: InstallConfig,
sources: LettaSources,
rendered: RenderedLettaDocument,
): Promise<PreparedLettaInstallation> {
await preflightLettaInstallation(config, sources);
const settingsSource = rendered.next ?? rendered.document.raw;
const prepared = await prepareManagedInstallation({
agentKey: AGENT_KEY,
displayName: DISPLAY_NAME,
hookSources: [{
directoryLabel: SETTINGS_DIRECTORY_LABEL,
label: lettaDocumentLabel(rendered.document),
filePath: rendered.document.filePath,
expectedSource: rendered.document.exists ? rendered.document.raw : undefined,
expectedSnapshot: rendered.document.snapshot,
source: settingsSource,
}],
config,
auditOptions: LETTA_AUDIT_OPTIONS,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
return {
...prepared,
transaction: {
...prepared.transaction,
preconditions: readOnlyPreconditions(sources, rendered.document.filePath),
},
};
}
export async function commitLettaInstallation(
prepared: PreparedLettaInstallation,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedInstallation(prepared, renameFile);
}
export async function prepareLettaUninstall(
sources: LettaSources,
rendered: RenderedLettaDocument,
): Promise<PreparedManagedTransaction> {
const change = rendered.changed
? await prepareManagedFileChange({
filePath: rendered.document.filePath,
label: lettaDocumentLabel(rendered.document),
next: rendered.next,
mode: 0o600,
maximumBytes: MAX_SOURCE_BYTES,
expectedSource: rendered.document.exists ? rendered.document.raw : undefined,
expectedSnapshot: rendered.document.snapshot,
verifyExpectedSource: true,
})
: undefined;
return {
displayName: DISPLAY_NAME,
operation: 'uninstall',
directories: [{
path: path.dirname(rendered.document.filePath),
label: SETTINGS_DIRECTORY_LABEL,
}],
changes: change ? [change] : [],
preconditions: readOnlyPreconditions(
sources,
rendered.changed ? rendered.document.filePath : undefined,
),
};
}
export async function commitLettaUninstall(
prepared: PreparedManagedTransaction,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedTransaction(prepared, renameFile);
}
+165
View File
@@ -0,0 +1,165 @@
import fsp from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import {
createLettaDocument,
lettaDocumentLabel,
lettaSourceLabel,
parseLettaDocument,
type LettaDocument,
type LettaDocumentKind,
} from './letta-config.js';
import {
inspectPhysicalDirectory,
readPhysicalFile,
type FileSnapshot,
} from './managed-files.js';
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
export interface LettaSourcePrecondition {
readonly filePath: string;
readonly label: string;
readonly maximumBytes: number;
readonly original?: FileSnapshot;
}
export interface LettaDisableControl {
readonly disabled: boolean;
readonly source?: LettaDocument;
}
export interface LettaSources {
readonly homeDirectory: string;
readonly global: LettaDocument;
readonly project: LettaDocument;
readonly projectLocal: LettaDocument;
readonly projectActive: boolean;
readonly disableControl: LettaDisableControl;
readonly preconditions: readonly LettaSourcePrecondition[];
}
function comparisonPath(filePath: string): string {
const resolved = path.resolve(filePath);
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
}
function hasCode(error: unknown, code: string): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
}
async function canonicalPath(filePath: string): Promise<string> {
try {
return await fsp.realpath(filePath);
} catch (error) {
if (hasCode(error, 'ENOENT') || hasCode(error, 'ENOTDIR')) return path.resolve(filePath);
throw new Error(`Resolve Letta Code path at ${filePath}: ${
error instanceof Error ? error.message : String(error)
}`, { cause: error instanceof Error ? error : new Error(String(error)) });
}
}
function resolveHomeDirectory(): string {
return path.resolve(process.env.HOME || os.homedir());
}
async function readDocument(
kind: LettaDocumentKind,
filePath: string,
): Promise<LettaDocument> {
const snapshot = await readPhysicalFile(filePath, lettaSourceLabel(kind), MAX_SOURCE_BYTES);
return snapshot
? parseLettaDocument({
kind,
exists: true,
filePath,
raw: snapshot.contents,
snapshot,
})
: createLettaDocument(kind, filePath);
}
function sourcePrecondition(document: LettaDocument): LettaSourcePrecondition {
return {
filePath: document.filePath,
label: lettaDocumentLabel(document),
maximumBytes: MAX_SOURCE_BYTES,
original: document.snapshot,
};
}
function deduplicatePreconditions(
values: readonly LettaSourcePrecondition[],
): LettaSourcePrecondition[] {
const result = new Map<string, LettaSourcePrecondition>();
for (const value of values) {
const key = comparisonPath(value.filePath);
if (!result.has(key)) result.set(key, value);
}
return [...result.values()];
}
function effectiveDisable(
global: LettaDocument,
project: LettaDocument,
projectLocal: LettaDocument,
projectActive: boolean,
): LettaDisableControl {
if (global.hooks.disabled === false) return { disabled: false, source: global };
if (global.hooks.disabled === true) return { disabled: true, source: global };
if (projectActive && project.hooks.disabled === true) {
return { disabled: true, source: project };
}
if (projectLocal.hooks.disabled === true) {
return { disabled: true, source: projectLocal };
}
return { disabled: false };
}
export async function readLettaSources(): Promise<LettaSources> {
const homeDirectory = resolveHomeDirectory();
const workspace = process.cwd();
const globalDirectory = path.join(homeDirectory, '.letta');
const projectDirectory = path.join(workspace, '.letta');
await inspectPhysicalDirectory(homeDirectory, 'Letta Code home directory');
await inspectPhysicalDirectory(globalDirectory, 'Letta Code global configuration directory');
if (comparisonPath(projectDirectory) !== comparisonPath(globalDirectory)) {
await inspectPhysicalDirectory(projectDirectory, 'Letta Code project configuration directory');
}
const globalPath = path.join(globalDirectory, 'settings.json');
const projectPath = path.join(projectDirectory, 'settings.json');
const projectLocalPath = path.join(projectDirectory, 'settings.local.json');
const [canonicalWorkspace, canonicalHome] = await Promise.all([
canonicalPath(workspace),
canonicalPath(homeDirectory),
]);
const projectActive = comparisonPath(canonicalWorkspace) !== comparisonPath(canonicalHome);
const global = await readDocument('global', globalPath);
const project = projectActive
? await readDocument('project', projectPath)
: createLettaDocument('project', projectPath);
const projectLocal = await readDocument('project-local', projectLocalPath);
const preconditions = deduplicatePreconditions([
sourcePrecondition(global),
...(projectActive ? [sourcePrecondition(project)] : []),
sourcePrecondition(projectLocal),
]);
return {
homeDirectory,
global,
project,
projectLocal,
projectActive,
disableControl: effectiveDisable(global, project, projectLocal, projectActive),
preconditions,
};
}
export function requireLettaHooksEnabled(sources: LettaSources): void {
if (!sources.disableControl.disabled) return;
const source = sources.disableControl.source;
const location = source
? `${lettaDocumentLabel(source)} at ${source.filePath}`
: 'effective settings';
throw new Error(`Letta Code hooks are disabled by hooks.disabled in ${location}`);
}
+63 -146
View File
@@ -1,173 +1,90 @@
import fsp from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
import {
AGENT_KEY,
LETTA_AUDIT_OPTIONS,
buildLettaGroup,
lettaRuntimeContracts,
type LettaGroup,
type ManagedLettaEvent,
} from './letta-contract.js';
import { renderLettaDocument } from './letta-config.js';
import {
commitLettaInstallation,
commitLettaUninstall,
preflightLettaInstallation,
prepareLettaInstallation,
prepareLettaUninstall,
} from './letta-installation.js';
import { managedRuntimeFilesExist } from './managed-runtime-status.js';
import { readLettaSources } from './letta-sources.js';
import { SUPPORTED_AGENTS } from './registry.js';
const AGENT_KEY = 'letta';
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
function resolveConfigDir(): string {
return entry.configDir.replace(/^~/, os.homedir());
function installedGroups(guardPath: string, auditPath: string): ReadonlyMap<
ManagedLettaEvent,
LettaGroup
> {
return new Map([
['PreToolUse', buildLettaGroup(guardPath)],
['PostToolUse', buildLettaGroup(auditPath)],
['PostToolUseFailure', buildLettaGroup(auditPath)],
]);
}
function resolveConfigPath(): string {
return path.join(resolveConfigDir(), entry.configFile);
}
function buildHookCommand(scriptPath: string): string {
return `node "${scriptPath}"`;
}
function isElydoraCommand(cmd: string, agentId?: string): boolean {
if (!cmd.includes('elydora')) return false;
if (agentId) {
return cmd.includes(agentId);
async function runtimeExists(
contracts: ReturnType<typeof lettaRuntimeContracts>,
): Promise<boolean> {
for (const contract of contracts) {
if (await managedRuntimeFilesExist(contract, AGENT_KEY, {
auditOptions: LETTA_AUDIT_OPTIONS,
})) return true;
}
return true;
}
function filterElydoraEntries(arr: Array<Record<string, unknown>>, agentId?: string): Array<Record<string, unknown>> {
return arr.filter((entry) => {
if (Array.isArray(entry.hooks)) {
const cmds = entry.hooks as Array<Record<string, unknown>>;
return !cmds.some((h) => typeof h.command === 'string' && isElydoraCommand(h.command, agentId));
}
if (typeof entry.command === 'string') {
return !isElydoraCommand(entry.command, agentId);
}
return true;
});
return false;
}
export const lettaPlugin: AgentPlugin = {
managesRuntime: true,
async preflightInstall(config: InstallConfig): Promise<void> {
const sources = await readLettaSources();
await preflightLettaInstallation(config, sources);
},
async install(config: InstallConfig): Promise<void> {
const configDir = resolveConfigDir();
await fsp.mkdir(configDir, { recursive: true });
const configPath = resolveConfigPath();
let settings: Record<string, unknown> = {};
try {
const raw = await fsp.readFile(configPath, 'utf-8');
settings = JSON.parse(raw);
} catch {
// File doesn't exist or isn't valid JSON — start fresh
}
if (!settings.hooks || typeof settings.hooks !== 'object') {
settings.hooks = {};
}
const hooks = settings.hooks as Record<string, unknown>;
// --- PreToolUse (guard — freeze enforcement) ---
if (!Array.isArray(hooks.PreToolUse)) {
hooks.PreToolUse = [];
}
const preFiltered = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>);
preFiltered.push({
matcher: '*',
hooks: [{ type: 'command', command: buildHookCommand(config.guardScriptPath) }],
});
hooks.PreToolUse = preFiltered;
// --- PostToolUse (audit logging) ---
if (!Array.isArray(hooks.PostToolUse)) {
hooks.PostToolUse = [];
}
const postFiltered = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>);
postFiltered.push({
matcher: '*',
hooks: [{ type: 'command', command: buildHookCommand(config.hookScriptPath) }],
});
hooks.PostToolUse = postFiltered;
settings.hooks = hooks;
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
const sources = await readLettaSources();
const paths = await preflightLettaInstallation(config, sources);
const rendered = renderLettaDocument(
sources.global,
undefined,
installedGroups(paths.guardPath, paths.auditPath),
);
await commitLettaInstallation(
await prepareLettaInstallation(config, sources, rendered),
);
console.log(` Letta Code hooks: ${sources.global.filePath}`);
console.log(' Letta Code verification: run /hooks and restart active sessions.');
},
async uninstall(agentId?: string): Promise<void> {
const configPath = resolveConfigPath();
let settings: Record<string, unknown>;
try {
const raw = await fsp.readFile(configPath, 'utf-8');
settings = JSON.parse(raw);
} catch {
return; // Nothing to uninstall
}
const hooks = settings.hooks as Record<string, unknown> | undefined;
if (!hooks) return;
if (Array.isArray(hooks.PreToolUse)) {
hooks.PreToolUse = filterElydoraEntries(hooks.PreToolUse as Array<Record<string, unknown>>, agentId);
}
if (Array.isArray(hooks.PostToolUse)) {
hooks.PostToolUse = filterElydoraEntries(hooks.PostToolUse as Array<Record<string, unknown>>, agentId);
}
await fsp.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
const sources = await readLettaSources();
const rendered = renderLettaDocument(sources.global, agentId, new Map());
if (!rendered.changed) return;
await commitLettaUninstall(await prepareLettaUninstall(sources, rendered));
},
async status(): Promise<PluginStatus> {
const configPath = resolveConfigPath();
let hookConfigured = false;
let hookScriptPath = '';
try {
const raw = await fsp.readFile(configPath, 'utf-8');
const settings = JSON.parse(raw);
const hooks = settings.hooks as Record<string, unknown> | undefined;
if (hooks) {
const checkArr = (arr: unknown) => {
if (!Array.isArray(arr)) return false;
return (arr as Array<Record<string, unknown>>).some((entry) => {
if (Array.isArray(entry.hooks)) {
return (entry.hooks as Array<Record<string, unknown>>).some(
(h) => typeof h.command === 'string' && h.command.includes('elydora'),
);
}
return typeof entry.command === 'string' && entry.command.includes('elydora');
});
};
hookConfigured = checkArr(hooks.PreToolUse) && checkArr(hooks.PostToolUse);
// Extract hook script path from PostToolUse command
if (hookConfigured && Array.isArray(hooks.PostToolUse)) {
for (const e of hooks.PostToolUse as Array<Record<string, unknown>>) {
if (Array.isArray(e.hooks)) {
for (const h of e.hooks as Array<Record<string, unknown>>) {
const cmd = h.command as string;
if (cmd && cmd.includes('elydora')) {
hookScriptPath = cmd.replace(/^node\s+"?/, '').replace(/"?\s*$/, '');
}
}
}
}
}
}
} catch {
// Config not readable
}
let hookScriptExists = false;
if (hookScriptPath) {
try {
await fsp.access(hookScriptPath);
hookScriptExists = true;
} catch {
// File doesn't exist
}
}
const sources = await readLettaSources();
const contracts = lettaRuntimeContracts(sources.global.hooks);
const hookConfigured = !sources.disableControl.disabled && contracts.length > 0;
const hookScriptExists = hookConfigured ? await runtimeExists(contracts) : false;
return {
installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY,
displayName: entry.name,
hookConfigured,
hookScriptExists,
configPath,
configPath: sources.global.filePath,
};
},
};