fix(opencode): install discoverable atomic plugin

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 17:46:37 -04:00
1 parent 3f43f4aad5
commit 69a75153f5
15 files changed
+1554 -348

No files matched your search

+1
View File
@@ -36,6 +36,7 @@ This repository owns the published `@elydora/sdk` package, its `elydora` CLI, lo
- Resolve Qwen Code 0.20.0 user settings through explicit `QWEN_HOME`, then the initial Qwen home `.env`, then the OS home `.env`; when bootstrap discovers a redirected home, read that home's `.env` as well. Treat empty overrides as unset while preserving their process-level ownership semantics. Read system defaults, user settings, the active trusted workspace, system overrides, and trusted-folder rules with Qwen's documented precedence; write only the user source. Validate the current twenty-one event names and command, HTTP, and prompt handler schemas while preserving future event payloads. Register exact matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups named `elydora-guard` and `elydora-audit` with ten-second millisecond timeouts. Preserve the complete native snake_case payload, propagate freeze and revocation through exit code `2`, and use PowerShell's call operator with explicit `$LASTEXITCODE` propagation on Windows. Commit user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction, with snapshot preconditions for every consulted routing, policy, trust, and settings source. Require physical files, exact runtime sources, canonical private keys, matching runtime identity, an effective enabled state, `/hooks` review, and restart of active Qwen Code sessions. - Resolve Qwen Code 0.20.0 user settings through explicit `QWEN_HOME`, then the initial Qwen home `.env`, then the OS home `.env`; when bootstrap discovers a redirected home, read that home's `.env` as well. Treat empty overrides as unset while preserving their process-level ownership semantics. Read system defaults, user settings, the active trusted workspace, system overrides, and trusted-folder rules with Qwen's documented precedence; write only the user source. Validate the current twenty-one event names and command, HTTP, and prompt handler schemas while preserving future event payloads. Register exact matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups named `elydora-guard` and `elydora-audit` with ten-second millisecond timeouts. Preserve the complete native snake_case payload, propagate freeze and revocation through exit code `2`, and use PowerShell's call operator with explicit `$LASTEXITCODE` propagation on Windows. Commit user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction, with snapshot preconditions for every consulted routing, policy, trust, and settings source. Require physical files, exact runtime sources, canonical private keys, matching runtime identity, an effective enabled state, `/hooks` review, and restart of active Qwen Code sessions.
- Resolve Kiro IDE 1.0 hooks through the current workspace `.kiro/hooks/elydora-audit.json`. Register exact `PreToolUse` and `PostToolUse` command entries in the v1 JSON schema with `.*` matchers, ten-second timeouts, and enabled state. Preserve Kiro's complete native payload and propagate frozen or revoked state through exit code `2`. Use the user-level `~/.kiro/hooks/elydora-audit.kiro.hook` source solely as exact Elydora legacy migration input. Commit the workspace hook file, exact legacy migration, generated runtimes, runtime config, and private key through one rollback-capable transaction. Require a physical workspace hook path, strict schemas, exact runtime sources, canonical private keys, matching runtime identity, and a complete enabled contract during status checks, then verify both entries in the Agent Hooks panel. - Resolve Kiro IDE 1.0 hooks through the current workspace `.kiro/hooks/elydora-audit.json`. Register exact `PreToolUse` and `PostToolUse` command entries in the v1 JSON schema with `.*` matchers, ten-second timeouts, and enabled state. Preserve Kiro's complete native payload and propagate frozen or revoked state through exit code `2`. Use the user-level `~/.kiro/hooks/elydora-audit.kiro.hook` source solely as exact Elydora legacy migration input. Commit the workspace hook file, exact legacy migration, generated runtimes, runtime config, and private key through one rollback-capable transaction. Require a physical workspace hook path, strict schemas, exact runtime sources, canonical private keys, matching runtime identity, and a complete enabled contract during status checks, then verify both entries in the Agent Hooks panel.
- Resolve Kiro CLI v2 through the global `~/.kiro/agents/elydora-audit.json` custom agent and CLI 2.13.0+ v3 through `~/.kiro/hooks/elydora-audit.json`. Register exact lower-camel `preToolUse` and `postToolUse` v2 handlers with `*` matchers and ten-second millisecond timeouts; register exact PascalCase `PreToolUse` and `PostToolUse` v3 handlers in Kiro's v1 schema with `.*` matchers and ten-second timeouts. Preserve the complete native payload, propagate frozen or revoked state through exit code `2`, and use encoded PowerShell commands on Windows. Commit both hook sources, generated runtimes, runtime config, and private key through one rollback-capable transaction. Reject path and reserved-name collisions, migrate only exact Elydora command ownership, and require physical sources, strict schemas, canonical private keys, exact runtime sources, matching identity, and a common complete v2/v3 contract during status checks. Validate v2 with `kiro-cli agent validate --path` and activate v3 with `kiro-cli --v3`. - Resolve Kiro CLI v2 through the global `~/.kiro/agents/elydora-audit.json` custom agent and CLI 2.13.0+ v3 through `~/.kiro/hooks/elydora-audit.json`. Register exact lower-camel `preToolUse` and `postToolUse` v2 handlers with `*` matchers and ten-second millisecond timeouts; register exact PascalCase `PreToolUse` and `PostToolUse` v3 handlers in Kiro's v1 schema with `.*` matchers and ten-second timeouts. Preserve the complete native payload, propagate frozen or revoked state through exit code `2`, and use encoded PowerShell commands on Windows. Commit both hook sources, generated runtimes, runtime config, and private key through one rollback-capable transaction. Reject path and reserved-name collisions, migrate only exact Elydora command ownership, and require physical sources, strict schemas, canonical private keys, exact runtime sources, matching identity, and a common complete v2/v3 contract during status checks. Validate v2 with `kiro-cli agent validate --path` and activate v3 with `kiro-cli --v3`.
- Resolve OpenCode 1.18.4 global plugins through `${XDG_CONFIG_HOME:-~/.config}/opencode/plugins/elydora-audit.js`; require an absolute `XDG_CONFIG_HOME` and use the official `.js` or `.ts` discovery contract. Export exactly one named `ElydoraAuditPlugin` function. Implement `tool.execute.before` with official `{ tool, sessionID, callID }` input plus `{ args }` output and `tool.execute.after` with official `{ tool, sessionID, callID, args }` input plus `{ title, output, metadata }` output. Preserve both native arguments. Run the guard synchronously with a bounded timeout and propagate frozen or revoked state by throwing; await the audit runtime with a bounded timeout and report failures to stderr after tool completion. Embed the current absolute Node executable in the generated plugin. Commit the plugin, exact legacy `.mjs` migration, generated runtimes, runtime config, and private key through one rollback-capable transaction. Require physical paths, strict metadata, exact generated sources, canonical private keys, executable identity, matching runtime identity, and a clean migration state during status checks. Restart active OpenCode sessions after installation.
## Code Quality ## Code Quality
+2
View File
@@ -108,6 +108,8 @@ Kiro IDE 1.0 installation is workspace-scoped. Run the installer from the worksp
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`. 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`.
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.
## API Reference ## API Reference
### Configuration ### Configuration
+2 -2
View File
@@ -12,8 +12,8 @@ import {
type KiroIdeDocument, type KiroIdeDocument,
type KiroIdeRuntimeContract, type KiroIdeRuntimeContract,
} from './kiroide-contract.js'; } from './kiroide-contract.js';
import { runtimeFilesExist } from './kiroide-io.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js'; import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { managedRuntimeFilesExist } from './managed-runtime-status.js';
const MAX_HOOK_BYTES = 512 * 1024; const MAX_HOOK_BYTES = 512 * 1024;
@@ -64,5 +64,5 @@ export async function readKiroCliSources(): Promise<KiroCliSources> {
export async function kiroCliRuntimeFilesExist( export async function kiroCliRuntimeFilesExist(
contract: KiroIdeRuntimeContract, contract: KiroIdeRuntimeContract,
): Promise<boolean> { ): Promise<boolean> {
return runtimeFilesExist(contract, AGENT_KEY); return managedRuntimeFilesExist(contract, AGENT_KEY);
} }
-96
View File
@@ -1,12 +1,5 @@
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { generateGuardScript } from './guard-template.js';
import { generateHookScript } from './hook-template.js';
import { sameKiroIdeAgentId, sameKiroIdePath } from './kiroide-command.js';
import { import {
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
createKiroIdeDocument, createKiroIdeDocument,
legacyKiroIdeRuntimeContract, legacyKiroIdeRuntimeContract,
parseKiroIdeDocument, parseKiroIdeDocument,
@@ -16,10 +9,6 @@ import {
type KiroIdeRuntimeContract, type KiroIdeRuntimeContract,
} from './kiroide-contract.js'; } from './kiroide-contract.js';
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js'; import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
const MAX_SECRET_BYTES = 64 * 1024;
const MAX_CONFIG_BYTES = 512 * 1024;
export interface LegacyKiroIdeDocument { export interface LegacyKiroIdeDocument {
readonly exists: boolean; readonly exists: boolean;
@@ -83,88 +72,3 @@ export async function requirePhysicalLegacyDirectory(
throw new Error(`Legacy Kiro IDE hooks directory is missing: ${hooksDirectory}`); throw new Error(`Legacy Kiro IDE hooks directory is missing: ${hooksDirectory}`);
} }
} }
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: KiroIdeRuntimeContract,
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 (!sameKiroIdeAgentId(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: KiroIdeRuntimeContract): boolean {
const agentDirectory = path.dirname(contract.guardPath);
return sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))
&& sameKiroIdePath(contract.guardPath, path.join(agentDirectory, GUARD_SCRIPT))
&& sameKiroIdePath(contract.auditPath, path.join(agentDirectory, AUDIT_SCRIPT));
}
export async function runtimeFilesExist(
contract: KiroIdeRuntimeContract,
agentKey = AGENT_KEY,
): 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)
&& audit.contents === generateHookScript(agentKey, contract.agentId, { nativePayload: true });
}
+5 -2
View File
@@ -14,7 +14,8 @@ import {
prepareKiroIdeInstallation, prepareKiroIdeInstallation,
prepareKiroIdeUninstall, prepareKiroIdeUninstall,
} from './kiroide-installation.js'; } from './kiroide-installation.js';
import { readKiroIdeSources, runtimeFilesExist } from './kiroide-io.js'; import { readKiroIdeSources } from './kiroide-io.js';
import { managedRuntimeFilesExist } from './managed-runtime-status.js';
import { SUPPORTED_AGENTS } from './registry.js'; import { SUPPORTED_AGENTS } from './registry.js';
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!; const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
@@ -60,7 +61,9 @@ export const kiroidePlugin: AgentPlugin = {
const contracts = kiroIdeRuntimeContracts(sources.document.hooks); const contracts = kiroIdeRuntimeContracts(sources.document.hooks);
const hookConfigured = contracts.length > 0; const hookConfigured = contracts.length > 0;
const hookScriptExists = hookConfigured const hookScriptExists = hookConfigured
&& (await Promise.all(contracts.map((contract) => runtimeFilesExist(contract)))).every(Boolean); && (await Promise.all(contracts.map((contract) => (
managedRuntimeFilesExist(contract, AGENT_KEY)
)))).every(Boolean);
return { return {
installed: hookConfigured && hookScriptExists, installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY, agentName: AGENT_KEY,
+119
View File
@@ -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);
}
+132
View File
@@ -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 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'; import { SUPPORTED_AGENTS } from './registry.js';
const AGENT_KEY = 'opencode';
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!; const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
function resolveConfigDir(): string { function validateSources(sources: Awaited<ReturnType<typeof readOpenCodeSources>>): void {
return path.join(os.homedir(), entry.configDir); requireAvailableOpenCodePlugin(sources.current);
}
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();
},
};
};
`;
} }
export const opencodePlugin: AgentPlugin = { export const opencodePlugin: AgentPlugin = {
async install(config: InstallConfig): Promise<void> { managesRuntime: true,
const configDir = resolveConfigDir();
await fsp.mkdir(configDir, { recursive: true });
const configPath = resolveConfigPath(); async preflightInstall(config: InstallConfig): Promise<void> {
const script = generatePluginScript(config.hookScriptPath, config.guardScriptPath); const sources = await readOpenCodeSources();
validateSources(sources);
await fsp.writeFile(configPath, script, 'utf-8'); await preflightOpenCodeInstallation(config, sources);
}, },
async uninstall(_agentId?: string): Promise<void> { async install(config: InstallConfig): Promise<void> {
const configPath = resolveConfigPath(); const sources = await readOpenCodeSources();
try { validateSources(sources);
await fsp.unlink(configPath); const prepared = await prepareOpenCodeInstallation(config, sources);
} catch { await commitOpenCodeInstallation(prepared);
// Already removed 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> { async status(): Promise<PluginStatus> {
const configPath = resolveConfigPath(); const sources = await readOpenCodeSources();
validateSources(sources);
let hookConfigured = false; const contract = sources.legacy.contract ? undefined : sources.current.contract;
let hookScriptPath = ''; const hookConfigured = contract !== undefined;
let guardScriptPath = ''; const hookScriptExists = contract !== undefined
try { && await openCodeRuntimeFilesExist(contract);
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
}
}
return { return {
installed: hookConfigured && hookScriptExists, installed: hookConfigured && hookScriptExists,
agentName: AGENT_KEY, agentName: AGENT_KEY,
displayName: entry.name, displayName: entry.name,
hookConfigured, hookConfigured,
hookScriptExists, hookScriptExists,
configPath, configPath: sources.paths.pluginPath,
}; };
}, },
}; };
+1 -1
View File
@@ -11,7 +11,7 @@ export const SUPPORTED_AGENTS: ReadonlyMap<string, AgentRegistryEntry> = new Map
['gemini', { name: 'Gemini CLI', configDir: '~/.gemini', configFile: 'settings.json' }], ['gemini', { name: 'Gemini CLI', configDir: '~/.gemini', configFile: 'settings.json' }],
['kirocli', { name: 'Kiro CLI', configDir: '~/.kiro/hooks', configFile: 'elydora-audit.json' }], ['kirocli', { name: 'Kiro CLI', configDir: '~/.kiro/hooks', configFile: 'elydora-audit.json' }],
['kiroide', { name: 'Kiro IDE', configDir: '.kiro/hooks', configFile: 'elydora-audit.json' }], ['kiroide', { name: 'Kiro IDE', configDir: '.kiro/hooks', configFile: 'elydora-audit.json' }],
['opencode', { name: 'OpenCode', configDir: '.config/opencode/plugins', configFile: 'elydora-audit.mjs' }], ['opencode', { name: 'OpenCode', configDir: '.config/opencode/plugins', configFile: 'elydora-audit.js' }],
['copilot', { name: 'GitHub Copilot CLI', configDir: '~/.copilot/hooks', configFile: 'elydora-audit.json' }], ['copilot', { name: 'GitHub Copilot CLI', configDir: '~/.copilot/hooks', configFile: 'elydora-audit.json' }],
['letta', { name: 'Letta Code', configDir: '~/.letta', configFile: 'settings.json' }], ['letta', { name: 'Letta Code', configDir: '~/.letta', configFile: 'settings.json' }],
['codex', { name: 'OpenAI Codex', configDir: '~/.codex', configFile: 'hooks.json' }], ['codex', { name: 'OpenAI Codex', configDir: '~/.codex', configFile: 'hooks.json' }],
+180
View File
@@ -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());
}),
};
}
+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();
}
},
);
+421 -136
View File
@@ -1,169 +1,454 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { spawn } from 'node:child_process'; import {
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; lstat,
import os from 'node:os'; mkdir,
readFile,
readdir,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';
import test from 'node:test'; 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) { async function assertMissing(filePath) {
return new Promise((resolve, reject) => { await assert.rejects(lstat(filePath), { code: 'ENOENT' });
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 createFixture({ guardSource, hookSource }) { async function writeSource(filePath, source) {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-opencode-')); await mkdir(path.dirname(filePath), { recursive: true });
const agentDir = path.join(homeDir, '.elydora', 'agent-1'); await writeFile(filePath, source, { mode: 0o600 });
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; async function legacySource(fixture) {
const installScript = ` const { buildLegacyOpenCodePlugin } = await import(contractModuleUrl);
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)}; return buildLegacyOpenCodePlugin(fixture.hookScriptPath, fixture.guardScriptPath);
await opencodePlugin.install(JSON.parse(process.env.ELYDORA_TEST_CONFIG)); }
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( return runNode(
['--input-type=module', '--eval', installScript], ['--input-type=module', '--eval', source],
{ {
HOME: homeDir, ...environment(fixture),
USERPROFILE: homeDir, ELYDORA_TEST_INPUT: JSON.stringify(input),
ELYDORA_TEST_CONFIG: JSON.stringify({ guardScriptPath, hookScriptPath }), 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) { test('OpenCode installs the discoverable global JavaScript plugin and exact runtime', async () => {
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode.js')).href; const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
const statusScript = ` assert.deepEqual(SUPPORTED_AGENTS.get('opencode'), {
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)}; name: 'OpenCode',
console.log(JSON.stringify(await opencodePlugin.status())); configDir: '.config/opencode/plugins',
`; configFile: 'elydora-audit.js',
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",
}); });
const fixture = await createFixture();
try { try {
await assert.rejects( const first = await fixture.install();
fixture.hooks['tool.execute.before']( assert.equal(first.code, 0, first.stderr);
{ tool: 'bash', sessionID: 'session-1', callID: 'call-1' }, assert.match(first.stdout, /elydora-audit\.js/);
{ args: { command: 'echo test' } }, assert.match(first.stdout, /restart active OpenCode sessions/);
), await assertMissing(fixture.legacyPluginPath);
/Agent is frozen by Elydora/, const firstSource = await readFile(fixture.pluginPath, 'utf-8');
); assert.match(firstSource, /@elydora-opencode-plugin/);
} finally { assert.match(firstSource, /tool\.execute\.before/);
await fixture.close(); 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']);
test('OpenCode blocks when the guard process cannot start', async () => { assert.deepEqual(
const fixture = await createFixture({}); JSON.parse(await readFile(path.join(fixture.agentDir, 'config.json'), 'utf-8')),
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'](
{ {
tool: 'bash', org_id: 'org-1',
sessionID: 'session-1', agent_id: fixture.agentId,
callID: 'call-1', kid: 'kid-1',
args: { command: 'echo test' }, base_url: fixture.baseUrl,
token: 'token-1',
agent_name: 'opencode',
}, },
{ title: 'Shell', output: 'test' },
); );
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
let payload; const status = await runPlugin(fixture, 'status', null);
const deadline = Date.now() + 3000; assert.equal(status.code, 0, status.stderr);
while (Date.now() < deadline) { assert.deepEqual(JSON.parse(status.stdout), {
try { installed: true,
payload = await readFile(capturePath, 'utf-8'); agentName: 'opencode',
break; displayName: 'OpenCode',
} catch (error) { hookConfigured: true,
if (error.code !== 'ENOENT') throw error; hookScriptExists: true,
await delay(20); 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'); } finally {
assert.deepEqual(JSON.parse(payload), { 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_name: 'bash',
tool_input: { command: 'echo test' }, tool_input: { command: 'echo test' },
session_id: 'session-1', 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 { } finally {
await api.close();
await fixture.close(); await fixture.close();
await rm(capturePath, { force: true });
} }
}); });
test('OpenCode status requires both generated runtime scripts', async () => { test('OpenCode bridge forwards both native arguments and surfaces runtime failures', async () => {
const fixture = await createFixture({ guardSource: 'process.exit(0);\n' }); const fixture = await createFixture();
const guardCapture = path.join(fixture.projectDir, 'guard-input.json');
const auditCapture = path.join(fixture.projectDir, 'audit-input.json');
try { 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); 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 { } finally {
await fixture.close(); await fixture.close();
} }
+110
View File
@@ -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();
}
});