Skip to content
This repository was archived by the owner on Dec 29, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 293 additions & 8 deletions packages/cli/src/commands/core.ts

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions packages/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ const MCP_CLIENTS = {
linux: join(homedir(), ".gemini/settings.json"),
},
},
"openai-codex": {
name: "OpenAI Codex",
configPaths: {
darwin: join(homedir(), ".codex/config.toml"),
win32: join(homedir(), ".codex/config.toml"),
linux: join(homedir(), ".codex/config.toml"),
}
}
};

// MCP server configurations
Expand Down Expand Up @@ -430,6 +438,51 @@ export async function installMcpServer(client: {
return;
}

if (client.id === "openai-codex") {
const configPath = client.configPath;
const configDir = join(configPath, "..");
await mkdir(configDir, { recursive: true });

let content = "";
if (existsSync(configPath)) {
content = await readFile(configPath, "utf-8");
}

// Helper to generate the TOML block
const generateTomlBlock = (name: string, pkg: string) => {
return [
`\n\n[mcp_servers.${name}]`,
`command = "npx"`,
`args = ["-y", "${pkg}"]`
].join('\n');
};

const updates: { name: string; pkg: string }[] = [];

if (serverType === "main" || serverType === "both") {
updates.push({ name: "enact", pkg: "@enactprotocol/mcp-server" });
}
if (serverType === "dev" || serverType === "both") {
updates.push({ name: "enact-dev", pkg: "@enactprotocol/mcp-dev-server" });
}

for (const update of updates) {
// 1. Remove existing block if it exists (Regex to match [mcp_servers.name] until next [section] or EOF)
// This ensures we update args if they changed, and don't duplicate keys
const regex = new RegExp(`\\[mcp_servers\\.${update.name}\\][\\s\\S]*?(?=(\\n\\[|$))`, "g");
content = content.replace(regex, "").trim();

// 2. Append new block
content += generateTomlBlock(update.name, update.pkg);
}

// Ensure we have a trailing newline
if (!content.endsWith("\n")) content += "\n";

await writeFile(configPath, content.trimStart(), "utf-8");
return;
}

// Original logic for file-based clients
const configPath = client.configPath;

Expand Down Expand Up @@ -521,6 +574,26 @@ export async function checkMcpServerInstalled(client: {
}
}

if (client.id === "openai-codex") {
if (!existsSync(client.configPath)) return false;
try {
const content = await readFile(client.configPath, "utf-8");

// We check for the specific section header based on server type
if (serverType === "main") {
return content.includes("[mcp_servers.enact]");
} else if (serverType === "dev") {
return content.includes("[mcp_servers.enact-dev]");
} else if (serverType === "both") {
return content.includes("[mcp_servers.enact]") &&
content.includes("[mcp_servers.enact-dev]");
}
return false;
} catch (error) {
return false;
}
}

if (!existsSync(client.configPath)) {
return false;
}
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { handleConfigCommand } from "./commands/config";
import {
handleCoreSearchCommand,
handleCoreExecCommand,
handleSignToolCommand,
handleCoreGetCommand,
handleCorePublishCommand,
} from "./commands/core";
Expand Down Expand Up @@ -128,6 +129,9 @@ const { values, positionals } = parseArgs({
mount: {
type: "string",
},
tool: {
type: "string",
},
},
allowPositionals: true,
strict: false,
Expand Down Expand Up @@ -218,6 +222,12 @@ async function main() {
});
break;

case "sign": // Sign command - core library only
await handleSignToolCommand(commandArgs, {
help: values.help as boolean | undefined,
tool: values.tool as string | undefined,
});
break;

case "get": // New case for get command (core library only)
await handleCoreGetCommand(commandArgs, {
Expand Down Expand Up @@ -266,6 +276,7 @@ async function main() {
{ value: "publish", label: "📤 Publish a tool" },
{ value: "init", label: "📝 Create a new tool definition" },
{ value: "env", label: "🌍 Manage environment variables" },
{ value: "sign", label: "✍️ Sign a tool definition" },
{ value: "config", label: "🔧 Configure Enact settings" },
{ value: "auth", label: "🔐 Manage authentication" },
{ value: "remote", label: "🌐 Manage remote servers" },
Expand Down Expand Up @@ -323,6 +334,11 @@ async function main() {
}
return;
}

if (action === "sign") {
// Sign a tool definition
await handleSignToolCommand([], {});
}

if (action === "auth") {
// Show auth submenu
Expand Down
7 changes: 7 additions & 0 deletions packages/security/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,11 @@ export class CryptoUtils {
static isPemFormat(key: string): boolean {
return key.includes('-----BEGIN') && key.includes('-----END');
}
}

export namespace CryptoUtils {
export type PrivateKey = {
fileName: string; // e.g., "id_key.pem"
key: string; // hex or PEM format
};
}
5 changes: 2 additions & 3 deletions packages/security/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export { CryptoUtils } from './crypto';
export { KeyManager } from './keyManager';
export { SecurityConfigManager } from './securityConfigManager';
export { FieldSelector, EnactFieldSelector, GenericFieldSelector } from './fieldConfig';
export type { EnactDocument, SigningOptions, Signature, KeyPair, SecurityConfig } from './types';
export type { EnactDocument, SigningOptions, Signature, KeyMetadata, KeyPair, SecurityConfig } from './types';
export { DEFAULT_SECURITY_CONFIG } from './types';
export type { FieldConfig, SigningFieldOptions } from './fieldConfig';
export type { KeyMetadata } from './keyManager';
export type { FieldConfig, SigningFieldOptions } from './fieldConfig';
145 changes: 99 additions & 46 deletions packages/security/src/keyManager.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { CryptoUtils } from './crypto';
import type { KeyPair } from './types';
import type { KeyPair, KeyMetadata } from './types';
import fs from 'fs';
import path from 'path';
import os from 'os';

export interface KeyMetadata {
keyId: string;
created: string;
algorithm: string;
description?: string;
}

export class KeyManager {
// Storage paths
private static readonly TRUSTED_KEYS_DIR = path.join(os.homedir(), '.enact', 'trusted-keys');
Expand All @@ -31,8 +24,8 @@ export class KeyManager {
return path.join(this.PRIVATE_KEYS_DIR, `${keyId}-private.pem`);
}

private static getMetadataPath(keyId: string): string {
return path.join(this.TRUSTED_KEYS_DIR, `${keyId}.meta`);
private static getMetadataPath(): string {
return path.join(this.PRIVATE_KEYS_DIR, `metadata.json`);
}

static generateAndStoreKey(keyId: string, description?: string): KeyPair {
Expand Down Expand Up @@ -69,18 +62,18 @@ export class KeyManager {
);

// Store metadata
const metadata: KeyMetadata = {
keyId,
created: new Date().toISOString(),
algorithm: 'secp256k1',
description
};

fs.writeFileSync(
this.getMetadataPath(keyId),
JSON.stringify(metadata, null, 2),
{ mode: 0o644 }
);
// const metadata: KeyMetadata = {
// keyId,
// created: new Date().toISOString(),
// algorithm: 'secp256k1',
// description
// };

// fs.writeFileSync(
// this.getMetadataPath(keyId),
// JSON.stringify(metadata, null, 2),
// { mode: 0o644 }
// );

} catch (error) {
// Clean up on error
Expand Down Expand Up @@ -129,18 +122,34 @@ export class KeyManager {
}
}

static getKeyMetadata(keyId: string): KeyMetadata | undefined {
static getKeyMetadata(): KeyMetadata[] | undefined {
try {
const metadataPath = this.getMetadataPath(keyId);
const metadataPath = this.getMetadataPath();

if (!fs.existsSync(metadataPath)) {
return undefined;
}

const metadataJson = fs.readFileSync(metadataPath, 'utf8');
return JSON.parse(metadataJson);
const rawData = fs.readFileSync(metadataPath, 'utf8');
const metadata = JSON.parse(rawData);

const keys: KeyMetadata[] = [];

metadata.forEach((key: any) => {
const publicKeyBytes = key['publicKey'];
const publickeyBase64 = Buffer.from(publicKeyBytes).toString('utf-8');
keys.push({
keyId: key['id'],
publicKey: publickeyBase64,
created: key['createdAt'],
algorithm: 'secp256k1',
});
});

return keys;

} catch (error) {
console.warn(`Failed to read metadata for key '${keyId}': ${error instanceof Error ? error.message : String(error)}`);
console.warn(`Failed to read metadata: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
}
Expand Down Expand Up @@ -170,11 +179,11 @@ export class KeyManager {
}

// Remove metadata
const metadataPath = this.getMetadataPath(keyId);
if (fs.existsSync(metadataPath)) {
fs.unlinkSync(metadataPath);
removed = true;
}
// const metadataPath = this.getMetadataPath(keyId);
// if (fs.existsSync(metadataPath)) {
// fs.unlinkSync(metadataPath);
// removed = true;
// }

return removed;
} catch (error) {
Expand Down Expand Up @@ -237,6 +246,50 @@ export class KeyManager {
}
}

static listPrivateKeys(): string[] {
try {
this.ensureDirectories();

// Return all private keys (including those without public keys)
return fs.readdirSync(this.PRIVATE_KEYS_DIR)
.filter(file => file.endsWith('-private.pem'))
.map(file => file.replace('-private.pem', ''));
} catch (error) {
console.warn(`Failed to list private keys: ${error instanceof Error ? error.message : String(error)}`);
return [];
}
}

static getAllPrivateKeys(): CryptoUtils.PrivateKey[] {
try {
this.ensureDirectories();

// Return all private key values from private keys directory
return fs.readdirSync(this.PRIVATE_KEYS_DIR)
.filter(file => file.endsWith('.pem'))
.map(file => {
try {
const privatekey: CryptoUtils.PrivateKey = {
fileName: file,
key: "",
};
//
const privateKeyPem = fs.readFileSync(path.join(this.PRIVATE_KEYS_DIR, file), 'utf8').trim();
// Convert PEM back to hex for internal use
privatekey.key = CryptoUtils.pemToHex(privateKeyPem, 'PRIVATE');
return privatekey;
} catch (error) {
console.warn(`Failed to read private key file ${file}: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
})
.filter(key => key !== null) as CryptoUtils.PrivateKey[];
} catch (error) {
console.warn(`Failed to get all private keys: ${error instanceof Error ? error.message : String(error)}`);
return [];
}
}

static exportKey(keyId: string): KeyPair | undefined {
return this.getKey(keyId);
}
Expand Down Expand Up @@ -265,24 +318,24 @@ export class KeyManager {
);

// Store metadata
const metadata: KeyMetadata = {
keyId,
created: new Date().toISOString(),
algorithm: 'secp256k1',
description: description || 'Imported public key'
};

fs.writeFileSync(
this.getMetadataPath(keyId),
JSON.stringify(metadata, null, 2),
{ mode: 0o644 }
);
// const metadata: KeyMetadata = {
// keyId,
// created: new Date().toISOString(),
// algorithm: 'secp256k1',
// description: description || 'Imported public key'
// };

// fs.writeFileSync(
// this.getMetadataPath(keyId),
// JSON.stringify(metadata, null, 2),
// { mode: 0o644 }
// );

} catch (error) {
// Clean up on error
try {
fs.unlinkSync(this.getPublicKeyPath(keyId));
fs.unlinkSync(this.getMetadataPath(keyId));
// fs.unlinkSync(this.getMetadataPath(keyId));
} catch {}
throw new Error(`Failed to import public key '${keyId}': ${error instanceof Error ? error.message : String(error)}`);
}
Expand All @@ -299,7 +352,7 @@ export class KeyManager {
// Backup/export functionality
static exportKeyToFile(keyId: string, outputPath: string, includePrivateKey: boolean = false): void {
const keyPair = this.getKey(keyId);
const metadata = this.getKeyMetadata(keyId);
const metadata = this.getKeyMetadata();

if (!keyPair) {
throw new Error(`Key '${keyId}' not found`);
Expand Down
Loading