chore(node-sdk): audit providers kimi grok qwen letta

Command files use shell-command and managedScriptReference; runtime checks use managed-runtime-status; jsonc-edit.ts and parseCommentedJsonObject replace copied JSONC helpers.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
雷电芽衣 committed 2026-09-02 22:07:30 -04:00
1 parent db4a6ffb62
commit 3b5cbd6e17
24 files changed
+209 -1084

No files matched your search

+24 -5
View File
@@ -1,3 +1,4 @@
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
export const MAX_SECRET_BYTES = 64 * 1024; export const MAX_SECRET_BYTES = 64 * 1024;
@@ -16,12 +17,13 @@ export function hasCode(error: unknown, code: string): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === code; return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
} }
export function pathKey(filePath: string): string {
const resolved = path.resolve(filePath);
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
}
export function samePath(left: string, right: string): boolean { export function samePath(left: string, right: string): boolean {
const normalizedLeft = path.resolve(left); return pathKey(left) === pathKey(right);
const normalizedRight = path.resolve(right);
return process.platform === 'win32'
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
: normalizedLeft === normalizedRight;
} }
export function sameAgentId(left: unknown, right: string): boolean { export function sameAgentId(left: unknown, right: string): boolean {
@@ -29,3 +31,20 @@ export function sameAgentId(left: unknown, right: string): boolean {
? left.toLowerCase() === right.toLowerCase() ? left.toLowerCase() === right.toLowerCase()
: left === right); : left === right);
} }
export interface ManagedScriptReference {
readonly agentId: string;
readonly scriptPath: string;
}
// Accepts only ~/.elydora/<agentId>/<scriptName>.
export function managedScriptReference(
scriptPath: string,
scriptName: string,
): ManagedScriptReference | undefined {
if (!path.isAbsolute(scriptPath) || path.basename(scriptPath) !== scriptName) return undefined;
const agentDirectory = path.dirname(scriptPath);
if (!samePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) return undefined;
const agentId = path.basename(agentDirectory);
return agentId && agentId !== '.' && agentId !== '..' ? { agentId, scriptPath } : undefined;
}
+7 -28
View File
@@ -1,9 +1,5 @@
import { import type { JSONPath } from 'jsonc-parser';
applyEdits, import { changeJsonc } from './jsonc-edit.js';
modify,
type FormattingOptions,
type JSONPath,
} from 'jsonc-parser';
import type { FileSnapshot } from './managed-files.js'; import type { FileSnapshot } from './managed-files.js';
import type { DroidPolicyState } from './droid-policy.js'; import type { DroidPolicyState } from './droid-policy.js';
import { import {
@@ -158,23 +154,6 @@ function eventPath(document: DroidDocument, event: string): JSONPath {
return [...document.basePath, event]; return [...document.basePath, event];
} }
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, path: JSONPath, value: unknown, isArrayInsertion = false): string {
return applyEdits(raw, modify(raw, path, value, {
formattingOptions: formatting(raw),
isArrayInsertion,
}));
}
function currentDocument(document: DroidDocument, raw: string): DroidDocument { function currentDocument(document: DroidDocument, raw: string): DroidDocument {
return parseDocument({ return parseDocument({
exists: document.exists, exists: document.exists,
@@ -194,17 +173,17 @@ function removeManagedEntries(document: DroidDocument, raw: string, agentId?: st
for (const removal of eventRemovals) { for (const removal of eventRemovals) {
const groupPath = [...eventPath(document, event), removal.groupIndex]; const groupPath = [...eventPath(document, event), removal.groupIndex];
if (removal.removeGroup) { if (removal.removeGroup) {
raw = change(raw, groupPath, undefined); raw = changeJsonc(raw, groupPath, undefined);
continue; continue;
} }
for (const handlerIndex of [...removal.handlerIndexes].sort((left, right) => right - left)) { for (const handlerIndex of [...removal.handlerIndexes].sort((left, right) => right - left)) {
raw = change(raw, [...groupPath, 'hooks', handlerIndex], undefined); raw = changeJsonc(raw, [...groupPath, 'hooks', handlerIndex], undefined);
} }
} }
if (eventRemovals.length > 0) { if (eventRemovals.length > 0) {
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if ((current.hooks[event] ?? []).length === 0) { if ((current.hooks[event] ?? []).length === 0) {
raw = change(raw, eventPath(document, event), undefined); raw = changeJsonc(raw, eventPath(document, event), undefined);
} }
} }
} }
@@ -220,9 +199,9 @@ function appendGroup(
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (Object.prototype.hasOwnProperty.call(current.hooks, event)) { if (Object.prototype.hasOwnProperty.call(current.hooks, event)) {
const groups = current.hooks[event] ?? []; const groups = current.hooks[event] ?? [];
return change(raw, [...eventPath(document, event), groups.length], group, true); return changeJsonc(raw, [...eventPath(document, event), groups.length], group, true);
} }
return change(raw, eventPath(document, event), [group]); return changeJsonc(raw, eventPath(document, event), [group]);
} }
function hookFileIsEmpty(document: DroidDocument, raw: string): boolean { function hookFileIsEmpty(document: DroidDocument, raw: string): boolean {
+10 -85
View File
@@ -1,14 +1,4 @@
import { import type { JSONPath } from 'jsonc-parser';
applyEdits,
modify,
parse,
parseTree,
printParseErrorCode,
type FormattingOptions,
type JSONPath,
type Node,
type ParseError,
} from 'jsonc-parser';
import { import {
MANAGED_EVENTS, MANAGED_EVENTS,
managedGeminiRemovals, managedGeminiRemovals,
@@ -19,7 +9,8 @@ import {
type GeminiHooks, type GeminiHooks,
type ManagedGeminiEvent, type ManagedGeminiEvent,
} from './gemini-contract.js'; } from './gemini-contract.js';
import { isObject, type JsonObject } from './strict-json.js'; import { changeJsonc } from './jsonc-edit.js';
import { parseCommentedJsonObject, type JsonObject } from './strict-json.js';
export const GEMINI_OWNED_FILE_MARKER = '// Managed by Elydora'; export const GEMINI_OWNED_FILE_MARKER = '// Managed by Elydora';
@@ -46,53 +37,9 @@ interface DocumentOptions {
readonly raw: string; readonly raw: string;
} }
function rejectDuplicateKeys(
node: Node | undefined,
label: string,
location: string[] = [],
): void {
if (!node) return;
if (node.type === 'object') {
const keys = new Set<string>();
for (const property of node.children ?? []) {
const key = String(property.children?.[0]?.value);
const childLocation = [...location, key];
if (keys.has(key)) {
throw new Error(`${label} contains duplicate field "${childLocation.join('.')}"`);
}
keys.add(key);
rejectDuplicateKeys(property.children?.[1], label, childLocation);
}
return;
}
if (node.type === 'array') {
for (const child of node.children ?? []) rejectDuplicateKeys(child, label, location);
}
}
function parseJsoncObject(raw: string, label: string): JsonObject {
const errors: ParseError[] = [];
const value: unknown = parse(raw, errors, {
allowTrailingComma: false,
disallowComments: false,
});
if (errors.length > 0) {
const details = errors
.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
.join(', ');
throw new Error(`Failed to parse ${label}: ${details}`);
}
if (!isObject(value)) throw new Error(`${label} must contain a JSON object`);
rejectDuplicateKeys(parseTree(raw, [], {
allowTrailingComma: false,
disallowComments: false,
}), label);
return value;
}
export function parseGeminiDocument(options: DocumentOptions): GeminiDocument { export function parseGeminiDocument(options: DocumentOptions): GeminiDocument {
const label = `Gemini CLI user settings at ${options.filePath}`; const label = `Gemini CLI user settings at ${options.filePath}`;
const root = parseJsoncObject(options.raw, label); const root = parseCommentedJsonObject(options.raw, label);
const hasHooksContainer = Object.hasOwn(root, 'hooks'); const hasHooksContainer = Object.hasOwn(root, 'hooks');
return { return {
...options, ...options,
@@ -112,28 +59,6 @@ export function createGeminiDocument(filePath: string): GeminiDocument {
}); });
} }
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: GeminiDocument, raw: string): GeminiDocument { function currentDocument(document: GeminiDocument, raw: string): GeminiDocument {
return parseGeminiDocument({ return parseGeminiDocument({
exists: document.exists, exists: document.exists,
@@ -155,25 +80,25 @@ function removeManagedEntries(
for (const removal of eventRemovals) { for (const removal of eventRemovals) {
const groupPath: JSONPath = ['hooks', event, removal.groupIndex]; const groupPath: JSONPath = ['hooks', event, removal.groupIndex];
if (removal.removeGroup) { if (removal.removeGroup) {
raw = change(raw, groupPath, undefined); raw = changeJsonc(raw, groupPath, undefined);
continue; continue;
} }
for (const handlerIndex of [...removal.handlerIndexes].sort( for (const handlerIndex of [...removal.handlerIndexes].sort(
(left, right) => right - left, (left, right) => right - left,
)) { )) {
raw = change(raw, [...groupPath, 'hooks', handlerIndex], undefined); raw = changeJsonc(raw, [...groupPath, 'hooks', handlerIndex], undefined);
} }
} }
if (eventRemovals.length > 0) { if (eventRemovals.length > 0) {
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if ((current.hooks[event] ?? []).length === 0) { if ((current.hooks[event] ?? []).length === 0) {
raw = change(raw, ['hooks', event], undefined); raw = changeJsonc(raw, ['hooks', event], undefined);
} }
} }
} }
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) { if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) {
raw = change(raw, ['hooks'], undefined); raw = changeJsonc(raw, ['hooks'], undefined);
} }
return raw; return raw;
} }
@@ -186,9 +111,9 @@ function appendGroup(
): string { ): string {
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (current.hooks[event]) { if (current.hooks[event]) {
return change(raw, ['hooks', event, current.hooks[event].length], group, true); return changeJsonc(raw, ['hooks', event, current.hooks[event].length], group, true);
} }
return change(raw, ['hooks', event], [group]); return changeJsonc(raw, ['hooks', event], [group]);
} }
export function renderGeminiDocument( export function renderGeminiDocument(
+14 -143
View File
@@ -1,140 +1,21 @@
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { managedScriptReference, type ManagedScriptReference } from './common.js';
import {
encodedWindowsCommand,
isNodeExecutable,
parseEncodedWindowsCommand,
parseLegacyWindowsCommand,
parsePosixCommand,
posixSource,
} from './shell-command.js';
interface ParsedArgument { export type GrokRuntimeReference = ManagedScriptReference;
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 { export function buildGrokCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) { if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Grok hook commands require absolute executable and script paths'); throw new Error('Grok hook commands require absolute executable and script paths');
} }
return process.platform === 'win32' return process.platform === 'win32' ? encodedWindowsCommand(scriptPath) : posixSource(scriptPath);
? 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( export function grokRuntimeReference(
@@ -142,18 +23,8 @@ export function grokRuntimeReference(
scriptName: string, scriptName: string,
): GrokRuntimeReference | undefined { ): GrokRuntimeReference | undefined {
const parsed = parsePosixCommand(command) const parsed = parsePosixCommand(command)
?? parseWindowsCommand(command) ?? parseEncodedWindowsCommand(command)
?? parseLegacyWindowsCommand(command); ?? parseLegacyWindowsCommand(command);
if (!parsed if (!parsed || !path.isAbsolute(parsed[0]) || !isNodeExecutable(parsed[0])) return undefined;
|| !path.isAbsolute(parsed[0]) return managedScriptReference(parsed[1], scriptName);
|| !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] };
} }
+6 -2
View File
@@ -1,15 +1,19 @@
import { sameAgentId } from './common.js';
import { import {
buildGrokCommand, buildGrokCommand,
grokRuntimeReference, grokRuntimeReference,
sameGrokAgentId,
type GrokRuntimeReference, type GrokRuntimeReference,
} from './grok-command.js'; } from './grok-command.js';
import type { GuardScriptOptions } from './guard-template.js';
import type { HookScriptOptions } from './hook-template.js';
import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js'; import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js';
export const AGENT_KEY = 'grok'; export const AGENT_KEY = 'grok';
export const GUARD_SCRIPT = 'guard.js'; export const GUARD_SCRIPT = 'guard.js';
export const AUDIT_SCRIPT = 'hook.js'; export const AUDIT_SCRIPT = 'hook.js';
export const HOOK_TIMEOUT_SECONDS = 10; export const HOOK_TIMEOUT_SECONDS = 10;
export const GUARD_OPTIONS: GuardScriptOptions = { denyProtocol: 'grok' };
export const AUDIT_OPTIONS: HookScriptOptions = { nativePayload: true };
const MATCHER_REJECTING_EVENTS = new Set([ const MATCHER_REJECTING_EVENTS = new Set([
'SessionStart', 'SessionStart',
@@ -172,7 +176,7 @@ function removeFromGroups(
const handlers = group.hooks.filter((handler) => { const handlers = group.hooks.filter((handler) => {
const reference = managedReference(handler, scriptName); const reference = managedReference(handler, scriptName);
return !reference return !reference
|| (agentId !== undefined && !sameGrokAgentId(reference.agentId, agentId)); || (agentId !== undefined && !sameAgentId(reference.agentId, agentId));
}); });
if (handlers.length > 0) result.push({ hooks: handlers }); if (handlers.length > 0) result.push({ hooks: handlers });
} }
+7 -9
View File
@@ -2,7 +2,9 @@ import path from 'node:path';
import type { InstallConfig } from './base.js'; import type { InstallConfig } from './base.js';
import { import {
AGENT_KEY, AGENT_KEY,
AUDIT_OPTIONS,
AUDIT_SCRIPT, AUDIT_SCRIPT,
GUARD_OPTIONS,
GUARD_SCRIPT, GUARD_SCRIPT,
type RenderedGrokDocument, type RenderedGrokDocument,
} from './grok-contract.js'; } from './grok-contract.js';
@@ -24,14 +26,10 @@ const DISPLAY_NAME = 'Grok Build';
const HOOKS_DIRECTORY_LABEL = 'Grok hooks directory'; const HOOKS_DIRECTORY_LABEL = 'Grok hooks directory';
const HOOKS_LABEL = 'Grok user hooks'; const HOOKS_LABEL = 'Grok user hooks';
export type GrokRuntimePaths = ManagedRuntimePaths;
export type PreparedGrokInstallation = PreparedManagedInstallation;
export type { RenameFile };
export async function preflightGrokInstallation( export async function preflightGrokInstallation(
config: InstallConfig, config: InstallConfig,
hooksPath: string, hooksPath: string,
): Promise<GrokRuntimePaths> { ): Promise<ManagedRuntimePaths> {
return preflightManagedInstallation({ return preflightManagedInstallation({
agentKey: AGENT_KEY, agentKey: AGENT_KEY,
hookLocations: [{ directoryLabel: HOOKS_DIRECTORY_LABEL, filePath: hooksPath }], hookLocations: [{ directoryLabel: HOOKS_DIRECTORY_LABEL, filePath: hooksPath }],
@@ -42,7 +40,7 @@ export async function preflightGrokInstallation(
export async function prepareGrokInstallation( export async function prepareGrokInstallation(
config: InstallConfig, config: InstallConfig,
rendered: RenderedGrokDocument, rendered: RenderedGrokDocument,
): Promise<PreparedGrokInstallation> { ): Promise<PreparedManagedInstallation> {
const hooksSource = rendered.next ?? rendered.document.raw; const hooksSource = rendered.next ?? rendered.document.raw;
if (hooksSource === undefined) throw new Error('Grok hook configuration is missing'); if (hooksSource === undefined) throw new Error('Grok hook configuration is missing');
const prepared = await prepareManagedInstallation({ const prepared = await prepareManagedInstallation({
@@ -56,8 +54,8 @@ export async function prepareGrokInstallation(
source: hooksSource, source: hooksSource,
}], }],
config, config,
guardOptions: { denyProtocol: 'grok' }, guardOptions: GUARD_OPTIONS,
auditOptions: { nativePayload: true }, auditOptions: AUDIT_OPTIONS,
}, GUARD_SCRIPT, AUDIT_SCRIPT); }, GUARD_SCRIPT, AUDIT_SCRIPT);
const hooksDirectory = path.dirname(rendered.document.filePath); const hooksDirectory = path.dirname(rendered.document.filePath);
return { return {
@@ -73,7 +71,7 @@ export async function prepareGrokInstallation(
} }
export async function commitGrokInstallation( export async function commitGrokInstallation(
prepared: PreparedGrokInstallation, prepared: PreparedManagedInstallation,
renameFile?: RenameFile, renameFile?: RenameFile,
): Promise<void> { ): Promise<void> {
await commitManagedInstallation(prepared, renameFile); await commitManagedInstallation(prepared, renameFile);
+8 -74
View File
@@ -2,14 +2,15 @@ import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { import {
AGENT_KEY, AGENT_KEY,
AUDIT_OPTIONS,
GUARD_OPTIONS,
createGrokDocument, createGrokDocument,
parseGrokDocument, parseGrokDocument,
type GrokDocument, type GrokDocument,
type GrokRuntimeContract, type GrokRuntimeContract,
} from './grok-contract.js'; } from './grok-contract.js';
import { sameGrokAgentId, sameGrokPath } from './grok-command.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js'; import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { parseStrictJsonObject, type JsonObject } from './strict-json.js'; import { managedRuntimeFilesExist } from './managed-runtime-status.js';
const CONFIG_FILE = 'elydora-audit.json'; const CONFIG_FILE = 'elydora-audit.json';
@@ -36,82 +37,15 @@ export async function readGrokDocument(): Promise<GrokDocument> {
: createGrokDocument(filePath); : 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( export async function grokRuntimeFilesExist(
contracts: readonly GrokRuntimeContract[], contracts: readonly GrokRuntimeContract[],
): Promise<boolean> { ): Promise<boolean> {
for (const contract of contracts) { for (const contract of contracts) {
if (await runtimeContractExists(contract)) return true; const exists = await managedRuntimeFilesExist(contract, AGENT_KEY, {
guardOptions: GUARD_OPTIONS,
auditOptions: AUDIT_OPTIONS,
});
if (exists) return true;
} }
return false; return false;
} }
+23
View File
@@ -0,0 +1,23 @@
import { applyEdits, modify, type FormattingOptions, type JSONPath } from 'jsonc-parser';
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,
};
}
export function changeJsonc(
raw: string,
jsonPath: JSONPath,
value: unknown,
isArrayInsertion = false,
): string {
return applyEdits(raw, modify(raw, jsonPath, value, {
formattingOptions: formatting(raw),
isArrayInsertion,
}));
}
+14 -143
View File
@@ -1,140 +1,21 @@
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { managedScriptReference, type ManagedScriptReference } from './common.js';
import {
encodedWindowsCommand,
isNodeExecutable,
parseEncodedWindowsCommand,
parseLegacyWindowsCommand,
parsePosixCommand,
posixSource,
} from './shell-command.js';
interface ParsedArgument { export type KimiRuntimeReference = ManagedScriptReference;
readonly value: string;
readonly next: number;
}
export interface KimiRuntimeReference {
readonly agentId: string;
readonly scriptPath: string;
}
export function sameKimiPath(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 sameKimiAgentId(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 buildKimiCommand(scriptPath: string): string { export function buildKimiCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) { if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Kimi hook commands require absolute executable and script paths'); throw new Error('Kimi hook commands require absolute executable and script paths');
} }
return process.platform === 'win32' return process.platform === 'win32' ? encodedWindowsCommand(scriptPath) : posixSource(scriptPath);
? 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 kimiRuntimeReference( export function kimiRuntimeReference(
@@ -142,18 +23,8 @@ export function kimiRuntimeReference(
scriptName: string, scriptName: string,
): KimiRuntimeReference | undefined { ): KimiRuntimeReference | undefined {
const parsed = parsePosixCommand(command) const parsed = parsePosixCommand(command)
?? parseWindowsCommand(command) ?? parseEncodedWindowsCommand(command)
?? parseLegacyWindowsCommand(command); ?? parseLegacyWindowsCommand(command);
if (!parsed if (!parsed || !path.isAbsolute(parsed[0]) || !isNodeExecutable(parsed[0])) return undefined;
|| !path.isAbsolute(parsed[0]) return managedScriptReference(parsed[1], scriptName);
|| !path.isAbsolute(parsed[1])
|| !isNodeExecutable(parsed[0])
|| path.basename(parsed[1]) !== scriptName) return undefined;
const agentDirectory = path.dirname(parsed[1]);
if (!sameKimiPath(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] };
} }
+5 -12
View File
@@ -1,9 +1,10 @@
import { sameAgentId } from './common.js';
import { import {
buildKimiCommand, buildKimiCommand,
kimiRuntimeReference, kimiRuntimeReference,
sameKimiAgentId,
type KimiRuntimeReference, type KimiRuntimeReference,
} from './kimi-command.js'; } from './kimi-command.js';
import { isObject, type JsonObject } from './strict-json.js';
export const AGENT_KEY = 'kimi'; export const AGENT_KEY = 'kimi';
export const GUARD_SCRIPT = 'guard.js'; export const GUARD_SCRIPT = 'guard.js';
@@ -34,7 +35,7 @@ export const STABLE_EVENTS = new Set<string>([
]); ]);
export const LEGACY_EVENTS = new Set<string>(SHARED_EVENTS); export const LEGACY_EVENTS = new Set<string>(SHARED_EVENTS);
export type TomlObject = Record<string, unknown>; export type TomlObject = JsonObject;
export type ManagedKimiEvent = 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure'; export type ManagedKimiEvent = 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure';
export interface KimiHook extends TomlObject { export interface KimiHook extends TomlObject {
@@ -75,14 +76,6 @@ export interface KimiRuntimeContract {
readonly configPath: string; readonly configPath: string;
} }
function isObject(value: unknown): value is TomlObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function hasOwn(value: TomlObject, key: string): boolean {
return Object.prototype.hasOwnProperty.call(value, key);
}
function exactManagedKeys(hook: KimiHook): boolean { function exactManagedKeys(hook: KimiHook): boolean {
return Object.keys(hook).sort().join('|') === 'command|event|timeout'; return Object.keys(hook).sort().join('|') === 'command|event|timeout';
} }
@@ -194,7 +187,7 @@ async function renderHookSource(
if (document.usesHookTables) return patchToml(document.raw ?? '', nextRoot); if (document.usesHookTables) return patchToml(document.raw ?? '', nextRoot);
let base = document.raw ?? ''; let base = document.raw ?? '';
if (hasOwn(document.root, 'hooks')) { if (Object.hasOwn(document.root, 'hooks')) {
const rootWithoutHooks = { ...document.root }; const rootWithoutHooks = { ...document.root };
delete rootWithoutHooks.hooks; delete rootWithoutHooks.hooks;
base = patchToml(base, rootWithoutHooks); base = patchToml(base, rootWithoutHooks);
@@ -248,7 +241,7 @@ export function removeManagedKimiHooks(
if (!expected) return true; if (!expected) return true;
const managedId = managedAgentId(hook, expected[0], expected[1]); const managedId = managedAgentId(hook, expected[0], expected[1]);
if (!managedId) return true; if (!managedId) return true;
return agentId !== undefined && !sameKimiAgentId(managedId, agentId); return agentId !== undefined && !sameAgentId(managedId, agentId);
}); });
} }
+3 -7
View File
@@ -23,10 +23,6 @@ import {
const DISPLAY_NAME = 'Kimi'; const DISPLAY_NAME = 'Kimi';
export type KimiRuntimePaths = ManagedRuntimePaths;
export type PreparedKimiInstallation = PreparedManagedInstallation;
export type { RenameFile };
function hookLocations(documents: readonly KimiConfigDocument[]) { function hookLocations(documents: readonly KimiConfigDocument[]) {
return documents.map((document) => ({ return documents.map((document) => ({
directoryLabel: document.contract.directoryLabel, directoryLabel: document.contract.directoryLabel,
@@ -37,7 +33,7 @@ function hookLocations(documents: readonly KimiConfigDocument[]) {
export async function preflightKimiInstallation( export async function preflightKimiInstallation(
config: InstallConfig, config: InstallConfig,
documents: readonly KimiConfigDocument[], documents: readonly KimiConfigDocument[],
): Promise<KimiRuntimePaths> { ): Promise<ManagedRuntimePaths> {
return preflightManagedInstallation({ return preflightManagedInstallation({
agentKey: AGENT_KEY, agentKey: AGENT_KEY,
hookLocations: hookLocations(documents), hookLocations: hookLocations(documents),
@@ -56,7 +52,7 @@ function renderedSource(rendered: RenderedKimiDocument): string {
export async function prepareKimiInstallation( export async function prepareKimiInstallation(
config: InstallConfig, config: InstallConfig,
rendered: readonly RenderedKimiDocument[], rendered: readonly RenderedKimiDocument[],
): Promise<PreparedKimiInstallation> { ): Promise<PreparedManagedInstallation> {
return prepareManagedInstallation({ return prepareManagedInstallation({
agentKey: AGENT_KEY, agentKey: AGENT_KEY,
displayName: DISPLAY_NAME, displayName: DISPLAY_NAME,
@@ -73,7 +69,7 @@ export async function prepareKimiInstallation(
} }
export async function commitKimiInstallation( export async function commitKimiInstallation(
prepared: PreparedKimiInstallation, prepared: PreparedManagedInstallation,
renameFile?: RenameFile, renameFile?: RenameFile,
): Promise<void> { ): Promise<void> {
await commitManagedInstallation(prepared, renameFile); await commitManagedInstallation(prepared, renameFile);
+4 -88
View File
@@ -1,6 +1,7 @@
import fsp from 'node:fs/promises'; import fsp from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { asError, errorMessage, hasCode, samePath } from './common.js';
import { import {
AGENT_KEY, AGENT_KEY,
LEGACY_EVENTS, LEGACY_EVENTS,
@@ -10,23 +11,9 @@ import {
type KimiConfigDocument, type KimiConfigDocument,
type KimiContract, type KimiContract,
type KimiRuntimeContract, type KimiRuntimeContract,
type TomlObject,
} from './kimi-contract.js'; } from './kimi-contract.js';
import { sameKimiAgentId, sameKimiPath } from './kimi-command.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js'; import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { parseStrictJsonObject } from './strict-json.js'; import { managedRuntimeFilesExist } from './managed-runtime-status.js';
function asError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function hasCode(error: unknown, code: string): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
}
async function pathEntryExists(filePath: string, label: string): Promise<boolean> { async function pathEntryExists(filePath: string, label: string): Promise<boolean> {
try { try {
@@ -72,7 +59,7 @@ export async function resolveKimiContracts(): Promise<KimiContract[]> {
const legacyHome = path.join(home, '.kimi'); const legacyHome = path.join(home, '.kimi');
const stable = stableContract(path.join(stableHome, 'config.toml')); const stable = stableContract(path.join(stableHome, 'config.toml'));
const legacy = legacyContract(path.join(legacyHome, 'config.toml')); const legacy = legacyContract(path.join(legacyHome, 'config.toml'));
if (sameKimiPath(stable.configPath, legacy.configPath)) return [stable]; if (samePath(stable.configPath, legacy.configPath)) return [stable];
const stableDetected = explicitHome !== undefined const stableDetected = explicitHome !== undefined
|| await pathEntryExists(stableHome, 'Kimi Code home'); || await pathEntryExists(stableHome, 'Kimi Code home');
@@ -96,82 +83,11 @@ export async function readKimiDocuments(): Promise<KimiConfigDocument[]> {
return documents; return documents;
} }
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: TomlObject,
contract: KimiRuntimeContract,
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 (!sameKimiAgentId(agentId, contract.agentId) || config.agent_name !== AGENT_KEY) {
throw new Error(`Elydora runtime identity does not match Kimi 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: asError(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: KimiRuntimeContract): Promise<boolean> {
const runtimeRoot = path.join(os.homedir(), '.elydora');
const agentDirectory = path.dirname(contract.guardPath);
if (!sameKimiPath(path.dirname(agentDirectory), runtimeRoot)
|| !sameKimiPath(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 kimiRuntimeFilesExist( export async function kimiRuntimeFilesExist(
contracts: readonly KimiRuntimeContract[], contracts: readonly KimiRuntimeContract[],
): Promise<boolean> { ): Promise<boolean> {
for (const contract of contracts) { for (const contract of contracts) {
if (await runtimeContractExists(contract)) return true; if (await managedRuntimeFilesExist(contract, AGENT_KEY)) return true;
} }
return false; return false;
} }
+14 -112
View File
@@ -1,10 +1,12 @@
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { managedScriptReference } from './common.js';
interface ParsedArgument { import {
readonly value: string; isNodeExecutable,
readonly next: number; parsePosixCommand,
} parsePowerShellSource,
posixSource,
powerShellSource,
} from './shell-command.js';
export interface LettaRuntimeReference { export interface LettaRuntimeReference {
readonly agentId: string; readonly agentId: string;
@@ -12,88 +14,11 @@ export interface LettaRuntimeReference {
readonly scriptPath: 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 { export function buildLettaCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) { if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Letta Code hook commands require absolute executable and script paths'); throw new Error('Letta Code hook commands require absolute executable and script paths');
} }
if (process.platform === 'win32') { return process.platform === 'win32' ? powerShellSource(scriptPath) : posixSource(scriptPath);
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 { function legacyScriptPath(command: string): string | undefined {
@@ -102,39 +27,16 @@ function legacyScriptPath(command: string): string | undefined {
return scriptPath.includes('"') ? undefined : scriptPath; 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( export function lettaRuntimeReference(
command: string, command: string,
scriptName: string, scriptName: string,
): LettaRuntimeReference | undefined { ): LettaRuntimeReference | undefined {
const parsed = process.platform === 'win32' const parsed = process.platform === 'win32'
? parsePowerShellCommand(command) ? parsePowerShellSource(command)
: parsePosixCommand(command); : parsePosixCommand(command);
if (parsed if (!parsed || !path.isAbsolute(parsed[0]) || !isNodeExecutable(parsed[0])) return undefined;
&& path.isAbsolute(parsed[0]) const reference = managedScriptReference(parsed[1], scriptName);
&& isNodeExecutable(parsed[0])) { return reference ? { ...reference, executablePath: parsed[0] } : undefined;
return runtimeReference(parsed[1], scriptName, parsed[0]);
}
return undefined;
} }
export function lettaLegacyRuntimeReference( export function lettaLegacyRuntimeReference(
@@ -142,5 +44,5 @@ export function lettaLegacyRuntimeReference(
scriptName: string, scriptName: string,
): LettaRuntimeReference | undefined { ): LettaRuntimeReference | undefined {
const legacy = legacyScriptPath(command); const legacy = legacyScriptPath(command);
return legacy ? runtimeReference(legacy, scriptName) : undefined; return legacy ? managedScriptReference(legacy, scriptName) : undefined;
} }
+8 -34
View File
@@ -1,9 +1,5 @@
import { import type { JSONPath } from 'jsonc-parser';
applyEdits, import { changeJsonc } from './jsonc-edit.js';
modify,
type FormattingOptions,
type JSONPath,
} from 'jsonc-parser';
import type { FileSnapshot } from './managed-files.js'; import type { FileSnapshot } from './managed-files.js';
import { import {
MANAGED_EVENTS, MANAGED_EVENTS,
@@ -73,28 +69,6 @@ export function createLettaDocument(
}); });
} }
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 { function currentDocument(document: LettaDocument, raw: string): LettaDocument {
return parseLettaDocument({ return parseLettaDocument({
kind: document.kind, kind: document.kind,
@@ -124,25 +98,25 @@ function removeManagedEntries(
for (const removal of eventRemovals) { for (const removal of eventRemovals) {
const groupPath: JSONPath = ['hooks', event, removal.groupIndex]; const groupPath: JSONPath = ['hooks', event, removal.groupIndex];
if (removal.removeGroup) { if (removal.removeGroup) {
raw = change(raw, groupPath, undefined); raw = changeJsonc(raw, groupPath, undefined);
continue; continue;
} }
for (const handlerIndex of [...removal.handlerIndexes].sort( for (const handlerIndex of [...removal.handlerIndexes].sort(
(left, right) => right - left, (left, right) => right - left,
)) { )) {
raw = change(raw, [...groupPath, 'hooks', handlerIndex], undefined); raw = changeJsonc(raw, [...groupPath, 'hooks', handlerIndex], undefined);
} }
} }
if (eventRemovals.length > 0) { if (eventRemovals.length > 0) {
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (groupsForEvent(current.hooks, event).length === 0) { if (groupsForEvent(current.hooks, event).length === 0) {
raw = change(raw, ['hooks', event], undefined); raw = changeJsonc(raw, ['hooks', event], undefined);
} }
} }
} }
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) { if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) {
raw = change(raw, ['hooks'], undefined); raw = changeJsonc(raw, ['hooks'], undefined);
} }
return raw; return raw;
} }
@@ -156,9 +130,9 @@ function appendGroup(
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
const groups = groupsForEvent(current.hooks, event); const groups = groupsForEvent(current.hooks, event);
if (groups.length > 0) { if (groups.length > 0) {
return change(raw, ['hooks', event, groups.length], group, true); return changeJsonc(raw, ['hooks', event, groups.length], group, true);
} }
return change(raw, ['hooks', event], [group]); return changeJsonc(raw, ['hooks', event], [group]);
} }
export function renderLettaDocument( export function renderLettaDocument(
+4 -5
View File
@@ -1,9 +1,8 @@
import { sameAgentId, samePath } from './common.js';
import { import {
buildLettaCommand, buildLettaCommand,
lettaLegacyRuntimeReference, lettaLegacyRuntimeReference,
lettaRuntimeReference, lettaRuntimeReference,
sameLettaAgentId,
sameLettaPath,
type LettaRuntimeReference, type LettaRuntimeReference,
} from './letta-command.js'; } from './letta-command.js';
import { isObject, type JsonObject } from './strict-json.js'; import { isObject, type JsonObject } from './strict-json.js';
@@ -220,7 +219,7 @@ export function managedLettaRemovals(
eventGroups(hooks, event).forEach((group, groupIndex) => { eventGroups(hooks, event).forEach((group, groupIndex) => {
const handlerIndexes = group.hooks.flatMap((handler, handlerIndex) => { const handlerIndexes = group.hooks.flatMap((handler, handlerIndex) => {
const reference = managedReference(handler, scriptName); const reference = managedReference(handler, scriptName);
return reference && (agentId === undefined || sameLettaAgentId(reference.agentId, agentId)) return reference && (agentId === undefined || sameAgentId(reference.agentId, agentId))
? [handlerIndex] ? [handlerIndex]
: []; : [];
}); });
@@ -248,7 +247,7 @@ function referencesForEvent(
const reference = exactCurrentReference(handler, scriptName); const reference = exactCurrentReference(handler, scriptName);
if (!reference if (!reference
|| !reference.executablePath || !reference.executablePath
|| !sameLettaPath(reference.executablePath, process.execPath)) continue; || !samePath(reference.executablePath, process.execPath)) continue;
const key = process.platform === 'win32' const key = process.platform === 'win32'
? reference.agentId.toLowerCase() ? reference.agentId.toLowerCase()
: reference.agentId; : reference.agentId;
@@ -270,7 +269,7 @@ export function lettaRuntimeContracts(hooks: LettaHooks): LettaRuntimeContract[]
const post = posts.get(key); const post = posts.get(key);
const failure = failures.get(key); const failure = failures.get(key);
if (guard.length !== 1 || post?.length !== 1 || failure?.length !== 1) continue; if (guard.length !== 1 || post?.length !== 1 || failure?.length !== 1) continue;
if (!sameLettaPath(post[0].scriptPath, failure[0].scriptPath)) continue; if (!samePath(post[0].scriptPath, failure[0].scriptPath)) continue;
contracts.push({ contracts.push({
agentId: guard[0].agentId, agentId: guard[0].agentId,
guardPath: guard[0].scriptPath, guardPath: guard[0].scriptPath,
+5 -10
View File
@@ -6,7 +6,7 @@ import {
GUARD_SCRIPT, GUARD_SCRIPT,
LETTA_AUDIT_OPTIONS, LETTA_AUDIT_OPTIONS,
} from './letta-contract.js'; } from './letta-contract.js';
import { sameLettaPath } from './letta-command.js'; import { MAX_SOURCE_BYTES, samePath } from './common.js';
import { import {
lettaDocumentLabel, lettaDocumentLabel,
type RenderedLettaDocument, type RenderedLettaDocument,
@@ -31,16 +31,11 @@ import {
const DISPLAY_NAME = 'Letta Code'; const DISPLAY_NAME = 'Letta Code';
const SETTINGS_DIRECTORY_LABEL = 'Letta Code global configuration directory'; 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( export async function preflightLettaInstallation(
config: InstallConfig, config: InstallConfig,
sources: LettaSources, sources: LettaSources,
): Promise<LettaRuntimePaths> { ): Promise<ManagedRuntimePaths> {
requireLettaHooksEnabled(sources); requireLettaHooksEnabled(sources);
return preflightManagedInstallation({ return preflightManagedInstallation({
agentKey: AGENT_KEY, agentKey: AGENT_KEY,
@@ -54,7 +49,7 @@ export async function preflightLettaInstallation(
function readOnlyPreconditions(sources: LettaSources, changedPath?: string) { function readOnlyPreconditions(sources: LettaSources, changedPath?: string) {
return sources.preconditions.filter((condition) => ( return sources.preconditions.filter((condition) => (
changedPath === undefined || !sameLettaPath(condition.filePath, changedPath) changedPath === undefined || !samePath(condition.filePath, changedPath)
)); ));
} }
@@ -62,7 +57,7 @@ export async function prepareLettaInstallation(
config: InstallConfig, config: InstallConfig,
sources: LettaSources, sources: LettaSources,
rendered: RenderedLettaDocument, rendered: RenderedLettaDocument,
): Promise<PreparedLettaInstallation> { ): Promise<PreparedManagedInstallation> {
await preflightLettaInstallation(config, sources); await preflightLettaInstallation(config, sources);
const settingsSource = rendered.next ?? rendered.document.raw; const settingsSource = rendered.next ?? rendered.document.raw;
const prepared = await prepareManagedInstallation({ const prepared = await prepareManagedInstallation({
@@ -89,7 +84,7 @@ export async function prepareLettaInstallation(
} }
export async function commitLettaInstallation( export async function commitLettaInstallation(
prepared: PreparedLettaInstallation, prepared: PreparedManagedInstallation,
renameFile?: RenameFile, renameFile?: RenameFile,
): Promise<void> { ): Promise<void> {
await commitManagedInstallation(prepared, renameFile); await commitManagedInstallation(prepared, renameFile);
+4 -14
View File
@@ -9,14 +9,13 @@ import {
type LettaDocument, type LettaDocument,
type LettaDocumentKind, type LettaDocumentKind,
} from './letta-config.js'; } from './letta-config.js';
import { hasCode, MAX_SOURCE_BYTES, pathKey, samePath } from './common.js';
import { import {
inspectPhysicalDirectory, inspectPhysicalDirectory,
readPhysicalFile, readPhysicalFile,
type FileSnapshot, type FileSnapshot,
} from './managed-files.js'; } from './managed-files.js';
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
export interface LettaSourcePrecondition { export interface LettaSourcePrecondition {
readonly filePath: string; readonly filePath: string;
readonly label: string; readonly label: string;
@@ -39,15 +38,6 @@ export interface LettaSources {
readonly preconditions: readonly LettaSourcePrecondition[]; 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> { async function canonicalPath(filePath: string): Promise<string> {
try { try {
return await fsp.realpath(filePath); return await fsp.realpath(filePath);
@@ -93,7 +83,7 @@ function deduplicatePreconditions(
): LettaSourcePrecondition[] { ): LettaSourcePrecondition[] {
const result = new Map<string, LettaSourcePrecondition>(); const result = new Map<string, LettaSourcePrecondition>();
for (const value of values) { for (const value of values) {
const key = comparisonPath(value.filePath); const key = pathKey(value.filePath);
if (!result.has(key)) result.set(key, value); if (!result.has(key)) result.set(key, value);
} }
return [...result.values()]; return [...result.values()];
@@ -123,7 +113,7 @@ export async function readLettaSources(): Promise<LettaSources> {
const projectDirectory = path.join(workspace, '.letta'); const projectDirectory = path.join(workspace, '.letta');
await inspectPhysicalDirectory(homeDirectory, 'Letta Code home directory'); await inspectPhysicalDirectory(homeDirectory, 'Letta Code home directory');
await inspectPhysicalDirectory(globalDirectory, 'Letta Code global configuration directory'); await inspectPhysicalDirectory(globalDirectory, 'Letta Code global configuration directory');
if (comparisonPath(projectDirectory) !== comparisonPath(globalDirectory)) { if (!samePath(projectDirectory, globalDirectory)) {
await inspectPhysicalDirectory(projectDirectory, 'Letta Code project configuration directory'); await inspectPhysicalDirectory(projectDirectory, 'Letta Code project configuration directory');
} }
const globalPath = path.join(globalDirectory, 'settings.json'); const globalPath = path.join(globalDirectory, 'settings.json');
@@ -133,7 +123,7 @@ export async function readLettaSources(): Promise<LettaSources> {
canonicalPath(workspace), canonicalPath(workspace),
canonicalPath(homeDirectory), canonicalPath(homeDirectory),
]); ]);
const projectActive = comparisonPath(canonicalWorkspace) !== comparisonPath(canonicalHome); const projectActive = !samePath(canonicalWorkspace, canonicalHome);
const global = await readDocument('global', globalPath); const global = await readDocument('global', globalPath);
const project = projectActive const project = projectActive
? await readDocument('project', projectPath) ? await readDocument('project', projectPath)
+13 -106
View File
@@ -1,103 +1,20 @@
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { managedScriptReference, type ManagedScriptReference } from './common.js';
import {
isNodeExecutable,
parsePosixCommand,
parsePowerShellSource,
posixSource,
powerShellSource,
} from './shell-command.js';
interface ParsedArgument { export type QwenRuntimeReference = ManagedScriptReference;
readonly value: string;
readonly next: number;
}
export interface QwenRuntimeReference {
readonly agentId: string;
readonly scriptPath: string;
}
export function sameQwenPath(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 sameQwenAgentId(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 buildQwenCommand(scriptPath: string): string { export function buildQwenCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) { if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Qwen Code hook commands require absolute executable and script paths'); throw new Error('Qwen Code hook commands require absolute executable and script paths');
} }
if (process.platform === 'win32') { return process.platform === 'win32' ? powerShellSource(scriptPath) : posixSource(scriptPath);
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 isNodeExecutable(filePath: string): boolean {
const basename = path.basename(filePath).toLowerCase();
return basename === 'node' || basename === 'node.exe';
} }
export function qwenRuntimeReference( export function qwenRuntimeReference(
@@ -105,18 +22,8 @@ export function qwenRuntimeReference(
scriptName: string, scriptName: string,
): QwenRuntimeReference | undefined { ): QwenRuntimeReference | undefined {
const parsed = process.platform === 'win32' const parsed = process.platform === 'win32'
? parsePowerShellCommand(command) ? parsePowerShellSource(command)
: parsePosixCommand(command); : parsePosixCommand(command);
if (!parsed if (!parsed || !path.isAbsolute(parsed[0]) || !isNodeExecutable(parsed[0])) return undefined;
|| !path.isAbsolute(parsed[0]) return managedScriptReference(parsed[1], scriptName);
|| !isNodeExecutable(parsed[0])
|| !path.isAbsolute(parsed[1])
|| path.basename(parsed[1]) !== scriptName) return undefined;
const agentDirectory = path.dirname(parsed[1]);
if (!sameQwenPath(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] };
} }
+10 -86
View File
@@ -1,25 +1,15 @@
import { import type { JSONPath } from 'jsonc-parser';
applyEdits,
modify,
parse,
parseTree,
printParseErrorCode,
type FormattingOptions,
type JSONPath,
type Node,
type ParseError,
} from 'jsonc-parser';
import type { FileSnapshot } from './managed-files.js'; import type { FileSnapshot } from './managed-files.js';
import { changeJsonc } from './jsonc-edit.js';
import { import {
MANAGED_EVENTS, MANAGED_EVENTS,
managedQwenRemovals, managedQwenRemovals,
readQwenHooks, readQwenHooks,
type JsonObject,
type ManagedQwenEvent, type ManagedQwenEvent,
type QwenGroup, type QwenGroup,
type QwenHooks, type QwenHooks,
isObject,
} from './qwen-contract.js'; } from './qwen-contract.js';
import { isObject, parseCommentedJsonObject, type JsonObject } from './strict-json.js';
export const QWEN_OWNED_FILE_MARKER = '// Managed by Elydora'; export const QWEN_OWNED_FILE_MARKER = '// Managed by Elydora';
@@ -53,50 +43,6 @@ interface DocumentOptions {
readonly snapshot?: FileSnapshot; readonly snapshot?: FileSnapshot;
} }
function rejectDuplicateKeys(
node: Node | undefined,
label: string,
location: string[] = [],
): void {
if (!node) return;
if (node.type === 'object') {
const keys = new Set<string>();
for (const property of node.children ?? []) {
const key = String(property.children?.[0]?.value);
const childLocation = [...location, key];
if (keys.has(key)) {
throw new Error(`${label} contains duplicate field "${childLocation.join('.')}"`);
}
keys.add(key);
rejectDuplicateKeys(property.children?.[1], label, childLocation);
}
return;
}
if (node.type === 'array') {
for (const child of node.children ?? []) rejectDuplicateKeys(child, label, location);
}
}
export function parseQwenJsoncObject(raw: string, label: string): JsonObject {
const errors: ParseError[] = [];
const value: unknown = parse(raw, errors, {
allowTrailingComma: false,
disallowComments: false,
});
if (errors.length > 0) {
const details = errors
.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
.join(', ');
throw new Error(`Failed to parse ${label}: ${details}`);
}
if (!isObject(value)) throw new Error(`${label} must contain a JSON object`);
rejectDuplicateKeys(parseTree(raw, [], {
allowTrailingComma: false,
disallowComments: false,
}), label);
return value;
}
export function qwenSourceLabel(kind: QwenDocumentKind): string { export function qwenSourceLabel(kind: QwenDocumentKind): string {
switch (kind) { switch (kind) {
case 'system-defaults': return 'Qwen Code system defaults'; case 'system-defaults': return 'Qwen Code system defaults';
@@ -122,7 +68,7 @@ function readFolderTrustEnabled(root: JsonObject, label: string): boolean | unde
export function parseQwenDocument(options: DocumentOptions): QwenDocument { export function parseQwenDocument(options: DocumentOptions): QwenDocument {
const label = `${qwenSourceLabel(options.kind)} at ${options.filePath}`; const label = `${qwenSourceLabel(options.kind)} at ${options.filePath}`;
const root = parseQwenJsoncObject(options.raw, label); const root = parseCommentedJsonObject(options.raw, label);
if (root.disableAllHooks !== undefined && typeof root.disableAllHooks !== 'boolean') { if (root.disableAllHooks !== undefined && typeof root.disableAllHooks !== 'boolean') {
throw new Error(`${label} field "disableAllHooks" must be a boolean`); throw new Error(`${label} field "disableAllHooks" must be a boolean`);
} }
@@ -150,28 +96,6 @@ export function createQwenDocument(
}); });
} }
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: QwenDocument, raw: string): QwenDocument { function currentDocument(document: QwenDocument, raw: string): QwenDocument {
return parseQwenDocument({ return parseQwenDocument({
kind: document.kind, kind: document.kind,
@@ -195,25 +119,25 @@ function removeManagedEntries(
for (const removal of eventRemovals) { for (const removal of eventRemovals) {
const groupPath: JSONPath = ['hooks', event, removal.groupIndex]; const groupPath: JSONPath = ['hooks', event, removal.groupIndex];
if (removal.removeGroup) { if (removal.removeGroup) {
raw = change(raw, groupPath, undefined); raw = changeJsonc(raw, groupPath, undefined);
continue; continue;
} }
for (const handlerIndex of [...removal.handlerIndexes].sort( for (const handlerIndex of [...removal.handlerIndexes].sort(
(left, right) => right - left, (left, right) => right - left,
)) { )) {
raw = change(raw, [...groupPath, 'hooks', handlerIndex], undefined); raw = changeJsonc(raw, [...groupPath, 'hooks', handlerIndex], undefined);
} }
} }
if (eventRemovals.length > 0) { if (eventRemovals.length > 0) {
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if ((current.hooks[event] ?? []).length === 0) { if ((current.hooks[event] ?? []).length === 0) {
raw = change(raw, ['hooks', event], undefined); raw = changeJsonc(raw, ['hooks', event], undefined);
} }
} }
} }
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) { if (current.hasHooksContainer && Object.keys(current.hooks).length === 0) {
raw = change(raw, ['hooks'], undefined); raw = changeJsonc(raw, ['hooks'], undefined);
} }
return raw; return raw;
} }
@@ -226,9 +150,9 @@ function appendGroup(
): string { ): string {
const current = currentDocument(document, raw); const current = currentDocument(document, raw);
if (current.hooks[event]) { if (current.hooks[event]) {
return change(raw, ['hooks', event, current.hooks[event].length], group, true); return changeJsonc(raw, ['hooks', event, current.hooks[event].length], group, true);
} }
return change(raw, ['hooks', event], [group]); return changeJsonc(raw, ['hooks', event], [group]);
} }
export function renderQwenDocument( export function renderQwenDocument(
+3 -6
View File
@@ -1,8 +1,7 @@
import { sameAgentId, samePath } from './common.js';
import { import {
buildQwenCommand, buildQwenCommand,
qwenRuntimeReference, qwenRuntimeReference,
sameQwenAgentId,
sameQwenPath,
type QwenRuntimeReference, type QwenRuntimeReference,
} from './qwen-command.js'; } from './qwen-command.js';
import { isObject, type JsonObject } from './strict-json.js'; import { isObject, type JsonObject } from './strict-json.js';
@@ -283,7 +282,7 @@ export function managedQwenRemovals(
(hooks[event] ?? []).forEach((group, groupIndex) => { (hooks[event] ?? []).forEach((group, groupIndex) => {
const handlerIndexes = group.hooks.flatMap((handler, handlerIndex) => { const handlerIndexes = group.hooks.flatMap((handler, handlerIndex) => {
const reference = managedReference(handler, scriptName, hookName, true); const reference = managedReference(handler, scriptName, hookName, true);
return reference && (agentId === undefined || sameQwenAgentId(reference.agentId, agentId)) return reference && (agentId === undefined || sameAgentId(reference.agentId, agentId))
? [handlerIndex] ? [handlerIndex]
: []; : [];
}); });
@@ -334,7 +333,7 @@ export function qwenRuntimeContracts(hooks: QwenHooks): QwenRuntimeContract[] {
const post = posts.get(key); const post = posts.get(key);
const failure = failures.get(key); const failure = failures.get(key);
if (guard.length !== 1 || post?.length !== 1 || failure?.length !== 1) continue; if (guard.length !== 1 || post?.length !== 1 || failure?.length !== 1) continue;
if (!sameQwenPath(post[0].scriptPath, failure[0].scriptPath)) continue; if (!samePath(post[0].scriptPath, failure[0].scriptPath)) continue;
contracts.push({ contracts.push({
agentId: guard[0].agentId, agentId: guard[0].agentId,
guardPath: guard[0].scriptPath, guardPath: guard[0].scriptPath,
@@ -343,5 +342,3 @@ export function qwenRuntimeContracts(hooks: QwenHooks): QwenRuntimeContract[] {
} }
return contracts; return contracts;
} }
export { isObject, type JsonObject };
+5 -10
View File
@@ -5,7 +5,7 @@ import {
AUDIT_SCRIPT, AUDIT_SCRIPT,
GUARD_SCRIPT, GUARD_SCRIPT,
} from './qwen-contract.js'; } from './qwen-contract.js';
import { sameQwenPath } from './qwen-command.js'; import { MAX_SOURCE_BYTES, samePath } from './common.js';
import { import {
qwenDocumentLabel, qwenDocumentLabel,
type RenderedQwenDocument, type RenderedQwenDocument,
@@ -30,16 +30,11 @@ import {
const DISPLAY_NAME = 'Qwen Code'; const DISPLAY_NAME = 'Qwen Code';
const SETTINGS_DIRECTORY_LABEL = 'Qwen Code user configuration directory'; const SETTINGS_DIRECTORY_LABEL = 'Qwen Code user configuration directory';
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
export type QwenRuntimePaths = ManagedRuntimePaths;
export type PreparedQwenInstallation = PreparedManagedInstallation;
export type { RenameFile };
export async function preflightQwenInstallation( export async function preflightQwenInstallation(
config: InstallConfig, config: InstallConfig,
sources: QwenSources, sources: QwenSources,
): Promise<QwenRuntimePaths> { ): Promise<ManagedRuntimePaths> {
requireQwenHooksEnabled(sources); requireQwenHooksEnabled(sources);
return preflightManagedInstallation({ return preflightManagedInstallation({
agentKey: AGENT_KEY, agentKey: AGENT_KEY,
@@ -53,7 +48,7 @@ export async function preflightQwenInstallation(
function readOnlyPreconditions(sources: QwenSources, changedPath?: string) { function readOnlyPreconditions(sources: QwenSources, changedPath?: string) {
return sources.preconditions.filter((condition) => ( return sources.preconditions.filter((condition) => (
changedPath === undefined || !sameQwenPath(condition.filePath, changedPath) changedPath === undefined || !samePath(condition.filePath, changedPath)
)); ));
} }
@@ -61,7 +56,7 @@ export async function prepareQwenInstallation(
config: InstallConfig, config: InstallConfig,
sources: QwenSources, sources: QwenSources,
rendered: RenderedQwenDocument, rendered: RenderedQwenDocument,
): Promise<PreparedQwenInstallation> { ): Promise<PreparedManagedInstallation> {
await preflightQwenInstallation(config, sources); await preflightQwenInstallation(config, sources);
const settingsSource = rendered.next ?? rendered.document.raw; const settingsSource = rendered.next ?? rendered.document.raw;
const prepared = await prepareManagedInstallation({ const prepared = await prepareManagedInstallation({
@@ -88,7 +83,7 @@ export async function prepareQwenInstallation(
} }
export async function commitQwenInstallation( export async function commitQwenInstallation(
prepared: PreparedQwenInstallation, prepared: PreparedManagedInstallation,
renameFile?: RenameFile, renameFile?: RenameFile,
): Promise<void> { ): Promise<void> {
await commitManagedInstallation(prepared, renameFile); await commitManagedInstallation(prepared, renameFile);
+3 -91
View File
@@ -1,99 +1,11 @@
import os from 'node:os'; import { AGENT_KEY, type QwenRuntimeContract } from './qwen-contract.js';
import path from 'node:path'; import { managedRuntimeFilesExist } from './managed-runtime-status.js';
import { generateGuardScript } from './guard-template.js';
import { generateHookScript } from './hook-template.js';
import {
AGENT_KEY,
type QwenRuntimeContract,
} from './qwen-contract.js';
import { sameQwenAgentId, sameQwenPath } from './qwen-command.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
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: QwenRuntimeContract,
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 (!sameQwenAgentId(agentId, contract.agentId) || config.agent_name !== AGENT_KEY) {
throw new Error(`Elydora runtime identity does not match Qwen Code 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: QwenRuntimeContract): Promise<boolean> {
const runtimeRoot = path.join(os.homedir(), '.elydora');
const agentDirectory = path.dirname(contract.guardPath);
if (!sameQwenPath(path.dirname(agentDirectory), runtimeRoot)
|| !sameQwenPath(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 === generateGuardScript(AGENT_KEY, contract.agentId)
&& audit.contents === generateHookScript(
AGENT_KEY,
contract.agentId,
{ nativePayload: true },
);
}
export async function qwenRuntimeFilesExist( export async function qwenRuntimeFilesExist(
contracts: readonly QwenRuntimeContract[], contracts: readonly QwenRuntimeContract[],
): Promise<boolean> { ): Promise<boolean> {
for (const contract of contracts) { for (const contract of contracts) {
if (await runtimeContractExists(contract)) return true; if (await managedRuntimeFilesExist(contract, AGENT_KEY)) return true;
} }
return false; return false;
} }
+8 -14
View File
@@ -2,18 +2,17 @@ import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import fsp from 'node:fs/promises'; import fsp from 'node:fs/promises';
import { parse as parseDotenv } from 'dotenv'; import { parse as parseDotenv } from 'dotenv';
import { MAX_SOURCE_BYTES, pathKey, samePath } from './common.js';
import { import {
createQwenDocument, createQwenDocument,
parseQwenDocument, parseQwenDocument,
parseQwenJsoncObject,
qwenDocumentLabel, qwenDocumentLabel,
qwenSourceLabel, qwenSourceLabel,
type QwenDocument, type QwenDocument,
type QwenDocumentKind, type QwenDocumentKind,
} from './qwen-config.js'; } from './qwen-config.js';
import { readPhysicalFile, type FileSnapshot } from './managed-files.js'; import { readPhysicalFile, type FileSnapshot } from './managed-files.js';
import { parseCommentedJsonObject } from './strict-json.js';
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
const HOME_ENV_KEYS = ['QWEN_HOME', 'QWEN_RUNTIME_DIR'] as const; const HOME_ENV_KEYS = ['QWEN_HOME', 'QWEN_RUNTIME_DIR'] as const;
export interface QwenSourcePrecondition { export interface QwenSourcePrecondition {
@@ -94,7 +93,7 @@ async function resolveQwenRouting(): Promise<RoutingResult> {
const readCandidate = async (filePath: string): Promise<void> => { const readCandidate = async (filePath: string): Promise<void> => {
const resolved = path.resolve(filePath); const resolved = path.resolve(filePath);
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved; const key = pathKey(resolved);
if (visited.has(key)) return; if (visited.has(key)) return;
visited.add(key); visited.add(key);
const snapshot = await readPhysicalFile(resolved, 'Qwen Code home environment'); const snapshot = await readPhysicalFile(resolved, 'Qwen Code home environment');
@@ -172,13 +171,8 @@ async function canonicalPath(filePath: string): Promise<string> {
} }
} }
function comparisonPath(filePath: string): string {
const resolved = path.resolve(filePath);
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
}
function isWithin(child: string, parent: string): boolean { function isWithin(child: string, parent: string): boolean {
const relative = path.relative(comparisonPath(parent), comparisonPath(child)); const relative = path.relative(pathKey(parent), pathKey(child));
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
} }
@@ -202,7 +196,7 @@ async function workspaceTrust(
original: snapshot, original: snapshot,
}; };
if (!snapshot) return { trusted: true, precondition }; if (!snapshot) return { trusted: true, precondition };
const rules = parseQwenJsoncObject(snapshot.contents, `Qwen Code trusted folders at ${filePath}`); const rules = parseCommentedJsonObject(snapshot.contents, `Qwen Code trusted folders at ${filePath}`);
for (const [rulePath, level] of Object.entries(rules)) { for (const [rulePath, level] of Object.entries(rules)) {
if (!['TRUST_FOLDER', 'TRUST_PARENT', 'DO_NOT_TRUST'].includes(String(level))) { if (!['TRUST_FOLDER', 'TRUST_PARENT', 'DO_NOT_TRUST'].includes(String(level))) {
throw new Error(`Qwen Code trusted folders has invalid trust level for "${rulePath}"`); throw new Error(`Qwen Code trusted folders has invalid trust level for "${rulePath}"`);
@@ -217,7 +211,7 @@ async function workspaceTrust(
} }
for (const [rulePath, level] of Object.entries(rules)) { for (const [rulePath, level] of Object.entries(rules)) {
if (level === 'DO_NOT_TRUST' if (level === 'DO_NOT_TRUST'
&& comparisonPath(workspace) === comparisonPath(await canonicalPath(rulePath))) { && samePath(workspace, await canonicalPath(rulePath))) {
return { trusted: false, precondition }; return { trusted: false, precondition };
} }
} }
@@ -256,7 +250,7 @@ function deduplicatePreconditions(
): QwenSourcePrecondition[] { ): QwenSourcePrecondition[] {
const result = new Map<string, QwenSourcePrecondition>(); const result = new Map<string, QwenSourcePrecondition>();
for (const value of values) { for (const value of values) {
const key = comparisonPath(value.filePath); const key = pathKey(value.filePath);
if (!result.has(key)) result.set(key, value); if (!result.has(key)) result.set(key, value);
} }
return [...result.values()]; return [...result.values()];
@@ -276,7 +270,7 @@ export async function readQwenSources(): Promise<QwenSources> {
canonicalPath(process.cwd()), canonicalPath(process.cwd()),
canonicalPath(os.homedir()), canonicalPath(os.homedir()),
]); ]);
const workspaceActive = comparisonPath(canonicalWorkspace) !== comparisonPath(canonicalHome); const workspaceActive = !samePath(canonicalWorkspace, canonicalHome);
const workspace = workspaceActive const workspace = workspaceActive
? await readDocument('workspace', workspacePath) ? await readDocument('workspace', workspacePath)
: createQwenDocument('workspace', workspacePath); : createQwenDocument('workspace', workspacePath);
+7
View File
@@ -66,6 +66,13 @@ export function parseStrictJsonObject(raw: string, label: string): JsonObject {
}); });
} }
export function parseCommentedJsonObject(raw: string, label: string): JsonObject {
return parseJsonObject(raw, label, {
allowTrailingComma: false,
disallowComments: false,
});
}
export function parseStrictJsoncObject(raw: string, label: string): JsonObject { export function parseStrictJsoncObject(raw: string, label: string): JsonObject {
return parseJsonObject(raw, label, { return parseJsonObject(raw, label, {
allowTrailingComma: true, allowTrailingComma: true,