sdks/node: mirror Elydora-Node-SDK 2.0.1

Rebuilt from the standalone 2101cb7 tree (durable transactions, Kiro IDE
adapter, hook retry with a bounded chain lock, version 2.0.1) with the
Open-Source API trims re-applied: no webhooks, members, admin events,
deep health, token rotation, or agent integration updates.

Claude-Session: https://claude.ai/code/session_01QoUjdc7HGKTsgj5GhG8HvF
This commit is contained in:
雷电芽衣 committed 2026-09-02 11:01:55 -04:00
1 parent 1656a94811
commit 98c61a0b6f
44 files changed
+6794 -1130

No files matched your search

+33 -27
View File
@@ -53,6 +53,35 @@ npx elydora install \
The CLI reads the private key and optional API token through hidden terminal prompts. For non-interactive installation, store each secret in an owner-only file and pass `--private_key_file <path>` and `--token_file <path>`. Secret values are rejected as command-line arguments because process listings and shell history can expose them. Agent IDs map to one physical directory directly under `~/.elydora`; portable filename rules and physical-directory checks apply before writes or recursive removal. Ambiguous uninstall discovery requires an explicit agent ID.
### Commands
| Command | Description |
|---------|-------------|
| `elydora install` | Install Elydora audit hook for a coding agent |
| `elydora uninstall` | Remove Elydora audit hook for a coding agent |
| `elydora status` | Show installation status for all agents |
| `elydora agents` | List supported coding agents |
### Supported Agents
| Agent | Key |
|-------|-----|
| Augment Code CLI | `augment` |
| Claude Code | `claudecode` |
| Codex CLI | `codex` |
| Cline | `cline` |
| Factory Droid | `droid` |
| Kimi Code | `kimi` |
| Grok Build | `grok` |
| Qwen Code | `qwen` |
| Cursor CLI | `cursor` |
| Gemini CLI | `gemini` |
| Kiro CLI | `kirocli` |
| Kiro IDE | `kiroide` |
| OpenCode | `opencode` |
| GitHub Copilot CLI | `copilot` |
| Letta Code | `letta` |
Claude Code installation writes exact matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups to `$CLAUDE_CONFIG_DIR/settings.json`, with `~/.claude/settings.json` selected when the variable is absent. Relative and empty overrides resolve from the current working directory, matching Claude Code's native path behavior. Each handler uses Claude Code's cross-platform exec form with the current Node.js executable, a single script argument, and a ten-second timeout. Managed runtimes preserve the complete native snake_case event payload and enforce frozen or revoked state through exit code `2`. Project, local, managed, plugin, skill, and agent hook sources remain unchanged and continue loading additively. User settings, generated runtimes, runtime config, and private key commit as one rollback-capable transaction. `disableAllHooks` blocks installation; managed hook policy and safe mode can suppress user hooks. Run `/hooks` and `claude doctor` after installation.
Codex installation writes exact `PreToolUse` and `PostToolUse` command groups to `$CODEX_HOME/hooks.json` (`~/.codex/hooks.json` by default) and preserves the complete native event payload. A configured `CODEX_HOME` follows Codex's existing-directory canonicalization rule. User TOML, project, plugin, and managed sources remain unchanged and continue loading additively. The hook file, generated runtimes, runtime config, and private key commit as one rollback-capable update. Run `/hooks` after installation and approve both Elydora definition hashes.
@@ -75,36 +104,13 @@ Qwen Code 0.20.0 installation writes exact, named `PreToolUse`, `PostToolUse`, a
Auggie installation commits `~/.augment/settings.json`, both platform wrappers, generated runtimes, runtime config, and private key as one rollback-capable transaction. The managed `PreToolUse` guard propagates exit code `2`; `PostToolUse` preserves Auggie's complete native hook payload. System, workspace, local workspace, and alternate `--augment-cache-dir` settings remain unchanged. Run `auggie tools list` to validate the effective user configuration.
Kiro CLI installation covers both runtime contracts. Kiro CLI v2 uses the generated custom agent through `kiro-cli --agent elydora-audit`. Kiro CLI v3 loads the global standalone hooks when started with `kiro-cli --v3`.
Kiro IDE 1.0 installation is workspace-scoped. Run the installer from the workspace root; it writes `.kiro/hooks/elydora-audit.json` with exact `PreToolUse` and `PostToolUse` command hooks in Kiro's v1 JSON schema. The hook file, any exact Elydora legacy migration, generated runtimes, runtime config, and private key commit through one rollback-capable transaction. Managed handlers preserve Kiro's complete native payload and propagate frozen or revoked state through exit code `2`. Open the Agent Hooks panel after installation and confirm both Elydora entries.
### Commands
Kiro CLI installation commits the global v2 custom agent at `~/.kiro/agents/elydora-audit.json`, the CLI 2.13.0+ v3 global hooks at `~/.kiro/hooks/elydora-audit.json`, generated runtimes, runtime config, and private key through one rollback-capable transaction. The v2 agent uses exact lower-camel `preToolUse` and `postToolUse` handlers with ten-second millisecond timeouts. The v3 source uses Kiro's v1 schema with exact PascalCase `PreToolUse` and `PostToolUse` command hooks. Both contracts preserve the complete native event payload and propagate frozen or revoked state through exit code `2`. Validate v2 with `kiro-cli agent validate --path ~/.kiro/agents/elydora-audit.json` and start it with `kiro-cli --agent elydora-audit`; start the v3 TUI with `kiro-cli --v3`.
| Command | Description |
|---------|-------------|
| `elydora install` | Install Elydora audit hook for a coding agent |
| `elydora uninstall` | Remove Elydora audit hook for a coding agent |
| `elydora status` | Show installation status for all agents |
| `elydora agents` | List supported coding agents |
OpenCode 1.18.4 installation writes the named `ElydoraAuditPlugin` export to `${XDG_CONFIG_HOME:-~/.config}/opencode/plugins/elydora-audit.js`, matching the official global plugin loader's `.js` and `.ts` discovery contract. `XDG_CONFIG_HOME` must be absolute. The `tool.execute.before` hook synchronously evaluates the full native input and argument payload and throws on frozen or revoked state. The awaited `tool.execute.after` hook preserves the complete native input and output payload and reports audit runtime failures to stderr. The plugin, exact legacy `.mjs` migration, generated runtimes, runtime config, and private key commit through one rollback-capable transaction. Status requires physical paths, exact generated sources, the installed Node executable, canonical private keys, and matching runtime identity. Restart active OpenCode sessions after installation.
### Supported Agents
| Agent | Key |
|-------|-----|
| Augment Code CLI | `augment` |
| Claude Code | `claudecode` |
| OpenAI Codex | `codex` |
| Cline | `cline` |
| Factory Droid | `droid` |
| Kimi Code | `kimi` |
| Grok Build | `grok` |
| Qwen Code | `qwen` |
| Cursor | `cursor` |
| Gemini CLI | `gemini` |
| Kiro CLI | `kirocli` |
| Kiro IDE | `kiroide` |
| OpenCode | `opencode` |
| GitHub Copilot CLI | `copilot` |
| Letta Code | `letta` |
Letta Code 0.28.13 installation writes exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups to `${HOME:-~}/.letta/settings.json`, following the [official hook source and event contract](https://docs.letta.com/reference/deprecated/hooks). Workspace `.letta/settings.json` and `.letta/settings.local.json` remain read-only and participate in effective `hooks.disabled` evaluation. An explicit global `false` enables hooks; global `true`, project `true`, and project-local `true` disable them according to Letta's precedence. Managed command handlers use ten-second millisecond timeouts, the current absolute Node.js executable, PowerShell exit-code propagation on Windows, and complete native snake_case payloads. Frozen and revoked state propagates through exit code `2`; audit delivery failures remain visible through stderr, owner-only `error.log`, and exit code `1`, while Letta post events remain nonblocking. Global settings, generated runtimes, runtime config, and private key commit through one rollback-capable transaction guarded by snapshots of all three settings sources. Status requires an effective enabled state, exact hook and executable identity, physical files, canonical private keys, and matching runtime identity. Run `/hooks` and restart active Letta Code sessions after installation.
## API Reference
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@elydora/sdk",
"version": "1.1.0",
"version": "2.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@elydora/sdk",
"version": "1.1.0",
"version": "2.0.1",
"license": "MIT",
"dependencies": {
"@decimalturn/toml-patch": "^2.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@elydora/sdk",
"version": "1.1.0",
"version": "2.0.1",
"description": "Node.js/TypeScript SDK for the Elydora tamper-evident audit platform",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+145 -51
View File
@@ -38,6 +38,8 @@ const KEY_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'private.key');
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'chain-state.json');
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'error.log');
const ZERO_CHAIN_HASH = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const MAX_CHAIN_ATTEMPTS = 5;
const SUBMIT_BUDGET_MS = 4000;
${PROTECTED_RUNTIME_READER}
@@ -165,6 +167,81 @@ function writeChainState(chainHash) {
writeProtectedJson(CHAIN_STATE_PATH, 'Chain state', { prev_chain_hash: chainHash });
}
function lockOwnerAlive(lockPath) {
let owner;
try {
owner = Number.parseInt(fs.readFileSync(lockPath, 'utf-8'), 10);
} catch (error) {
if (!hasRuntimeErrorCode(error, 'ENOENT')) throw error;
return false;
}
if (!Number.isInteger(owner) || owner <= 0) return false;
try {
process.kill(owner, 0);
return true;
} catch (error) {
if (hasRuntimeErrorCode(error, 'ESRCH')) return false;
return true;
}
}
function withChainLock(update) {
const lockPath = CHAIN_STATE_PATH + '.lock';
const deadline = performance.now() + 1000;
for (;;) {
let descriptor;
try {
descriptor = fs.openSync(lockPath, 'wx', 0o600);
fs.writeSync(descriptor, String(process.pid));
} catch (error) {
if (!hasRuntimeErrorCode(error, 'EEXIST')) throw error;
if (lockOwnerAlive(lockPath)) {
if (performance.now() > deadline) throw new Error('Chain state lock timed out: ' + lockPath);
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
continue;
}
let age = 0;
try { age = Date.now() - fs.statSync(lockPath).mtimeMs; } catch (statError) {
if (!hasRuntimeErrorCode(statError, 'ENOENT')) throw statError;
}
if (age > 5000) {
const reclaimed = lockPath + '.' + process.pid + '.stale';
try {
fs.renameSync(lockPath, reclaimed);
fs.unlinkSync(reclaimed);
} catch (reclaimError) {
if (!hasRuntimeErrorCode(reclaimError, 'ENOENT')) throw reclaimError;
}
continue;
}
if (performance.now() > deadline) throw new Error('Chain state lock timed out: ' + lockPath);
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
continue;
}
try {
return update();
} finally {
let current = null;
try {
current = fs.lstatSync(lockPath);
} catch (statError) {
if (!hasRuntimeErrorCode(statError, 'ENOENT')) throw statError;
}
const own = fs.fstatSync(descriptor);
fs.closeSync(descriptor);
if (current && current.ino === own.ino && current.dev === own.dev) fs.unlinkSync(lockPath);
}
}
}
function advanceChainState(fromHash, toHash) {
return withChainLock(() => {
if (readChainState() !== fromHash) return false;
writeChainState(toHash);
return true;
});
}
function readConfigAndKey() {
const raw = readProtectedFile(
CONFIG_PATH,
@@ -219,27 +296,43 @@ async function readHookInput() {
return value;
}
async function submitOperation(hookData, runtime) {
const toolEvent = hookData.tool_result && typeof hookData.tool_result === 'object'
? hookData.tool_result
: hookData.tool_call && typeof hookData.tool_call === 'object'
? hookData.tool_call
: {};
const toolName = hookData.tool_name || hookData.toolName || hookData.name ||
toolEvent.name || 'unknown';
const toolInput = hookData.tool_input || hookData.toolInput || hookData.input ||
hookData.parameters || toolEvent.input || {};
const sessionId = hookData.conversation_id || hookData.session_id || hookData.sessionId ||
hookData.session || hookData.taskId || 'unknown';
const previousHash = readChainState();
async function postOperation(operation, runtime, deadline) {
const remaining = deadline - performance.now();
if (remaining <= 0) throw new Error('Audit API retry budget exhausted');
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), Math.min(5000, remaining));
try {
const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
if (runtime.config.token) headers.Authorization = 'Bearer ' + runtime.config.token;
const response = await fetch(runtime.baseUrl + '/v1/operations', {
method: 'POST',
headers,
body: JSON.stringify(operation),
signal: controller.signal,
});
if (response.ok) return { accepted: true };
const responseBody = await response.text();
if (response.status === 400 && responseBody) {
let failure;
try { failure = JSON.parse(responseBody); } catch (error) {
throw new Error('Audit API returned invalid error JSON: ' + error.message);
}
if (failure.error && failure.error.code === 'PREV_HASH_MISMATCH') {
const match = String(failure.error.message || '').match(
/Expected prev_chain_hash "([A-Za-z0-9_-]{43})"/,
);
if (match) return { accepted: false, expected: match[1] };
}
}
throw new Error('Audit API returned HTTP ' + response.status);
} finally {
clearTimeout(timeout);
}
}
function buildOperation(runtime, payload, payloadHash, toolName, sessionId, previousHash) {
const operationId = uuidv7();
const issuedAt = Date.now();
const payload = NATIVE_PAYLOAD ? hookData : {
tool_name: toolName,
tool_input: toolInput,
session_id: sessionId,
};
const payloadHash = computePayloadHash(payload);
const chainHash = computeChainHash(previousHash, payloadHash, operationId, issuedAt);
const unsigned = {
op_version: '1.0',
@@ -261,41 +354,42 @@ async function submitOperation(hookData, runtime) {
runtime.privateKey,
Buffer.from(jcsCanonicalise(unsigned), 'utf-8'),
);
const operation = { ...unsigned, chain_hash: chainHash, signature };
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
if (runtime.config.token) headers.Authorization = 'Bearer ' + runtime.config.token;
const response = await fetch(runtime.baseUrl + '/v1/operations', {
method: 'POST',
headers,
body: JSON.stringify(operation),
signal: controller.signal,
});
if (response.ok) {
writeChainState(chainHash);
return { operation: { ...unsigned, chain_hash: chainHash, signature }, chainHash };
}
async function submitOperation(hookData, runtime) {
const toolEvent = hookData.tool_result && typeof hookData.tool_result === 'object'
? hookData.tool_result
: hookData.tool_call && typeof hookData.tool_call === 'object'
? hookData.tool_call
: {};
const toolName = hookData.tool_name || hookData.toolName || hookData.name ||
toolEvent.name || 'unknown';
const toolInput = hookData.tool_input || hookData.toolInput || hookData.input ||
hookData.parameters || toolEvent.input || {};
const sessionId = hookData.conversation_id || hookData.session_id || hookData.sessionId ||
hookData.session || hookData.taskId || 'unknown';
const payload = NATIVE_PAYLOAD ? hookData : {
tool_name: toolName,
tool_input: toolInput,
session_id: sessionId,
};
const payloadHash = computePayloadHash(payload);
const deadline = performance.now() + SUBMIT_BUDGET_MS;
let previousHash = readChainState();
for (let attempt = 1; ; attempt += 1) {
const built = buildOperation(runtime, payload, payloadHash, toolName, sessionId, previousHash);
const result = await postOperation(built.operation, runtime, deadline);
if (result.accepted) {
advanceChainState(previousHash, built.chainHash);
return;
}
const responseBody = await response.text();
if (response.status === 400 && responseBody) {
let failure;
try { failure = JSON.parse(responseBody); } catch (error) {
throw new Error('Audit API returned invalid error JSON: ' + error.message);
}
if (failure.error && failure.error.code === 'PREV_HASH_MISMATCH') {
const match = String(failure.error.message || '').match(
/Expected prev_chain_hash "([A-Za-z0-9_-]{43})"/,
);
if (match) {
writeChainState(match[1]);
logError(new Error('Chain hash resynced to server: ' + match[1]));
}
}
advanceChainState(previousHash, result.expected);
logError(new Error('Chain hash resynced to server: ' + result.expected));
if (attempt >= MAX_CHAIN_ATTEMPTS) {
throw new Error('Audit API rejected prev_chain_hash ' + attempt + ' times');
}
throw new Error('Audit API returned HTTP ' + response.status);
} finally {
clearTimeout(timeout);
previousHash = readChainState();
}
}
+416
View File
@@ -0,0 +1,416 @@
import os from 'node:os';
import path from 'node:path';
import {
buildKiroIdeCommand,
kiroIdeRuntimeReference,
sameKiroIdeAgentId,
sameKiroIdePath,
type KiroIdeRuntimeReference,
} from './kiroide-command.js';
import {
buildKiroIdeHook,
kiroIdeRuntimeContracts,
renderKiroIdeDocument,
type KiroIdeDocument,
type KiroIdeHook,
type RenderedKiroIdeDocument,
type KiroIdeRuntimeContract,
} from './kiroide-contract.js';
import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js';
export const AGENT_KEY = 'kirocli';
export const AUDIT_SCRIPT = 'hook.js';
export const GUARD_SCRIPT = 'guard.js';
export const V2_AGENT_NAME = 'elydora-audit';
export const V3_CONFIG_FILE = 'elydora-audit.json';
export const HOOK_TIMEOUT_MILLISECONDS = 10_000;
const V2_DESCRIPTION = 'Kiro CLI with Elydora audit and freeze enforcement';
const V3_GUARD_NAME = 'elydora-guard';
const V3_AUDIT_NAME = 'elydora-audit';
const V2_EVENTS = new Set([
'agentSpawn',
'userPromptSubmit',
'preToolUse',
'postToolUse',
'stop',
]);
export interface KiroCliPaths {
readonly homeDirectory: string;
readonly kiroDirectory: string;
readonly agentsDirectory: string;
readonly hooksDirectory: string;
readonly v2Path: string;
readonly v3Path: string;
}
export interface KiroCliV2Hook extends JsonObject {
readonly command: string;
readonly matcher?: string;
readonly timeout_ms?: number;
readonly cache_ttl_seconds?: number;
}
export interface KiroCliV2Document {
readonly exists: boolean;
readonly filePath: string;
readonly root: JsonObject;
readonly hooks: JsonObject;
readonly raw?: string;
}
export interface RenderedKiroCliV2Document {
readonly document: KiroCliV2Document;
readonly changed: boolean;
readonly next?: string;
}
export function resolveKiroCliPaths(homeDirectory = os.homedir()): KiroCliPaths {
const resolvedHome = path.resolve(homeDirectory);
const kiroDirectory = path.join(resolvedHome, '.kiro');
const agentsDirectory = path.join(kiroDirectory, 'agents');
const hooksDirectory = path.join(kiroDirectory, 'hooks');
return {
homeDirectory: resolvedHome,
kiroDirectory,
agentsDirectory,
hooksDirectory,
v2Path: path.join(agentsDirectory, `${V2_AGENT_NAME}.json`),
v3Path: path.join(hooksDirectory, V3_CONFIG_FILE),
};
}
function validateOptionalInteger(value: unknown, field: string, label: string): void {
if (value !== undefined && (!Number.isSafeInteger(value) || (value as number) < 0)) {
throw new Error(`${label} ${field} must be a non-negative integer`);
}
}
function validateV2Hook(value: unknown, event: string, index: number): KiroCliV2Hook {
const label = `Kiro CLI v2 agent hooks.${event}[${index}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (typeof value.command !== 'string' || !value.command) {
throw new Error(`${label} command must be a non-empty string`);
}
if (value.matcher !== undefined) {
if (!['preToolUse', 'postToolUse'].includes(event)) {
throw new Error(`${label} matcher is valid only for tool events`);
}
if (typeof value.matcher !== 'string' || !value.matcher) {
throw new Error(`${label} matcher must be a non-empty string`);
}
}
validateOptionalInteger(value.timeout_ms, 'timeout_ms', label);
validateOptionalInteger(value.cache_ttl_seconds, 'cache_ttl_seconds', label);
return value as KiroCliV2Hook;
}
function validateV2Hooks(value: unknown, filePath: string): JsonObject {
if (value === undefined) return {};
if (!isObject(value)) {
throw new Error(`Kiro CLI v2 agent field "hooks" must be an object: ${filePath}`);
}
const hooks: JsonObject = { ...value };
for (const [event, entries] of Object.entries(value)) {
if (!V2_EVENTS.has(event)) {
throw new Error(`Kiro CLI v2 agent has unsupported hook event "${event}": ${filePath}`);
}
if (!Array.isArray(entries)) {
throw new Error(`Kiro CLI v2 agent hooks.${event} must be an array: ${filePath}`);
}
hooks[event] = entries.map((entry, index) => validateV2Hook(entry, event, index));
}
return hooks;
}
function validateOptionalV2Fields(root: JsonObject, filePath: string): void {
for (const field of ['name', 'description', 'prompt', 'model', 'keyboardShortcut', 'welcomeMessage']) {
if (root[field] !== undefined && typeof root[field] !== 'string') {
throw new Error(`Kiro CLI v2 agent field "${field}" must be a string: ${filePath}`);
}
}
for (const field of ['tools', 'allowedTools', 'resources']) {
const value = root[field];
if (value !== undefined
&& (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string'))) {
throw new Error(`Kiro CLI v2 agent field "${field}" must be an array of strings: ${filePath}`);
}
}
if (root.includeMcpJson !== undefined && typeof root.includeMcpJson !== 'boolean') {
throw new Error(`Kiro CLI v2 agent field "includeMcpJson" must be a boolean: ${filePath}`);
}
}
export function parseKiroCliV2Document(filePath: string, raw: string): KiroCliV2Document {
const root = parseStrictJsonObject(raw, `Kiro CLI v2 agent at ${filePath}`);
validateOptionalV2Fields(root, filePath);
return {
exists: true,
filePath,
root,
hooks: validateV2Hooks(root.hooks, filePath),
raw,
};
}
export function createKiroCliV2Document(filePath: string): KiroCliV2Document {
return { exists: false, filePath, root: {}, hooks: {} };
}
function eventHooks(document: KiroCliV2Document, event: string): readonly KiroCliV2Hook[] {
return (document.hooks[event] as readonly KiroCliV2Hook[] | undefined) ?? [];
}
function exactKeys(value: JsonObject, keys: readonly string[]): boolean {
return Object.keys(value).sort().join('|') === [...keys].sort().join('|');
}
function legacyWindowsReference(
command: unknown,
scriptName: string,
): KiroIdeRuntimeReference | undefined {
if (typeof command !== 'string') return undefined;
const match = /^"([^"\r\n]+)" "([^"\r\n]+)"$/.exec(command);
if (!match
|| !path.isAbsolute(match[1])
|| !['node', 'node.exe'].includes(path.basename(match[1]).toLowerCase())
|| !path.isAbsolute(match[2])
|| path.basename(match[2]) !== scriptName) return undefined;
const agentDirectory = path.dirname(match[2]);
if (!sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) {
return undefined;
}
const agentId = path.basename(agentDirectory);
return agentId ? { agentId, scriptPath: match[2] } : undefined;
}
function ownedReference(command: unknown, scriptName: string): KiroIdeRuntimeReference | undefined {
return typeof command === 'string'
? kiroIdeRuntimeReference(command, scriptName) ?? legacyWindowsReference(command, scriptName)
: undefined;
}
function v2OwnedReference(hook: KiroCliV2Hook, event: string) {
if (event === 'preToolUse') return ownedReference(hook.command, GUARD_SCRIPT);
if (event === 'postToolUse') return ownedReference(hook.command, AUDIT_SCRIPT);
return undefined;
}
function v2ManagedReference(hook: KiroCliV2Hook, event: string) {
const reference = v2OwnedReference(hook, event);
if (!reference
|| !exactKeys(hook, ['matcher', 'command', 'timeout_ms'])
|| hook.matcher !== '*'
|| hook.timeout_ms !== HOOK_TIMEOUT_MILLISECONDS) return undefined;
return reference;
}
function managedMetadata(root: JsonObject): boolean {
return root.name === V2_AGENT_NAME
&& root.description === V2_DESCRIPTION
&& Array.isArray(root.tools)
&& root.tools.length === 1
&& root.tools[0] === '*'
&& root.includeMcpJson === true;
}
function hasOwnedV2Hooks(document: KiroCliV2Document, agentId?: string): boolean {
return ['preToolUse', 'postToolUse'].some((event) => eventHooks(document, event).some((hook) => {
const reference = v2OwnedReference(hook, event);
return reference !== undefined
&& (agentId === undefined || sameKiroIdeAgentId(reference.agentId, agentId));
}));
}
export function requireAvailableKiroCliV2Document(document: KiroCliV2Document): void {
if (document.exists && !managedMetadata(document.root) && !hasOwnedV2Hooks(document)) {
throw new Error(`Kiro CLI v2 agent path conflicts with the Elydora contract: ${document.filePath}`);
}
}
export function withoutManagedKiroCliV2Hooks(
document: KiroCliV2Document,
agentId?: string,
): JsonObject {
const next = { ...document.hooks };
for (const event of ['preToolUse', 'postToolUse']) {
const remaining = eventHooks(document, event).filter((hook) => {
const reference = v2OwnedReference(hook, event);
return !reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId));
});
if (remaining.length > 0) next[event] = remaining;
else delete next[event];
}
return next;
}
function buildV2Hook(scriptPath: string): KiroCliV2Hook {
return {
matcher: '*',
command: buildKiroIdeCommand(scriptPath),
timeout_ms: HOOK_TIMEOUT_MILLISECONDS,
};
}
export function buildKiroCliV2Hooks(
document: KiroCliV2Document,
guardPath: string,
auditPath: string,
): JsonObject {
const hooks = withoutManagedKiroCliV2Hooks(document);
hooks.preToolUse = [...eventHooks({ ...document, hooks }, 'preToolUse'), buildV2Hook(guardPath)];
hooks.postToolUse = [...eventHooks({ ...document, hooks }, 'postToolUse'), buildV2Hook(auditPath)];
return hooks;
}
export function renderKiroCliV2Installation(
document: KiroCliV2Document,
hooks: JsonObject,
): RenderedKiroCliV2Document {
const next = `${JSON.stringify({
...document.root,
name: V2_AGENT_NAME,
description: V2_DESCRIPTION,
tools: ['*'],
includeMcpJson: true,
hooks,
}, null, 2)}\n`;
return { document, changed: next !== document.raw, next };
}
function entirelyManagedV2Document(document: KiroCliV2Document, agentId?: string): boolean {
if (!document.exists
|| !managedMetadata(document.root)
|| !exactKeys(document.root, ['name', 'description', 'tools', 'includeMcpJson', 'hooks'])
|| !exactKeys(document.hooks, ['preToolUse', 'postToolUse'])) return false;
for (const event of ['preToolUse', 'postToolUse']) {
const hooks = eventHooks(document, event);
if (hooks.length !== 1) return false;
const reference = v2OwnedReference(hooks[0], event);
if (!reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId))) {
return false;
}
}
return true;
}
export function renderKiroCliV2Uninstall(
document: KiroCliV2Document,
hooks: JsonObject,
agentId?: string,
): RenderedKiroCliV2Document {
if (!hasOwnedV2Hooks(document, agentId)) return { document, changed: false };
if (entirelyManagedV2Document(document, agentId) && Object.keys(hooks).length === 0) {
return { document, changed: true };
}
const next = `${JSON.stringify({ ...document.root, hooks }, null, 2)}\n`;
return { document, changed: next !== document.raw, next };
}
export function kiroCliV2RuntimeContracts(
document: KiroCliV2Document,
): KiroIdeRuntimeContract[] {
for (const event of ['preToolUse', 'postToolUse']) {
if (eventHooks(document, event).some((hook) => (
v2OwnedReference(hook, event) !== undefined
&& v2ManagedReference(hook, event) === undefined
))) return [];
}
const guards = eventHooks(document, 'preToolUse')
.map((hook) => v2ManagedReference(hook, 'preToolUse'))
.filter((value): value is KiroIdeRuntimeReference => value !== undefined);
const audits = eventHooks(document, 'postToolUse')
.map((hook) => v2ManagedReference(hook, 'postToolUse'))
.filter((value): value is KiroIdeRuntimeReference => value !== undefined);
if (guards.length !== 1 || audits.length !== 1
|| !sameKiroIdeAgentId(guards[0].agentId, audits[0].agentId)) return [];
return [{ agentId: guards[0].agentId, guardPath: guards[0].scriptPath, auditPath: audits[0].scriptPath }];
}
function v3Specification(name: string) {
if (name === V3_GUARD_NAME) return { scriptName: GUARD_SCRIPT };
if (name === V3_AUDIT_NAME) return { scriptName: AUDIT_SCRIPT };
return undefined;
}
function v3OwnedReference(hook: KiroIdeHook) {
const specification = v3Specification(hook.name);
return specification && hook.action.type === 'command'
? ownedReference(hook.action.command, specification.scriptName)
: undefined;
}
export function requireAvailableKiroCliV3Hooks(hooks: readonly KiroIdeHook[]): void {
for (const hook of hooks) {
if (v3Specification(hook.name) && !v3OwnedReference(hook)) {
throw new Error(`Kiro CLI v3 hook name "${hook.name}" conflicts with the Elydora contract`);
}
}
}
export function withoutManagedKiroCliV3Hooks(
hooks: readonly KiroIdeHook[],
agentId?: string,
): KiroIdeHook[] {
return hooks.filter((hook) => {
const reference = v3OwnedReference(hook);
return !reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId));
});
}
function hasOwnedV3Hooks(hooks: readonly KiroIdeHook[], agentId?: string): boolean {
return hooks.some((hook) => {
const reference = v3OwnedReference(hook);
return reference !== undefined
&& (agentId === undefined || sameKiroIdeAgentId(reference.agentId, agentId));
});
}
function entirelyManagedV3Document(document: KiroIdeDocument, agentId?: string): boolean {
return document.exists
&& exactKeys(document.root, ['version', 'hooks'])
&& document.hooks.length > 0
&& document.hooks.every((hook) => {
const reference = v3OwnedReference(hook);
return reference !== undefined
&& (agentId === undefined || sameKiroIdeAgentId(reference.agentId, agentId));
});
}
export function renderKiroCliV3Uninstall(
document: KiroIdeDocument,
hooks: readonly KiroIdeHook[],
agentId?: string,
): RenderedKiroIdeDocument {
if (!hasOwnedV3Hooks(document.hooks, agentId)) return { document, changed: false };
if (hooks.length === 0 && entirelyManagedV3Document(document, agentId)) {
return { document, changed: true };
}
return renderKiroIdeDocument(document, hooks);
}
export function buildKiroCliV3Hooks(
hooks: readonly KiroIdeHook[],
guardPath: string,
auditPath: string,
): KiroIdeHook[] {
return [
...withoutManagedKiroCliV3Hooks(hooks),
buildKiroIdeHook(V3_GUARD_NAME, guardPath),
buildKiroIdeHook(V3_AUDIT_NAME, auditPath),
];
}
export function commonKiroCliRuntimeContract(
v2Document: KiroCliV2Document,
v3Hooks: readonly KiroIdeHook[],
): KiroIdeRuntimeContract | undefined {
const v2 = kiroCliV2RuntimeContracts(v2Document);
const v3 = kiroIdeRuntimeContracts(v3Hooks);
if (v2.length !== 1 || v3.length !== 1
|| !sameKiroIdeAgentId(v2[0].agentId, v3[0].agentId)
|| !sameKiroIdePath(v2[0].guardPath, v3[0].guardPath)
|| !sameKiroIdePath(v2[0].auditPath, v3[0].auditPath)) return undefined;
return v2[0];
}
@@ -0,0 +1,127 @@
import path from 'node:path';
import type { InstallConfig } from './base.js';
import {
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
type RenderedKiroCliV2Document,
} from './kirocli-contract.js';
import type { KiroCliSources } from './kirocli-io.js';
import type { RenderedKiroIdeDocument } from './kiroide-contract.js';
import {
commitManagedInstallation,
preflightManagedInstallation,
prepareManagedInstallation,
type ManagedRuntimePaths,
type PreparedManagedInstallation,
type RenameFile,
} from './managed-installation.js';
import {
commitManagedTransaction,
prepareManagedFileChange,
type ManagedFileChange,
type PreparedManagedTransaction,
} from './managed-transaction.js';
const DISPLAY_NAME = 'Kiro CLI';
const V2_DIRECTORY_LABEL = 'Kiro CLI agents directory';
const V3_DIRECTORY_LABEL = 'Kiro CLI global hooks directory';
export type KiroCliRuntimePaths = ManagedRuntimePaths;
export type PreparedKiroCliInstallation = PreparedManagedInstallation;
export type { RenameFile };
function hookLocations(sources: KiroCliSources) {
return [
{ directoryLabel: V2_DIRECTORY_LABEL, filePath: sources.paths.v2Path },
{ directoryLabel: V3_DIRECTORY_LABEL, filePath: sources.paths.v3Path },
];
}
export async function preflightKiroCliInstallation(
config: InstallConfig,
sources: KiroCliSources,
): Promise<KiroCliRuntimePaths> {
return preflightManagedInstallation({
agentKey: AGENT_KEY,
hookLocations: hookLocations(sources),
config,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
export async function prepareKiroCliInstallation(
config: InstallConfig,
sources: KiroCliSources,
v2: RenderedKiroCliV2Document,
v3: RenderedKiroIdeDocument,
): Promise<PreparedKiroCliInstallation> {
if (v2.next === undefined || v3.next === undefined) {
throw new Error('Kiro CLI installation requires both v2 and v3 hook documents');
}
return prepareManagedInstallation({
agentKey: AGENT_KEY,
displayName: DISPLAY_NAME,
hookSources: [
{
directoryLabel: V2_DIRECTORY_LABEL,
label: 'Kiro CLI v2 agent config',
filePath: sources.paths.v2Path,
expectedSource: sources.v2.raw,
source: v2.next,
},
{
directoryLabel: V3_DIRECTORY_LABEL,
label: 'Kiro CLI v3 global hooks',
filePath: sources.paths.v3Path,
expectedSource: sources.v3.raw,
source: v3.next,
},
],
config,
auditOptions: { nativePayload: true },
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
export async function commitKiroCliInstallation(
prepared: PreparedKiroCliInstallation,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedInstallation(prepared, renameFile);
}
export async function prepareKiroCliUninstall(
sources: KiroCliSources,
v2: RenderedKiroCliV2Document,
v3: RenderedKiroIdeDocument,
): Promise<PreparedManagedTransaction> {
const changes: ManagedFileChange[] = [];
const directories = [];
for (const item of [
{ rendered: v2, label: 'Kiro CLI v2 agent config', directoryLabel: V2_DIRECTORY_LABEL },
{ rendered: v3, label: 'Kiro CLI v3 global hooks', directoryLabel: V3_DIRECTORY_LABEL },
]) {
if (!item.rendered.changed) continue;
const change = await prepareManagedFileChange({
filePath: item.rendered.document.filePath,
label: item.label,
next: item.rendered.next,
mode: 0o600,
expectedSource: item.rendered.document.raw,
verifyExpectedSource: true,
});
if (!change) continue;
changes.push(change);
directories.push({
path: path.dirname(item.rendered.document.filePath),
label: item.directoryLabel,
});
}
return { displayName: DISPLAY_NAME, operation: 'uninstall', directories, changes };
}
export async function commitKiroCliUninstall(
prepared: PreparedManagedTransaction,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedTransaction(prepared, renameFile);
}
+68
View File
@@ -0,0 +1,68 @@
import {
AGENT_KEY,
createKiroCliV2Document,
parseKiroCliV2Document,
resolveKiroCliPaths,
type KiroCliPaths,
type KiroCliV2Document,
} from './kirocli-contract.js';
import {
createKiroIdeDocument,
parseKiroIdeDocument,
type KiroIdeDocument,
type KiroIdeRuntimeContract,
} from './kiroide-contract.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { managedRuntimeFilesExist } from './managed-runtime-status.js';
const MAX_HOOK_BYTES = 512 * 1024;
export interface KiroCliSources {
readonly paths: KiroCliPaths;
readonly v2: KiroCliV2Document;
readonly v3: KiroIdeDocument;
}
async function inspectKiroCliDirectories(paths: KiroCliPaths): Promise<void> {
await inspectPhysicalDirectory(paths.homeDirectory, 'Kiro CLI home directory');
await inspectPhysicalDirectory(paths.kiroDirectory, 'Kiro CLI configuration directory');
await Promise.all([
inspectPhysicalDirectory(paths.agentsDirectory, 'Kiro CLI agents directory'),
inspectPhysicalDirectory(paths.hooksDirectory, 'Kiro CLI hooks directory'),
]);
}
async function readV2Document(paths: KiroCliPaths): Promise<KiroCliV2Document> {
const snapshot = await readPhysicalFile(
paths.v2Path,
'Kiro CLI v2 agent config',
MAX_HOOK_BYTES,
);
return snapshot
? parseKiroCliV2Document(paths.v2Path, snapshot.contents)
: createKiroCliV2Document(paths.v2Path);
}
async function readV3Document(paths: KiroCliPaths): Promise<KiroIdeDocument> {
const snapshot = await readPhysicalFile(
paths.v3Path,
'Kiro CLI v3 global hooks',
MAX_HOOK_BYTES,
);
return snapshot
? parseKiroIdeDocument(paths.v3Path, snapshot.contents, 'Kiro CLI v3 global hooks')
: createKiroIdeDocument(paths.v3Path);
}
export async function readKiroCliSources(): Promise<KiroCliSources> {
const paths = resolveKiroCliPaths();
await inspectKiroCliDirectories(paths);
const [v2, v3] = await Promise.all([readV2Document(paths), readV3Document(paths)]);
return { paths, v2, v3 };
}
export async function kiroCliRuntimeFilesExist(
contract: KiroIdeRuntimeContract,
): Promise<boolean> {
return managedRuntimeFilesExist(contract, AGENT_KEY);
}
+73 -335
View File
@@ -1,359 +1,97 @@
import fsp from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
import {
AGENT_KEY,
buildKiroCliV2Hooks,
buildKiroCliV3Hooks,
commonKiroCliRuntimeContract,
renderKiroCliV2Installation,
renderKiroCliV2Uninstall,
renderKiroCliV3Uninstall,
requireAvailableKiroCliV2Document,
requireAvailableKiroCliV3Hooks,
withoutManagedKiroCliV2Hooks,
withoutManagedKiroCliV3Hooks,
} from './kirocli-contract.js';
import {
commitKiroCliInstallation,
commitKiroCliUninstall,
preflightKiroCliInstallation,
prepareKiroCliInstallation,
prepareKiroCliUninstall,
} from './kirocli-installation.js';
import { kiroCliRuntimeFilesExist, readKiroCliSources } from './kirocli-io.js';
import { renderKiroIdeDocument } from './kiroide-contract.js';
import { SUPPORTED_AGENTS } from './registry.js';
const AGENT_KEY = 'kirocli';
const V2_AGENT_NAME = 'elydora-audit';
const V2_DESCRIPTION = 'Kiro CLI with Elydora audit and freeze enforcement';
const V3_GUARD_NAME = 'elydora-guard';
const V3_AUDIT_NAME = 'elydora-audit';
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
type JsonObject = Record<string, unknown>;
interface HookContract {
readonly guard: string;
readonly audit: string;
readonly configPath: string;
}
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function resolveV2Path(): string {
return path.join(os.homedir(), '.kiro', 'agents', `${V2_AGENT_NAME}.json`);
}
function resolveV3Path(): string {
const configDir = entry.configDir.replace(/^~/, os.homedir());
return path.join(configDir, entry.configFile);
}
function quotePosix(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function quoteWindows(value: string): string {
return `"${value.replaceAll('"', '\\"')}"`;
}
function buildCommand(scriptPath: string): string {
const quote = process.platform === 'win32' ? quoteWindows : quotePosix;
return `${quote(process.execPath)} ${quote(scriptPath)}`;
}
async function readJsonFile(configPath: string, label: string): Promise<JsonObject | undefined> {
let raw: string;
try {
raw = await fsp.readFile(configPath, 'utf-8');
} catch (error) {
if (isObject(error) && error.code === 'ENOENT') return undefined;
throw new Error(`Read ${label}: ${error instanceof Error ? error.message : String(error)}`);
}
let value: unknown;
try {
value = JSON.parse(raw);
} catch (error) {
throw new Error(`Failed to parse ${label}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!isObject(value)) throw new Error(`${label} must contain a JSON object`);
return value;
}
async function writeJsonFile(configPath: string, value: JsonObject, label: string): Promise<void> {
await fsp.mkdir(path.dirname(configPath), { recursive: true });
const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
try {
await fsp.writeFile(tempPath, JSON.stringify(value, null, 2) + '\n', {
encoding: 'utf-8',
mode: 0o600,
});
await fsp.rename(tempPath, configPath);
} catch (error) {
await fsp.unlink(tempPath).catch(() => undefined);
throw new Error(`Write ${label}: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function removeFile(configPath: string, label: string): Promise<void> {
try {
await fsp.unlink(configPath);
} catch (error) {
if (isObject(error) && error.code === 'ENOENT') return;
throw new Error(`Remove ${label}: ${error instanceof Error ? error.message : String(error)}`);
}
}
function hooksObject(settings: JsonObject, label: string): JsonObject {
if (settings.hooks === undefined) return {};
if (!isObject(settings.hooks)) throw new Error(`${label} field "hooks" must be an object`);
return { ...settings.hooks };
}
function hookEntries(hooks: JsonObject, event: string, label: string): JsonObject[] {
const value = hooks[event];
if (value === undefined) return [];
if (!Array.isArray(value) || !value.every(isObject)) {
throw new Error(`${label} field "hooks.${event}" must be an array of objects`);
}
return value;
}
function isManagedCommand(command: unknown, scriptName: string, agentId?: string): command is string {
if (typeof command !== 'string') return false;
const normalized = command.toLowerCase();
if (!normalized.includes('.elydora') || !normalized.includes(scriptName)) return false;
return !agentId || command.includes(agentId);
}
function withoutV2Hooks(entries: JsonObject[], agentId?: string): JsonObject[] {
return entries.filter(
(hook) => !isManagedCommand(hook.command, 'guard.js', agentId)
&& !isManagedCommand(hook.command, 'hook.js', agentId),
);
}
function buildV2Hook(scriptPath: string): JsonObject {
return {
matcher: '*',
command: buildCommand(scriptPath),
timeout_ms: 5000,
};
}
function isOwnedV2Config(settings: JsonObject, hooks: JsonObject): boolean {
const configKeys = new Set(['name', 'description', 'tools', 'includeMcpJson', 'hooks']);
const hookKeys = new Set(['preToolUse', 'postToolUse']);
return Object.keys(settings).every((key) => configKeys.has(key))
&& settings.name === V2_AGENT_NAME
&& settings.description === V2_DESCRIPTION
&& Array.isArray(settings.tools)
&& settings.tools.length === 1
&& settings.tools[0] === '*'
&& settings.includeMcpJson === true
&& Object.keys(hooks).every((key) => hookKeys.has(key))
&& Object.values(hooks).every((value) => Array.isArray(value) && value.length === 0);
}
function v3Hooks(settings: JsonObject): JsonObject[] {
if (settings.version !== undefined && settings.version !== 'v1') {
throw new Error('Kiro CLI v3 hooks config field "version" must be "v1"');
}
if (settings.hooks === undefined) return [];
if (!Array.isArray(settings.hooks) || !settings.hooks.every(isObject)) {
throw new Error('Kiro CLI v3 hooks config field "hooks" must be an array of objects');
}
return settings.hooks;
}
function v3ActionCommand(hook: JsonObject): unknown {
return isObject(hook.action) ? hook.action.command : undefined;
}
function isManagedV3Hook(hook: JsonObject, agentId?: string): boolean {
if (hook.name === V3_GUARD_NAME) {
return isManagedCommand(v3ActionCommand(hook), 'guard.js', agentId);
}
if (hook.name === V3_AUDIT_NAME) {
return isManagedCommand(v3ActionCommand(hook), 'hook.js', agentId);
}
return false;
}
function buildV3Hook(
name: string,
description: string,
trigger: 'PreToolUse' | 'PostToolUse',
scriptPath: string,
): JsonObject {
return {
name,
description,
trigger,
matcher: '.*',
action: { type: 'command', command: buildCommand(scriptPath) },
timeout: 5,
enabled: true,
};
}
function findV2Command(entries: JsonObject[], scriptName: string): string | undefined {
return entries.find((hook) => isManagedCommand(hook.command, scriptName))?.command as string | undefined;
}
function findV3Command(entries: JsonObject[], name: string, scriptName: string): string | undefined {
const hook = entries.find(
(candidate) => candidate.name === name && isManagedCommand(v3ActionCommand(candidate), scriptName),
);
return hook ? v3ActionCommand(hook) as string : undefined;
}
async function runtimeScriptsExist(contracts: HookContract[]): Promise<boolean> {
const root = path.join(os.homedir(), '.elydora');
let entries: Array<{ isDirectory(): boolean; name: string }>;
try {
entries = await fsp.readdir(root, { withFileTypes: true });
} catch (error) {
if (isObject(error) && error.code === 'ENOENT') return false;
throw new Error(`Read Elydora runtime directory: ${error instanceof Error ? error.message : String(error)}`);
}
for (const directory of entries) {
if (!directory.isDirectory()) continue;
const agentDir = path.join(root, directory.name);
const guardPath = path.join(agentDir, 'guard.js');
const hookPath = path.join(agentDir, 'hook.js');
const referencesScripts = contracts.some(
({ guard, audit }) => guard.includes(guardPath) && audit.includes(hookPath),
);
if (!referencesScripts) continue;
const configPath = path.join(agentDir, 'config.json');
const config = await readJsonFile(configPath, `Elydora runtime config at ${configPath}`);
if (!config || config.agent_name !== AGENT_KEY) continue;
for (const [scriptPath, label] of [
[guardPath, 'Elydora guard runtime'],
[hookPath, 'Elydora audit runtime'],
] as const) {
try {
await fsp.access(scriptPath);
} catch (error) {
if (isObject(error) && error.code === 'ENOENT') return false;
throw new Error(`${label} at ${scriptPath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return true;
}
return false;
function validateSources(
sources: Awaited<ReturnType<typeof readKiroCliSources>>,
): void {
requireAvailableKiroCliV2Document(sources.v2);
requireAvailableKiroCliV3Hooks(sources.v3.hooks);
}
export const kirocliPlugin: AgentPlugin = {
managesRuntime: true,
async preflightInstall(config: InstallConfig): Promise<void> {
const sources = await readKiroCliSources();
validateSources(sources);
await preflightKiroCliInstallation(config, sources);
},
async install(config: InstallConfig): Promise<void> {
const v2Path = resolveV2Path();
const v3Path = resolveV3Path();
const v2Settings = (await readJsonFile(v2Path, 'Kiro CLI v2 agent config')) ?? {};
const v3Settings = (await readJsonFile(v3Path, 'Kiro CLI v3 hooks config')) ?? {};
const v2Hooks = hooksObject(v2Settings, 'Kiro CLI v2 agent config');
const currentV3Hooks = v3Hooks(v3Settings);
v2Hooks.preToolUse = [
...withoutV2Hooks(hookEntries(v2Hooks, 'preToolUse', 'Kiro CLI v2 agent config')),
buildV2Hook(config.guardScriptPath),
];
v2Hooks.postToolUse = [
...withoutV2Hooks(hookEntries(v2Hooks, 'postToolUse', 'Kiro CLI v2 agent config')),
buildV2Hook(config.hookScriptPath),
];
const nextV2Settings: JsonObject = {
name: V2_AGENT_NAME,
description: V2_DESCRIPTION,
tools: ['*'],
includeMcpJson: true,
...v2Settings,
hooks: v2Hooks,
};
const nextV3Settings: JsonObject = {
...v3Settings,
version: 'v1',
hooks: [
...currentV3Hooks.filter((hook) => !isManagedV3Hook(hook)),
buildV3Hook(
V3_GUARD_NAME,
'Block tool use when the Elydora agent is frozen',
'PreToolUse',
config.guardScriptPath,
),
buildV3Hook(
V3_AUDIT_NAME,
'Record tool use in the Elydora audit trail',
'PostToolUse',
config.hookScriptPath,
),
],
};
await writeJsonFile(v2Path, nextV2Settings, 'Kiro CLI v2 agent config');
await writeJsonFile(v3Path, nextV3Settings, 'Kiro CLI v3 hooks config');
console.log(' Kiro CLI v2: start with "kiro-cli --agent elydora-audit".');
console.log(' Kiro CLI v3: start with "kiro-cli --v3"; global hooks load automatically.');
const sources = await readKiroCliSources();
validateSources(sources);
const paths = await preflightKiroCliInstallation(config, sources);
const v2 = renderKiroCliV2Installation(
sources.v2,
buildKiroCliV2Hooks(sources.v2, paths.guardPath, paths.auditPath),
);
const v3 = renderKiroIdeDocument(
sources.v3,
buildKiroCliV3Hooks(sources.v3.hooks, paths.guardPath, paths.auditPath),
);
const prepared = await prepareKiroCliInstallation(config, sources, v2, v3);
await commitKiroCliInstallation(prepared);
console.log(` Kiro CLI v2 agent: ${sources.paths.v2Path}`);
console.log(' Kiro CLI v2 verification: run "kiro-cli agent validate --path <agent-file>".');
console.log(' Kiro CLI v2 activation: start with "kiro-cli --agent elydora-audit".');
console.log(` Kiro CLI v3 global hooks (2.13.0+): ${sources.paths.v3Path}`);
console.log(' Kiro CLI v3 activation: start the TUI with "kiro-cli --v3".');
},
async uninstall(agentId?: string): Promise<void> {
const v2Path = resolveV2Path();
const v3Path = resolveV3Path();
const v2Settings = await readJsonFile(v2Path, 'Kiro CLI v2 agent config');
const v3Settings = await readJsonFile(v3Path, 'Kiro CLI v3 hooks config');
if (v2Settings) {
const hooks = hooksObject(v2Settings, 'Kiro CLI v2 agent config');
hooks.preToolUse = withoutV2Hooks(
hookEntries(hooks, 'preToolUse', 'Kiro CLI v2 agent config'),
agentId,
);
hooks.postToolUse = withoutV2Hooks(
hookEntries(hooks, 'postToolUse', 'Kiro CLI v2 agent config'),
agentId,
);
if (isOwnedV2Config(v2Settings, hooks)) {
await removeFile(v2Path, 'Kiro CLI v2 agent config');
} else {
await writeJsonFile(v2Path, { ...v2Settings, hooks }, 'Kiro CLI v2 agent config');
}
}
if (v3Settings) {
const hooks = v3Hooks(v3Settings).filter((hook) => !isManagedV3Hook(hook, agentId));
const owned = Object.keys(v3Settings).every((key) => key === 'version' || key === 'hooks');
if (owned && hooks.length === 0) {
await removeFile(v3Path, 'Kiro CLI v3 hooks config');
} else {
await writeJsonFile(v3Path, { ...v3Settings, hooks }, 'Kiro CLI v3 hooks config');
}
}
const sources = await readKiroCliSources();
const v2 = renderKiroCliV2Uninstall(
sources.v2,
withoutManagedKiroCliV2Hooks(sources.v2, agentId),
agentId,
);
const v3 = renderKiroCliV3Uninstall(
sources.v3,
withoutManagedKiroCliV3Hooks(sources.v3.hooks, agentId),
agentId,
);
const prepared = await prepareKiroCliUninstall(sources, v2, v3);
await commitKiroCliUninstall(prepared);
},
async status(): Promise<PluginStatus> {
const v2Path = resolveV2Path();
const v3Path = resolveV3Path();
const v2Settings = await readJsonFile(v2Path, 'Kiro CLI v2 agent config');
const v3Settings = await readJsonFile(v3Path, 'Kiro CLI v3 hooks config');
const contracts: HookContract[] = [];
if (v2Settings) {
const hooks = hooksObject(v2Settings, 'Kiro CLI v2 agent config');
const guard = findV2Command(
hookEntries(hooks, 'preToolUse', 'Kiro CLI v2 agent config'),
'guard.js',
);
const audit = findV2Command(
hookEntries(hooks, 'postToolUse', 'Kiro CLI v2 agent config'),
'hook.js',
);
if (guard && audit) contracts.push({ guard, audit, configPath: v2Path });
}
if (v3Settings) {
const hooks = v3Hooks(v3Settings);
const guard = findV3Command(hooks, V3_GUARD_NAME, 'guard.js');
const audit = findV3Command(hooks, V3_AUDIT_NAME, 'hook.js');
if (guard && audit) contracts.push({ guard, audit, configPath: v3Path });
}
const hookConfigured = contracts.length > 0;
const hookScriptExists = hookConfigured ? await runtimeScriptsExist(contracts) : false;
const sources = await readKiroCliSources();
validateSources(sources);
const contract = commonKiroCliRuntimeContract(sources.v2, sources.v3.hooks);
const hookConfigured = contract !== undefined;
const hookScriptExists = contract !== undefined
&& await kiroCliRuntimeFilesExist(contract);
return {
installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY,
displayName: entry.name,
hookConfigured,
hookScriptExists,
configPath: contracts.at(-1)?.configPath ?? v3Path,
configPath: sources.paths.v3Path,
};
},
};
+152
View File
@@ -0,0 +1,152 @@
import os from 'node:os';
import path from 'node:path';
interface ParsedArgument {
readonly value: string;
readonly next: number;
}
export interface KiroIdeRuntimeReference {
readonly agentId: string;
readonly scriptPath: string;
}
export function sameKiroIdePath(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 sameKiroIdeAgentId(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.env.SystemRoot;
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 buildKiroIdeCommand(scriptPath: string): string {
if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) {
throw new Error('Kiro IDE hook commands require absolute executable and script paths');
}
return process.platform === 'win32'
? windowsCommand(scriptPath)
: `${quotePosix(process.execPath)} ${quotePosix(scriptPath)}`;
}
function readPosixArgument(command: string, start: number): ParsedArgument | undefined {
if (command[start] !== "'") return undefined;
const apostrophe = `'"'"'`;
let value = '';
for (let index = start + 1; index < command.length;) {
if (command.startsWith(apostrophe, index)) {
value += "'";
index += apostrophe.length;
continue;
}
if (command[index] === "'") return { value, next: index + 1 };
value += command[index];
index += 1;
}
return undefined;
}
function parsePosixCommand(command: string): readonly [string, string] | undefined {
const executable = readPosixArgument(command, 0);
if (!executable || command[executable.next] !== ' ') return undefined;
const script = readPosixArgument(command, executable.next + 1);
return script && script.next === command.length ? [executable.value, script.value] : undefined;
}
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);
return script && source.slice(script.next) === '; exit $LASTEXITCODE'
? [executable.value, script.value]
: undefined;
}
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 buffer = Buffer.from(match[2], 'base64');
if (buffer.length % 2 !== 0 || buffer.toString('base64') !== match[2]) return undefined;
return parsePowerShellSource(buffer.toString('utf16le'));
}
function isNodeExecutable(filePath: string): boolean {
const basename = path.basename(filePath).toLowerCase();
return basename === 'node' || basename === 'node.exe';
}
export function kiroIdeRuntimeReference(
command: string,
scriptName: string,
): KiroIdeRuntimeReference | undefined {
const parsed = process.platform === 'win32'
? parseWindowsCommand(command)
: parsePosixCommand(command);
if (!parsed
|| !path.isAbsolute(parsed[0])
|| !isNodeExecutable(parsed[0])
|| !path.isAbsolute(parsed[1])
|| path.basename(parsed[1]) !== scriptName) return undefined;
const agentDirectory = path.dirname(parsed[1]);
if (!sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) {
return undefined;
}
const agentId = path.basename(agentDirectory);
return agentId && agentId !== '.' && agentId !== '..'
? { agentId, scriptPath: parsed[1] }
: undefined;
}
+316
View File
@@ -0,0 +1,316 @@
import os from 'node:os';
import path from 'node:path';
import {
buildKiroIdeCommand,
kiroIdeRuntimeReference,
sameKiroIdeAgentId,
sameKiroIdePath,
type KiroIdeRuntimeReference,
} from './kiroide-command.js';
import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js';
export const AGENT_KEY = 'kiroide';
export const AUDIT_SCRIPT = 'hook.js';
export const CONFIG_FILE = 'elydora-audit.json';
export const GUARD_SCRIPT = 'guard.js';
export const LEGACY_CONFIG_FILE = 'elydora-audit.kiro.hook';
export const HOOK_TIMEOUT_SECONDS = 10;
const GUARD_NAME = 'elydora-guard';
const AUDIT_NAME = 'elydora-audit';
const GUARD_DESCRIPTION = 'Block tool use when the Elydora agent is frozen';
const AUDIT_DESCRIPTION = 'Record tool use in the Elydora audit trail';
const KIRO_TRIGGERS = new Set([
'SessionStart',
'Stop',
'PreToolUse',
'PostToolUse',
'PreTaskExec',
'PostTaskExec',
'UserPromptSubmit',
'PostFileCreate',
'PostFileSave',
'PostFileDelete',
]);
export interface KiroIdeAction extends JsonObject {
readonly type: 'command' | 'agent';
readonly command?: string;
readonly prompt?: string;
}
export interface KiroIdeHook extends JsonObject {
readonly name: string;
readonly description?: string;
readonly trigger: string;
readonly matcher?: string;
readonly action: KiroIdeAction;
readonly timeout?: number;
readonly enabled?: boolean;
}
export interface KiroIdeDocument {
readonly exists: boolean;
readonly filePath: string;
readonly root: JsonObject;
readonly hooks: readonly KiroIdeHook[];
readonly raw?: string;
}
export interface RenderedKiroIdeDocument {
readonly document: KiroIdeDocument;
readonly changed: boolean;
readonly next?: string;
}
export interface KiroIdeRuntimeContract {
readonly agentId: string;
readonly guardPath: string;
readonly auditPath: string;
}
export interface KiroIdePaths {
readonly workspaceRoot: string;
readonly kiroDirectory: string;
readonly hooksDirectory: string;
readonly configPath: string;
readonly legacyConfigPath: string;
}
export function resolveKiroIdePaths(
workspaceRoot = process.cwd(),
homeDirectory = os.homedir(),
): KiroIdePaths {
const resolvedWorkspace = path.resolve(workspaceRoot);
const kiroDirectory = path.join(resolvedWorkspace, '.kiro');
const hooksDirectory = path.join(kiroDirectory, 'hooks');
return {
workspaceRoot: resolvedWorkspace,
kiroDirectory,
hooksDirectory,
configPath: path.join(hooksDirectory, CONFIG_FILE),
legacyConfigPath: path.join(homeDirectory, '.kiro', 'hooks', LEGACY_CONFIG_FILE),
};
}
function validateAction(value: unknown, label: string): KiroIdeAction {
if (!isObject(value)) throw new Error(`${label} action must be an object`);
if (value.type === 'command') {
if (typeof value.command !== 'string' || !value.command) {
throw new Error(`${label} command action requires a non-empty command`);
}
return value as KiroIdeAction;
}
if (value.type === 'agent') {
if (typeof value.prompt !== 'string' || !value.prompt) {
throw new Error(`${label} agent action requires a non-empty prompt`);
}
return value as KiroIdeAction;
}
throw new Error(`${label} action has unsupported type "${String(value.type)}"`);
}
function validateHook(value: unknown, index: number, documentLabel: string): KiroIdeHook {
const label = `${documentLabel} hooks[${index}]`;
if (!isObject(value)) throw new Error(`${label} must be an object`);
if (typeof value.name !== 'string' || !value.name) throw new Error(`${label} requires a name`);
if (typeof value.trigger !== 'string' || !KIRO_TRIGGERS.has(value.trigger)) {
throw new Error(`${label} has unsupported trigger "${String(value.trigger)}"`);
}
if (value.description !== undefined && typeof value.description !== 'string') {
throw new Error(`${label} description must be a string`);
}
if (value.matcher !== undefined && typeof value.matcher !== 'string') {
throw new Error(`${label} matcher must be a string`);
}
if (value.timeout !== undefined
&& (!Number.isSafeInteger(value.timeout) || (value.timeout as number) < 0)) {
throw new Error(`${label} timeout must be a non-negative integer`);
}
if (value.enabled !== undefined && typeof value.enabled !== 'boolean') {
throw new Error(`${label} enabled must be a boolean`);
}
return { ...value, action: validateAction(value.action, label) } as KiroIdeHook;
}
export function parseKiroIdeDocument(
filePath: string,
raw: string,
documentLabel = 'Kiro IDE hooks',
): KiroIdeDocument {
const root = parseStrictJsonObject(raw, `${documentLabel} at ${filePath}`);
if (root.version !== 'v1') throw new Error(`${documentLabel} version must be "v1": ${filePath}`);
if (!Array.isArray(root.hooks)) throw new Error(`${documentLabel} field "hooks" must be an array: ${filePath}`);
return {
exists: true,
filePath,
root,
hooks: root.hooks.map((hook, index) => validateHook(hook, index, documentLabel)),
raw,
};
}
export function createKiroIdeDocument(filePath: string): KiroIdeDocument {
return { exists: false, filePath, root: {}, hooks: [] };
}
function exactKeys(value: JsonObject, keys: readonly string[]): boolean {
return Object.keys(value).sort().join('|') === [...keys].sort().join('|');
}
function managedSpecification(name: string): {
readonly description: string;
readonly trigger: 'PreToolUse' | 'PostToolUse';
readonly scriptName: string;
} | undefined {
if (name === GUARD_NAME) {
return { description: GUARD_DESCRIPTION, trigger: 'PreToolUse', scriptName: GUARD_SCRIPT };
}
if (name === AUDIT_NAME) {
return { description: AUDIT_DESCRIPTION, trigger: 'PostToolUse', scriptName: AUDIT_SCRIPT };
}
return undefined;
}
function managedReference(hook: KiroIdeHook) {
const specification = managedSpecification(hook.name);
const owned = ownedReference(hook);
if (!specification
|| !owned
|| !exactKeys(hook, ['name', 'description', 'trigger', 'matcher', 'action', 'timeout', 'enabled'])
|| hook.description !== specification.description
|| hook.trigger !== specification.trigger
|| hook.matcher !== '.*'
|| hook.timeout !== HOOK_TIMEOUT_SECONDS
|| hook.enabled !== true
|| !exactKeys(hook.action, ['type', 'command'])) return undefined;
return owned;
}
function ownedReference(hook: KiroIdeHook) {
const specification = managedSpecification(hook.name);
if (!specification
|| hook.action.type !== 'command'
|| typeof hook.action.command !== 'string') return undefined;
return kiroIdeRuntimeReference(hook.action.command, specification.scriptName);
}
export function requireAvailableKiroIdeHooks(
hooks: readonly KiroIdeHook[],
productLabel = 'Kiro IDE',
): void {
for (const hook of hooks) {
if (managedSpecification(hook.name) && !ownedReference(hook)) {
throw new Error(`${productLabel} hook name "${hook.name}" conflicts with the Elydora contract`);
}
}
}
export function buildKiroIdeHook(
name: typeof GUARD_NAME | typeof AUDIT_NAME,
scriptPath: string,
): KiroIdeHook {
const specification = managedSpecification(name)!;
return {
name,
description: specification.description,
trigger: specification.trigger,
matcher: '.*',
action: { type: 'command', command: buildKiroIdeCommand(scriptPath) },
timeout: HOOK_TIMEOUT_SECONDS,
enabled: true,
};
}
export function withoutManagedKiroIdeHooks(
hooks: readonly KiroIdeHook[],
agentId?: string,
): KiroIdeHook[] {
return hooks.filter((hook) => {
const reference = ownedReference(hook);
return !reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId));
});
}
function entirelyManaged(document: KiroIdeDocument): boolean {
return document.exists
&& exactKeys(document.root, ['version', 'hooks'])
&& document.hooks.length > 0
&& document.hooks.every((hook) => managedReference(hook) !== undefined);
}
export function renderKiroIdeDocument(
document: KiroIdeDocument,
hooks: readonly KiroIdeHook[],
): RenderedKiroIdeDocument {
if (!document.exists && hooks.length === 0) return { document, changed: false };
if (document.exists
&& hooks.length === document.hooks.length
&& hooks.every((hook, index) => hook === document.hooks[index])) {
return { document, changed: false };
}
if (hooks.length === 0 && entirelyManaged(document)) return { document, changed: true };
const next = `${JSON.stringify({ ...document.root, version: 'v1', hooks }, null, 2)}\n`;
return { document, changed: next !== document.raw, next };
}
export function kiroIdeRuntimeContracts(
hooks: readonly KiroIdeHook[],
): KiroIdeRuntimeContract[] {
const guards = new Map<string, KiroIdeRuntimeReference[]>();
const audits = new Map<string, KiroIdeRuntimeReference[]>();
for (const hook of hooks) {
if (!managedSpecification(hook.name)) continue;
const reference = managedReference(hook);
if (!reference) return [];
const key = process.platform === 'win32' ? reference.agentId.toLowerCase() : reference.agentId;
const target = hook.name === GUARD_NAME ? guards : audits;
const entries = target.get(key) ?? [];
entries.push(reference);
target.set(key, entries);
}
const contracts: KiroIdeRuntimeContract[] = [];
for (const [key, guard] of guards) {
const audit = audits.get(key);
if (guard.length !== 1 || audit?.length !== 1 || !guard[0] || !audit[0]) continue;
contracts.push({
agentId: guard[0].agentId,
guardPath: guard[0].scriptPath,
auditPath: audit[0].scriptPath,
});
}
return contracts;
}
function legacyReference(command: unknown, scriptName: string) {
if (typeof command !== 'string') return undefined;
const match = /^node "([^"\r\n]+)"$/.exec(command);
if (!match || !path.isAbsolute(match[1]) || path.basename(match[1]) !== scriptName) return undefined;
const agentDirectory = path.dirname(match[1]);
if (!sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) {
return undefined;
}
return { agentId: path.basename(agentDirectory), scriptPath: match[1] };
}
export function legacyKiroIdeRuntimeContract(raw: string, filePath: string): KiroIdeRuntimeContract | undefined {
const root = parseStrictJsonObject(raw, `legacy Kiro IDE hook at ${filePath}`);
if (!exactKeys(root, ['name', 'description', 'version', 'hooks'])
|| root.name !== 'Elydora Audit'
|| root.description !== 'Sends tool-use events to the Elydora tamper-evident audit platform'
|| root.version !== '1.0.0'
|| !isObject(root.hooks)
|| !exactKeys(root.hooks, ['pre_tool_use', 'post_tool_use'])) return undefined;
const guardHook = root.hooks.pre_tool_use;
const auditHook = root.hooks.post_tool_use;
if (!isObject(guardHook)
|| !isObject(auditHook)
|| !exactKeys(guardHook, ['command', 'timeout_ms'])
|| !exactKeys(auditHook, ['command', 'timeout_ms'])
|| guardHook.timeout_ms !== 5000
|| auditHook.timeout_ms !== 5000) return undefined;
const guard = legacyReference(guardHook.command, GUARD_SCRIPT);
const audit = legacyReference(auditHook.command, AUDIT_SCRIPT);
if (!guard || !audit || !sameKiroIdeAgentId(guard.agentId, audit.agentId)) return undefined;
return { agentId: guard.agentId, guardPath: guard.scriptPath, auditPath: audit.scriptPath };
}
@@ -0,0 +1,158 @@
import path from 'node:path';
import type { InstallConfig } from './base.js';
import { sameKiroIdeAgentId } from './kiroide-command.js';
import {
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
type KiroIdePaths,
type RenderedKiroIdeDocument,
} from './kiroide-contract.js';
import {
type KiroIdeSources,
type LegacyKiroIdeDocument,
requirePhysicalLegacyDirectory,
} from './kiroide-io.js';
import {
commitManagedInstallation,
preflightManagedInstallation,
prepareManagedInstallation,
type ManagedRuntimePaths,
type PreparedManagedInstallation,
type RenameFile,
} from './managed-installation.js';
import {
commitManagedTransaction,
prepareManagedFileChange,
type ManagedFileChange,
type PreparedManagedTransaction,
} from './managed-transaction.js';
const DISPLAY_NAME = 'Kiro IDE';
const DIRECTORY_LABEL = 'Kiro IDE hooks directory';
export type KiroIdeRuntimePaths = ManagedRuntimePaths;
export type PreparedKiroIdeInstallation = PreparedManagedInstallation;
export type { RenameFile };
function hookLocation(paths: KiroIdePaths) {
return [{ directoryLabel: DIRECTORY_LABEL, filePath: paths.configPath }];
}
function removesLegacy(legacy: LegacyKiroIdeDocument, agentId?: string): boolean {
return legacy.contract !== undefined
&& (agentId === undefined || sameKiroIdeAgentId(legacy.contract.agentId, agentId));
}
async function prepareLegacyRemoval(
legacy: LegacyKiroIdeDocument,
agentId?: string,
): Promise<ManagedFileChange | undefined> {
if (!removesLegacy(legacy, agentId)) return undefined;
if (legacy.raw === undefined) throw new Error(`Legacy Kiro IDE hook source is missing: ${legacy.filePath}`);
await requirePhysicalLegacyDirectory(legacy);
return prepareManagedFileChange({
filePath: legacy.filePath,
label: 'legacy Kiro IDE hook',
next: undefined,
mode: 0o600,
expectedSource: legacy.raw,
verifyExpectedSource: true,
});
}
export async function preflightKiroIdeInstallation(
config: InstallConfig,
paths: KiroIdePaths,
): Promise<KiroIdeRuntimePaths> {
return preflightManagedInstallation({
agentKey: AGENT_KEY,
hookLocations: hookLocation(paths),
config,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
export async function prepareKiroIdeInstallation(
config: InstallConfig,
sources: KiroIdeSources,
rendered: RenderedKiroIdeDocument,
): Promise<PreparedKiroIdeInstallation> {
if (rendered.next === undefined) throw new Error('Kiro IDE installation requires a hook document');
const prepared = await prepareManagedInstallation({
agentKey: AGENT_KEY,
displayName: DISPLAY_NAME,
hookSources: [{
directoryLabel: DIRECTORY_LABEL,
label: 'Kiro IDE hooks',
filePath: sources.paths.configPath,
expectedSource: sources.document.raw,
source: rendered.next,
}],
config,
auditOptions: { nativePayload: true },
}, GUARD_SCRIPT, AUDIT_SCRIPT);
const legacyChange = await prepareLegacyRemoval(sources.legacy, config.agentId);
if (!legacyChange) return prepared;
return {
...prepared,
transaction: {
...prepared.transaction,
directories: [
...prepared.transaction.directories,
{ path: path.dirname(sources.legacy.filePath), label: 'legacy Kiro IDE hooks directory' },
],
changes: [...prepared.transaction.changes, legacyChange],
},
};
}
export async function commitKiroIdeInstallation(
prepared: PreparedKiroIdeInstallation,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedInstallation(prepared, renameFile);
}
export async function prepareKiroIdeUninstall(
sources: KiroIdeSources,
rendered: RenderedKiroIdeDocument,
agentId?: string,
): Promise<PreparedManagedTransaction> {
const changes: ManagedFileChange[] = [];
const directories = [];
if (rendered.changed) {
const configChange = await prepareManagedFileChange({
filePath: sources.document.filePath,
label: 'Kiro IDE hooks',
next: rendered.next,
mode: 0o600,
expectedSource: sources.document.raw,
verifyExpectedSource: true,
});
if (configChange) {
changes.push(configChange);
directories.push({ path: sources.paths.hooksDirectory, label: DIRECTORY_LABEL });
}
}
const legacyChange = await prepareLegacyRemoval(sources.legacy, agentId);
if (legacyChange) {
changes.push(legacyChange);
directories.push({
path: path.dirname(sources.legacy.filePath),
label: 'legacy Kiro IDE hooks directory',
});
}
return {
displayName: DISPLAY_NAME,
operation: 'uninstall',
directories,
changes,
};
}
export async function commitKiroIdeUninstall(
prepared: PreparedManagedTransaction,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedTransaction(prepared, renameFile);
}
+74
View File
@@ -0,0 +1,74 @@
import path from 'node:path';
import {
createKiroIdeDocument,
legacyKiroIdeRuntimeContract,
parseKiroIdeDocument,
resolveKiroIdePaths,
type KiroIdeDocument,
type KiroIdePaths,
type KiroIdeRuntimeContract,
} from './kiroide-contract.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
export interface LegacyKiroIdeDocument {
readonly exists: boolean;
readonly filePath: string;
readonly raw?: string;
readonly contract?: KiroIdeRuntimeContract;
}
export interface KiroIdeSources {
readonly paths: KiroIdePaths;
readonly document: KiroIdeDocument;
readonly legacy: LegacyKiroIdeDocument;
}
async function inspectWorkspace(paths: KiroIdePaths): Promise<void> {
if (!await inspectPhysicalDirectory(paths.workspaceRoot, 'Kiro IDE workspace')) {
throw new Error(`Kiro IDE workspace is missing: ${paths.workspaceRoot}`);
}
await inspectPhysicalDirectory(paths.kiroDirectory, 'Kiro IDE configuration directory');
await inspectPhysicalDirectory(paths.hooksDirectory, 'Kiro IDE hooks directory');
}
async function readDocument(paths: KiroIdePaths): Promise<KiroIdeDocument> {
await inspectWorkspace(paths);
const snapshot = await readPhysicalFile(paths.configPath, 'Kiro IDE hooks');
return snapshot
? parseKiroIdeDocument(paths.configPath, snapshot.contents)
: createKiroIdeDocument(paths.configPath);
}
async function readLegacyDocument(paths: KiroIdePaths): Promise<LegacyKiroIdeDocument> {
const snapshot = await readPhysicalFile(paths.legacyConfigPath, 'legacy Kiro IDE hook');
if (!snapshot) return { exists: false, filePath: paths.legacyConfigPath };
return {
exists: true,
filePath: paths.legacyConfigPath,
raw: snapshot.contents,
contract: legacyKiroIdeRuntimeContract(snapshot.contents, paths.legacyConfigPath),
};
}
export async function readKiroIdeSources(): Promise<KiroIdeSources> {
const paths = resolveKiroIdePaths();
const [document, legacy] = await Promise.all([
readDocument(paths),
readLegacyDocument(paths),
]);
return { paths, document, legacy };
}
export async function requirePhysicalLegacyDirectory(
legacy: LegacyKiroIdeDocument,
): Promise<void> {
if (!legacy.exists) return;
const hooksDirectory = path.dirname(legacy.filePath);
const kiroDirectory = path.dirname(hooksDirectory);
if (!await inspectPhysicalDirectory(kiroDirectory, 'legacy Kiro IDE configuration directory')) {
throw new Error(`Legacy Kiro IDE configuration directory is missing: ${kiroDirectory}`);
}
if (!await inspectPhysicalDirectory(hooksDirectory, 'legacy Kiro IDE hooks directory')) {
throw new Error(`Legacy Kiro IDE hooks directory is missing: ${hooksDirectory}`);
}
}
+55 -73
View File
@@ -1,94 +1,76 @@
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,
buildKiroIdeHook,
kiroIdeRuntimeContracts,
renderKiroIdeDocument,
requireAvailableKiroIdeHooks,
withoutManagedKiroIdeHooks,
} from './kiroide-contract.js';
import {
commitKiroIdeInstallation,
commitKiroIdeUninstall,
preflightKiroIdeInstallation,
prepareKiroIdeInstallation,
prepareKiroIdeUninstall,
} from './kiroide-installation.js';
import { readKiroIdeSources } from './kiroide-io.js';
import { managedRuntimeFilesExist } from './managed-runtime-status.js';
import { SUPPORTED_AGENTS } from './registry.js';
const AGENT_KEY = 'kiroide';
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
function resolveConfigDir(): string {
// .kiro/hooks is project-relative, but for global install use home dir
return path.join(os.homedir(), entry.configDir);
}
function resolveConfigPath(): string {
return path.join(resolveConfigDir(), entry.configFile);
}
export const kiroidePlugin: AgentPlugin = {
async install(config: InstallConfig): Promise<void> {
const configDir = resolveConfigDir();
await fsp.mkdir(configDir, { recursive: true });
managesRuntime: true,
const configPath = resolveConfigPath();
const hookConfig = {
name: 'Elydora Audit',
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
version: '1.0.0',
hooks: {
pre_tool_use: {
command: `node "${config.guardScriptPath}"`,
timeout_ms: 5000,
},
post_tool_use: {
command: `node "${config.hookScriptPath}"`,
timeout_ms: 5000,
},
},
};
await fsp.writeFile(configPath, JSON.stringify(hookConfig, null, 2) + '\n', 'utf-8');
async preflightInstall(config: InstallConfig): Promise<void> {
const sources = await readKiroIdeSources();
requireAvailableKiroIdeHooks(sources.document.hooks);
await preflightKiroIdeInstallation(config, sources.paths);
},
async uninstall(_agentId?: string): Promise<void> {
const configPath = resolveConfigPath();
try {
await fsp.unlink(configPath);
} catch {
// Already removed
}
async install(config: InstallConfig): Promise<void> {
const sources = await readKiroIdeSources();
requireAvailableKiroIdeHooks(sources.document.hooks);
const paths = await preflightKiroIdeInstallation(config, sources.paths);
const hooks = withoutManagedKiroIdeHooks(sources.document.hooks);
const rendered = renderKiroIdeDocument(sources.document, [
...hooks,
buildKiroIdeHook('elydora-guard', paths.guardPath),
buildKiroIdeHook('elydora-audit', paths.auditPath),
]);
const prepared = await prepareKiroIdeInstallation(config, sources, rendered);
await commitKiroIdeInstallation(prepared);
console.log(` Kiro IDE workspace hooks: ${sources.paths.configPath}`);
console.log(' Kiro IDE verification: confirm Elydora hooks in the Agent Hooks panel.');
},
async uninstall(agentId?: string): Promise<void> {
const sources = await readKiroIdeSources();
const rendered = renderKiroIdeDocument(
sources.document,
withoutManagedKiroIdeHooks(sources.document.hooks, agentId),
);
const prepared = await prepareKiroIdeUninstall(sources, rendered, agentId);
await commitKiroIdeUninstall(prepared);
},
async status(): Promise<PluginStatus> {
const configPath = resolveConfigPath();
let hookConfigured = false;
let hookScriptPath = '';
try {
const raw = await fsp.readFile(configPath, 'utf-8');
const config = JSON.parse(raw);
hookConfigured = !!(config.hooks && config.hooks.pre_tool_use && config.hooks.post_tool_use);
// Extract hook script path from post_tool_use command
if (hookConfigured && config.hooks.post_tool_use) {
const cmd = config.hooks.post_tool_use.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 readKiroIdeSources();
requireAvailableKiroIdeHooks(sources.document.hooks);
const contracts = kiroIdeRuntimeContracts(sources.document.hooks);
const hookConfigured = contracts.length > 0;
const hookScriptExists = hookConfigured
&& (await Promise.all(contracts.map((contract) => (
managedRuntimeFilesExist(contract, AGENT_KEY)
)))).every(Boolean);
return {
installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY,
displayName: entry.name,
hookConfigured,
hookScriptExists,
configPath,
configPath: sources.paths.configPath,
};
},
};
+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,
};
},
};
@@ -0,0 +1,119 @@
import os from 'node:os';
import path from 'node:path';
import { generateGuardScript, type GuardScriptOptions } from './guard-template.js';
import { generateHookScript, type HookScriptOptions } from './hook-template.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { samePath } from './managed-installation.js';
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
const MAX_SECRET_BYTES = 64 * 1024;
const MAX_CONFIG_BYTES = 512 * 1024;
const GUARD_SCRIPT = 'guard.js';
const AUDIT_SCRIPT = 'hook.js';
export interface ManagedRuntimeContract {
readonly agentId: string;
readonly guardPath: string;
readonly auditPath: string;
}
export interface ManagedRuntimeStatusOptions {
readonly guardOptions?: GuardScriptOptions;
readonly auditOptions?: HookScriptOptions;
}
function sameAgentId(left: string, right: string): boolean {
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
}
function requireString(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: ManagedRuntimeContract,
configPath: string,
agentKey: 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}`);
requireString(config.org_id, 'org_id', configPath);
requireString(config.kid, 'kid', configPath);
const agentId = requireString(config.agent_id, 'agent_id', configPath);
if (!sameAgentId(agentId, contract.agentId) || config.agent_name !== agentKey) {
throw new Error(`Elydora runtime identity does not match ${agentKey} hooks: ${configPath}`);
}
if (config.token !== undefined) requireString(config.token, 'token', configPath);
const rawBaseUrl = requireString(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}`);
}
}
function validContractPaths(contract: ManagedRuntimeContract): boolean {
const runtimeRoot = path.join(os.homedir(), '.elydora');
const agentDirectory = path.dirname(contract.guardPath);
return samePath(path.dirname(agentDirectory), runtimeRoot)
&& sameAgentId(path.basename(agentDirectory), contract.agentId)
&& samePath(contract.guardPath, path.join(agentDirectory, GUARD_SCRIPT))
&& samePath(contract.auditPath, path.join(agentDirectory, AUDIT_SCRIPT));
}
export async function managedRuntimeFilesExist(
contract: ManagedRuntimeContract,
agentKey: string,
options: ManagedRuntimeStatusOptions = {},
): Promise<boolean> {
if (!validContractPaths(contract)) return false;
const runtimeRoot = path.join(os.homedir(), '.elydora');
const agentDirectory = path.dirname(contract.guardPath);
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', MAX_CONFIG_BYTES),
readPhysicalFile(keyPath, 'Elydora private key', MAX_SECRET_BYTES),
readPhysicalFile(contract.guardPath, 'Elydora guard runtime'),
readPhysicalFile(contract.auditPath, 'Elydora audit runtime'),
]);
if (!config || !key || !guard || !audit) return false;
validateRuntimeConfig(
parseStrictJsonObject(config.contents, `Elydora runtime config at ${configPath}`),
contract,
configPath,
agentKey,
);
validatePrivateKey(key.contents, keyPath);
return guard.contents === generateGuardScript(agentKey, contract.agentId, options.guardOptions)
&& audit.contents === generateHookScript(
agentKey,
contract.agentId,
options.auditOptions ?? { nativePayload: true },
);
}
+405
View File
@@ -0,0 +1,405 @@
import os from 'node:os';
import path from 'node:path';
import type { HookScriptOptions } from './hook-template.js';
import { samePath } from './managed-installation.js';
import { parseStrictJsonObject } from './strict-json.js';
export const AGENT_KEY = 'opencode';
export const AUDIT_SCRIPT = 'hook.js';
export const GUARD_SCRIPT = 'guard.js';
export const PLUGIN_FILE = 'elydora-audit.js';
export const LEGACY_PLUGIN_FILE = 'elydora-audit.mjs';
export const OPENCODE_AUDIT_OPTIONS = Object.freeze({
failClosed: true,
nativePayload: true,
}) satisfies HookScriptOptions;
const METADATA_MARKER = '// @elydora-opencode-plugin ';
const METADATA_VERSION = 1;
const MAX_RUNTIME_STDERR_BYTES = 64 * 1024;
const GUARD_TIMEOUT_MILLISECONDS = 5_000;
const AUDIT_TIMEOUT_MILLISECONDS = 7_000;
export interface OpenCodePaths {
readonly homeDirectory: string;
readonly configRoot: string;
readonly configDirectory: string;
readonly pluginsDirectory: string;
readonly pluginPath: string;
readonly legacyPluginPath: string;
}
export interface OpenCodeRuntimeContract {
readonly agentId: string;
readonly executablePath: string;
readonly guardPath: string;
readonly auditPath: string;
}
export interface OpenCodeLegacyContract {
readonly agentId: string;
readonly guardPath: string;
readonly auditPath: string;
}
interface OpenCodePluginMetadata {
readonly version: 1;
readonly agentName: 'opencode';
readonly agentId: string;
readonly executablePath: string;
readonly guardPath: string;
readonly auditPath: string;
}
export function sameOpenCodeAgentId(left: string, right: string): boolean {
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
}
export function resolveOpenCodePaths(
homeDirectory = os.homedir(),
xdgConfigHome = process.env.XDG_CONFIG_HOME,
): OpenCodePaths {
const resolvedHome = path.resolve(homeDirectory);
const configRoot = xdgConfigHome || path.join(resolvedHome, '.config');
if (!path.isAbsolute(configRoot)) {
throw new Error('XDG_CONFIG_HOME must be an absolute path for OpenCode integration');
}
const configDirectory = path.join(configRoot, 'opencode');
const pluginsDirectory = path.join(configDirectory, 'plugins');
return {
homeDirectory: resolvedHome,
configRoot: path.normalize(configRoot),
configDirectory,
pluginsDirectory,
pluginPath: path.join(pluginsDirectory, PLUGIN_FILE),
legacyPluginPath: path.join(pluginsDirectory, LEGACY_PLUGIN_FILE),
};
}
function validAgentSegment(agentId: string): boolean {
return Boolean(agentId)
&& agentId !== '.'
&& agentId !== '..'
&& path.basename(agentId) === agentId;
}
function isNodeExecutable(filePath: string): boolean {
const basename = path.basename(filePath).toLowerCase();
return basename === 'node' || basename === 'node.exe';
}
function validateRuntimePaths(metadata: OpenCodePluginMetadata): void {
if (!validAgentSegment(metadata.agentId)) {
throw new Error('OpenCode plugin metadata contains an invalid agentId');
}
if (!path.isAbsolute(metadata.executablePath) || !isNodeExecutable(metadata.executablePath)) {
throw new Error('OpenCode plugin metadata executablePath must reference Node.js');
}
const agentDirectory = path.join(os.homedir(), '.elydora', metadata.agentId);
if (!samePath(metadata.guardPath, path.join(agentDirectory, GUARD_SCRIPT))
|| !samePath(metadata.auditPath, path.join(agentDirectory, AUDIT_SCRIPT))) {
throw new Error('OpenCode plugin metadata references an unexpected runtime path');
}
}
function validateMetadata(value: unknown): OpenCodePluginMetadata {
const object = value as Record<string, unknown>;
if (!object || typeof object !== 'object' || Array.isArray(object)) {
throw new Error('OpenCode plugin metadata must be an object');
}
if (Object.keys(object).sort().join(',') !== 'agentId,agentName,auditPath,executablePath,guardPath,version') {
throw new Error('OpenCode plugin metadata contains an unexpected field set');
}
if (object.version !== METADATA_VERSION) throw new Error('OpenCode plugin metadata version must be 1');
if (object.agentName !== AGENT_KEY) throw new Error('OpenCode plugin metadata agentName must be opencode');
for (const field of ['agentId', 'executablePath', 'guardPath', 'auditPath']) {
if (typeof object[field] !== 'string' || object[field].length === 0) {
throw new Error(`OpenCode plugin metadata ${field} must be a non-empty string`);
}
}
const metadata = object as unknown as OpenCodePluginMetadata;
validateRuntimePaths(metadata);
return metadata;
}
function encodeMetadata(metadata: OpenCodePluginMetadata): string {
return Buffer.from(JSON.stringify(metadata), 'utf-8').toString('base64url');
}
export function buildOpenCodeMetadata(
agentId: string,
executablePath: string,
guardPath: string,
auditPath: string,
): OpenCodeRuntimeContract {
return validateMetadata({
version: METADATA_VERSION,
agentName: AGENT_KEY,
agentId,
executablePath,
guardPath,
auditPath,
});
}
function normalizeGeneratedSource(source: string): string {
return source.replaceAll('\r\n', '\n');
}
export function buildOpenCodePlugin(contract: OpenCodeRuntimeContract): string {
const metadata = validateMetadata({
version: METADATA_VERSION,
agentName: AGENT_KEY,
...contract,
});
return normalizeGeneratedSource(`// Elydora Audit Plugin for OpenCode
${METADATA_MARKER}${encodeMetadata(metadata)}
// Generated by @elydora/sdk. DO NOT EDIT.
import { spawn, spawnSync } from 'node:child_process';
const RUNTIME_EXECUTABLE = ${JSON.stringify(metadata.executablePath)};
const GUARD_SCRIPT_PATH = ${JSON.stringify(metadata.guardPath)};
const AUDIT_SCRIPT_PATH = ${JSON.stringify(metadata.auditPath)};
const MAX_STDERR_BYTES = ${MAX_RUNTIME_STDERR_BYTES};
const GUARD_TIMEOUT_MS = ${GUARD_TIMEOUT_MILLISECONDS};
const AUDIT_TIMEOUT_MS = ${AUDIT_TIMEOUT_MILLISECONDS};
function eventPayload(eventName, input, output) {
return JSON.stringify({
hook_event_name: eventName,
tool_name: input?.tool || 'unknown',
tool_input: eventName === 'tool.execute.before' ? output?.args : input?.args,
session_id: input?.sessionID || 'unknown',
call_id: input?.callID || 'unknown',
input,
output,
});
}
function runGuard(input, output) {
let payload;
try {
payload = eventPayload('tool.execute.before', input, output);
} catch (error) {
throw new Error('Elydora guard input serialization failed: ' + error.message);
}
const result = spawnSync(RUNTIME_EXECUTABLE, [GUARD_SCRIPT_PATH], {
input: payload,
encoding: 'utf-8',
maxBuffer: MAX_STDERR_BYTES,
timeout: GUARD_TIMEOUT_MS,
windowsHide: true,
});
if (result.error) throw new Error('Elydora guard failed: ' + result.error.message);
const stderr = String(result.stderr || '').trim();
if (result.status !== 0) {
const detail = stderr || (result.signal
? 'Guard terminated by signal ' + result.signal
: 'Guard exited with status ' + result.status);
throw new Error((result.status === 2 ? '' : 'Elydora guard failed: ') + detail);
}
if (stderr) console.error(stderr);
}
function runAudit(input, output) {
let payload;
try {
payload = eventPayload('tool.execute.after', input, output);
} catch (error) {
console.error('[elydora] OpenCode audit input serialization failed:', error.message);
return Promise.resolve();
}
return new Promise((resolve) => {
const child = spawn(RUNTIME_EXECUTABLE, [AUDIT_SCRIPT_PATH], {
stdio: ['pipe', 'ignore', 'pipe'],
windowsHide: true,
});
const stderr = [];
let stderrBytes = 0;
let truncated = false;
let settled = false;
let timer;
const report = (message) => {
console.error('[elydora] OpenCode audit runtime failed:', message);
};
const complete = (failure) => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
if (failure) report(failure);
resolve();
};
const stderrText = () => {
const detail = Buffer.concat(stderr).toString('utf-8').trim();
return truncated ? detail + '\\n[stderr truncated]' : detail;
};
child.stderr.on('data', (chunk) => {
const value = Buffer.from(chunk);
const remaining = MAX_STDERR_BYTES - stderrBytes;
if (remaining <= 0) {
truncated = true;
return;
}
stderr.push(value.subarray(0, remaining));
stderrBytes += Math.min(value.length, remaining);
if (value.length > remaining) truncated = true;
});
child.once('error', (error) => complete(error.message));
child.stdin.once('error', (error) => {
child.kill();
complete('failed to write hook input: ' + error.message);
});
child.once('close', (code, signal) => {
const detail = stderrText();
if (code !== 0 || signal) {
complete(detail || (signal
? 'runtime terminated by signal ' + signal
: 'runtime exited with code ' + code));
return;
}
if (detail) report(detail);
complete();
});
timer = setTimeout(() => {
child.kill();
complete('runtime timed out after ' + AUDIT_TIMEOUT_MS + 'ms');
}, AUDIT_TIMEOUT_MS);
try {
child.stdin.end(payload);
} catch (error) {
child.kill();
complete('failed to write hook input: ' + error.message);
}
});
}
export const ElydoraAuditPlugin = async () => ({
'tool.execute.before': async (input, output) => {
runGuard(input, output);
},
'tool.execute.after': async (input, output) => {
await runAudit(input, output);
},
});
`);
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export function parseOpenCodePlugin(
filePath: string,
source: string,
): OpenCodeRuntimeContract | undefined {
const markerLine = source.split(/\r?\n/, 3)[1];
if (!markerLine?.startsWith(METADATA_MARKER)) return undefined;
let metadata: OpenCodePluginMetadata;
try {
const encoded = markerLine.slice(METADATA_MARKER.length);
const decoded = Buffer.from(encoded, 'base64url').toString('utf-8');
if (Buffer.from(decoded, 'utf-8').toString('base64url') !== encoded) {
throw new Error('metadata encoding is not canonical base64url');
}
metadata = validateMetadata(parseStrictJsonObject(decoded, 'OpenCode plugin metadata'));
} catch (error) {
throw new Error(`Failed to parse Elydora OpenCode plugin metadata at ${filePath}: ${errorMessage(error)}`, {
cause: error instanceof Error ? error : new Error(String(error)),
});
}
if (source !== buildOpenCodePlugin(metadata)) {
throw new Error(`Elydora OpenCode plugin at ${filePath} does not match the managed template`);
}
return metadata;
}
export function buildLegacyOpenCodePlugin(auditPath: string, guardPath: string): string {
return normalizeGeneratedSource(`// Elydora Audit Plugin for OpenCode
// Generated by @elydora/sdk — DO NOT EDIT
import { spawn, spawnSync } from 'node:child_process';
const HOOK_SCRIPT_PATH = ${JSON.stringify(auditPath)};
const GUARD_SCRIPT_PATH = ${JSON.stringify(guardPath)};
export const ElydoraAuditPlugin = async (ctx) => {
return {
"tool.execute.before": async () => {
// Guard — blocks tool if agent is frozen
let result;
try {
result = spawnSync('node', [GUARD_SCRIPT_PATH], {
timeout: 5000,
stdio: ['pipe', 'ignore', 'pipe'],
});
} catch (error) {
throw new Error('Elydora guard failed: ' + error.message);
}
if (result.error) {
throw new Error('Elydora guard failed: ' + result.error.message);
}
if (result.status !== 0) {
const message = result.stderr?.toString().trim() || 'Guard exited with status ' + result.status;
const prefix = result.status === 2 ? '' : 'Elydora guard failed: ';
throw new Error(prefix + message);
}
},
"tool.execute.after": async (input, output) => {
const data = JSON.stringify({
tool_name: input?.tool || 'unknown',
tool_input: input?.args || output?.args || {},
session_id: input?.sessionID || ctx?.project?.name || 'unknown',
});
const child = spawn('node', [HOOK_SCRIPT_PATH], {
detached: true,
stdio: ['pipe', 'ignore', 'ignore'],
});
child.on('error', (error) => {
console.error('[elydora] audit hook failed:', error.message);
});
child.stdin.on('error', (error) => {
console.error('[elydora] audit input failed:', error.message);
});
child.stdin.end(data);
child.unref();
},
};
};
`);
}
function legacyRuntimeContract(
guardPath: string,
auditPath: string,
): OpenCodeLegacyContract | undefined {
if (!path.isAbsolute(guardPath) || !path.isAbsolute(auditPath)) return undefined;
const agentDirectory = path.dirname(guardPath);
if (!samePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))
|| !samePath(auditPath, path.join(agentDirectory, AUDIT_SCRIPT))
|| !samePath(guardPath, path.join(agentDirectory, GUARD_SCRIPT))) return undefined;
const agentId = path.basename(agentDirectory);
return validAgentSegment(agentId) ? { agentId, guardPath, auditPath } : undefined;
}
export function parseLegacyOpenCodePlugin(source: string): OpenCodeLegacyContract | undefined {
const normalized = normalizeGeneratedSource(source);
const auditMatch = normalized.match(/^const HOOK_SCRIPT_PATH = ("(?:\\.|[^"\\])*");$/m);
const guardMatch = normalized.match(/^const GUARD_SCRIPT_PATH = ("(?:\\.|[^"\\])*");$/m);
if (!auditMatch || !guardMatch) return undefined;
let auditPath: unknown;
let guardPath: unknown;
try {
auditPath = JSON.parse(auditMatch[1]);
guardPath = JSON.parse(guardMatch[1]);
} catch {
return undefined;
}
if (typeof auditPath !== 'string' || typeof guardPath !== 'string'
|| normalized !== buildLegacyOpenCodePlugin(auditPath, guardPath)) return undefined;
return legacyRuntimeContract(guardPath, auditPath);
}
@@ -0,0 +1,132 @@
import path from 'node:path';
import type { InstallConfig } from './base.js';
import {
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
OPENCODE_AUDIT_OPTIONS,
buildOpenCodeMetadata,
buildOpenCodePlugin,
sameOpenCodeAgentId,
} from './opencode-contract.js';
import type { OpenCodePluginFile, OpenCodeSources } from './opencode-io.js';
import {
commitManagedInstallation,
preflightManagedInstallation,
prepareManagedInstallation,
type ManagedHookSource,
type ManagedRuntimePaths,
type PreparedManagedInstallation,
type RenameFile,
} from './managed-installation.js';
import {
commitManagedTransaction,
prepareManagedFileChange,
type ManagedFileChange,
type PreparedManagedTransaction,
} from './managed-transaction.js';
const DISPLAY_NAME = 'OpenCode';
const DIRECTORY_LABEL = 'OpenCode plugins directory';
export type OpenCodeRuntimePaths = ManagedRuntimePaths;
export type PreparedOpenCodeInstallation = PreparedManagedInstallation;
export type { RenameFile };
export async function preflightOpenCodeInstallation(
config: InstallConfig,
sources: OpenCodeSources,
): Promise<OpenCodeRuntimePaths> {
return preflightManagedInstallation({
agentKey: AGENT_KEY,
hookLocations: [{
directoryLabel: DIRECTORY_LABEL,
filePath: sources.paths.pluginPath,
}],
config,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
export async function prepareOpenCodeInstallation(
config: InstallConfig,
sources: OpenCodeSources,
): Promise<PreparedOpenCodeInstallation> {
const paths = await preflightOpenCodeInstallation(config, sources);
const metadata = buildOpenCodeMetadata(
config.agentId,
process.execPath,
paths.guardPath,
paths.auditPath,
);
const hookSources: ManagedHookSource[] = [{
directoryLabel: DIRECTORY_LABEL,
label: 'OpenCode plugin',
filePath: sources.current.filePath,
expectedSource: sources.current.source,
expectedSnapshot: sources.current.snapshot,
source: buildOpenCodePlugin(metadata),
}];
if (sources.legacy.contract) {
hookSources.push({
directoryLabel: DIRECTORY_LABEL,
label: 'legacy OpenCode plugin',
filePath: sources.legacy.filePath,
expectedSource: sources.legacy.source,
expectedSnapshot: sources.legacy.snapshot,
});
}
return prepareManagedInstallation({
agentKey: AGENT_KEY,
displayName: DISPLAY_NAME,
hookSources,
config,
auditOptions: OPENCODE_AUDIT_OPTIONS,
}, GUARD_SCRIPT, AUDIT_SCRIPT);
}
export async function commitOpenCodeInstallation(
prepared: PreparedOpenCodeInstallation,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedInstallation(prepared, renameFile);
}
function ownedByAgent<TContract extends { readonly agentId: string }>(
file: OpenCodePluginFile<TContract>,
agentId?: string,
): boolean {
return file.contract !== undefined
&& (agentId === undefined || sameOpenCodeAgentId(file.contract.agentId, agentId));
}
export async function prepareOpenCodeUninstall(
sources: OpenCodeSources,
agentId?: string,
): Promise<PreparedManagedTransaction> {
const owned = [sources.current, sources.legacy].filter((file) => ownedByAgent(file, agentId));
const changes = await Promise.all(owned.map((file) => prepareManagedFileChange({
filePath: file.filePath,
label: file.filePath === sources.paths.pluginPath ? 'OpenCode plugin' : 'legacy OpenCode plugin',
next: undefined,
mode: 0o600,
expectedSource: file.source,
expectedSnapshot: file.snapshot,
verifyExpectedSource: true,
})));
return {
displayName: DISPLAY_NAME,
operation: 'uninstall',
directories: owned.map((file) => ({
path: path.dirname(file.filePath),
label: DIRECTORY_LABEL,
})),
changes: changes.filter((change): change is ManagedFileChange => change !== undefined),
};
}
export async function commitOpenCodeUninstall(
prepared: PreparedManagedTransaction,
renameFile?: RenameFile,
): Promise<void> {
await commitManagedTransaction(prepared, renameFile);
}
+98
View File
@@ -0,0 +1,98 @@
import fsp from 'node:fs/promises';
import {
AGENT_KEY,
OPENCODE_AUDIT_OPTIONS,
parseLegacyOpenCodePlugin,
parseOpenCodePlugin,
resolveOpenCodePaths,
type OpenCodeLegacyContract,
type OpenCodePaths,
type OpenCodeRuntimeContract,
} from './opencode-contract.js';
import { inspectPhysicalDirectory, readPhysicalFile, type FileSnapshot } from './managed-files.js';
import { managedRuntimeFilesExist } from './managed-runtime-status.js';
const MAX_PLUGIN_BYTES = 512 * 1024;
export interface OpenCodePluginFile<TContract> {
readonly exists: boolean;
readonly filePath: string;
readonly source?: string;
readonly snapshot?: FileSnapshot;
readonly contract?: TContract;
}
export interface OpenCodeSources {
readonly paths: OpenCodePaths;
readonly current: OpenCodePluginFile<OpenCodeRuntimeContract>;
readonly legacy: OpenCodePluginFile<OpenCodeLegacyContract>;
}
async function inspectOpenCodeDirectories(paths: OpenCodePaths): Promise<void> {
await inspectPhysicalDirectory(paths.homeDirectory, 'OpenCode home directory');
await inspectPhysicalDirectory(paths.configRoot, 'OpenCode XDG config directory');
await inspectPhysicalDirectory(paths.configDirectory, 'OpenCode config directory');
await inspectPhysicalDirectory(paths.pluginsDirectory, 'OpenCode plugins directory');
}
async function readPluginFile<TContract>(
filePath: string,
label: string,
parse: (filePath: string, source: string) => TContract | undefined,
): Promise<OpenCodePluginFile<TContract>> {
const snapshot = await readPhysicalFile(filePath, label, MAX_PLUGIN_BYTES);
if (!snapshot) return { exists: false, filePath };
return {
exists: true,
filePath,
source: snapshot.contents,
snapshot,
contract: parse(filePath, snapshot.contents),
};
}
export async function readOpenCodeSources(): Promise<OpenCodeSources> {
const paths = resolveOpenCodePaths();
await inspectOpenCodeDirectories(paths);
const [current, legacy] = await Promise.all([
readPluginFile(paths.pluginPath, 'OpenCode plugin', parseOpenCodePlugin),
readPluginFile(
paths.legacyPluginPath,
'legacy OpenCode plugin',
(_filePath, source) => parseLegacyOpenCodePlugin(source),
),
]);
return { paths, current, legacy };
}
export function requireAvailableOpenCodePlugin(
file: OpenCodePluginFile<OpenCodeRuntimeContract>,
): void {
if (file.exists && !file.contract) {
throw new Error(`OpenCode plugin path is owned by another integration: ${file.filePath}`);
}
}
function hasCode(error: unknown, code: string): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
}
async function executableExists(filePath: string): Promise<boolean> {
try {
return (await fsp.stat(filePath)).isFile();
} catch (error) {
if (hasCode(error, 'ENOENT') || hasCode(error, 'ENOTDIR')) return false;
throw new Error(`Inspect OpenCode Node.js runtime at ${filePath}: ${
error instanceof Error ? error.message : String(error)
}`, { cause: error instanceof Error ? error : new Error(String(error)) });
}
}
export async function openCodeRuntimeFilesExist(
contract: OpenCodeRuntimeContract,
): Promise<boolean> {
return await executableExists(contract.executablePath)
&& await managedRuntimeFilesExist(contract, AGENT_KEY, {
auditOptions: OPENCODE_AUDIT_OPTIONS,
});
}
+40 -111
View File
@@ -1,134 +1,63 @@
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 } from './opencode-contract.js';
import {
commitOpenCodeInstallation,
commitOpenCodeUninstall,
preflightOpenCodeInstallation,
prepareOpenCodeInstallation,
prepareOpenCodeUninstall,
} from './opencode-installation.js';
import {
openCodeRuntimeFilesExist,
readOpenCodeSources,
requireAvailableOpenCodePlugin,
} from './opencode-io.js';
import { SUPPORTED_AGENTS } from './registry.js';
const AGENT_KEY = 'opencode';
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
function resolveConfigDir(): string {
return path.join(os.homedir(), entry.configDir);
}
function resolveConfigPath(): string {
return path.join(resolveConfigDir(), entry.configFile);
}
function generatePluginScript(hookScriptPath: string, guardScriptPath: string): string {
return `// Elydora Audit Plugin for OpenCode
// Generated by @elydora/sdk — DO NOT EDIT
import { spawn, spawnSync } from 'node:child_process';
const HOOK_SCRIPT_PATH = ${JSON.stringify(hookScriptPath)};
const GUARD_SCRIPT_PATH = ${JSON.stringify(guardScriptPath)};
export const ElydoraAuditPlugin = async (ctx) => {
return {
"tool.execute.before": async () => {
// Guard — blocks tool if agent is frozen
let result;
try {
result = spawnSync('node', [GUARD_SCRIPT_PATH], {
timeout: 5000,
stdio: ['pipe', 'ignore', 'pipe'],
});
} catch (error) {
throw new Error('Elydora guard failed: ' + error.message);
}
if (result.error) {
throw new Error('Elydora guard failed: ' + result.error.message);
}
if (result.status !== 0) {
const message = result.stderr?.toString().trim() || 'Guard exited with status ' + result.status;
const prefix = result.status === 2 ? '' : 'Elydora guard failed: ';
throw new Error(prefix + message);
}
},
"tool.execute.after": async (input, output) => {
const data = JSON.stringify({
tool_name: input?.tool || 'unknown',
tool_input: input?.args || output?.args || {},
session_id: input?.sessionID || ctx?.project?.name || 'unknown',
});
const child = spawn('node', [HOOK_SCRIPT_PATH], {
detached: true,
stdio: ['pipe', 'ignore', 'ignore'],
});
child.on('error', (error) => {
console.error('[elydora] audit hook failed:', error.message);
});
child.stdin.on('error', (error) => {
console.error('[elydora] audit input failed:', error.message);
});
child.stdin.end(data);
child.unref();
},
};
};
`;
function validateSources(sources: Awaited<ReturnType<typeof readOpenCodeSources>>): void {
requireAvailableOpenCodePlugin(sources.current);
}
export const opencodePlugin: AgentPlugin = {
async install(config: InstallConfig): Promise<void> {
const configDir = resolveConfigDir();
await fsp.mkdir(configDir, { recursive: true });
managesRuntime: true,
const configPath = resolveConfigPath();
const script = generatePluginScript(config.hookScriptPath, config.guardScriptPath);
await fsp.writeFile(configPath, script, 'utf-8');
async preflightInstall(config: InstallConfig): Promise<void> {
const sources = await readOpenCodeSources();
validateSources(sources);
await preflightOpenCodeInstallation(config, sources);
},
async uninstall(_agentId?: string): Promise<void> {
const configPath = resolveConfigPath();
try {
await fsp.unlink(configPath);
} catch {
// Already removed
}
async install(config: InstallConfig): Promise<void> {
const sources = await readOpenCodeSources();
validateSources(sources);
const prepared = await prepareOpenCodeInstallation(config, sources);
await commitOpenCodeInstallation(prepared);
console.log(` OpenCode global plugin: ${sources.paths.pluginPath}`);
console.log(' OpenCode activation: restart active OpenCode sessions.');
},
async uninstall(agentId?: string): Promise<void> {
const sources = await readOpenCodeSources();
const prepared = await prepareOpenCodeUninstall(sources, agentId);
await commitOpenCodeUninstall(prepared);
},
async status(): Promise<PluginStatus> {
const configPath = resolveConfigPath();
let hookConfigured = false;
let hookScriptPath = '';
let guardScriptPath = '';
try {
const raw = await fsp.readFile(configPath, 'utf-8');
const hookMatch = raw.match(/const HOOK_SCRIPT_PATH = ("(?:\\.|[^"\\])*");/);
const guardMatch = raw.match(/const GUARD_SCRIPT_PATH = ("(?:\\.|[^"\\])*");/);
if (hookMatch && guardMatch) {
hookScriptPath = JSON.parse(hookMatch[1]);
guardScriptPath = JSON.parse(guardMatch[1]);
hookConfigured = true;
}
} catch {
// File doesn't exist
}
let hookScriptExists = false;
if (hookScriptPath && guardScriptPath) {
try {
await fsp.access(hookScriptPath);
await fsp.access(guardScriptPath);
hookScriptExists = true;
} catch {
// File doesn't exist
}
}
const sources = await readOpenCodeSources();
validateSources(sources);
const contract = sources.legacy.contract ? undefined : sources.current.contract;
const hookConfigured = contract !== undefined;
const hookScriptExists = contract !== undefined
&& await openCodeRuntimeFilesExist(contract);
return {
installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY,
displayName: entry.name,
hookConfigured,
hookScriptExists,
configPath,
configPath: sources.paths.pluginPath,
};
},
};
+4 -4
View File
@@ -7,14 +7,14 @@ export interface AgentRegistryEntry {
export const SUPPORTED_AGENTS: ReadonlyMap<string, AgentRegistryEntry> = new Map([
['augment', { name: 'Augment Code CLI', configDir: '~/.augment', configFile: 'settings.json' }],
['claudecode', { name: 'Claude Code', configDir: '~/.claude', configFile: 'settings.json' }],
['cursor', { name: 'Cursor', configDir: '~/.cursor', configFile: 'hooks.json' }],
['cursor', { name: 'Cursor CLI', configDir: '~/.cursor', configFile: 'hooks.json' }],
['gemini', { name: 'Gemini CLI', configDir: '~/.gemini', configFile: 'settings.json' }],
['kirocli', { name: 'Kiro CLI', configDir: '~/.kiro/hooks', configFile: 'elydora-audit.json' }],
['kiroide', { name: 'Kiro IDE', configDir: '.kiro/hooks', configFile: 'elydora-audit.kiro.hook' }],
['opencode', { name: 'OpenCode', configDir: '.config/opencode/plugins', configFile: 'elydora-audit.mjs' }],
['kiroide', { name: 'Kiro IDE', configDir: '.kiro/hooks', configFile: 'elydora-audit.json' }],
['opencode', { name: 'OpenCode', configDir: '.config/opencode/plugins', configFile: 'elydora-audit.js' }],
['copilot', { name: 'GitHub Copilot CLI', configDir: '~/.copilot/hooks', configFile: 'elydora-audit.json' }],
['letta', { name: 'Letta Code', configDir: '~/.letta', configFile: 'settings.json' }],
['codex', { name: 'OpenAI Codex', configDir: '~/.codex', configFile: 'hooks.json' }],
['codex', { name: 'Codex CLI', configDir: '~/.codex', configFile: 'hooks.json' }],
['cline', { name: 'Cline', configDir: '~/.cline/hooks', configFile: 'PreToolUse.mjs' }],
['droid', { name: 'Factory Droid', configDir: '~/.factory', configFile: 'hooks.json' }],
['kimi', { name: 'Kimi Code', configDir: '~/.kimi-code', configFile: 'config.toml' }],
@@ -0,0 +1,182 @@
import { spawn } from 'node:child_process';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export const VALID_PRIVATE_KEY = Buffer.alloc(32, 17).toString('base64url');
export const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/kirocli.js')).href;
export const contractModuleUrl = pathToFileURL(
path.resolve('dist/plugins/kirocli-contract.js'),
).href;
export const installationModuleUrl = pathToFileURL(
path.resolve('dist/plugins/kirocli-installation.js'),
).href;
export const ioModuleUrl = pathToFileURL(path.resolve('dist/plugins/kirocli-io.js')).href;
export const kiroV1ModuleUrl = pathToFileURL(
path.resolve('dist/plugins/kiroide-contract.js'),
).href;
export const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
export const cliPath = path.resolve('dist/cli.js');
export function runProcess(command, args, env, cwd, input = '', shell = false) {
return new Promise((resolve, reject) => {
const childEnvironment = { ...process.env, ...env };
for (const [name, value] of Object.entries(env)) {
if (value === undefined) delete childEnvironment[name];
}
const child = spawn(command, args, {
cwd,
env: childEnvironment,
shell,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
export function runNode(args, env, cwd, input = '') {
return runProcess(process.execPath, args, env, cwd, input);
}
export function runShell(command, env, cwd, input = '') {
return runProcess(command, [], env, cwd, input, true);
}
export function environment(fixture) {
return { HOME: fixture.homeDir, USERPROFILE: fixture.homeDir };
}
export function installConfig(fixture, overrides = {}) {
return {
agentName: 'kirocli',
orgId: 'org-1',
agentId: fixture.agentId,
privateKey: VALID_PRIVATE_KEY,
kid: 'kid-1',
token: 'token-1',
baseUrl: fixture.baseUrl,
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
...overrides,
};
}
export async function runPlugin(fixture, method, argument) {
const source = `
import { kirocliPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await kirocliPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
},
fixture.projectDir,
);
}
async function writeOptional(filePath, value) {
if (value === undefined) return;
await mkdir(path.dirname(filePath), { recursive: true });
const source = typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`;
await writeFile(filePath, source, { mode: 0o600 });
}
export function managedV2Document(overrides = {}) {
return {
name: 'elydora-audit',
description: 'Kiro CLI with Elydora audit and freeze enforcement',
tools: ['*'],
includeMcpJson: true,
hooks: {},
...overrides,
};
}
export async function createFixture({
agentId = 'agent-1',
baseUrl = 'http://127.0.0.1:9',
existingV2,
existingV3,
} = {}) {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kirocli-'));
const homeDir = path.join(rootDir, "home with spaces and 'quote %KIRO%");
const projectDir = path.join(rootDir, 'workspace with spaces');
const v2Path = path.join(homeDir, '.kiro', 'agents', 'elydora-audit.json');
const v3Path = path.join(homeDir, '.kiro', 'hooks', 'elydora-audit.json');
const agentDir = path.join(homeDir, '.elydora', agentId);
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(homeDir, { recursive: true });
await mkdir(projectDir, { recursive: true });
await writeOptional(v2Path, existingV2);
await writeOptional(v3Path, existingV3);
return {
agentDir,
agentId,
baseUrl,
guardScriptPath,
homeDir,
hookScriptPath,
projectDir,
rootDir,
v2Path,
v3Path,
install(overrides = {}) {
return runPlugin(this, 'install', installConfig(this, overrides));
},
async close() {
await rm(rootDir, { recursive: true, force: true });
},
};
}
export async function readJson(filePath) {
return JSON.parse(await readFile(filePath, 'utf-8'));
}
export function findV3Hook(document, name) {
return document.hooks.find((hook) => hook.name === name);
}
export async function startApiServer() {
const requests = [];
const server = http.createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8');
requests.push({ method: request.method, url: request.url, raw });
if (request.method === 'GET') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end('{"agent":{"status":"active"}}');
return;
}
response.writeHead(201, { 'Content-Type': 'application/json' });
response.end('{"operation":{"accepted":true}}');
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
close: () => new Promise((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
}),
};
}
@@ -0,0 +1,170 @@
import { spawn } from 'node:child_process';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export const VALID_PRIVATE_KEY = Buffer.alloc(32, 11).toString('base64url');
export const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/kiroide.js')).href;
export const contractModuleUrl = pathToFileURL(
path.resolve('dist/plugins/kiroide-contract.js'),
).href;
export const installationModuleUrl = pathToFileURL(
path.resolve('dist/plugins/kiroide-installation.js'),
).href;
export const ioModuleUrl = pathToFileURL(path.resolve('dist/plugins/kiroide-io.js')).href;
export const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
export const cliPath = path.resolve('dist/cli.js');
export function runProcess(command, args, env, cwd, input = '', shell = false) {
return new Promise((resolve, reject) => {
const childEnvironment = { ...process.env, ...env };
for (const [name, value] of Object.entries(env)) {
if (value === undefined) delete childEnvironment[name];
}
const child = spawn(command, args, {
cwd,
env: childEnvironment,
shell,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
export function runNode(args, env, cwd, input = '') {
return runProcess(process.execPath, args, env, cwd, input);
}
export function runShell(command, env, cwd, input = '') {
return runProcess(command, [], env, cwd, input, true);
}
export function environment(fixture) {
return { HOME: fixture.homeDir, USERPROFILE: fixture.homeDir };
}
export function installConfig(fixture, overrides = {}) {
return {
agentName: 'kiroide',
orgId: 'org-1',
agentId: fixture.agentId,
privateKey: VALID_PRIVATE_KEY,
kid: 'kid-1',
token: 'token-1',
baseUrl: fixture.baseUrl,
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
...overrides,
};
}
export async function runPlugin(fixture, method, argument) {
const source = `
import { kiroidePlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await kiroidePlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
},
fixture.projectDir,
);
}
async function writeOptional(filePath, value) {
if (value === undefined) return;
await mkdir(path.dirname(filePath), { recursive: true });
const source = typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`;
await writeFile(filePath, source, { mode: 0o600 });
}
export async function createFixture({
agentId = 'agent-1',
baseUrl = 'http://127.0.0.1:9',
existingConfig,
existingLegacy,
} = {}) {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kiroide-'));
const homeDir = path.join(rootDir, "home with spaces and 'quote %KIRO%");
const projectDir = path.join(rootDir, 'workspace with spaces');
const hooksDir = path.join(projectDir, '.kiro', 'hooks');
const configPath = path.join(hooksDir, 'elydora-audit.json');
const legacyConfigPath = path.join(homeDir, '.kiro', 'hooks', 'elydora-audit.kiro.hook');
const agentDir = path.join(homeDir, '.elydora', agentId);
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(projectDir, { recursive: true });
await writeOptional(configPath, existingConfig);
await writeOptional(legacyConfigPath, existingLegacy);
const fixture = {
agentDir,
agentId,
baseUrl,
configPath,
guardScriptPath,
homeDir,
hookScriptPath,
hooksDir,
legacyConfigPath,
projectDir,
rootDir,
install(overrides = {}) {
return runPlugin(this, 'install', installConfig(this, overrides));
},
async close() {
await rm(rootDir, { recursive: true, force: true });
},
};
return fixture;
}
export async function readJson(filePath) {
return JSON.parse(await readFile(filePath, 'utf-8'));
}
export function findHook(document, name) {
return document.hooks.find((hook) => hook.name === name);
}
export async function startApiServer() {
const requests = [];
const server = http.createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8');
requests.push({ method: request.method, url: request.url, raw });
if (request.method === 'GET') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end('{"agent":{"status":"active"}}');
return;
}
response.writeHead(201, { 'Content-Type': 'application/json' });
response.end('{"operation":{"accepted":true}}');
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
close: () => new Promise((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
}),
};
}
+187
View File
@@ -0,0 +1,187 @@
import { spawn } from 'node:child_process';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export const VALID_PRIVATE_KEY = Buffer.alloc(32, 23).toString('base64url');
export const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/letta.js')).href;
export const configModuleUrl = pathToFileURL(path.resolve('dist/plugins/letta-config.js')).href;
export const contractModuleUrl = pathToFileURL(path.resolve('dist/plugins/letta-contract.js')).href;
export const installationModuleUrl = pathToFileURL(
path.resolve('dist/plugins/letta-installation.js'),
).href;
export const sourcesModuleUrl = pathToFileURL(path.resolve('dist/plugins/letta-sources.js')).href;
export const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
export const cliPath = path.resolve('dist/cli.js');
export function runProcess(command, args, env, cwd, input = '') {
return new Promise((resolve, reject) => {
const childEnvironment = { ...process.env, ...env };
for (const [name, value] of Object.entries(env)) {
if (value === undefined) delete childEnvironment[name];
}
const child = spawn(command, args, {
cwd,
env: childEnvironment,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
export function runNode(args, env, cwd, input = '') {
return runProcess(process.execPath, args, env, cwd, input);
}
export function environment(fixture) {
return {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
};
}
export function installConfig(fixture, overrides = {}) {
return {
agentName: 'letta',
orgId: 'org-1',
agentId: fixture.agentId,
privateKey: VALID_PRIVATE_KEY,
kid: 'kid-1',
token: 'token-1',
baseUrl: fixture.baseUrl,
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
...overrides,
};
}
export async function runPlugin(fixture, method, argument) {
const source = `
import { lettaPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await lettaPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
},
fixture.projectDir,
);
}
async function writeOptional(filePath, value) {
if (value === undefined) return;
await mkdir(path.dirname(filePath), { recursive: true });
const source = typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`;
await writeFile(filePath, source, { mode: 0o600 });
}
export async function createFixture({
agentId = 'agent-1',
baseUrl = 'http://127.0.0.1:9',
globalSettings,
projectSettings,
localSettings,
} = {}) {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-letta-'));
const homeDir = path.join(rootDir, "home with spaces and 'quote %HOME%");
const projectDir = path.join(rootDir, 'workspace with spaces');
const globalPath = path.join(homeDir, '.letta', 'settings.json');
const projectPath = path.join(projectDir, '.letta', 'settings.json');
const localPath = path.join(projectDir, '.letta', 'settings.local.json');
const agentDir = path.join(homeDir, '.elydora', agentId);
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(homeDir, { recursive: true });
await mkdir(projectDir, { recursive: true });
await writeOptional(globalPath, globalSettings);
await writeOptional(projectPath, projectSettings);
await writeOptional(localPath, localSettings);
return {
agentDir,
agentId,
baseUrl,
globalPath,
guardScriptPath,
homeDir,
hookScriptPath,
localPath,
projectDir,
projectPath,
rootDir,
install(overrides = {}) {
return runPlugin(this, 'install', installConfig(this, overrides));
},
async close() {
await rm(rootDir, { recursive: true, force: true });
},
};
}
export async function readSettings(filePath) {
return JSON.parse(await readFile(filePath, 'utf-8'));
}
export function managedHandler(settings, event) {
return settings.hooks[event].find((group) => (
group.matcher === '*'
&& group.hooks.length === 1
&& group.hooks[0].timeout === 10_000
))?.hooks[0];
}
export function runLettaHook(handler, input, fixture) {
const shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/sh';
const args = process.platform === 'win32'
? ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', handler.command]
: ['-c', handler.command];
return runProcess(shell, args, environment(fixture), fixture.projectDir, input);
}
export async function startApiServer(initialStatus = 'active') {
const requests = [];
let status = initialStatus;
let operationStatus = 201;
const server = http.createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8');
requests.push({ method: request.method, url: request.url, raw });
if (request.method === 'GET') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ agent: { status } }));
return;
}
response.writeHead(operationStatus, { 'Content-Type': 'application/json' });
response.end(operationStatus < 400
? '{"operation":{"accepted":true}}'
: '{"error":{"code":"AUDIT_UNAVAILABLE"}}');
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
setStatus(value) { status = value; },
setOperationStatus(value) { operationStatus = value; },
close: () => new Promise((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
}),
};
}
@@ -0,0 +1,180 @@
import { spawn } from 'node:child_process';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export const VALID_PRIVATE_KEY = Buffer.alloc(32, 19).toString('base64url');
export const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode.js')).href;
export const contractModuleUrl = pathToFileURL(
path.resolve('dist/plugins/opencode-contract.js'),
).href;
export const installationModuleUrl = pathToFileURL(
path.resolve('dist/plugins/opencode-installation.js'),
).href;
export const ioModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode-io.js')).href;
export const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
export const cliPath = path.resolve('dist/cli.js');
export function runProcess(command, args, env, cwd, input = '') {
return new Promise((resolve, reject) => {
const childEnvironment = { ...process.env, ...env };
for (const [name, value] of Object.entries(env)) {
if (value === undefined) delete childEnvironment[name];
}
const child = spawn(command, args, {
cwd,
env: childEnvironment,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
export function runNode(args, env, cwd, input = '') {
return runProcess(process.execPath, args, env, cwd, input);
}
export function environment(fixture) {
return {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
XDG_CONFIG_HOME: fixture.configRoot,
};
}
export function installConfig(fixture, overrides = {}) {
return {
agentName: 'opencode',
orgId: 'org-1',
agentId: fixture.agentId,
privateKey: VALID_PRIVATE_KEY,
kid: 'kid-1',
token: 'token-1',
baseUrl: fixture.baseUrl,
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
...overrides,
};
}
export async function runPlugin(fixture, method, argument) {
const source = `
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await opencodePlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
},
fixture.projectDir,
);
}
async function writeOptional(filePath, value) {
if (value === undefined) return;
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, value, { mode: 0o600 });
}
export async function createFixture({
agentId = 'agent-1',
baseUrl = 'http://127.0.0.1:9',
existingPlugin,
existingLegacy,
} = {}) {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-opencode-'));
const homeDir = path.join(rootDir, "home with spaces and 'quote %OPEN%");
const configRoot = path.join(rootDir, 'xdg config');
const projectDir = path.join(rootDir, 'workspace with spaces');
const pluginsDir = path.join(configRoot, 'opencode', 'plugins');
const pluginPath = path.join(pluginsDir, 'elydora-audit.js');
const legacyPluginPath = path.join(pluginsDir, 'elydora-audit.mjs');
const agentDir = path.join(homeDir, '.elydora', agentId);
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(homeDir, { recursive: true });
await mkdir(projectDir, { recursive: true });
await writeOptional(pluginPath, existingPlugin);
await writeOptional(legacyPluginPath, existingLegacy);
return {
agentDir,
agentId,
baseUrl,
configRoot,
guardScriptPath,
homeDir,
hookScriptPath,
legacyPluginPath,
pluginPath,
pluginsDir,
projectDir,
rootDir,
install(overrides = {}) {
return runPlugin(this, 'install', installConfig(this, overrides));
},
async loadPlugin() {
const module = await import(`${pathToFileURL(this.pluginPath).href}?test=${Date.now()}`);
return module.ElydoraAuditPlugin({});
},
async close() {
await rm(rootDir, { recursive: true, force: true });
},
};
}
export async function readJson(filePath) {
return JSON.parse(await readFile(filePath, 'utf-8'));
}
export async function startApiServer(initialStatus = 'active') {
const requests = [];
let status = initialStatus;
let operationStatus = 201;
const server = http.createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8');
requests.push({ method: request.method, url: request.url, raw });
if (request.method === 'GET') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ agent: { status } }));
return;
}
response.writeHead(operationStatus, { 'Content-Type': 'application/json' });
response.end(operationStatus < 400
? '{"operation":{"accepted":true}}'
: '{"error":{"code":"AUDIT_UNAVAILABLE"}}');
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
setStatus(value) {
status = value;
},
setOperationStatus(value) {
operationStatus = value;
},
close: () => new Promise((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
}),
};
}
+195
View File
@@ -1,5 +1,7 @@
import assert from 'node:assert/strict';
import { existsSync, utimesSync, writeFileSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import http from 'node:http';
import path from 'node:path';
import test from 'node:test';
import {
@@ -143,3 +145,196 @@ test('Claude runtime failures stay observable while guard and audit delivery rem
await fixture.close();
}
});
function mismatchServer(respond) {
const submissions = [];
const server = http.createServer((request, response) => {
let raw = '';
request.on('data', (chunk) => { raw += chunk; });
request.on('end', () => {
if (request.url !== '/v1/operations') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ agent: { status: 'active' } }));
return;
}
submissions.push(JSON.parse(raw));
const reply = respond(submissions) ?? mismatch(String(submissions.length).padStart(43, 'A'), 'x');
const send = () => {
response.writeHead(reply.status, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(reply.body));
};
if (api.slow) setTimeout(send, 2600); else send();
});
});
const api = { submissions, slow: false };
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(Object.assign(api, {
baseUrl: 'http://127.0.0.1:' + server.address().port,
close: () => new Promise((done) => { server.closeAllConnections?.(); server.close(done); }),
}))));
}
function mismatch(expected, actual) {
return {
status: 400,
body: { error: { code: 'PREV_HASH_MISMATCH', message: 'Expected prev_chain_hash "' + expected + '", got "' + actual + '".' } },
};
}
test('Claude audit retries a rejected prev_chain_hash with the server value', async () => {
const expected = 'Rxlf4j36C3KvIQ3hWuOkX698BR5iDypUFuB70JjEuvM';
const api = await mismatchServer((submissions) => (submissions.length === 1
? mismatch(expected, submissions[0].prev_chain_hash)
: { status: 202, body: { receipt: { seq_no: 2 } } }));
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { settings } = await readSettings(fixture.settingsPath);
const audit = managedHandler(settings, 'PostToolUse');
const result = await runClaudeHook(
audit,
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
fixture,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(api.submissions.length, 2);
assert.equal(api.submissions[1].prev_chain_hash, expected);
assert.notEqual(api.submissions[1].operation_id, api.submissions[0].operation_id);
assert.notEqual(api.submissions[1].nonce, api.submissions[0].nonce);
const state = JSON.parse(await readFile(path.join(fixture.agentDir, 'chain-state.json'), 'utf-8'));
assert.equal(state.prev_chain_hash, api.submissions[1].chain_hash);
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /resynced to server: Rxlf/);
} finally {
await fixture.close();
await api.close();
}
});
test('Claude audit stops after five rejected prev_chain_hash attempts', async () => {
const api = await mismatchServer((submissions) => (
mismatch(String(submissions.length).padStart(43, 'A'), submissions.at(-1).prev_chain_hash)
));
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { settings } = await readSettings(fixture.settingsPath);
const audit = managedHandler(settings, 'PostToolUse');
const result = await runClaudeHook(
audit,
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
fixture,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(api.submissions.length, 5);
assert.equal(api.submissions[4].prev_chain_hash, '4'.padStart(43, 'A'));
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /rejected prev_chain_hash 5 times/);
} finally {
await fixture.close();
await api.close();
}
});
test('Claude audit stops retrying when the submit budget is spent', async () => {
const api = await mismatchServer(() => null);
api.slow = true;
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { settings } = await readSettings(fixture.settingsPath);
const audit = managedHandler(settings, 'PostToolUse');
const started = Date.now();
const result = await runClaudeHook(
audit,
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
fixture,
);
const elapsed = Date.now() - started;
assert.equal(result.code, 0, result.stderr);
assert.ok(elapsed < 7000, `hook ran ${elapsed}ms`);
assert.ok(api.submissions.length >= 2 && api.submissions.length <= 3, `${api.submissions.length} submissions`);
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /aborted|retry budget exhausted/i);
} finally {
await fixture.close();
await api.close();
}
});
test('Claude audit does not regress a chain head advanced by a concurrent hook', async () => {
const newerHead = 'B'.repeat(43);
let statePath;
const api = await mismatchServer(() => {
writeFileSync(statePath, JSON.stringify({ prev_chain_hash: newerHead }), { mode: 0o600 });
return { status: 202, body: { receipt: { seq_no: 1 } } };
});
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
statePath = path.join(fixture.agentDir, 'chain-state.json');
const { settings } = await readSettings(fixture.settingsPath);
const audit = managedHandler(settings, 'PostToolUse');
const result = await runClaudeHook(
audit,
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
fixture,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(api.submissions[0].prev_chain_hash, 'A'.repeat(43));
const state = JSON.parse(await readFile(statePath, 'utf-8'));
assert.equal(state.prev_chain_hash, newerHead);
} finally {
await fixture.close();
await api.close();
}
});
test('Claude audit clears a stale chain-state lock and proceeds', async () => {
const api = await mismatchServer(() => ({ status: 202, body: { receipt: { seq_no: 1 } } }));
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const lockPath = path.join(fixture.agentDir, 'chain-state.json.lock');
writeFileSync(lockPath, '', { mode: 0o600 });
const stale = new Date(Date.now() - 10_000);
utimesSync(lockPath, stale, stale);
const { settings } = await readSettings(fixture.settingsPath);
const audit = managedHandler(settings, 'PostToolUse');
const result = await runClaudeHook(
audit,
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
fixture,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(api.submissions.length, 1);
assert.equal(existsSync(lockPath), false);
const state = JSON.parse(await readFile(path.join(fixture.agentDir, 'chain-state.json'), 'utf-8'));
assert.equal(state.prev_chain_hash, api.submissions[0].chain_hash);
} finally {
await fixture.close();
await api.close();
}
});
test('Claude audit does not reclaim a chain-state lock whose owner is alive', async () => {
const api = await mismatchServer(() => ({ status: 202, body: { receipt: { seq_no: 1 } } }));
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const lockPath = path.join(fixture.agentDir, 'chain-state.json.lock');
writeFileSync(lockPath, String(process.pid), { mode: 0o600 });
const stale = new Date(Date.now() - 10_000);
utimesSync(lockPath, stale, stale);
const { settings } = await readSettings(fixture.settingsPath);
const audit = managedHandler(settings, 'PostToolUse');
const result = await runClaudeHook(
audit,
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
fixture,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(api.submissions.length, 1);
assert.equal(existsSync(lockPath), true);
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /lock timed out/);
} finally {
await fixture.close();
await api.close();
}
});
+1 -1
View File
@@ -32,7 +32,7 @@ const AUDIT_STATUS = 'Recording Elydora tool use';
test('Codex is registered with the native user hooks file', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('codex'), {
name: 'OpenAI Codex',
name: 'Codex CLI',
configDir: '~/.codex',
configFile: 'hooks.json',
});
+1 -1
View File
@@ -25,7 +25,7 @@ import {
test('Cursor is registered with the native user hook file', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('cursor'), {
name: 'Cursor',
name: 'Cursor CLI',
configDir: '~/.cursor',
configFile: 'hooks.json',
});
+31
View File
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createFixture,
runProcess,
} from '../test-support/kirocli-test-helpers.mjs';
test('official Kiro CLI 2.13 validates the installed v2 agent contract', {
skip: process.env.ELYDORA_KIRO_CLI_BINARY
? false
: 'set ELYDORA_KIRO_CLI_BINARY to the official Kiro CLI executable',
}, async () => {
const fixture = await createFixture();
try {
const install = await fixture.install();
assert.equal(install.code, 0, install.stderr);
const binary = process.env.ELYDORA_KIRO_CLI_BINARY;
const version = await runProcess(binary, ['--version'], {}, fixture.projectDir);
assert.equal(version.code, 0, version.stderr);
assert.match(version.stdout, /\b2\.13\.\d+\b/);
const validation = await runProcess(
binary,
['agent', 'validate', '--path', fixture.v2Path],
{},
fixture.projectDir,
);
assert.equal(validation.code, 0, validation.stderr);
} finally {
await fixture.close();
}
});
+379 -245
View File
@@ -1,314 +1,448 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import {
lstat,
mkdir,
readFile,
readdir,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import {
VALID_PRIVATE_KEY,
cliPath,
createFixture,
environment,
findV3Hook,
managedV2Document,
readJson,
registryModuleUrl,
runNode,
runPlugin,
runShell,
startApiServer,
} from '../test-support/kirocli-test-helpers.mjs';
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/kirocli.js')).href;
const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
function runNode(args, env, input = '') {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
function runCommand(command, input = '') {
return new Promise((resolve, reject) => {
const child = spawn(command, { shell: true, stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
async function runPlugin(homeDir, method, argument) {
const script = `
import { kirocliPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await kirocliPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
return runNode(
['--input-type=module', '--eval', script],
{
HOME: homeDir,
USERPROFILE: homeDir,
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
},
);
}
async function writeJsonOrText(filePath, value) {
async function writeJson(filePath, value) {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, typeof value === 'string' ? value : JSON.stringify(value, null, 2));
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
}
async function createFixture({ existingV2, existingV3, guardSource, hookSource } = {}) {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kirocli-'));
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
const v2Path = path.join(homeDir, '.kiro', 'agents', 'elydora-audit.json');
const v3Path = path.join(homeDir, '.kiro', 'hooks', 'elydora-audit.json');
await mkdir(agentDir, { recursive: true });
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
const runtimeConfigPath = path.join(agentDir, 'config.json');
await writeFile(
guardScriptPath,
guardSource ?? "process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n",
);
await writeFile(hookScriptPath, hookSource ?? 'process.exit(0);\n');
await writeFile(runtimeConfigPath, JSON.stringify({
agent_id: 'agent-1',
agent_name: 'kirocli',
}));
if (existingV2 !== undefined) await writeJsonOrText(v2Path, existingV2);
if (existingV3 !== undefined) await writeJsonOrText(v3Path, existingV3);
const config = {
agentName: 'kirocli',
agentId: 'agent-1',
baseUrl: 'https://api.elydora.com',
guardScriptPath,
hookScriptPath,
};
const installResult = await runPlugin(homeDir, 'install', config);
function userV3Hook() {
return {
config,
guardScriptPath,
homeDir,
hookScriptPath,
installResult,
runtimeConfigPath,
v2Path,
v3Path,
async close() {
await rm(homeDir, { recursive: true, force: true });
name: 'global-context',
trigger: 'SessionStart',
action: { type: 'agent', prompt: 'Read AGENTS.md' },
enabled: true,
};
}
function legacyCommand(scriptPath) {
if (process.platform === 'win32') return `"${process.execPath}" "${scriptPath}"`;
const quote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
return `${quote(process.execPath)} ${quote(scriptPath)}`;
}
function legacyDocuments(fixture) {
const guard = legacyCommand(fixture.guardScriptPath);
const audit = legacyCommand(fixture.hookScriptPath);
return {
v2: managedV2Document({
hooks: {
preToolUse: [{ matcher: '*', command: guard, timeout_ms: 5000 }],
postToolUse: [{ matcher: '*', command: audit, timeout_ms: 5000 }],
},
}),
v3: {
version: 'v1',
hooks: [
{
name: 'elydora-guard',
description: 'Block tool use when the Elydora agent is frozen',
trigger: 'PreToolUse',
matcher: '.*',
action: { type: 'command', command: guard },
timeout: 5,
enabled: true,
},
{
name: 'elydora-audit',
description: 'Record tool use in the Elydora audit trail',
trigger: 'PostToolUse',
matcher: '.*',
action: { type: 'command', command: audit },
timeout: 5,
enabled: true,
},
],
},
};
}
function findV3Hook(config, name) {
return config.hooks.find((hook) => hook.name === name);
}
test('Kiro CLI registry points at the v3 global hook contract', async () => {
test('Kiro CLI installs exact v2 and 2.13 v3 global contracts atomically', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('kirocli'), {
name: 'Kiro CLI',
configDir: '~/.kiro/hooks',
configFile: 'elydora-audit.json',
});
});
test('Kiro CLI install preserves user hooks and writes idempotent v2 and v3 contracts', async () => {
const fixture = await createFixture({
existingV2: {
description: 'User Kiro agent',
tools: ['read'],
existingV2: managedV2Document({
owner: 'user',
hooks: {
agentSpawn: [{ command: 'existing-spawn' }],
preToolUse: [{ matcher: 'read', command: 'existing-v2' }],
},
},
existingV3: {
version: 'v1',
hooks: [{
name: 'existing-v3',
trigger: 'SessionStart',
action: { type: 'command', command: 'existing-command' },
}],
},
}),
existingV3: { version: 'v1', owner: 'user', hooks: [userV3Hook()] },
});
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
assert.match(fixture.installResult.stdout, /--agent elydora-audit/);
assert.match(fixture.installResult.stdout, /--v3/);
const secondInstall = await runPlugin(fixture.homeDir, 'install', fixture.config);
assert.equal(secondInstall.code, 0, secondInstall.stderr);
const v2 = JSON.parse(await readFile(fixture.v2Path, 'utf-8'));
assert.equal(v2.description, 'User Kiro agent');
assert.deepEqual(v2.tools, ['read']);
assert.equal(v2.hooks.agentSpawn[0].command, 'existing-spawn');
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /2\.13\.0\+/);
assert.match(first.stdout, /agent validate/);
assert.match(first.stdout, /--agent elydora-audit/);
const firstV2Source = await readFile(fixture.v2Path, 'utf-8');
const firstV3Source = await readFile(fixture.v3Path, 'utf-8');
const v2 = JSON.parse(firstV2Source);
assert.equal(v2.owner, 'user');
assert.deepEqual(v2.hooks.agentSpawn, [{ command: 'existing-spawn' }]);
assert.equal(v2.hooks.preToolUse.length, 2);
assert.equal(v2.hooks.postToolUse.length, 1);
assert.equal(v2.hooks.preToolUse[1].matcher, '*');
assert.equal(typeof v2.hooks.preToolUse[1].command, 'string');
const v3 = JSON.parse(await readFile(fixture.v3Path, 'utf-8'));
assert.equal(v3.version, 'v1');
assert.equal(v3.hooks.length, 3);
assert.equal(v3.hooks[0].name, 'existing-v3');
assert.deepEqual(v2.hooks.preToolUse[1], {
matcher: '*',
command: v2.hooks.preToolUse[1].command,
timeout_ms: 10_000,
});
assert.deepEqual(v2.hooks.postToolUse[0], {
matcher: '*',
command: v2.hooks.postToolUse[0].command,
timeout_ms: 10_000,
});
const v3 = JSON.parse(firstV3Source);
assert.equal(v3.owner, 'user');
assert.deepEqual(v3.hooks[0], userV3Hook());
assert.deepEqual(findV3Hook(v3, 'elydora-guard'), {
name: 'elydora-guard',
description: 'Block tool use when the Elydora agent is frozen',
trigger: 'PreToolUse',
matcher: '.*',
action: { type: 'command', command: findV3Hook(v3, 'elydora-guard').action.command },
timeout: 5,
timeout: 10,
enabled: true,
});
assert.equal(findV3Hook(v3, 'elydora-audit').trigger, 'PostToolUse');
assert.deepEqual(await readJson(path.join(fixture.agentDir, 'config.json')), {
org_id: 'org-1',
agent_id: fixture.agentId,
kid: 'kid-1',
base_url: fixture.baseUrl,
token: 'token-1',
agent_name: 'kirocli',
});
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
assert.equal(await readFile(fixture.v2Path, 'utf-8'), firstV2Source);
assert.equal(await readFile(fixture.v3Path, 'utf-8'), firstV3Source);
const names = [
...await readdir(path.dirname(fixture.v2Path)),
...await readdir(path.dirname(fixture.v3Path)),
...await readdir(fixture.agentDir),
];
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false);
if (process.platform !== 'win32') {
assert.equal((await stat(fixture.v2Path)).mode & 0o777, 0o600);
assert.equal((await stat(fixture.guardScriptPath)).mode & 0o777, 0o700);
}
} finally {
await fixture.close();
}
});
test('Kiro CLI hook commands enforce freezes and forward the official event payload', async () => {
const capturePath = path.join(os.tmpdir(), `elydora-kiro-event-${process.pid}-${Date.now()}.json`);
const fixture = await createFixture({
hookSource: `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => fs.writeFileSync(${JSON.stringify(capturePath)}, Buffer.concat(chunks)));
`,
});
test('Kiro CLI commands block frozen agents and preserve native events', async () => {
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const v3 = JSON.parse(await readFile(fixture.v3Path, 'utf-8'));
assert.equal((await fixture.install()).code, 0);
const v2 = await readJson(fixture.v2Path);
const v3 = await readJson(fixture.v3Path);
assert.equal(v2.hooks.preToolUse[0].command, findV3Hook(v3, 'elydora-guard').action.command);
const payload = {
hook_event_name: 'PreToolUse',
cwd: fixture.homeDir,
hook_event_name: 'preToolUse',
cwd: fixture.projectDir,
session_id: 'session-1',
tool_name: 'execute_bash',
tool_input: { command: 'echo test' },
tool_input: { command: 'npm test' },
future_field: { retained: true },
};
const guardResult = await runCommand(findV3Hook(v3, 'elydora-guard').action.command, JSON.stringify(payload));
assert.equal(guardResult.code, 2);
assert.match(guardResult.stderr, /Agent is frozen by Elydora/);
payload.hook_event_name = 'PostToolUse';
payload.tool_response = { success: true, result: 'test' };
const auditResult = await runCommand(findV3Hook(v3, 'elydora-audit').action.command, JSON.stringify(payload));
assert.equal(auditResult.code, 0, auditResult.stderr);
assert.deepEqual(JSON.parse(await readFile(capturePath, 'utf-8')), payload);
} finally {
await fixture.close();
await rm(capturePath, { force: true });
}
});
test('Kiro CLI status accepts either contract and requires both runtime scripts', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const status = await runPlugin(fixture.homeDir, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
await rm(fixture.v3Path);
const v2Only = await runPlugin(fixture.homeDir, 'status', null);
const v2Status = JSON.parse(v2Only.stdout);
assert.equal(v2Status.installed, true);
assert.equal(v2Status.configPath, fixture.v2Path);
const reinstall = await runPlugin(fixture.homeDir, 'install', fixture.config);
assert.equal(reinstall.code, 0, reinstall.stderr);
await rm(fixture.v2Path);
const v3Only = await runPlugin(fixture.homeDir, 'status', null);
const v3Status = JSON.parse(v3Only.stdout);
assert.equal(v3Status.installed, true);
assert.equal(v3Status.configPath, fixture.v3Path);
await rm(fixture.guardScriptPath);
const degraded = await runPlugin(fixture.homeDir, 'status', null);
assert.equal(JSON.parse(degraded.stdout).installed, false);
await writeJson(path.join(fixture.agentDir, 'status-cache.json'), {
status: 'frozen',
cached_at: Date.now(),
});
const guard = await runShell(
findV3Hook(v3, 'elydora-guard').action.command,
environment(fixture),
fixture.projectDir,
JSON.stringify(payload),
);
assert.equal(guard.code, 2, guard.stderr);
await rm(path.join(fixture.agentDir, 'status-cache.json'));
payload.hook_event_name = 'postToolUse';
payload.tool_response = { success: true, result: '12 passed' };
const audit = await runShell(
findV3Hook(v3, 'elydora-audit').action.command,
environment(fixture),
fixture.projectDir,
JSON.stringify(payload),
);
assert.equal(audit.code, 0, audit.stderr);
const operation = JSON.parse(api.requests.find(({ method }) => method === 'POST').raw);
assert.deepEqual(operation.payload, payload);
assert.deepEqual(operation.action, { tool: 'execute_bash' });
assert.deepEqual(operation.subject, { session_id: 'session-1' });
} finally {
await api.close();
await fixture.close();
}
});
test('Kiro CLI status surfaces malformed referenced runtime metadata', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
await writeFile(fixture.runtimeConfigPath, '{ malformed');
const status = await runPlugin(fixture.homeDir, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /parse Elydora runtime config/i);
} finally {
await fixture.close();
}
});
test('Kiro CLI uninstall preserves unrelated v2 and v3 hooks', async () => {
const fixture = await createFixture({
existingV2: { hooks: { preToolUse: [{ matcher: 'read', command: 'existing-v2' }] } },
existingV3: {
version: 'v1',
hooks: [{
name: 'existing-v3',
trigger: 'SessionStart',
action: { type: 'command', command: 'existing-command' },
}],
},
test('Kiro CLI status requires both exact contracts and physical runtime identity', async (t) => {
await t.test('missing home is an empty installation', async () => {
const fixture = await createFixture();
try {
await rm(fixture.homeDir, { recursive: true });
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, false);
} finally {
await fixture.close();
}
});
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const result = await runPlugin(fixture.homeDir, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
const v2 = JSON.parse(await readFile(fixture.v2Path, 'utf-8'));
assert.deepEqual(v2.hooks.preToolUse, [{ matcher: 'read', command: 'existing-v2' }]);
assert.deepEqual(v2.hooks.postToolUse, []);
const v3 = JSON.parse(await readFile(fixture.v3Path, 'utf-8'));
assert.equal(v3.hooks.length, 1);
assert.equal(v3.hooks[0].name, 'existing-v3');
} finally {
await fixture.close();
await t.test('missing or disabled contract is degraded and reinstall repairs it', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
let status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
const v3 = await readJson(fixture.v3Path);
findV3Hook(v3, 'elydora-guard').enabled = false;
await writeJson(fixture.v3Path, v3);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
assert.equal((await fixture.install()).code, 0);
await rm(fixture.v2Path);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
} finally {
await fixture.close();
}
});
await t.test('recognized altered hooks degrade status', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const v2 = await readJson(fixture.v2Path);
v2.hooks.preToolUse.push({ ...v2.hooks.preToolUse[0], timeout_ms: 5000 });
await writeJson(fixture.v2Path, v2);
let status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
assert.equal((await fixture.install()).code, 0);
const v3 = await readJson(fixture.v3Path);
v3.hooks.push({ ...findV3Hook(v3, 'elydora-guard'), enabled: false });
await writeJson(fixture.v3Path, v3);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
} finally {
await fixture.close();
}
});
for (const [label, target, contents, expected] of [
['guard runtime', 'guard', 'tampered\n', /"installed":false/],
['runtime config', 'config', '{ malformed', /parse Elydora runtime config/i],
['runtime identity', 'config', JSON.stringify({
org_id: 'org-1',
agent_id: 'agent-1',
kid: 'kid-1',
base_url: 'http://127.0.0.1:9',
token: 'token-1',
agent_name: 'kiroide',
}), /runtime identity/i],
['private key', 'key', 'invalid', /private key is invalid/i],
]) {
await t.test(label, async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const targets = {
guard: fixture.guardScriptPath,
config: path.join(fixture.agentDir, 'config.json'),
key: path.join(fixture.agentDir, 'private.key'),
};
await writeFile(targets[target], contents);
const status = await runPlugin(fixture, 'status', null);
assert.match(`${status.stdout}${status.stderr}`, expected);
} finally {
await fixture.close();
}
});
}
});
test('Kiro CLI uninstall removes configs owned entirely by Elydora', async () => {
test('Kiro CLI rejects malformed, colliding, invalid, and linked inputs before writes', async (t) => {
for (const [label, options, pattern] of [
['duplicate v2', { existingV2: '{"name":"elydora-audit","name":"other"}' }, /duplicate/i],
['v2 path collision', { existingV2: { name: 'user-agent', hooks: {} } }, /v2 agent path conflicts/i],
['v3 name collision', { existingV3: { version: 'v1', hooks: [{ name: 'elydora-guard', trigger: 'PreToolUse', action: { type: 'command', command: 'user-command' } }] } }, /hook name.*conflicts/i],
]) {
await t.test(label, async () => {
const fixture = await createFixture(options);
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, pattern);
await assertMissing(fixture.guardScriptPath);
} finally {
await fixture.close();
}
});
}
await t.test('non-canonical private key', async () => {
const fixture = await createFixture();
try {
const result = await fixture.install({ privateKey: 'invalid' });
assert.equal(result.code, 1);
assert.match(result.stderr, /canonical 32-byte/);
await assertMissing(fixture.v2Path);
await assertMissing(fixture.v3Path);
} finally {
await fixture.close();
}
});
await t.test('linked Kiro directory', async () => {
const fixture = await createFixture();
const external = path.join(fixture.rootDir, 'external');
try {
await mkdir(external);
await symlink(external, path.join(fixture.homeDir, '.kiro'), 'junction');
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /not a physical directory|symbolic link/i);
await assertMissing(path.join(external, 'agents', 'elydora-audit.json'));
} finally {
await fixture.close();
}
});
});
test('Kiro CLI migrates exact legacy commands and repairs both schemas', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const result = await runPlugin(fixture.homeDir, 'uninstall', 'agent-1');
const legacy = legacyDocuments(fixture);
await writeJson(fixture.v2Path, legacy.v2);
await writeJson(fixture.v3Path, legacy.v3);
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
await assert.rejects(readFile(fixture.v2Path), { code: 'ENOENT' });
await assert.rejects(readFile(fixture.v3Path), { code: 'ENOENT' });
const v2 = await readJson(fixture.v2Path);
const v3 = await readJson(fixture.v3Path);
assert.equal(v2.hooks.preToolUse.length, 1);
assert.equal(v2.hooks.preToolUse[0].timeout_ms, 10_000);
assert.equal(findV3Hook(v3, 'elydora-guard').timeout, 10);
assert.equal(findV3Hook(v3, 'elydora-guard').action.command, v2.hooks.preToolUse[0].command);
} finally {
await fixture.close();
}
});
test('Kiro CLI install preserves malformed configs for recovery', async () => {
const fixture = await createFixture({ existingV3: '{ malformed' });
test('Kiro CLI uninstall preserves user hooks and removes fully owned files', async (t) => {
await t.test('mixed files', async () => {
const fixture = await createFixture({
existingV2: managedV2Document({ hooks: { agentSpawn: [{ command: 'existing-spawn' }] } }),
existingV3: { version: 'v1', hooks: [userV3Hook()] },
});
try {
assert.equal((await fixture.install()).code, 0);
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
assert.equal(result.code, 0, result.stderr);
const v2 = await readJson(fixture.v2Path);
assert.deepEqual(v2.hooks, { agentSpawn: [{ command: 'existing-spawn' }] });
const v3 = await readJson(fixture.v3Path);
assert.deepEqual(v3.hooks, [userV3Hook()]);
} finally {
await fixture.close();
}
});
await t.test('fully owned files', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
assert.equal(result.code, 0, result.stderr);
await assertMissing(fixture.v2Path);
await assertMissing(fixture.v3Path);
} finally {
await fixture.close();
}
});
await t.test('legacy files', async () => {
const fixture = await createFixture();
try {
const legacy = legacyDocuments(fixture);
await writeJson(fixture.v2Path, legacy.v2);
await writeJson(fixture.v3Path, legacy.v3);
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
assert.equal(result.code, 0, result.stderr);
await assertMissing(fixture.v2Path);
await assertMissing(fixture.v3Path);
} finally {
await fixture.close();
}
});
await t.test('unrelated v3 source bytes', async () => {
const source = '{ "version": "v1", "hooks": [{ "name": "user", "trigger": "SessionStart", "action": { "type": "command", "command": "echo user" } }] }\n';
const fixture = await createFixture({ existingV3: source });
try {
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
assert.equal(result.code, 0, result.stderr);
assert.equal(await readFile(fixture.v3Path, 'utf-8'), source);
} finally {
await fixture.close();
}
});
});
test('Kiro CLI command completes install, status, and uninstall end to end', async () => {
const fixture = await createFixture();
const privateKeyFile = path.join(fixture.projectDir, 'private-key.txt');
const tokenFile = path.join(fixture.projectDir, 'token.txt');
try {
assert.equal(fixture.installResult.code, 1);
assert.match(fixture.installResult.stderr, /parse Kiro CLI v3 hooks config/i);
assert.equal(await readFile(fixture.v3Path, 'utf-8'), '{ malformed');
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
const install = await runNode([
cliPath, 'install', '--agent', 'kirocli', '--org_id', 'org-1',
'--agent_id', fixture.agentId, '--kid', 'kid-1',
'--private_key_file', privateKeyFile, '--token_file', tokenFile,
'--base_url', fixture.baseUrl,
], environment(fixture), fixture.projectDir);
assert.equal(install.code, 0, install.stderr);
const status = await runNode([cliPath, 'status'], environment(fixture), fixture.projectDir);
assert.equal(status.code, 0, status.stderr);
assert.match(status.stdout, /Kiro CLI \(kirocli\) \[installed\]/);
const uninstall = await runNode([
cliPath, 'uninstall', '--agent', 'kirocli', '--agent_id', fixture.agentId,
], environment(fixture), fixture.projectDir);
assert.equal(uninstall.code, 0, uninstall.stderr);
await assertMissing(fixture.v2Path);
await assertMissing(fixture.v3Path);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
const v2Fixture = await createFixture({ existingV2: '{ malformed' });
try {
assert.equal(v2Fixture.installResult.code, 1);
assert.match(v2Fixture.installResult.stderr, /parse Kiro CLI v2 agent config/i);
assert.equal(await readFile(v2Fixture.v2Path, 'utf-8'), '{ malformed');
} finally {
await v2Fixture.close();
}
});
+131
View File
@@ -0,0 +1,131 @@
import assert from 'node:assert/strict';
import { lstat, readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
contractModuleUrl,
createFixture,
environment,
installConfig,
installationModuleUrl,
ioModuleUrl,
kiroV1ModuleUrl,
managedV2Document,
runNode,
} from '../test-support/kirocli-test-helpers.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
async function runPreparedInstallation(fixture, { failV3 = false, raceV2 } = {}) {
const source = `
import fsp from 'node:fs/promises';
import {
buildKiroCliV2Hooks,
buildKiroCliV3Hooks,
renderKiroCliV2Installation,
} from ${JSON.stringify(contractModuleUrl)};
import {
commitKiroCliInstallation,
preflightKiroCliInstallation,
prepareKiroCliInstallation,
} from ${JSON.stringify(installationModuleUrl)};
import { readKiroCliSources } from ${JSON.stringify(ioModuleUrl)};
import { renderKiroIdeDocument } from ${JSON.stringify(kiroV1ModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
const sources = await readKiroCliSources();
const paths = await preflightKiroCliInstallation(config, sources);
const v2 = renderKiroCliV2Installation(
sources.v2,
buildKiroCliV2Hooks(sources.v2, paths.guardPath, paths.auditPath),
);
const v3 = renderKiroIdeDocument(
sources.v3,
buildKiroCliV3Hooks(sources.v3.hooks, paths.guardPath, paths.auditPath),
);
const prepared = await prepareKiroCliInstallation(config, sources, v2, v3);
if (process.env.ELYDORA_TEST_RACE_V2) {
await fsp.writeFile(sources.paths.v2Path, process.env.ELYDORA_TEST_RACE_V2);
}
let failed = false;
await commitKiroCliInstallation(prepared, async (from, to) => {
if (!failed
&& process.env.ELYDORA_TEST_FAIL_V3 === 'true'
&& to === sources.paths.v3Path
&& from.endsWith('.tmp')) {
failed = true;
throw new Error('injected Kiro CLI v3 rename failure');
}
await fsp.rename(from, to);
});
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
ELYDORA_TEST_FAIL_V3: String(failV3),
ELYDORA_TEST_RACE_V2: raceV2,
},
fixture.projectDir,
);
}
async function assertNoTransactionFiles(...directories) {
const names = [];
for (const directory of directories) {
try {
names.push(...await readdir(directory));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
test('Kiro CLI rolls back v2, v3, and runtime files after a late commit failure', async () => {
const originalV2 = managedV2Document({ owner: 'v2', hooks: {} });
const originalV3 = { version: 'v1', owner: 'v3', hooks: [] };
const fixture = await createFixture({ existingV2: originalV2, existingV3: originalV3 });
try {
const result = await runPreparedInstallation(fixture, { failV3: true });
assert.equal(result.code, 1);
assert.match(result.stderr, /injected Kiro CLI v3 rename failure/);
assert.deepEqual(JSON.parse(await readFile(fixture.v2Path, 'utf-8')), originalV2);
assert.deepEqual(JSON.parse(await readFile(fixture.v3Path, 'utf-8')), originalV3);
for (const filePath of [
fixture.guardScriptPath,
fixture.hookScriptPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) await assertMissing(filePath);
await assertNoTransactionFiles(
path.dirname(fixture.v2Path),
path.dirname(fixture.v3Path),
fixture.agentDir,
);
} finally {
await fixture.close();
}
});
test('Kiro CLI detects concurrent source changes before the first commit', async () => {
const originalV2 = managedV2Document({ hooks: {} });
const originalV3 = { version: 'v1', hooks: [] };
const raceV2 = '{"name":"elydora-audit","race":true,"hooks":{}}\n';
const fixture = await createFixture({ existingV2: originalV2, existingV3: originalV3 });
try {
const result = await runPreparedInstallation(fixture, { raceV2 });
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during Kiro CLI installation/i);
assert.equal(await readFile(fixture.v2Path, 'utf-8'), raceV2);
assert.deepEqual(JSON.parse(await readFile(fixture.v3Path, 'utf-8')), originalV3);
await assertMissing(fixture.guardScriptPath);
await assertMissing(fixture.hookScriptPath);
await assertNoTransactionFiles(path.dirname(fixture.v2Path), fixture.agentDir);
} finally {
await fixture.close();
}
});
+412
View File
@@ -0,0 +1,412 @@
import assert from 'node:assert/strict';
import {
lstat,
mkdir,
readFile,
readdir,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
VALID_PRIVATE_KEY,
cliPath,
createFixture,
environment,
findHook,
installConfig,
readJson,
registryModuleUrl,
runNode,
runPlugin,
runShell,
startApiServer,
} from '../test-support/kiroide-test-helpers.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
function userHook() {
return {
name: 'workspace-context',
description: 'Load workspace context',
trigger: 'SessionStart',
action: { type: 'agent', prompt: 'Read AGENTS.md' },
enabled: true,
};
}
function legacyDocument(fixture, agentId = fixture.agentId) {
const agentDirectory = path.join(fixture.homeDir, '.elydora', agentId);
return {
name: 'Elydora Audit',
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
version: '1.0.0',
hooks: {
pre_tool_use: {
command: `node "${path.join(agentDirectory, 'guard.js')}"`,
timeout_ms: 5000,
},
post_tool_use: {
command: `node "${path.join(agentDirectory, 'hook.js')}"`,
timeout_ms: 5000,
},
},
};
}
async function writeJson(filePath, value) {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
}
test('Kiro IDE uses the workspace v1 hook contract', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('kiroide'), {
name: 'Kiro IDE',
configDir: '.kiro/hooks',
configFile: 'elydora-audit.json',
});
const fixture = await createFixture({
existingConfig: { version: 'v1', owner: 'workspace', hooks: [userHook()] },
});
try {
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /workspace hooks/);
assert.match(first.stdout, /Agent Hooks panel/);
const firstSource = await readFile(fixture.configPath, 'utf-8');
const document = JSON.parse(firstSource);
assert.equal(document.version, 'v1');
assert.equal(document.owner, 'workspace');
assert.deepEqual(document.hooks[0], userHook());
assert.deepEqual(findHook(document, 'elydora-guard'), {
name: 'elydora-guard',
description: 'Block tool use when the Elydora agent is frozen',
trigger: 'PreToolUse',
matcher: '.*',
action: {
type: 'command',
command: findHook(document, 'elydora-guard').action.command,
},
timeout: 10,
enabled: true,
});
assert.deepEqual(findHook(document, 'elydora-audit'), {
name: 'elydora-audit',
description: 'Record tool use in the Elydora audit trail',
trigger: 'PostToolUse',
matcher: '.*',
action: {
type: 'command',
command: findHook(document, 'elydora-audit').action.command,
},
timeout: 10,
enabled: true,
});
assert.deepEqual(await readJson(path.join(fixture.agentDir, 'config.json')), {
org_id: 'org-1',
agent_id: fixture.agentId,
kid: 'kid-1',
base_url: fixture.baseUrl,
token: 'token-1',
agent_name: 'kiroide',
});
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
assert.equal(await readFile(fixture.configPath, 'utf-8'), firstSource);
const transactionFiles = [
...await readdir(fixture.hooksDir),
...await readdir(fixture.agentDir),
];
assert.equal(transactionFiles.some((name) => /\.(tmp|rollback)$/.test(name)), false);
await assertMissing(path.join(fixture.homeDir, '.kiro', 'hooks', 'elydora-audit.json'));
if (process.platform !== 'win32') {
assert.equal((await stat(fixture.agentDir)).mode & 0o777, 0o700);
assert.equal((await stat(fixture.configPath)).mode & 0o777, 0o600);
assert.equal((await stat(fixture.guardScriptPath)).mode & 0o777, 0o700);
}
} finally {
await fixture.close();
}
});
test('Kiro IDE commands block frozen agents and preserve the native event', async () => {
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const document = await readJson(fixture.configPath);
const payload = {
hook_event_name: 'preToolUse',
cwd: fixture.projectDir,
session_id: 'session-1',
tool_name: 'execute_bash',
tool_input: { command: 'npm test' },
future_field: { retained: true },
};
await writeJson(path.join(fixture.agentDir, 'status-cache.json'), {
status: 'frozen',
cached_at: Date.now(),
});
const guard = await runShell(
findHook(document, 'elydora-guard').action.command,
environment(fixture),
fixture.projectDir,
JSON.stringify(payload),
);
assert.equal(guard.code, 2, guard.stderr);
assert.match(guard.stderr, /Tool execution blocked/);
await rm(path.join(fixture.agentDir, 'status-cache.json'));
payload.hook_event_name = 'postToolUse';
payload.tool_response = { success: true, result: '12 passed' };
const audit = await runShell(
findHook(document, 'elydora-audit').action.command,
environment(fixture),
fixture.projectDir,
JSON.stringify(payload),
);
assert.equal(audit.code, 0, audit.stderr);
const request = api.requests.find(({ method }) => method === 'POST');
assert.ok(request);
const operation = JSON.parse(request.raw);
assert.deepEqual(operation.payload, payload);
assert.deepEqual(operation.action, { tool: 'execute_bash' });
assert.deepEqual(operation.subject, { session_id: 'session-1' });
} finally {
await api.close();
await fixture.close();
}
});
test('Kiro IDE status requires the exact enabled contract and physical runtime', async (t) => {
await t.test('disabled hook is inactive and reinstall repairs it', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const document = await readJson(fixture.configPath);
findHook(document, 'elydora-guard').enabled = false;
await writeJson(fixture.configPath, document);
let status = await runPlugin(fixture, 'status', null);
assert.deepEqual(JSON.parse(status.stdout), {
installed: false,
agentName: 'kiroide',
displayName: 'Kiro IDE',
hookConfigured: false,
hookScriptExists: false,
configPath: fixture.configPath,
});
assert.equal((await fixture.install()).code, 0);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
} finally {
await fixture.close();
}
});
await t.test('recognized altered hook degrades status', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const document = await readJson(fixture.configPath);
document.hooks.push({ ...findHook(document, 'elydora-guard'), enabled: false });
await writeJson(fixture.configPath, document);
const status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
} finally {
await fixture.close();
}
});
for (const [label, target, contents, expected] of [
['guard runtime', 'guard', 'tampered\n', /"installed":false/],
['runtime config', 'config', '{ malformed', /parse Elydora runtime config/i],
['private key', 'key', 'invalid', /private key is invalid/i],
]) {
await t.test(label, async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const targets = {
guard: fixture.guardScriptPath,
config: path.join(fixture.agentDir, 'config.json'),
key: path.join(fixture.agentDir, 'private.key'),
};
await writeFile(targets[target], contents);
const status = await runPlugin(fixture, 'status', null);
assert.match(`${status.stdout}\n${status.stderr}`, expected);
} finally {
await fixture.close();
}
});
}
});
test('Kiro IDE rejects malformed files, name collisions, and unsafe workspaces before writes', async (t) => {
await t.test('malformed hook file', async () => {
const fixture = await createFixture({ existingConfig: '{ malformed' });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /parse Kiro IDE hooks/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), '{ malformed');
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
await t.test('managed name collision', async () => {
const existing = {
version: 'v1',
hooks: [{
name: 'elydora-guard',
trigger: 'PreToolUse',
action: { type: 'command', command: 'user-command' },
}],
};
const fixture = await createFixture({ existingConfig: existing });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /conflicts with the Elydora contract/i);
assert.deepEqual(await readJson(fixture.configPath), existing);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
await t.test('non-canonical private key', async () => {
const fixture = await createFixture();
try {
const result = await fixture.install({ privateKey: `${VALID_PRIVATE_KEY}=` });
assert.equal(result.code, 1);
assert.match(result.stderr, /canonical 32-byte base64url/i);
await assertMissing(fixture.agentDir);
await assertMissing(fixture.configPath);
} finally {
await fixture.close();
}
});
await t.test('linked .kiro directory', async () => {
const fixture = await createFixture();
const external = path.join(fixture.rootDir, 'external-kiro');
const kiroDirectory = path.join(fixture.projectDir, '.kiro');
try {
await mkdir(external);
await symlink(external, kiroDirectory, process.platform === 'win32' ? 'junction' : 'dir');
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /not a physical directory/i);
await assertMissing(path.join(external, 'hooks', 'elydora-audit.json'));
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
});
test('Kiro IDE migrates only the matching legacy global hook', async () => {
const fixture = await createFixture();
try {
await writeJson(fixture.legacyConfigPath, legacyDocument(fixture));
assert.equal((await fixture.install()).code, 0);
await assertMissing(fixture.legacyConfigPath);
await writeJson(fixture.legacyConfigPath, legacyDocument(fixture, 'agent-2'));
assert.equal((await fixture.install()).code, 0);
assert.equal((await readJson(fixture.legacyConfigPath)).version, '1.0.0');
} finally {
await fixture.close();
}
});
test('Kiro IDE uninstall removes exact ownership and preserves workspace hooks', async () => {
const fixture = await createFixture({
existingConfig: { version: 'v1', owner: 'workspace', hooks: [userHook()] },
});
try {
assert.equal((await fixture.install()).code, 0);
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-2')).code, 0);
assert.equal((await readJson(fixture.configPath)).hooks.length, 3);
assert.equal((await runPlugin(fixture, 'uninstall', fixture.agentId)).code, 0);
assert.deepEqual(await readJson(fixture.configPath), {
version: 'v1',
owner: 'workspace',
hooks: [userHook()],
});
} finally {
await fixture.close();
}
const owned = await createFixture();
try {
assert.equal((await owned.install()).code, 0);
assert.equal((await runPlugin(owned, 'uninstall', owned.agentId)).code, 0);
await assertMissing(owned.configPath);
} finally {
await owned.close();
}
const source = '{ "version": "v1", "hooks": [{ "name": "user", "trigger": "SessionStart", "action": { "type": "command", "command": "echo user" } }] }\n';
const unrelated = await createFixture({ existingConfig: source });
try {
assert.equal((await runPlugin(unrelated, 'uninstall', unrelated.agentId)).code, 0);
assert.equal(await readFile(unrelated.configPath, 'utf-8'), source);
} finally {
await unrelated.close();
}
});
test('Kiro IDE CLI completes install, status, and uninstall in the current workspace', async () => {
const fixture = await createFixture();
const privateKeyFile = path.join(fixture.rootDir, 'private.key');
const tokenFile = path.join(fixture.rootDir, 'token');
try {
await writeFile(privateKeyFile, VALID_PRIVATE_KEY, { mode: 0o600 });
await writeFile(tokenFile, 'token-1', { mode: 0o600 });
const install = await runNode([
'--no-warnings',
cliPath,
'install',
'--agent', 'kiroide',
'--org_id', 'org-1',
'--agent_id', fixture.agentId,
'--kid', 'kid-1',
'--private_key_file', privateKeyFile,
'--token_file', tokenFile,
'--base_url', fixture.baseUrl,
], environment(fixture), fixture.projectDir);
assert.equal(install.code, 0, install.stderr);
assert.match(install.stdout, /Kiro IDE workspace hooks/);
const status = await runNode(
['--no-warnings', cliPath, 'status'],
environment(fixture),
fixture.projectDir,
);
assert.equal(status.code, 0, status.stderr);
assert.match(status.stdout, /Kiro IDE \(kiroide\) \[installed\]/);
const uninstall = await runNode([
'--no-warnings',
cliPath,
'uninstall',
'--agent', 'kiroide',
'--agent_id', fixture.agentId,
], environment(fixture), fixture.projectDir);
assert.equal(uninstall.code, 0, uninstall.stderr);
await assertMissing(fixture.configPath);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
+142
View File
@@ -0,0 +1,142 @@
import assert from 'node:assert/strict';
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
contractModuleUrl,
createFixture,
environment,
installConfig,
installationModuleUrl,
ioModuleUrl,
runNode,
} from '../test-support/kiroide-test-helpers.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
function legacyDocument(fixture) {
return {
name: 'Elydora Audit',
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
version: '1.0.0',
hooks: {
pre_tool_use: {
command: `node "${fixture.guardScriptPath}"`,
timeout_ms: 5000,
},
post_tool_use: {
command: `node "${fixture.hookScriptPath}"`,
timeout_ms: 5000,
},
},
};
}
async function writeJson(filePath, value) {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
}
async function runPreparedInstallation(fixture, { failLegacy = false, raceSource } = {}) {
const source = `
import fsp from 'node:fs/promises';
import {
buildKiroIdeHook,
renderKiroIdeDocument,
withoutManagedKiroIdeHooks,
} from ${JSON.stringify(contractModuleUrl)};
import { readKiroIdeSources } from ${JSON.stringify(ioModuleUrl)};
import {
commitKiroIdeInstallation,
preflightKiroIdeInstallation,
prepareKiroIdeInstallation,
} from ${JSON.stringify(installationModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
const sources = await readKiroIdeSources();
const paths = await preflightKiroIdeInstallation(config, sources.paths);
const rendered = renderKiroIdeDocument(sources.document, [
...withoutManagedKiroIdeHooks(sources.document.hooks),
buildKiroIdeHook('elydora-guard', paths.guardPath),
buildKiroIdeHook('elydora-audit', paths.auditPath),
]);
const prepared = await prepareKiroIdeInstallation(config, sources, rendered);
if (process.env.ELYDORA_TEST_RACE_SOURCE) {
await fsp.writeFile(sources.paths.configPath, process.env.ELYDORA_TEST_RACE_SOURCE);
}
await commitKiroIdeInstallation(prepared, async (from, to) => {
if (process.env.ELYDORA_TEST_FAIL_LEGACY === 'true'
&& from === sources.paths.legacyConfigPath) {
throw new Error('injected legacy rename failure');
}
await fsp.rename(from, to);
});
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
ELYDORA_TEST_FAIL_LEGACY: String(failLegacy),
ELYDORA_TEST_RACE_SOURCE: raceSource,
},
fixture.projectDir,
);
}
async function assertNoTransactionFiles(...directories) {
const names = [];
for (const directory of directories) {
try {
names.push(...await readdir(directory));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
test('Kiro IDE rolls back runtime and workspace hooks when legacy cleanup fails', async () => {
const original = { version: 'v1', owner: 'workspace', hooks: [] };
const fixture = await createFixture({ existingConfig: original });
try {
await writeJson(fixture.legacyConfigPath, legacyDocument(fixture));
const result = await runPreparedInstallation(fixture, { failLegacy: true });
assert.equal(result.code, 1);
assert.match(result.stderr, /injected legacy rename failure/);
assert.deepEqual(JSON.parse(await readFile(fixture.configPath, 'utf-8')), original);
assert.equal((await readFile(fixture.legacyConfigPath, 'utf-8')).length > 0, true);
for (const filePath of [
fixture.guardScriptPath,
fixture.hookScriptPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) await assertMissing(filePath);
await assertNoTransactionFiles(
fixture.hooksDir,
fixture.agentDir,
path.dirname(fixture.legacyConfigPath),
);
} finally {
await fixture.close();
}
});
test('Kiro IDE detects workspace hook races before the first commit', async () => {
const original = { version: 'v1', hooks: [] };
const raceSource = '{"version":"v1","hooks":[],"race":true}\n';
const fixture = await createFixture({ existingConfig: original });
try {
const result = await runPreparedInstallation(fixture, { raceSource });
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during Kiro IDE installation/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), raceSource);
await assertMissing(fixture.guardScriptPath);
await assertMissing(fixture.hookScriptPath);
await assertNoTransactionFiles(fixture.hooksDir, fixture.agentDir);
} finally {
await fixture.close();
}
});
+153
View File
@@ -0,0 +1,153 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import {
createFixture,
environment,
managedHandler,
readSettings,
runProcess,
startApiServer,
} from '../test-support/letta.mjs';
const sourceRoot = process.env.ELYDORA_LETTA_SOURCE;
const bunBinary = process.env.ELYDORA_BUN_BINARY ?? 'bun';
function moduleUrl(relativePath) {
return pathToFileURL(path.join(sourceRoot, relativePath)).href;
}
function resultJson(stdout) {
const line = stdout.trim().split(/\r?\n/).at(-1);
return JSON.parse(line);
}
async function runOfficialProbe(fixture, mode) {
const settings = await readSettings(fixture.globalPath);
const commands = {
guard: managedHandler(settings, 'PreToolUse').command,
audit: managedHandler(settings, 'PostToolUse').command,
failure: managedHandler(settings, 'PostToolUseFailure').command,
};
const source = `
import { settingsManager } from ${JSON.stringify(moduleUrl('src/settings-manager.ts'))};
import { getHooksForEvent } from ${JSON.stringify(moduleUrl('src/hooks/loader.ts'))};
import { executeCommandHook } from ${JSON.stringify(moduleUrl('src/hooks/executor.ts'))};
import { runPostToolUseHooks } from ${JSON.stringify(moduleUrl('src/hooks/index.ts'))};
const workspace = process.env.ELYDORA_OFFICIAL_WORKSPACE;
const commands = JSON.parse(process.env.ELYDORA_OFFICIAL_COMMANDS);
await settingsManager.initialize();
await settingsManager.loadProjectSettings(workspace);
await settingsManager.loadLocalProjectSettings(workspace);
const pre = await getHooksForEvent('PreToolUse', 'Bash', workspace);
const post = await getHooksForEvent('PostToolUse', 'Bash', workspace);
const failure = await getHooksForEvent('PostToolUseFailure', 'Bash', workspace);
const managed = (hooks, command) => hooks.find((hook) => (
hook.type === 'command' && hook.command === command
));
if (process.env.ELYDORA_OFFICIAL_MODE === 'active') {
const guardResult = await executeCommandHook(
managed(pre, commands.guard),
{
event_type: 'PreToolUse',
working_directory: workspace,
tool_name: 'Bash',
tool_input: { command: 'echo official' },
tool_call_id: 'official-pre',
agent_id: 'letta-agent',
},
workspace,
);
const postResult = await executeCommandHook(
managed(post, commands.audit),
{
event_type: 'PostToolUse',
working_directory: workspace,
tool_name: 'Bash',
tool_input: { command: 'echo official' },
tool_result: { status: 'success', output: 'official' },
tool_call_id: 'official-post',
},
workspace,
);
const failureResult = await executeCommandHook(
managed(failure, commands.failure),
{
event_type: 'PostToolUseFailure',
working_directory: workspace,
tool_name: 'Bash',
tool_input: { command: 'exit 1' },
error_message: 'official failure',
error_type: 'ProcessError',
tool_call_id: 'official-failure',
},
workspace,
);
console.log(JSON.stringify({
counts: [pre.length, post.length, failure.length],
exits: [guardResult.exitCode, postResult.exitCode, failureResult.exitCode],
}));
} else {
const result = await runPostToolUseHooks(
'Bash',
{ command: 'echo unavailable' },
{ status: 'success', output: 'unavailable' },
'official-post-error',
workspace,
);
console.log(JSON.stringify({
blocked: result.blocked,
errored: result.errored,
exits: result.results.map((entry) => entry.exitCode),
}));
}
await settingsManager.flush();
`;
return runProcess(
bunBinary,
['--silent', '--eval', source],
{
...environment(fixture),
LETTA_SKIP_KEYCHAIN_CHECK: '1',
ELYDORA_OFFICIAL_COMMANDS: JSON.stringify(commands),
ELYDORA_OFFICIAL_MODE: mode,
ELYDORA_OFFICIAL_WORKSPACE: fixture.projectDir,
},
sourceRoot,
);
}
test('official Letta Code 0.28.13 loads and executes all managed hooks', {
skip: sourceRoot ? false : 'set ELYDORA_LETTA_SOURCE to the official Letta Code source tree',
timeout: 60_000,
}, async () => {
const manifest = JSON.parse(await readFile(path.join(sourceRoot, 'package.json'), 'utf-8'));
assert.equal(manifest.version, '0.28.13');
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
let apiClosed = false;
try {
assert.equal((await fixture.install()).code, 0);
const active = await runOfficialProbe(fixture, 'active');
assert.equal(active.code, 0, active.stderr);
assert.deepEqual(resultJson(active.stdout), {
counts: [1, 1, 1],
exits: [0, 0, 0],
});
assert.equal(api.requests.filter((request) => request.method === 'POST').length, 2);
await api.close();
apiClosed = true;
const unavailable = await runOfficialProbe(fixture, 'unavailable');
assert.equal(unavailable.code, 0, unavailable.stderr);
assert.deepEqual(resultJson(unavailable.stdout), {
blocked: false,
errored: true,
exits: [1],
});
} finally {
await fixture.close();
if (!apiClosed) await api.close();
}
});
+334
View File
@@ -0,0 +1,334 @@
import assert from 'node:assert/strict';
import { lstat, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
VALID_PRIVATE_KEY,
cliPath,
createFixture,
environment,
managedHandler,
readSettings,
registryModuleUrl,
runNode,
runPlugin,
} from '../test-support/letta.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
function assertManagedTriple(settings) {
for (const event of ['PreToolUse', 'PostToolUse', 'PostToolUseFailure']) {
const group = settings.hooks[event].find((entry) => entry.hooks[0]?.timeout === 10_000);
assert.deepEqual(Object.keys(group).sort(), ['hooks', 'matcher']);
assert.equal(group.matcher, '*');
assert.equal(group.hooks.length, 1);
assert.deepEqual(Object.keys(group.hooks[0]).sort(), ['command', 'timeout', 'type']);
assert.equal(group.hooks[0].type, 'command');
assert.equal(group.hooks[0].timeout, 10_000);
assert.doesNotMatch(group.hooks[0].command, /^node /);
}
assert.equal(
managedHandler(settings, 'PostToolUse').command,
managedHandler(settings, 'PostToolUseFailure').command,
);
}
test('Letta Code is registered in the SDK and CLI', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('letta'), {
name: 'Letta Code',
configDir: '~/.letta',
configFile: 'settings.json',
});
const fixture = await createFixture();
try {
const result = await runNode(
['--no-warnings', cliPath, 'status'],
environment(fixture),
fixture.projectDir,
);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /Letta Code \(letta\)/);
} finally {
await fixture.close();
}
});
test('Letta installs the current three-event contract and preserves every source', async () => {
const globalSettings = [
'{',
' "theme": "dark",',
' "hooks": {',
' "FutureEvent": [null],',
' "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "user-hook", "quiet": true }] }]',
' }',
'}',
'',
].join('\r\n');
const projectSettings = { hooks: { PostToolUse: [{
matcher: 'Read',
hooks: [{ type: 'prompt', prompt: 'Review $ARGUMENTS', model: 'fast' }],
}] } };
const localSettings = { windowTitle: 'workspace' };
const fixture = await createFixture({ globalSettings, projectSettings, localSettings });
try {
const projectBefore = await readFile(fixture.projectPath, 'utf-8');
const localBefore = await readFile(fixture.localPath, 'utf-8');
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /run \/hooks/i);
assert.match(first.stdout, /restart active sessions/i);
const raw = await readFile(fixture.globalPath, 'utf-8');
assert.match(raw, /\r\n/);
const settings = JSON.parse(raw);
assert.equal(settings.theme, 'dark');
assert.deepEqual(settings.hooks.FutureEvent, [null]);
assert.equal(settings.hooks.PreToolUse[0].hooks[0].command, 'user-hook');
assertManagedTriple(settings);
assert.equal(await readFile(fixture.projectPath, 'utf-8'), projectBefore);
assert.equal(await readFile(fixture.localPath, 'utf-8'), localBefore);
for (const filePath of [
fixture.guardScriptPath,
fixture.hookScriptPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) await lstat(filePath);
const before = await readFile(fixture.globalPath, 'utf-8');
assert.equal((await fixture.install()).code, 0);
assert.equal(await readFile(fixture.globalPath, 'utf-8'), before);
} finally {
await fixture.close();
}
});
test('Letta validates known hook schemas and preserves future events', async () => {
const fixture = await createFixture({ globalSettings: {
hooks: {
FutureEvent: [null],
PreToolUse: [{ matcher: '[', hooks: [{
type: 'command',
command: 'user-command',
timeout: 0,
quiet: false,
futureField: true,
}] }],
Stop: [{ hooks: [{
type: 'prompt',
prompt: 'Evaluate $ARGUMENTS',
model: 'fast',
timeout: 30,
}] }],
},
} });
try {
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
const settings = await readSettings(fixture.globalPath);
assert.deepEqual(settings.hooks.FutureEvent, [null]);
assert.equal(settings.hooks.PreToolUse[0].matcher, '[');
assert.equal(settings.hooks.PreToolUse[0].hooks[0].futureField, true);
assert.equal(settings.hooks.Stop[0].hooks[0].type, 'prompt');
} finally {
await fixture.close();
}
});
test('Letta rejects malformed affected settings before runtime writes', async (t) => {
const cases = [
['syntax', '{ malformed', /parse Letta Code global settings/i],
['root', '[]', /must contain a JSON object/i],
['comment', '{ // comment\n "hooks": {} }', /parse Letta Code global settings/i],
['trailing comma', '{ "theme": true, }', /parse Letta Code global settings/i],
['duplicate', '{ "hooks": {}, "hooks": {} }', /duplicate field "hooks"/i],
['hooks shape', '{ "hooks": [] }', /field "hooks" must be an object/i],
['disabled', '{ "hooks": { "disabled": "yes" } }', /hooks.disabled.*boolean/i],
['event shape', '{ "hooks": { "PreToolUse": null } }', /must be an array/i],
['group shape', '{ "hooks": { "PreToolUse": [null] } }', /group.*object/i],
['tool matcher', '{ "hooks": { "PreToolUse": [{ "hooks": [] }] } }', /matcher must be a string/i],
['simple matcher', '{ "hooks": { "Stop": [{ "matcher": "*", "hooks": [] }] } }', /matcher is unsupported/i],
['hooks missing', '{ "hooks": { "PreToolUse": [{ "matcher": "*" }] } }', /hooks array/i],
['handler shape', '{ "hooks": { "Stop": [{ "hooks": [null] }] } }', /hooks\[0\].*object/i],
['handler type', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "http" }] }] } }', /unsupported type/i],
['command', '{ "hooks": { "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command" }] }] } }', /non-empty command/i],
['prompt', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt" }] }] } }', /non-empty prompt/i],
['model', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt", "prompt": "x", "model": 1 }] }] } }', /model must be a string/i],
['quiet', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "quiet": 1 }] }] } }', /quiet must be a boolean/i],
['timeout', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "timeout": -1 }] }] } }', /non-negative finite number/i],
];
for (const [name, source, pattern] of cases) {
await t.test(name, async () => {
const fixture = await createFixture({ globalSettings: source });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, pattern);
assert.equal(await readFile(fixture.globalPath, 'utf-8'), source);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
}
});
test('Letta surfaces malformed project sources before writes', async (t) => {
for (const field of ['projectSettings', 'localSettings']) {
await t.test(field, async () => {
const fixture = await createFixture({ [field]: '{ malformed' });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /parse Letta Code (project|project-local) settings/i);
await assertMissing(fixture.globalPath);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
}
});
test('Letta applies the official hooks.disabled precedence', async (t) => {
const cases = [
['global true', { globalSettings: { hooks: { disabled: true } } }, 1],
['project true', { projectSettings: { hooks: { disabled: true } } }, 1],
['local true', { localSettings: { hooks: { disabled: true } } }, 1],
['global false override', {
globalSettings: { hooks: { disabled: false } },
projectSettings: { hooks: { disabled: true } },
localSettings: { hooks: { disabled: true } },
}, 0],
];
for (const [name, options, expectedCode] of cases) {
await t.test(name, async () => {
const fixture = await createFixture(options);
try {
const result = await fixture.install();
assert.equal(result.code, expectedCode, result.stderr);
if (expectedCode === 1) {
assert.match(result.stderr, /hooks.disabled/i);
await assertMissing(fixture.agentDir);
}
} finally {
await fixture.close();
}
});
}
});
test('Letta migrates exact legacy handlers and preserves ownership lookalikes', async () => {
const fixture = await createFixture({ globalSettings: {} });
const legacy = (scriptPath) => ({
matcher: '*',
hooks: [{ type: 'command', command: `node "${scriptPath}"` }],
});
const lookalike = legacy(fixture.guardScriptPath);
lookalike.hooks[0].quiet = true;
await writeFile(fixture.globalPath, `${JSON.stringify({ hooks: {
PreToolUse: [legacy(fixture.guardScriptPath), lookalike],
PostToolUse: [legacy(fixture.hookScriptPath)],
} }, null, 2)}\n`, { mode: 0o600 });
try {
assert.equal((await fixture.install()).code, 0);
let settings = await readSettings(fixture.globalPath);
assertManagedTriple(settings);
assert(settings.hooks.PreToolUse.some((group) => group.hooks[0].quiet === true));
const managed = settings.hooks.PreToolUse.find((group) => group.hooks[0].timeout === 10_000);
managed.userField = 'preserve-group';
managed.hooks.push({ type: 'command', command: 'user-command' });
await writeFile(fixture.globalPath, `${JSON.stringify(settings, null, 2)}\n`);
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
settings = await readSettings(fixture.globalPath);
assert(settings.hooks.PreToolUse.some((group) => group.hooks[0].quiet === true));
assert(settings.hooks.PreToolUse.some((group) => (
group.hooks.some((handler) => handler.command === 'user-command')
)));
assert.equal(settings.hooks.PostToolUse, undefined);
assert.equal(settings.hooks.PostToolUseFailure, undefined);
} finally {
await fixture.close();
}
});
test('Letta status requires exact hooks and strict runtime identity', async () => {
const fixture = await createFixture({ projectSettings: {} });
try {
assert.equal((await fixture.install()).code, 0);
let status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
assert.equal(status.installed, true);
await writeFile(fixture.projectPath, '{"hooks":{"disabled":true}}\n');
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
assert.equal(status.hookConfigured, false);
await writeFile(fixture.projectPath, '{}\n');
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
assert.equal(status.installed, true);
const settings = await readSettings(fixture.globalPath);
settings.hooks.PostToolUseFailure.push(settings.hooks.PostToolUseFailure.at(-1));
await writeFile(fixture.globalPath, `${JSON.stringify(settings, null, 2)}\n`);
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
assert.equal(status.installed, false);
assert.equal((await fixture.install()).code, 0);
await writeFile(path.join(fixture.agentDir, 'private.key'), 'invalid');
const invalid = await runPlugin(fixture, 'status', null);
assert.equal(invalid.code, 1);
assert.match(invalid.stderr, /private key is invalid/i);
assert.equal((await fixture.install()).code, 0);
await writeFile(fixture.hookScriptPath, 'tampered');
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
assert.equal(status.installed, false);
} finally {
await fixture.close();
}
});
test('Letta uninstall removes a settings file containing only managed hooks', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
await assertMissing(fixture.globalPath);
} finally {
await fixture.close();
}
});
test('Letta CLI completes install, status, and uninstall end to end', async () => {
const fixture = await createFixture({ globalSettings: { theme: 'dark' } });
const privateKeyFile = path.join(fixture.rootDir, 'install-private.key');
const tokenFile = path.join(fixture.rootDir, 'install-token.txt');
try {
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
const install = await runNode([
'--no-warnings', cliPath, 'install',
'--agent', 'letta',
'--org_id', 'org-1',
'--agent_id', 'agent-1',
'--kid', 'kid-1',
'--private_key_file', privateKeyFile,
'--token_file', tokenFile,
'--base_url', fixture.baseUrl,
], environment(fixture), fixture.projectDir);
assert.equal(install.code, 0, install.stderr);
assertManagedTriple(await readSettings(fixture.globalPath));
const status = await runNode(
['--no-warnings', cliPath, 'status'],
environment(fixture),
fixture.projectDir,
);
assert.equal(status.code, 0, status.stderr);
assert.match(status.stdout, /Letta Code \(letta\) \[installed\]/);
const uninstall = await runNode([
'--no-warnings', cliPath, 'uninstall',
'--agent', 'letta',
'--agent_id', 'agent-1',
], environment(fixture), fixture.projectDir);
assert.equal(uninstall.code, 0, uninstall.stderr);
assert.deepEqual(await readSettings(fixture.globalPath), { theme: 'dark' });
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
+179
View File
@@ -0,0 +1,179 @@
import assert from 'node:assert/strict';
import { readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
createFixture,
managedHandler,
readSettings,
runLettaHook,
startApiServer,
} from '../test-support/letta.mjs';
function officialInput(fixture, event) {
const base = {
event_type: event,
working_directory: fixture.projectDir,
session_id: 'session-1',
};
if (event === 'PreToolUse') {
return {
...base,
tool_name: 'Bash',
tool_input: { command: 'echo test' },
tool_call_id: 'call-1',
agent_id: 'letta-agent-1',
};
}
if (event === 'PostToolUse') {
return {
...base,
tool_name: 'Bash',
tool_input: { command: 'echo test' },
tool_call_id: 'call-1',
tool_result: { status: 'success', output: 'test' },
agent_id: 'letta-agent-1',
preceding_reasoning: 'Run the command.',
preceding_assistant_message: 'Executing.',
};
}
return {
...base,
tool_name: 'Bash',
tool_input: { command: 'exit 1' },
tool_call_id: 'call-2',
error_message: 'Command failed',
error_type: 'ProcessError',
agent_id: 'letta-agent-1',
preceding_reasoning: 'Run the command.',
preceding_assistant_message: 'Executing.',
};
}
async function installedHandlers(fixture) {
const settings = await readSettings(fixture.globalPath);
return {
guard: managedHandler(settings, 'PreToolUse'),
audit: managedHandler(settings, 'PostToolUse'),
failure: managedHandler(settings, 'PostToolUseFailure'),
};
}
test('Letta guard accepts active agents with native exit semantics', async () => {
const api = await startApiServer('active');
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { guard } = await installedHandlers(fixture);
const result = await runLettaHook(
guard,
JSON.stringify(officialInput(fixture, 'PreToolUse')),
fixture,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.stdout, '');
assert.equal(api.requests.filter((request) => request.method === 'GET').length, 1);
} finally {
await fixture.close();
await api.close();
}
});
test('Letta guard propagates frozen and revoked states through exit code 2', async (t) => {
for (const status of ['frozen', 'revoked']) {
await t.test(status, async () => {
const api = await startApiServer(status);
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { guard } = await installedHandlers(fixture);
const result = await runLettaHook(
guard,
JSON.stringify(officialInput(fixture, 'PreToolUse')),
fixture,
);
assert.equal(result.code, 2, result.stderr);
assert.equal(result.stdout, '');
assert.match(result.stderr, new RegExp(`is ${status}`, 'i'));
} finally {
await fixture.close();
await api.close();
}
});
}
});
test('Letta audit forwards complete success and failure payloads', async () => {
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const { audit, failure } = await installedHandlers(fixture);
for (const [handler, event] of [
[audit, 'PostToolUse'],
[failure, 'PostToolUseFailure'],
]) {
const payload = {
...officialInput(fixture, event),
future_provider_field: { preserved: true },
};
const result = await runLettaHook(handler, JSON.stringify(payload), fixture);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.stdout, '');
}
const posts = api.requests.filter((request) => request.method === 'POST');
assert.equal(posts.length, 2);
assert.deepEqual(JSON.parse(posts[0].raw).payload, {
...officialInput(fixture, 'PostToolUse'),
future_provider_field: { preserved: true },
});
assert.deepEqual(JSON.parse(posts[1].raw).payload, {
...officialInput(fixture, 'PostToolUseFailure'),
future_provider_field: { preserved: true },
});
} finally {
await fixture.close();
await api.close();
}
});
test('Letta audit failures emit stderr, persist error.log, and exit 1', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const { audit } = await installedHandlers(fixture);
const result = await runLettaHook(
audit,
JSON.stringify(officialInput(fixture, 'PostToolUse')),
fixture,
);
assert.equal(result.code, 1);
assert.match(result.stderr, /Elydora audit|fetch failed|ECONNREFUSED/i);
assert.match(
await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'),
/Elydora audit|fetch failed|ECONNREFUSED/i,
);
} finally {
await fixture.close();
}
});
test('Letta runtime artifacts use private modes on POSIX', {
skip: process.platform === 'win32' ? 'POSIX mode bits are not authoritative on Windows' : false,
}, async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
for (const [filePath, expected] of [
[path.join(fixture.agentDir, 'config.json'), 0o600],
[path.join(fixture.agentDir, 'private.key'), 0o600],
[fixture.guardScriptPath, 0o700],
[fixture.hookScriptPath, 0o700],
[fixture.globalPath, 0o600],
]) {
assert.equal((await stat(filePath)).mode & 0o777, expected);
}
} finally {
await fixture.close();
}
});
+157
View File
@@ -0,0 +1,157 @@
import assert from 'node:assert/strict';
import { lstat, mkdir, readFile, readdir, symlink, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
configModuleUrl,
contractModuleUrl,
createFixture,
environment,
installConfig,
installationModuleUrl,
runNode,
sourcesModuleUrl,
} from '../test-support/letta.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
async function assertNoTransactionFiles(...directories) {
const names = [];
for (const directory of directories) {
try {
names.push(...await readdir(directory));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
async function runPreparedInstallation(fixture, {
failSettings = false,
racePath,
raceSource,
} = {}) {
const source = `
import fsp from 'node:fs/promises';
import { buildLettaGroup } from ${JSON.stringify(contractModuleUrl)};
import { renderLettaDocument } from ${JSON.stringify(configModuleUrl)};
import { readLettaSources } from ${JSON.stringify(sourcesModuleUrl)};
import {
commitLettaInstallation,
preflightLettaInstallation,
prepareLettaInstallation,
} from ${JSON.stringify(installationModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
const sources = await readLettaSources();
const paths = await preflightLettaInstallation(config, sources);
const rendered = renderLettaDocument(sources.global, undefined, new Map([
['PreToolUse', buildLettaGroup(paths.guardPath)],
['PostToolUse', buildLettaGroup(paths.auditPath)],
['PostToolUseFailure', buildLettaGroup(paths.auditPath)],
]));
const prepared = await prepareLettaInstallation(config, sources, rendered);
if (process.env.ELYDORA_TEST_RACE_PATH) {
await fsp.writeFile(
process.env.ELYDORA_TEST_RACE_PATH,
process.env.ELYDORA_TEST_RACE_SOURCE,
);
}
await commitLettaInstallation(prepared, async (from, to) => {
if (process.env.ELYDORA_TEST_FAIL_SETTINGS === 'true'
&& to === sources.global.filePath) {
throw new Error('injected Letta settings rename failure');
}
await fsp.rename(from, to);
});
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
ELYDORA_TEST_FAIL_SETTINGS: String(failSettings),
ELYDORA_TEST_RACE_PATH: racePath,
ELYDORA_TEST_RACE_SOURCE: raceSource,
},
fixture.projectDir,
);
}
test('Letta rolls back every runtime file when the settings commit fails', async () => {
const original = '{"theme":"dark"}\n';
const fixture = await createFixture({ globalSettings: original });
try {
const result = await runPreparedInstallation(fixture, { failSettings: true });
assert.equal(result.code, 1);
assert.match(result.stderr, /injected Letta settings rename failure/i);
assert.equal(await readFile(fixture.globalPath, 'utf-8'), original);
for (const filePath of [
fixture.guardScriptPath,
fixture.hookScriptPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) await assertMissing(filePath);
await assertNoTransactionFiles(path.dirname(fixture.globalPath), fixture.agentDir);
} finally {
await fixture.close();
}
});
test('Letta aborts when a read-only project source changes after prepare', async () => {
const original = '{"windowTitle":"before"}\n';
const raceSource = '{"windowTitle":"after"}\n';
const fixture = await createFixture({ projectSettings: original });
try {
const result = await runPreparedInstallation(fixture, {
racePath: fixture.projectPath,
raceSource,
});
assert.equal(result.code, 1);
assert.match(result.stderr, /project settings changed during Letta Code installation/i);
assert.equal(await readFile(fixture.projectPath, 'utf-8'), raceSource);
await assertMissing(fixture.globalPath);
for (const filePath of [
fixture.guardScriptPath,
fixture.hookScriptPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) await assertMissing(filePath);
await assertNoTransactionFiles(path.dirname(fixture.globalPath), fixture.agentDir);
} finally {
await fixture.close();
}
});
test('Letta rejects linked configuration directories before writes', {
skip: process.platform === 'win32' ? 'directory symlinks require platform privileges on Windows' : false,
}, async () => {
const fixture = await createFixture();
const target = path.join(fixture.rootDir, 'redirected-letta');
await mkdir(target);
await symlink(target, path.dirname(fixture.globalPath), 'dir');
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /global configuration directory is not a physical directory/i);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
test('Letta rejects a settings directory at the settings file path', async () => {
const fixture = await createFixture();
await mkdir(fixture.globalPath, { recursive: true });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /global settings path is not a physical file/i);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
+38
View File
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import test from 'node:test';
import {
createFixture,
environment,
runProcess,
} from '../test-support/opencode-test-helpers.mjs';
const binary = process.env.ELYDORA_OPENCODE_BINARY;
test(
'official OpenCode 1.18 discovers the generated global JavaScript plugin',
{ skip: binary ? false : 'set ELYDORA_OPENCODE_BINARY to the official OpenCode executable' },
async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const env = {
...environment(fixture),
OPENCODE_DISABLE_AUTOUPDATE: 'true',
OPENCODE_DISABLE_MODELS_FETCH: 'true',
};
const version = await runProcess(binary, ['--version'], env, fixture.projectDir);
assert.equal(version.code, 0, version.stderr);
assert.match(version.stdout, /^1\.18\./);
const paths = await runProcess(binary, ['debug', 'paths'], env, fixture.projectDir);
assert.equal(paths.code, 0, paths.stderr);
assert.equal(paths.stdout.includes(path.join(fixture.configRoot, 'opencode')), true);
const info = await runProcess(binary, ['debug', 'info'], env, fixture.projectDir);
assert.equal(info.code, 0, info.stderr);
assert.match(info.stdout, /elydora-audit\.js/);
assert.doesNotMatch(info.stdout, /elydora-audit\.mjs/);
} finally {
await fixture.close();
}
},
);
+422 -133
View File
@@ -1,165 +1,454 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import {
lstat,
mkdir,
readFile,
readdir,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import {
VALID_PRIVATE_KEY,
cliPath,
contractModuleUrl,
createFixture,
environment,
registryModuleUrl,
runNode,
runPlugin,
startApiServer,
} from '../test-support/opencode-test-helpers.mjs';
function runNode(args, env) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
});
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
async function createFixture({ guardSource, hookSource }) {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-opencode-'));
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
await mkdir(agentDir, { recursive: true });
const guardScriptPath = path.join(agentDir, 'guard.cjs');
const hookScriptPath = path.join(agentDir, 'hook.cjs');
if (guardSource !== undefined) await writeFile(guardScriptPath, guardSource);
await writeFile(hookScriptPath, hookSource ?? 'process.exit(0);\n');
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode.js')).href;
const installScript = `
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)};
await opencodePlugin.install(JSON.parse(process.env.ELYDORA_TEST_CONFIG));
async function writeSource(filePath, source) {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, source, { mode: 0o600 });
}
async function legacySource(fixture) {
const { buildLegacyOpenCodePlugin } = await import(contractModuleUrl);
return buildLegacyOpenCodePlugin(fixture.hookScriptPath, fixture.guardScriptPath);
}
async function runGeneratedHook(fixture, name, input, output) {
const source = `
const module = await import(${JSON.stringify(pathToFileURL(fixture.pluginPath).href)});
const hooks = await module.ElydoraAuditPlugin({});
await hooks[${JSON.stringify(name)}](
JSON.parse(process.env.ELYDORA_TEST_INPUT),
JSON.parse(process.env.ELYDORA_TEST_OUTPUT),
);
`;
const installResult = await runNode(
['--input-type=module', '--eval', installScript],
return runNode(
['--input-type=module', '--eval', source],
{
HOME: homeDir,
USERPROFILE: homeDir,
ELYDORA_TEST_CONFIG: JSON.stringify({ guardScriptPath, hookScriptPath }),
...environment(fixture),
ELYDORA_TEST_INPUT: JSON.stringify(input),
ELYDORA_TEST_OUTPUT: JSON.stringify(output),
},
fixture.projectDir,
);
assert.equal(installResult.code, 0, installResult.stderr);
const generatedPath = path.join(
homeDir,
'.config',
'opencode',
'plugins',
'elydora-audit.mjs',
);
const generatedPlugin = await import(`${pathToFileURL(generatedPath).href}?test=${Date.now()}`);
const hooks = await generatedPlugin.ElydoraAuditPlugin({ project: { name: 'project-fallback' } });
return {
guardScriptPath,
homeDir,
hooks,
async close() {
await rm(homeDir, { recursive: true, force: true });
},
};
}
async function readPluginStatus(homeDir) {
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode.js')).href;
const statusScript = `
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)};
console.log(JSON.stringify(await opencodePlugin.status()));
`;
const result = await runNode(
['--input-type=module', '--eval', statusScript],
{ HOME: homeDir, USERPROFILE: homeDir },
);
assert.equal(result.code, 0, result.stderr);
return JSON.parse(result.stdout);
}
test('OpenCode rejects tool execution when the Elydora guard blocks', async () => {
const fixture = await createFixture({
guardSource: "process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n",
test('OpenCode installs the discoverable global JavaScript plugin and exact runtime', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('opencode'), {
name: 'OpenCode',
configDir: '.config/opencode/plugins',
configFile: 'elydora-audit.js',
});
const fixture = await createFixture();
try {
await assert.rejects(
fixture.hooks['tool.execute.before'](
{ tool: 'bash', sessionID: 'session-1', callID: 'call-1' },
{ args: { command: 'echo test' } },
),
/Agent is frozen by Elydora/,
);
} finally {
await fixture.close();
}
});
test('OpenCode blocks when the guard process cannot start', async () => {
const fixture = await createFixture({});
try {
await assert.rejects(
fixture.hooks['tool.execute.before'](
{ tool: 'bash', sessionID: 'session-1', callID: 'call-1' },
{ args: { command: 'echo test' } },
),
/Elydora guard failed/,
);
} finally {
await fixture.close();
}
});
test('OpenCode forwards the current tool event contract to the audit hook', async () => {
const capturePath = path.join(os.tmpdir(), `elydora-event-${process.pid}-${Date.now()}.json`);
const hookSource = `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => fs.writeFileSync(${JSON.stringify(capturePath)}, Buffer.concat(chunks)));
`;
const fixture = await createFixture({
guardSource: 'process.exit(0);\n',
hookSource,
});
try {
await fixture.hooks['tool.execute.after'](
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /elydora-audit\.js/);
assert.match(first.stdout, /restart active OpenCode sessions/);
await assertMissing(fixture.legacyPluginPath);
const firstSource = await readFile(fixture.pluginPath, 'utf-8');
assert.match(firstSource, /@elydora-opencode-plugin/);
assert.match(firstSource, /tool\.execute\.before/);
assert.match(firstSource, /tool\.execute\.after/);
assert.doesNotMatch(firstSource, /spawn\('node'/);
const hooks = await fixture.loadPlugin();
assert.deepEqual(Object.keys(hooks).sort(), ['tool.execute.after', 'tool.execute.before']);
assert.deepEqual(
JSON.parse(await readFile(path.join(fixture.agentDir, 'config.json'), 'utf-8')),
{
tool: 'bash',
sessionID: 'session-1',
callID: 'call-1',
args: { command: 'echo test' },
org_id: 'org-1',
agent_id: fixture.agentId,
kid: 'kid-1',
base_url: fixture.baseUrl,
token: 'token-1',
agent_name: 'opencode',
},
{ title: 'Shell', output: 'test' },
);
let payload;
const deadline = Date.now() + 3000;
while (Date.now() < deadline) {
try {
payload = await readFile(capturePath, 'utf-8');
break;
} catch (error) {
if (error.code !== 'ENOENT') throw error;
await delay(20);
}
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.deepEqual(JSON.parse(status.stdout), {
installed: true,
agentName: 'opencode',
displayName: 'OpenCode',
hookConfigured: true,
hookScriptExists: true,
configPath: fixture.pluginPath,
});
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
assert.equal(await readFile(fixture.pluginPath, 'utf-8'), firstSource);
const names = [
...await readdir(fixture.pluginsDir),
...await readdir(fixture.agentDir),
];
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false);
if (process.platform !== 'win32') {
assert.equal((await stat(fixture.pluginPath)).mode & 0o777, 0o600);
assert.equal((await stat(fixture.guardScriptPath)).mode & 0o777, 0o700);
}
assert(payload, 'audit hook did not receive the tool event');
assert.deepEqual(JSON.parse(payload), {
} finally {
await fixture.close();
}
});
test('OpenCode guard enforces state and audit preserves the complete native event', async () => {
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const beforeInput = { tool: 'bash', sessionID: 'session-1', callID: 'call-1' };
const beforeOutput = { args: { command: 'echo test' } };
const guard = await runGeneratedHook(
fixture,
'tool.execute.before',
beforeInput,
beforeOutput,
);
assert.equal(guard.code, 0, guard.stderr);
assert.equal(api.requests.some(({ method }) => method === 'GET'), true);
const afterInput = { ...beforeInput, args: beforeOutput.args };
const afterOutput = { title: 'Shell', output: 'test', metadata: { exit: 0 } };
const audit = await runGeneratedHook(
fixture,
'tool.execute.after',
afterInput,
afterOutput,
);
assert.equal(audit.code, 0, audit.stderr);
const request = api.requests.find(({ method }) => method === 'POST');
assert.ok(request);
const operation = JSON.parse(request.raw);
assert.deepEqual(operation.action, { tool: 'bash' });
assert.deepEqual(operation.subject, { session_id: 'session-1' });
assert.deepEqual(operation.payload, {
hook_event_name: 'tool.execute.after',
tool_name: 'bash',
tool_input: { command: 'echo test' },
session_id: 'session-1',
call_id: 'call-1',
input: afterInput,
output: afterOutput,
});
api.setOperationStatus(503);
const failedAudit = await runGeneratedHook(
fixture,
'tool.execute.after',
afterInput,
afterOutput,
);
assert.equal(failedAudit.code, 0, failedAudit.stderr);
assert.match(failedAudit.stderr, /OpenCode audit runtime failed: \[Elydora audit\]/);
assert.match(
await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'),
/Audit API returned HTTP 503/,
);
await rm(path.join(fixture.agentDir, 'status-cache.json'));
api.setStatus('frozen');
const frozen = await runGeneratedHook(
fixture,
'tool.execute.before',
beforeInput,
beforeOutput,
);
assert.equal(frozen.code, 1);
assert.match(frozen.stderr, /Agent "opencode" is frozen.*Tool execution blocked/s);
} finally {
await api.close();
await fixture.close();
await rm(capturePath, { force: true });
}
});
test('OpenCode status requires both generated runtime scripts', async () => {
const fixture = await createFixture({ guardSource: 'process.exit(0);\n' });
test('OpenCode bridge forwards both native arguments and surfaces runtime failures', async () => {
const fixture = await createFixture();
const guardCapture = path.join(fixture.projectDir, 'guard-input.json');
const auditCapture = path.join(fixture.projectDir, 'audit-input.json');
try {
assert.equal((await readPluginStatus(fixture.homeDir)).installed, true);
assert.equal((await fixture.install()).code, 0);
await writeFile(fixture.guardScriptPath, `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(guardCapture)}, Buffer.concat(chunks));
process.stderr.write('[guard warning] degraded status API\\n');
});
`);
const beforeInput = { tool: 'write', sessionID: 'session-2', callID: 'call-2' };
const beforeOutput = { args: { filePath: 'a.txt', content: 'value' } };
const guard = await runGeneratedHook(
fixture,
'tool.execute.before',
beforeInput,
beforeOutput,
);
assert.equal(guard.code, 0, guard.stderr);
assert.match(guard.stderr, /guard warning/);
assert.deepEqual(JSON.parse(await readFile(guardCapture, 'utf-8')), {
hook_event_name: 'tool.execute.before',
tool_name: 'write',
tool_input: beforeOutput.args,
session_id: 'session-2',
call_id: 'call-2',
input: beforeInput,
output: beforeOutput,
});
await writeFile(fixture.hookScriptPath, `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(auditCapture)}, Buffer.concat(chunks));
process.stderr.write('audit transport failed\\n');
process.exitCode = 3;
});
`);
const afterInput = { ...beforeInput, args: beforeOutput.args };
const afterOutput = { title: 'Write', output: 'saved', metadata: { bytes: 5 } };
const audit = await runGeneratedHook(
fixture,
'tool.execute.after',
afterInput,
afterOutput,
);
assert.equal(audit.code, 0, audit.stderr);
assert.match(audit.stderr, /OpenCode audit runtime failed: audit transport failed/);
assert.deepEqual(JSON.parse(await readFile(auditCapture, 'utf-8')).input, afterInput);
await rm(fixture.guardScriptPath);
assert.equal((await readPluginStatus(fixture.homeDir)).installed, false);
const missing = await runGeneratedHook(
fixture,
'tool.execute.before',
beforeInput,
beforeOutput,
);
assert.equal(missing.code, 1);
assert.match(missing.stderr, /Elydora guard failed/);
} finally {
await fixture.close();
}
});
test('OpenCode status requires an exact plugin, runtime identity, and clean migration state', async (t) => {
await t.test('tampered managed plugin', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
await writeFile(fixture.pluginPath, `${await readFile(fixture.pluginPath, 'utf-8')}\n// tampered\n`);
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /does not match the managed template/);
} finally {
await fixture.close();
}
});
for (const [label, target, contents, expected] of [
['guard runtime', 'guard', 'tampered\n', /"installed":false/],
['runtime config', 'config', '{ malformed', /parse Elydora runtime config/i],
['runtime identity', 'config', JSON.stringify({
org_id: 'org-1',
agent_id: 'different-agent',
kid: 'kid-1',
base_url: 'http://127.0.0.1:9',
token: 'token-1',
agent_name: 'opencode',
}), /runtime identity/i],
['private key', 'key', 'invalid', /private key is invalid/i],
]) {
await t.test(label, async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const targets = {
guard: fixture.guardScriptPath,
config: path.join(fixture.agentDir, 'config.json'),
key: path.join(fixture.agentDir, 'private.key'),
};
await writeFile(targets[target], contents);
const status = await runPlugin(fixture, 'status', null);
assert.match(`${status.stdout}\n${status.stderr}`, expected);
} finally {
await fixture.close();
}
});
}
await t.test('stale owned mjs source', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
await writeSource(fixture.legacyPluginPath, await legacySource(fixture));
let status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
assert.equal((await fixture.install()).code, 0);
await assertMissing(fixture.legacyPluginPath);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
} finally {
await fixture.close();
}
});
});
test('OpenCode rejects collisions, invalid inputs, and linked plugin paths before writes', async (t) => {
await t.test('user plugin collision', async () => {
const source = 'export const UserPlugin = async () => ({});\n';
const fixture = await createFixture({ existingPlugin: source });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /owned by another integration/);
assert.equal(await readFile(fixture.pluginPath, 'utf-8'), source);
await assertMissing(fixture.guardScriptPath);
} finally {
await fixture.close();
}
});
await t.test('non-canonical private key', async () => {
const fixture = await createFixture();
try {
const result = await fixture.install({ privateKey: 'invalid' });
assert.equal(result.code, 1);
assert.match(result.stderr, /canonical 32-byte/);
await assertMissing(fixture.pluginPath);
} finally {
await fixture.close();
}
});
await t.test('relative XDG_CONFIG_HOME', async () => {
const fixture = await createFixture();
try {
const source = `
import { opencodePlugin } from ${JSON.stringify(
pathToFileURL(path.resolve('dist/plugins/opencode.js')).href,
)};
await opencodePlugin.status();
`;
const result = await runNode(
['--input-type=module', '--eval', source],
{ HOME: fixture.homeDir, USERPROFILE: fixture.homeDir, XDG_CONFIG_HOME: 'relative' },
fixture.projectDir,
);
assert.equal(result.code, 1);
assert.match(result.stderr, /XDG_CONFIG_HOME must be an absolute path/);
} finally {
await fixture.close();
}
});
await t.test('linked plugins directory', async () => {
const fixture = await createFixture();
const external = path.join(fixture.rootDir, 'external-plugins');
try {
await mkdir(external);
await mkdir(path.dirname(fixture.pluginsDir), { recursive: true });
await symlink(external, fixture.pluginsDir, process.platform === 'win32' ? 'junction' : 'dir');
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /plugins directory is not a physical directory/i);
await assertMissing(fixture.guardScriptPath);
} finally {
await fixture.close();
}
});
});
test('OpenCode migrates exact legacy ownership and preserves unrelated mjs bytes', async (t) => {
await t.test('install and uninstall exact ownership', async () => {
const fixture = await createFixture();
try {
await writeSource(fixture.legacyPluginPath, await legacySource(fixture));
assert.equal((await fixture.install()).code, 0);
await assertMissing(fixture.legacyPluginPath);
const uninstall = await runPlugin(fixture, 'uninstall', fixture.agentId);
assert.equal(uninstall.code, 0, uninstall.stderr);
await assertMissing(fixture.pluginPath);
} finally {
await fixture.close();
}
});
await t.test('unrelated legacy file', async () => {
const source = 'export const UserPlugin = async () => ({});\n';
const fixture = await createFixture({ existingLegacy: source });
try {
assert.equal((await fixture.install()).code, 0);
assert.equal(await readFile(fixture.legacyPluginPath, 'utf-8'), source);
assert.equal((await runPlugin(fixture, 'uninstall', fixture.agentId)).code, 0);
assert.equal(await readFile(fixture.legacyPluginPath, 'utf-8'), source);
} finally {
await fixture.close();
}
});
await t.test('legacy-only uninstall', async () => {
const fixture = await createFixture();
try {
await writeSource(fixture.legacyPluginPath, await legacySource(fixture));
assert.equal((await runPlugin(fixture, 'uninstall', fixture.agentId)).code, 0);
await assertMissing(fixture.legacyPluginPath);
} finally {
await fixture.close();
}
});
});
test('OpenCode CLI completes install, status, and uninstall end to end', async () => {
const fixture = await createFixture();
const privateKeyFile = path.join(fixture.projectDir, 'private-key.txt');
const tokenFile = path.join(fixture.projectDir, 'token.txt');
try {
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
const install = await runNode([
cliPath, 'install', '--agent', 'opencode', '--org_id', 'org-1',
'--agent_id', fixture.agentId, '--kid', 'kid-1',
'--private_key_file', privateKeyFile, '--token_file', tokenFile,
'--base_url', fixture.baseUrl,
], environment(fixture), fixture.projectDir);
assert.equal(install.code, 0, install.stderr);
const status = await runNode([cliPath, 'status'], environment(fixture), fixture.projectDir);
assert.equal(status.code, 0, status.stderr);
assert.match(status.stdout, /OpenCode \(opencode\) \[installed\]/);
const uninstall = await runNode([
cliPath, 'uninstall', '--agent', 'opencode', '--agent_id', fixture.agentId,
], environment(fixture), fixture.projectDir);
assert.equal(uninstall.code, 0, uninstall.stderr);
await assertMissing(fixture.pluginPath);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
@@ -0,0 +1,110 @@
import assert from 'node:assert/strict';
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
contractModuleUrl,
createFixture,
environment,
installConfig,
installationModuleUrl,
ioModuleUrl,
runNode,
} from '../test-support/opencode-test-helpers.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
async function runPreparedInstallation(fixture, { failLegacy = false, racePlugin } = {}) {
const source = `
import fsp from 'node:fs/promises';
import path from 'node:path';
import {
commitOpenCodeInstallation,
prepareOpenCodeInstallation,
} from ${JSON.stringify(installationModuleUrl)};
import { readOpenCodeSources } from ${JSON.stringify(ioModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
const sources = await readOpenCodeSources();
const prepared = await prepareOpenCodeInstallation(config, sources);
if (process.env.ELYDORA_TEST_RACE_PLUGIN) {
await fsp.mkdir(path.dirname(sources.paths.pluginPath), { recursive: true });
await fsp.writeFile(sources.paths.pluginPath, process.env.ELYDORA_TEST_RACE_PLUGIN);
}
let failed = false;
await commitOpenCodeInstallation(prepared, async (from, to) => {
if (!failed
&& process.env.ELYDORA_TEST_FAIL_LEGACY === 'true'
&& from === sources.paths.legacyPluginPath
&& to.endsWith('.rollback')) {
failed = true;
throw new Error('injected OpenCode legacy migration failure');
}
await fsp.rename(from, to);
});
`;
return runNode(
['--input-type=module', '--eval', source],
{
...environment(fixture),
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
ELYDORA_TEST_FAIL_LEGACY: String(failLegacy),
ELYDORA_TEST_RACE_PLUGIN: racePlugin,
},
fixture.projectDir,
);
}
async function assertNoTransactionFiles(...directories) {
const names = [];
for (const directory of directories) {
try {
names.push(...await readdir(directory));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
test('OpenCode rolls back plugin, legacy migration, and runtime after a late failure', async () => {
const fixture = await createFixture();
const { buildLegacyOpenCodePlugin } = await import(contractModuleUrl);
const legacy = buildLegacyOpenCodePlugin(fixture.hookScriptPath, fixture.guardScriptPath);
try {
await mkdir(path.dirname(fixture.legacyPluginPath), { recursive: true });
await writeFile(fixture.legacyPluginPath, legacy);
const result = await runPreparedInstallation(fixture, { failLegacy: true });
assert.equal(result.code, 1);
assert.match(result.stderr, /injected OpenCode legacy migration failure/);
assert.equal(await readFile(fixture.legacyPluginPath, 'utf-8'), legacy);
await assertMissing(fixture.pluginPath);
for (const filePath of [
fixture.guardScriptPath,
fixture.hookScriptPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) await assertMissing(filePath);
await assertNoTransactionFiles(fixture.pluginsDir, fixture.agentDir);
} finally {
await fixture.close();
}
});
test('OpenCode detects concurrent plugin creation before committing runtime files', async () => {
const fixture = await createFixture();
const racePlugin = 'export const RacePlugin = async () => ({});\n';
try {
const result = await runPreparedInstallation(fixture, { racePlugin });
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during OpenCode installation/i);
assert.equal(await readFile(fixture.pluginPath, 'utf-8'), racePlugin);
await assertMissing(fixture.guardScriptPath);
await assertMissing(fixture.hookScriptPath);
await assertNoTransactionFiles(fixture.pluginsDir, fixture.agentDir);
} finally {
await fixture.close();
}
});