From e8e2c4a1777def964e7bf2521f72914abd22e5b8 Mon Sep 17 00:00:00 2001 From: Krylix Date: Fri, 5 Sep 2025 18:53:16 -0700 Subject: [PATCH 1/3] Improved UI, Added signing functionality (without db syncing), fixed a flaw with signature checking Improved the UI, made it look better by adding a verification column and cleaned up the formatting Fixed an error with trusted keys being checked only against the newest signature and made it so that only one key has to match Added signing to the cli (only works with server side tools currently) pushing to the db is not yet implemented --- packages/cli/src/commands/core.ts | 270 +++++++++++++++++++++++++- packages/cli/src/index.ts | 16 ++ packages/security/src/crypto.ts | 7 + packages/security/src/keyManager.ts | 44 +++++ packages/security/src/signing.ts | 67 ++++++- packages/shared/src/api/enact-api.ts | 15 ++ packages/shared/src/api/types.ts | 13 ++ packages/shared/src/core/EnactCore.ts | 94 ++++++--- 8 files changed, 487 insertions(+), 39 deletions(-) diff --git a/packages/cli/src/commands/core.ts b/packages/cli/src/commands/core.ts index 4d89313..225c684 100644 --- a/packages/cli/src/commands/core.ts +++ b/packages/cli/src/commands/core.ts @@ -19,6 +19,7 @@ import { getAuthHeaders } from "./auth"; import { addToHistory, getFrontendUrl, getApiUrl } from "@enactprotocol/shared/utils"; import { getCurrentConfig } from "./config"; import stripAnsi from "strip-ansi"; +import { CryptoUtils, KeyManager, SecurityConfigManager, SigningService } from "@enactprotocol/security"; // Create core instance with configuration let core: EnactCore; @@ -61,6 +62,11 @@ interface CoreSearchOptions { author?: string; } +interface CoreSignOptions { + help?: boolean; + tool?: string; +} + interface CoreExecOptions { help?: boolean; input?: string; @@ -236,10 +242,13 @@ ${pc.bold("EXAMPLES:")} try { const tool = await apiClient.getTool(result.name); if (tool) { + const isValid = await EnactCore.checkToolVerificationStatus(tool); + // console.log("šŸ” TRACE: core.ts - Tool:", tool.name, "isValid:", isValid); // Convert to EnactTool format const enactTool: EnactTool = { name: tool.name, description: tool.description || "", + verified: isValid, command: tool.command, from: tool.from, version: tool.version || "1.0.0", @@ -508,6 +517,215 @@ function parseTimeout(timeout: string): number { } } +/** + * Sign a tool definition using local private key + */ +export async function handleSignToolCommand( + args: string[], + options: CoreSignOptions, +) { + if (options.help) { + console.error(` +Usage: enact sign [options] +Sign a tool definition using your private key +Options: + --help, -h Show this help message + --tool query Search for the tool to sign (name) +Examples: + enact sign --tool myorg/mytool + `); + return; + } + + // Show a spinner during search + + if (!options.tool){ + const inputSchema = { + type: "object", + properties: { + toolName: { + type: "string", + description: "The name of the tool to run" + } + }, + required: ["tool"] + }; + + const params = await collectParametersInteractively(inputSchema); + // console.log("Collected tool name:", params.toolName); + options.tool = params.toolName + + } + const spinner = p.spinner(); + spinner.start("Searching for tools..."); + const results: EnactTool[] = []; + try { + const searchOptions: ToolSearchOptions = { + query: options.tool ? options.tool : "", + limit: 20, + tags: undefined, + author: undefined, + format: "table" as any, + }; + + // Use API client directly for search - no need for EnactCore + const apiClient = await EnactApiClient.create(); + const searchResults = await apiClient.searchTools(searchOptions); + + // Convert API results to EnactTool format + for (const result of searchResults) { + if (result.name) { + try { + const tool = await apiClient.getTool(result.name); + if (tool) { + const documentForVerification = { + command: tool.command, + description: tool.description, + from: tool.from, + name: tool.name, + }; + const isValid = await EnactCore.checkToolVerificationStatus(tool); + // console.log("šŸ” TRACE: core.ts - Tool:", tool.name, "isValid:", isValid); + // Convert to EnactTool format + const enactTool: EnactTool = { + name: tool.name, + description: tool.description || "", + verified: isValid, + command: tool.command, + from: tool.from, + version: tool.version || "1.0.0", + timeout: tool.timeout, + tags: tool.tags || [], + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + env: tool.env_vars + ? Object.fromEntries( + Object.entries(tool.env_vars).map(([key, config]: [string, any]) => [ + key, + { ...config, source: config.source || "env" }, + ]), + ) + : undefined, + signature: tool.signature, + signatures: Array.isArray(tool.signatures) ? tool.signatures : + (tool.signatures ? Object.values(tool.signatures) : undefined), + namespace: tool.namespace, + resources: tool.resources, + license: tool.license, + authors: tool.authors, + examples: tool.examples, + annotations: tool.annotations, + }; + results.push(enactTool); + } + } catch (error) { + // Skip tools that can't be fetched + continue; + } + } + } + + spinner.stop( + `Found ${results.length} tool${results.length === 1 ? "" : "s"}`, + ); + + if (results.length === 0) { + p.note("No tools found matching your criteria.", "No Results"); + p.note( + "Try:\n• Broader keywords\n• Removing filters\n• Different spelling", + "Suggestions", + ); + + return; + } + }catch (error: any) { + spinner.stop("Search failed"); + + if ( + error.message?.includes("ENOTFOUND") || + error.message?.includes("ECONNREFUSED") + ) { + p.note( + "Could not connect to the Enact registry. Check your internet connection.", + "Connection Error", + ); + } else { + p.note(error instanceof Error ? error.message : String(error), "Error"); + } + + + console.error( + pc.red( + `Search failed: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + process.exit(1); + } + + const selected_tool = await p.select({ + message: "Select a tool to sign:", + options: results.map((tool) => ({ + value: tool, + label: `${tool.name} - ${tool.description.substring(0, 60)}${tool.description.length > 60 ? "..." : ""} - Verified: ${tool.verified ? pc.green("Yes") : pc.red("No")}`, + })) + }) as EnactTool; + + const documentForVerification = { + command: selected_tool.command, + description: selected_tool.description, + from: selected_tool.from, + name: selected_tool.name, + } + + const privateKeys = await KeyManager.getAllPrivateKeys(); + + const privateKey = await p.select({ + message: "Select a private key to sign with:", + options: privateKeys.map((key) => ({ + value: key, + label: `${key.fileName}`, + })) + }) as CryptoUtils.PrivateKey; + + const TrustedKey = CryptoUtils.getPublicKeyFromPrivate(privateKey.key); + + + if (selected_tool.signatures && selected_tool.signatures.length > 0) { + const alreadySigned = selected_tool.signatures.some(sig => { + const referenceSignature = { + signature: sig.value, + publicKey: "", // Correct public key for UUID 71e02e2c-148c-4534-9900-bd9646e99333 + algorithm: sig.algorithm, + timestamp: new Date(sig.created).getTime() + }; + + return SigningService.verifyDocumentWithPublicKey( + documentForVerification, + referenceSignature, + TrustedKey, + { includeFields: ['command', 'description', 'from', 'name'] } + ); + }); + + if (alreadySigned) { + console.log(pc.green("āœ“ Tool has already been signed with the selected key.")); + return; + } + } + const spinnerSign = p.spinner(); + console.error(spinnerSign.start("Signing tool...")); + const signature = await SigningService.signDocument(documentForVerification, privateKey.key, { includeFields: ['command', 'description', 'from', 'name']}); + spinnerSign.stop(pc.green("āœ“ Tool signed successfully")); + console.error(pc.cyan("\nSignature Details:")); + console.error(pc.cyan(`\tSignature: ${signature.signature}`)); + console.error(pc.cyan(`\tAlgorithm: ${signature.algorithm}`)); + console.error(pc.cyan(`\tCreated: ${new Date(signature.timestamp).toISOString()}`)); + console.error(pc.cyan(`\tPublic Key: ${TrustedKey}`)); + console.error(pc.red("\nPushing the signature to the registry is not yet implemented.")); + return signature + +}; + /** * Enhanced handle execute command using core library with full legacy feature parity */ @@ -874,6 +1092,7 @@ Examples: const enactTool: EnactTool = { name: toolDefinition.name, description: toolDefinition.description || "", + verified: true, // Verification handled separately command: toolDefinition.command, from: toolDefinition.from, version: toolDefinition.version || "1.0.0", @@ -1095,10 +1314,13 @@ ${pc.bold("EXAMPLES:")} process.exit(1); } + const isValid = EnactCore.checkToolVerificationStatus(toolDefinition); + // Convert to EnactTool format const tool: EnactTool = { name: toolDefinition.name, description: toolDefinition.description || "", + verified: isValid, command: toolDefinition.command, from: toolDefinition.from, version: toolDefinition.version || "1.0.0", @@ -1145,6 +1367,12 @@ ${pc.bold("EXAMPLES:")} if (tool.command) { console.error(`${pc.bold("Command:")} ${pc.gray(tool.command)}`); } + + if (tool.verified) { + console.error(`${pc.bold("Verification:")} ${pc.green("Verified āœ“")}`); + } else { + console.error(`${pc.bold("Verification:")} ${pc.red("Unverified āœ—")}`); + } if (tool.from) { console.error(`${pc.bold("Container:")} ${pc.gray(tool.from)}`); @@ -1221,22 +1449,34 @@ ${pc.bold("EXAMPLES:")} */ function displayResultsTable(results: EnactTool[]): void { console.error("\n" + pc.bold("Search Results:")); - console.error("═".repeat(100)); - // Header const nameWidth = 40; + const statusWidth = 15; // moved up const descWidth = 45; const tagsWidth = 20; + // Dynamically calculate the total table width + const totalWidth = nameWidth + statusWidth + descWidth + tagsWidth + 9; + // 9 = 3 separators ( " │ " ) Ɨ 3 + + console.error("═".repeat(totalWidth)); + + // Header row console.error( pc.bold(pc.cyan("NAME".padEnd(nameWidth))) + + " │ " + + pc.bold(pc.cyan("STATUS".padEnd(statusWidth))) + " │ " + pc.bold(pc.cyan("DESCRIPTION".padEnd(descWidth))) + " │ " + pc.bold(pc.cyan("TAGS".padEnd(tagsWidth))), ); + + // Separator row console.error( "─".repeat(nameWidth) + + "─┼─" + + "─".repeat(statusWidth) + "─┼─" + "─".repeat(descWidth) + "─┼─" + @@ -1245,31 +1485,47 @@ function displayResultsTable(results: EnactTool[]): void { // Rows results.forEach((tool) => { - const name = + // Format name + const nameText = tool.name.length > nameWidth ? tool.name.substring(0, nameWidth - 3) + "..." - : tool.name.padEnd(nameWidth); + : tool.name.padEnd(nameWidth); + + // Format status + const statusText = tool.verified ? "Verified" : "Unverified"; + const statusColor = tool.verified ? pc.green : pc.red; + const status = statusColor(statusText.padEnd(statusWidth)); + + // const name = statusColor(nameText) + + // Format description const desc = tool.description.length > descWidth ? tool.description.substring(0, descWidth - 3) + "..." : tool.description.padEnd(descWidth); + + // Format tags const tags = (tool.tags || []).join(", "); const tagsDisplay = tags.length > tagsWidth ? tags.substring(0, tagsWidth - 3) + "..." : tags.padEnd(tagsWidth); + // Print row console.error( - pc.green(name) + " │ " + pc.dim(desc) + " │ " + pc.yellow(tagsDisplay), + pc.green(nameText) + " │ " + status + " │ " + pc.dim(desc) + " │ " + pc.yellow(tagsDisplay), ); }); - console.error("═".repeat(100)); + console.error("═".repeat(totalWidth)); console.error( pc.dim(`Total: ${results.length} tool${results.length === 1 ? "" : "s"}`), ); } + + + /** * Display results in a simple list format */ @@ -1279,7 +1535,7 @@ function displayResultsList(results: EnactTool[]): void { results.forEach((tool, index) => { console.error( - `${pc.cyan(`${index + 1}.`)} ${pc.bold(pc.green(tool.name))}`, + `${pc.cyan(`${index + 1}.`)} ${pc.bold((tool.verified ? pc.green(tool.name) : pc.red(tool.name)) + (tool.verified ? pc.green(" (verified)") : pc.red(" (unverified)")))}`, ); console.error(` ${pc.dim(tool.description)}`); if (tool.tags && tool.tags.length > 0) { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 027840e..e147c6c 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -15,6 +15,7 @@ import { handleConfigCommand } from "./commands/config"; import { handleCoreSearchCommand, handleCoreExecCommand, + handleSignToolCommand, handleCoreGetCommand, handleCorePublishCommand, } from "./commands/core"; @@ -128,6 +129,9 @@ const { values, positionals } = parseArgs({ mount: { type: "string", }, + tool: { + type: "string", + }, }, allowPositionals: true, strict: false, @@ -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, { @@ -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" }, @@ -323,6 +334,11 @@ async function main() { } return; } + + if (action === "sign") { + // Sign a tool definition + await handleSignToolCommand([], {}); + } if (action === "auth") { // Show auth submenu diff --git a/packages/security/src/crypto.ts b/packages/security/src/crypto.ts index 4e9f588..e23adc8 100644 --- a/packages/security/src/crypto.ts +++ b/packages/security/src/crypto.ts @@ -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 + }; } \ No newline at end of file diff --git a/packages/security/src/keyManager.ts b/packages/security/src/keyManager.ts index 780f8c8..6f21f20 100644 --- a/packages/security/src/keyManager.ts +++ b/packages/security/src/keyManager.ts @@ -237,6 +237,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); } diff --git a/packages/security/src/signing.ts b/packages/security/src/signing.ts index b3c006a..e6c49c4 100644 --- a/packages/security/src/signing.ts +++ b/packages/security/src/signing.ts @@ -87,7 +87,7 @@ export class SigningService { const trustedPublicKeys = KeyManager.getAllTrustedPublicKeys(); // All signatures must be valid and from trusted keys - return signatures.every(sig => { + return signatures.some(sig => { // Check if we have a valid public key in the signature const hasValidPublicKey = sig.publicKey && typeof sig.publicKey === 'string' && @@ -106,8 +106,9 @@ export class SigningService { // - signature.publicKey is null/undefined/empty // - signature.publicKey is invalid/corrupted // - we want to verify against any trusted key - return trustedPublicKeys.some(trustedKey => { + const validity = trustedPublicKeys.some(trustedKey => { try { + // console.log("The trusted key tried against the signature:", CryptoUtils.verify(trustedKey, messageHash, sig.signature)); return CryptoUtils.verify( trustedKey, messageHash, @@ -118,7 +119,69 @@ export class SigningService { return false; } }); + // console.log("Validity:", validity); + return validity; + } + }); + + } + + static verifyDocumentWithPublicKey( + document: EnactDocument, + signature: Signature, + publicKey: string, + options: SigningOptions = {}, + securityConfig?: SecurityConfig + ): boolean { + const { + useEnactDefaults = false, + includeFields, + excludeFields, + additionalCriticalFields + } = options; + + // Load security config from ~/.enact/security if not provided + const loadedConfig = securityConfig ?? SecurityConfigManager.loadConfig(); + const config = { ...DEFAULT_SECURITY_CONFIG, ...loadedConfig }; + + // Get signatures from document or use provided signature + const signatures = document.signatures || [signature]; + + // Check minimum signatures requirement + if (signatures.length < (config.minimumSignatures ?? 1)) { + // If allowLocalUnsigned is true and we have no signatures, allow it + if (config.allowLocalUnsigned && signatures.length === 0) { + return true; } + return false; + } + + // Verify each signature + const fieldSelector = useEnactDefaults ? EnactFieldSelector : GenericFieldSelector; + + const canonicalDocument = fieldSelector.createCanonicalObject(document, { + includeFields, + excludeFields, + additionalCriticalFields + }); + + const documentString = JSON.stringify(canonicalDocument); + const messageHash = CryptoUtils.hash(documentString); + + + return signatures.every(sig => { + + // Try verifying against trusted public key + try { + return CryptoUtils.verify( + publicKey, + messageHash, + sig.signature + ); + } catch { + return false; + } + }); } diff --git a/packages/shared/src/api/enact-api.ts b/packages/shared/src/api/enact-api.ts index 749f69c..bf39a7a 100644 --- a/packages/shared/src/api/enact-api.ts +++ b/packages/shared/src/api/enact-api.ts @@ -1,5 +1,6 @@ import { EnactToolDefinition, + ToolSignaturePayload, ToolUsage, ToolSearchQuery, CLITokenCreate, @@ -480,6 +481,20 @@ export class EnactApiClient { throw new EnactApiError("Unknown error occurred", 0); } } + + async signTool( + toolId: string, + payload: ToolSignaturePayload, + token: string, + tokenType: "jwt" | "cli" = "cli" +): Promise { + const endpoint = `/functions/v1/tools/${encodeURIComponent(toolId)}/signatures`; + + return this.makeRequest(endpoint, { + method: "POST", + body: JSON.stringify(payload), + }, token, tokenType); +} // =================== // OAUTH FLOW HELPERS diff --git a/packages/shared/src/api/types.ts b/packages/shared/src/api/types.ts index 62c6a72..c6af239 100644 --- a/packages/shared/src/api/types.ts +++ b/packages/shared/src/api/types.ts @@ -1,6 +1,7 @@ export interface EnactToolDefinition { name: string; description: string; + verified?: boolean; // Indicates if the tool has been verified command: string; from?: string; version?: string; @@ -99,3 +100,15 @@ export interface EnactExecOptions { dangerouslySkipVerification?: boolean; // Skip all signature verification (DANGEROUS) mount?: string; // Mount local directory to container (format: "local:container") } + +export interface ToolSignaturePayload { + algorithm: "sha256"; + created: string; // Time of signing + key_id: string; // ID of the private key + public_key: string; // The corresponding public key + role: "author"; + signer: string; // The userID of the signer + timestamp: number; + type: "ecdsa-p256"; + value: string; // Signature +} \ No newline at end of file diff --git a/packages/shared/src/core/EnactCore.ts b/packages/shared/src/core/EnactCore.ts index 8de7769..d70e520 100644 --- a/packages/shared/src/core/EnactCore.ts +++ b/packages/shared/src/core/EnactCore.ts @@ -19,6 +19,7 @@ import fs from "fs"; import path from "path"; import { CryptoUtils, KeyManager, SecurityConfigManager, SigningService } from "@enactprotocol/security"; import { getFrontendUrl, getApiUrl } from "../utils/config"; +import { EnactToolDefinition } from "../api/types.js"; export interface EnactCoreOptions { apiUrl?: string; @@ -408,6 +409,43 @@ export class EnactCore { } } +public static async checkToolVerificationStatus(tool: EnactToolDefinition): Promise { + const documentForVerification = { + command: tool.command, + description: tool.description, + from: tool.from, + name: tool.name, + signatures: tool.signatures?.map(sig => ({ + signature: sig.value, + publicKey: "", // TODO: Look up the correct public key + algorithm: sig.algorithm, + timestamp: new Date(sig.created).getTime(), + })), + }; + + let isValid = false; + + if (tool.signatures && tool.signatures.length > 0) { + isValid = tool.signatures.some(sig => { + const referenceSignature = { + signature: sig.value, + publicKey: "", // TODO: Lookup correct public key based on signature UUID + algorithm: sig.algorithm, + timestamp: new Date(sig.created).getTime() + }; + + return SigningService.verifyDocument( + documentForVerification, + referenceSignature, + { includeFields: ['command', 'description', 'from', 'name'] } + ); + }); + } + + return isValid; +} + + private async verifyTool(tool: EnactTool, dangerouslySkipVerification: boolean = false): Promise { if (dangerouslySkipVerification) { logger.warn(`Skipping signature verification for tool: ${tool.name}`); @@ -419,46 +457,42 @@ private async verifyTool(tool: EnactTool, dangerouslySkipVerification: boolean = throw new Error(`Tool ${tool.name} does not have any signatures`); } - const documentForVerification = { - command: tool.command, - description: tool.description, - from: tool.from, - name: tool.name, - }; - - const referenceSignature = { - signature: tool.signatures[0].value, - publicKey: "", // Correct public key for UUID 71e02e2c-148c-4534-9900-bd9646e99333 - algorithm: tool.signatures[0].algorithm, - timestamp: new Date(tool.signatures[0].created).getTime() - }; + // const documentForVerification = { + // command: tool.command, + // description: tool.description, + // from: tool.from, + // name: tool.name, + // }; + + // const referenceSignature = { + // signature: tool.signatures[0].value, + // publicKey: "", // Correct public key for UUID 71e02e2c-148c-4534-9900-bd9646e99333 + // algorithm: tool.signatures[0].algorithm, + // timestamp: new Date(tool.signatures[0].created).getTime() + // }; - // Check what canonical document looks like - const canonicalDoc = SigningService.getCanonicalDocument(documentForVerification, { includeFields: ['command', 'description', 'from', 'name'] } -); +// // Check what canonical document looks like +// const canonicalDoc = SigningService.getCanonicalDocument(documentForVerification, { includeFields: ['command', 'description', 'from', 'name'] } +// ); - const docString = JSON.stringify(canonicalDoc); - const messageHash = CryptoUtils.hash(docString); +// const docString = JSON.stringify(canonicalDoc); +// const messageHash = CryptoUtils.hash(docString); - // Test direct crypto verification - const directVerify = CryptoUtils.verify( - referenceSignature.publicKey, - messageHash, - referenceSignature.signature - ); +// // Test direct crypto verification +// const directVerify = CryptoUtils.verify( +// referenceSignature.publicKey, +// messageHash, +// referenceSignature.signature +// ); // Check trusted keys // const trustedKeys = KeyManager.getAllTrustedPublicKeys(); - const isValid = SigningService.verifyDocument( - documentForVerification, - referenceSignature, - { includeFields: ['command', 'description', 'from', 'name'] } - ); + const isValid = await EnactCore.checkToolVerificationStatus(tool); - // console.log("Final verification result:", isValid); + console.log("Final verification result:", isValid); if (!isValid) { throw new Error(`Tool ${tool.name} has invalid signatures`); From 200655079a8659d3867d14e262b1292ed44d2b74 Mon Sep 17 00:00:00 2001 From: Krylix Date: Sat, 11 Oct 2025 14:13:26 -0700 Subject: [PATCH 2/3] Pushing Signatures to DB Tools can now be signed from cli --- packages/cli/src/commands/core.ts | 43 +++++++++-- packages/security/src/index.ts | 5 +- packages/security/src/keyManager.ts | 101 ++++++++++++++------------ packages/security/src/types.ts | 8 ++ packages/shared/src/api/enact-api.ts | 8 +- packages/shared/src/api/types.ts | 7 +- packages/shared/src/core/EnactCore.ts | 1 + packages/shared/src/types.ts | 1 + 8 files changed, 111 insertions(+), 63 deletions(-) diff --git a/packages/cli/src/commands/core.ts b/packages/cli/src/commands/core.ts index 225c684..246ddf2 100644 --- a/packages/cli/src/commands/core.ts +++ b/packages/cli/src/commands/core.ts @@ -5,7 +5,7 @@ import { resolve, extname } from "path"; import { EnactCore } from "@enactprotocol/shared/core"; import type { ToolSearchOptions, ToolExecuteOptions } from "@enactprotocol/shared/core"; import type { EnactTool } from "@enactprotocol/shared"; -import type { EnactToolDefinition } from "@enactprotocol/shared/api"; +import type { EnactToolDefinition, ToolSignaturePayload } from "@enactprotocol/shared/api"; import pc from "picocolors"; import * as p from "@clack/prompts"; import yaml from "yaml"; @@ -19,7 +19,7 @@ import { getAuthHeaders } from "./auth"; import { addToHistory, getFrontendUrl, getApiUrl } from "@enactprotocol/shared/utils"; import { getCurrentConfig } from "./config"; import stripAnsi from "strip-ansi"; -import { CryptoUtils, KeyManager, SecurityConfigManager, SigningService } from "@enactprotocol/security"; +import { CryptoUtils, KeyManager, KeyMetadata, SecurityConfigManager, SigningService } from "@enactprotocol/security"; // Create core instance with configuration let core: EnactCore; @@ -246,6 +246,7 @@ ${pc.bold("EXAMPLES:")} // console.log("šŸ” TRACE: core.ts - Tool:", tool.name, "isValid:", isValid); // Convert to EnactTool format const enactTool: EnactTool = { + id: tool.id, name: tool.name, description: tool.description || "", verified: isValid, @@ -588,6 +589,7 @@ Examples: // console.log("šŸ” TRACE: core.ts - Tool:", tool.name, "isValid:", isValid); // Convert to EnactTool format const enactTool: EnactTool = { + id: tool.id, name: tool.name, description: tool.description || "", verified: isValid, @@ -687,7 +689,7 @@ Examples: })) }) as CryptoUtils.PrivateKey; - const TrustedKey = CryptoUtils.getPublicKeyFromPrivate(privateKey.key); + const correspondingPublicKey = CryptoUtils.getPublicKeyFromPrivate(privateKey.key); if (selected_tool.signatures && selected_tool.signatures.length > 0) { @@ -702,7 +704,7 @@ Examples: return SigningService.verifyDocumentWithPublicKey( documentForVerification, referenceSignature, - TrustedKey, + correspondingPublicKey, { includeFields: ['command', 'description', 'from', 'name'] } ); }); @@ -712,6 +714,10 @@ Examples: return; } } + + const buffer = Buffer.from(correspondingPublicKey, "hex"); + const publickeyBase64 = buffer.toString("base64") + const spinnerSign = p.spinner(); console.error(spinnerSign.start("Signing tool...")); const signature = await SigningService.signDocument(documentForVerification, privateKey.key, { includeFields: ['command', 'description', 'from', 'name']}); @@ -720,9 +726,30 @@ Examples: console.error(pc.cyan(`\tSignature: ${signature.signature}`)); console.error(pc.cyan(`\tAlgorithm: ${signature.algorithm}`)); console.error(pc.cyan(`\tCreated: ${new Date(signature.timestamp).toISOString()}`)); - console.error(pc.cyan(`\tPublic Key: ${TrustedKey}`)); - console.error(pc.red("\nPushing the signature to the registry is not yet implemented.")); - return signature + console.error(pc.cyan(`\tPublic Key: ${publickeyBase64}`)); + + let keyId = "None" + let toolId = selected_tool.id; + + const user_id = "anon" + + const signaturePayload: ToolSignaturePayload = { + algorithm: "sha256", + created: new Date(signature.timestamp).toISOString(), // Time of signing + keyId: keyId, // ID of the private key + public_key: publickeyBase64, // The corresponding public key in base64 + role: "author", + signer: user_id, // The userID of the signer + timestamp: Math.floor(Date.now() / 1000), // Unix epoch + type: "ecdsa-p256", + value: signature.signature, // Signature + }; + console.log(toolId) + const apiClient = await EnactApiClient.create(); + await apiClient.signTool(toolId, signaturePayload); + + console.error(pc.red("\nSignature was successfully added to the Database!")); + return signature; }; @@ -1090,6 +1117,7 @@ Examples: // Convert tool definition to EnactTool format for core const enactTool: EnactTool = { + id: toolDefinition.id, name: toolDefinition.name, description: toolDefinition.description || "", verified: true, // Verification handled separately @@ -1318,6 +1346,7 @@ ${pc.bold("EXAMPLES:")} // Convert to EnactTool format const tool: EnactTool = { + id: toolDefinition.id, name: toolDefinition.name, description: toolDefinition.description || "", verified: isValid, diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts index 34946d4..ea1bd97 100644 --- a/packages/security/src/index.ts +++ b/packages/security/src/index.ts @@ -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'; \ No newline at end of file +export type { FieldConfig, SigningFieldOptions } from './fieldConfig'; \ No newline at end of file diff --git a/packages/security/src/keyManager.ts b/packages/security/src/keyManager.ts index 6f21f20..b42e53a 100644 --- a/packages/security/src/keyManager.ts +++ b/packages/security/src/keyManager.ts @@ -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'); @@ -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 { @@ -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 @@ -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; } } @@ -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) { @@ -309,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)}`); } @@ -343,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`); diff --git a/packages/security/src/types.ts b/packages/security/src/types.ts index 22fed9b..27fecaa 100644 --- a/packages/security/src/types.ts +++ b/packages/security/src/types.ts @@ -35,6 +35,14 @@ export interface Signature { timestamp: number; } +export interface KeyMetadata { + keyId: string; + publicKey: string, + created: string; + algorithm: string; + description?: string; +} + export interface KeyPair { privateKey: string; publicKey: string; diff --git a/packages/shared/src/api/enact-api.ts b/packages/shared/src/api/enact-api.ts index bf39a7a..599af32 100644 --- a/packages/shared/src/api/enact-api.ts +++ b/packages/shared/src/api/enact-api.ts @@ -485,15 +485,15 @@ export class EnactApiClient { async signTool( toolId: string, payload: ToolSignaturePayload, - token: string, - tokenType: "jwt" | "cli" = "cli" + // token: string, + // tokenType: "jwt" | "cli" = "cli" ): Promise { - const endpoint = `/functions/v1/tools/${encodeURIComponent(toolId)}/signatures`; + const endpoint = `/functions/v1/tools/${encodeURIComponent(toolId)}/anon-sign`; return this.makeRequest(endpoint, { method: "POST", body: JSON.stringify(payload), - }, token, tokenType); + }); } // =================== diff --git a/packages/shared/src/api/types.ts b/packages/shared/src/api/types.ts index c6af239..ad250d5 100644 --- a/packages/shared/src/api/types.ts +++ b/packages/shared/src/api/types.ts @@ -1,4 +1,5 @@ export interface EnactToolDefinition { + id: string, name: string; description: string; verified?: boolean; // Indicates if the tool has been verified @@ -104,11 +105,11 @@ export interface EnactExecOptions { export interface ToolSignaturePayload { algorithm: "sha256"; created: string; // Time of signing - key_id: string; // ID of the private key - public_key: string; // The corresponding public key + keyId: string; // ID of the private key + public_key: string; // The corresponding public key in base64 role: "author"; signer: string; // The userID of the signer - timestamp: number; + timestamp: number; // Unix epoch type: "ecdsa-p256"; value: string; // Signature } \ No newline at end of file diff --git a/packages/shared/src/core/EnactCore.ts b/packages/shared/src/core/EnactCore.ts index d70e520..3d98d88 100644 --- a/packages/shared/src/core/EnactCore.ts +++ b/packages/shared/src/core/EnactCore.ts @@ -315,6 +315,7 @@ export class EnactCore { } else { // Fallback: map database fields to tool format (may cause signature verification issues) tool = { + id: response.id, name: response.name, description: response.description, command: response.command, diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 86b4656..69c0d7f 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,6 +1,7 @@ // src/types.ts - Type definitions for Enact CLI Core export interface EnactTool { // REQUIRED FIELDS + id: string, name: string; // Tool identifier with hierarchical path description: string; // Human-readable description command: string; // Shell command to execute with version pins From 00af625bd3cd68a74aa3f5a3c5cf79af1f3e9fd6 Mon Sep 17 00:00:00 2001 From: Krylix Date: Thu, 27 Nov 2025 14:31:26 -0800 Subject: [PATCH 3/3] Added OpenAI Codex MCP (mcp is still broken but it will work if the file is ran directly) --- packages/cli/src/commands/mcp.ts | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts index a6d2f28..b6ad8c7 100644 --- a/packages/cli/src/commands/mcp.ts +++ b/packages/cli/src/commands/mcp.ts @@ -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 @@ -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; @@ -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; }