Skip to content

Commit b099518

Browse files
committed
docs(plugins): add comprehensive plugin development documentation
Add documentation in /docs/plugins/: - README.md: Main plugin development guide with architecture, manifest, and capabilities - GETTING_STARTED.md: Step-by-step tutorial for creating first plugin - HOOKS.md: Complete reference for all 35+ hook types - SECURITY.md: Security model including WASM sandboxing, resource limits, and best practices
1 parent e0e54d1 commit b099518

File tree

11 files changed

+2693
-141
lines changed

11 files changed

+2693
-141
lines changed

docs/plugins/GETTING_STARTED.md

Lines changed: 530 additions & 0 deletions
Large diffs are not rendered by default.

docs/plugins/HOOKS.md

Lines changed: 961 additions & 0 deletions
Large diffs are not rendered by default.

docs/plugins/README.md

Lines changed: 404 additions & 0 deletions
Large diffs are not rendered by default.

docs/plugins/SECURITY.md

Lines changed: 489 additions & 0 deletions
Large diffs are not rendered by default.

src/cortex-cli/src/plugin_cmd.rs

Lines changed: 122 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
1515
use anyhow::{Context, Result, bail};
1616
use clap::Parser;
17-
use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind};
17+
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
1818
use serde::Serialize;
1919
use std::path::{Path, PathBuf};
2020
use std::process::{Command, Stdio};
@@ -868,7 +868,10 @@ fn scaffold_advanced_plugin(
868868
fs::write(plugin_dir.join("package.json"), package_json)?;
869869

870870
// gitignore for TypeScript
871-
fs::write(plugin_dir.join(".gitignore"), "node_modules/\ndist/\n*.wasm\n")?;
871+
fs::write(
872+
plugin_dir.join(".gitignore"),
873+
"node_modules/\ndist/\n*.wasm\n",
874+
)?;
872875
} else {
873876
// Rust project with advanced template
874877
let rust_code = generate_advanced_rust_code(plugin_id, plugin_name, "example");
@@ -1241,14 +1244,20 @@ async fn run_show(args: PluginShowArgs) -> Result<()> {
12411244
// =============================================================================
12421245

12431246
async fn run_new(args: PluginNewArgs) -> Result<()> {
1244-
let output_dir = args.output.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1247+
let output_dir = args
1248+
.output
1249+
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
12451250

12461251
// Validate plugin name
12471252
if args.name.is_empty() {
12481253
bail!("Plugin name cannot be empty");
12491254
}
12501255

1251-
if !args.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
1256+
if !args
1257+
.name
1258+
.chars()
1259+
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
1260+
{
12521261
bail!("Plugin name can only contain alphanumeric characters, hyphens, and underscores");
12531262
}
12541263

@@ -1270,7 +1279,9 @@ async fn run_new(args: PluginNewArgs) -> Result<()> {
12701279
.ok()
12711280
.and_then(|output| {
12721281
if output.status.success() {
1273-
String::from_utf8(output.stdout).ok().map(|s| s.trim().to_string())
1282+
String::from_utf8(output.stdout)
1283+
.ok()
1284+
.map(|s| s.trim().to_string())
12741285
} else {
12751286
None
12761287
}
@@ -1350,7 +1361,9 @@ async fn run_new(args: PluginNewArgs) -> Result<()> {
13501361
// =============================================================================
13511362

13521363
async fn run_dev(args: PluginDevArgs) -> Result<()> {
1353-
let plugin_dir = args.path.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1364+
let plugin_dir = args
1365+
.path
1366+
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
13541367

13551368
// Verify this is a plugin directory
13561369
let manifest_path = plugin_dir.join("plugin.toml");
@@ -1362,10 +1375,10 @@ async fn run_dev(args: PluginDevArgs) -> Result<()> {
13621375
}
13631376

13641377
// Read plugin info
1365-
let manifest_content = std::fs::read_to_string(&manifest_path)
1366-
.context("Failed to read plugin.toml")?;
1367-
let manifest: toml::Value = toml::from_str(&manifest_content)
1368-
.context("Failed to parse plugin.toml")?;
1378+
let manifest_content =
1379+
std::fs::read_to_string(&manifest_path).context("Failed to read plugin.toml")?;
1380+
let manifest: toml::Value =
1381+
toml::from_str(&manifest_content).context("Failed to parse plugin.toml")?;
13691382

13701383
let plugin_name = manifest
13711384
.get("plugin")
@@ -1408,7 +1421,11 @@ async fn run_dev(args: PluginDevArgs) -> Result<()> {
14081421

14091422
// Watch src directory if it exists, otherwise watch the plugin directory
14101423
let src_dir = plugin_dir.join("src");
1411-
let watch_path = if src_dir.exists() { &src_dir } else { &plugin_dir };
1424+
let watch_path = if src_dir.exists() {
1425+
&src_dir
1426+
} else {
1427+
&plugin_dir
1428+
};
14121429

14131430
watcher
14141431
.watch(watch_path, RecursiveMode::Recursive)
@@ -1443,7 +1460,11 @@ async fn run_dev(args: PluginDevArgs) -> Result<()> {
14431460
.filter_map(|n| n.to_str())
14441461
.collect();
14451462

1446-
println!("\n[{}] File changed: {:?}", chrono::Local::now().format("%H:%M:%S"), changed_files);
1463+
println!(
1464+
"\n[{}] File changed: {:?}",
1465+
chrono::Local::now().format("%H:%M:%S"),
1466+
changed_files
1467+
);
14471468
println!("Rebuilding...");
14481469

14491470
match run_plugin_build(&plugin_dir, false, None) {
@@ -1517,7 +1538,10 @@ fn run_plugin_build(plugin_dir: &Path, debug: bool, output: Option<PathBuf>) ->
15171538
cmd.arg("--release");
15181539
}
15191540

1520-
println!(" Running: cargo build --target wasm32-wasi {}", if debug { "" } else { "--release" });
1541+
println!(
1542+
" Running: cargo build --target wasm32-wasi {}",
1543+
if debug { "" } else { "--release" }
1544+
);
15211545

15221546
let output_result = cmd
15231547
.stdout(Stdio::inherit())
@@ -1526,7 +1550,10 @@ fn run_plugin_build(plugin_dir: &Path, debug: bool, output: Option<PathBuf>) ->
15261550
.context("Failed to execute cargo build")?;
15271551

15281552
if !output_result.success() {
1529-
bail!("Cargo build failed with exit code: {:?}", output_result.code());
1553+
bail!(
1554+
"Cargo build failed with exit code: {:?}",
1555+
output_result.code()
1556+
);
15301557
}
15311558

15321559
// Locate the built WASM file
@@ -1539,25 +1566,37 @@ fn run_plugin_build(plugin_dir: &Path, debug: bool, output: Option<PathBuf>) ->
15391566

15401567
if !wasm_source.exists() {
15411568
// Try to find any .wasm file
1542-
let target_dir = plugin_dir.join("target").join("wasm32-wasi").join(profile_dir);
1569+
let target_dir = plugin_dir
1570+
.join("target")
1571+
.join("wasm32-wasi")
1572+
.join(profile_dir);
15431573
if let Ok(entries) = std::fs::read_dir(&target_dir) {
15441574
for entry in entries.flatten() {
1545-
if entry.path().extension().map(|e| e == "wasm").unwrap_or(false) {
1575+
if entry
1576+
.path()
1577+
.extension()
1578+
.map(|e| e == "wasm")
1579+
.unwrap_or(false)
1580+
{
15461581
let found_wasm = entry.path();
1547-
let output_path = output.clone().unwrap_or_else(|| plugin_dir.join("plugin.wasm"));
1582+
let output_path = output
1583+
.clone()
1584+
.unwrap_or_else(|| plugin_dir.join("plugin.wasm"));
15481585
std::fs::copy(&found_wasm, &output_path)
15491586
.context("Failed to copy WASM file")?;
15501587
return Ok(output_path);
15511588
}
15521589
}
15531590
}
1554-
bail!("WASM file not found at expected path: {}", wasm_source.display());
1591+
bail!(
1592+
"WASM file not found at expected path: {}",
1593+
wasm_source.display()
1594+
);
15551595
}
15561596

15571597
// Copy to output location
15581598
let output_path = output.unwrap_or_else(|| plugin_dir.join("plugin.wasm"));
1559-
std::fs::copy(&wasm_source, &output_path)
1560-
.context("Failed to copy WASM file to output")?;
1599+
std::fs::copy(&wasm_source, &output_path).context("Failed to copy WASM file to output")?;
15611600

15621601
Ok(output_path)
15631602
} else {
@@ -1589,7 +1628,9 @@ fn run_plugin_build(plugin_dir: &Path, debug: bool, output: Option<PathBuf>) ->
15891628
}
15901629

15911630
async fn run_build(args: PluginBuildArgs) -> Result<()> {
1592-
let plugin_dir = args.path.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1631+
let plugin_dir = args
1632+
.path
1633+
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
15931634

15941635
println!("Building plugin in: {}", plugin_dir.display());
15951636

@@ -1644,7 +1685,9 @@ struct ValidationResult {
16441685
}
16451686

16461687
async fn run_validate(args: PluginValidateArgs) -> Result<()> {
1647-
let plugin_dir = args.path.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1688+
let plugin_dir = args
1689+
.path
1690+
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
16481691
let manifest_path = plugin_dir.join("plugin.toml");
16491692

16501693
let mut result = ValidationResult {
@@ -1718,7 +1761,11 @@ async fn run_validate(args: PluginValidateArgs) -> Result<()> {
17181761
}
17191762

17201763
// Validate version
1721-
if plugin_section.get("version").and_then(|v| v.as_str()).is_none() {
1764+
if plugin_section
1765+
.get("version")
1766+
.and_then(|v| v.as_str())
1767+
.is_none()
1768+
{
17221769
result.issues.push(ValidationIssue {
17231770
severity: ValidationSeverity::Warning,
17241771
message: "Missing version field (defaults to 0.0.0)".to_string(),
@@ -1727,7 +1774,11 @@ async fn run_validate(args: PluginValidateArgs) -> Result<()> {
17271774
}
17281775

17291776
// Validate description
1730-
if plugin_section.get("description").and_then(|v| v.as_str()).is_none() {
1777+
if plugin_section
1778+
.get("description")
1779+
.and_then(|v| v.as_str())
1780+
.is_none()
1781+
{
17311782
if args.verbose {
17321783
result.issues.push(ValidationIssue {
17331784
severity: ValidationSeverity::Info,
@@ -1750,9 +1801,12 @@ async fn run_validate(args: PluginValidateArgs) -> Result<()> {
17501801
let has_built_wasm = if target_wasm.exists() {
17511802
std::fs::read_dir(&target_wasm)
17521803
.map(|entries| {
1753-
entries
1754-
.filter_map(|e| e.ok())
1755-
.any(|e| e.path().extension().map(|ext| ext == "wasm").unwrap_or(false))
1804+
entries.filter_map(|e| e.ok()).any(|e| {
1805+
e.path()
1806+
.extension()
1807+
.map(|ext| ext == "wasm")
1808+
.unwrap_or(false)
1809+
})
17561810
})
17571811
.unwrap_or(false)
17581812
} else {
@@ -1768,20 +1822,33 @@ async fn run_validate(args: PluginValidateArgs) -> Result<()> {
17681822
} else if args.verbose {
17691823
result.issues.push(ValidationIssue {
17701824
severity: ValidationSeverity::Info,
1771-
message: format!("WASM file found: {}", if has_wasm { "plugin.wasm" } else { "target/wasm32-wasi/release/*.wasm" }),
1825+
message: format!(
1826+
"WASM file found: {}",
1827+
if has_wasm {
1828+
"plugin.wasm"
1829+
} else {
1830+
"target/wasm32-wasi/release/*.wasm"
1831+
}
1832+
),
17721833
field: None,
17731834
});
17741835
}
17751836

17761837
// Validate permissions if present
1777-
if let Some(permissions) = manifest.get("permissions").or_else(|| plugin_section.get("permissions")) {
1838+
if let Some(permissions) = manifest
1839+
.get("permissions")
1840+
.or_else(|| plugin_section.get("permissions"))
1841+
{
17781842
if let Some(perms_array) = permissions.as_array() {
17791843
validate_permissions(perms_array, &mut result, args.verbose);
17801844
}
17811845
}
17821846

17831847
// Validate capabilities if present
1784-
if let Some(capabilities) = manifest.get("capabilities").or_else(|| plugin_section.get("capabilities")) {
1848+
if let Some(capabilities) = manifest
1849+
.get("capabilities")
1850+
.or_else(|| plugin_section.get("capabilities"))
1851+
{
17851852
if let Some(caps_array) = capabilities.as_array() {
17861853
validate_capabilities(caps_array, &mut result, args.verbose);
17871854
}
@@ -1803,7 +1870,8 @@ async fn run_validate(args: PluginValidateArgs) -> Result<()> {
18031870
if !cargo_toml.exists() && !package_json.exists() {
18041871
result.issues.push(ValidationIssue {
18051872
severity: ValidationSeverity::Warning,
1806-
message: "No Cargo.toml or package.json found. Build configuration missing.".to_string(),
1873+
message: "No Cargo.toml or package.json found. Build configuration missing."
1874+
.to_string(),
18071875
field: None,
18081876
});
18091877
}
@@ -1869,7 +1937,11 @@ fn validate_permissions(permissions: &[toml::Value], result: &mut ValidationResu
18691937
}
18701938
}
18711939

1872-
fn validate_capabilities(capabilities: &[toml::Value], result: &mut ValidationResult, verbose: bool) {
1940+
fn validate_capabilities(
1941+
capabilities: &[toml::Value],
1942+
result: &mut ValidationResult,
1943+
verbose: bool,
1944+
) {
18731945
const KNOWN_CAPABILITIES: &[&str] = &[
18741946
"commands",
18751947
"hooks",
@@ -1977,7 +2049,9 @@ fn output_validation_result(result: ValidationResult, as_json: bool) -> Result<(
19772049
// =============================================================================
19782050

19792051
async fn run_publish(args: PluginPublishArgs) -> Result<()> {
1980-
let plugin_dir = args.path.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
2052+
let plugin_dir = args
2053+
.path
2054+
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
19812055

19822056
println!("Preparing plugin for publication...");
19832057
println!(" Directory: {}", plugin_dir.display());
@@ -1991,7 +2065,10 @@ async fn run_publish(args: PluginPublishArgs) -> Result<()> {
19912065
};
19922066

19932067
if let Err(e) = run_validate(validate_args).await {
1994-
bail!("Plugin validation failed: {}. Fix issues before publishing.", e);
2068+
bail!(
2069+
"Plugin validation failed: {}. Fix issues before publishing.",
2070+
e
2071+
);
19952072
}
19962073

19972074
// Read plugin info
@@ -2030,7 +2107,12 @@ async fn run_publish(args: PluginPublishArgs) -> Result<()> {
20302107
if target_wasm.exists() {
20312108
if let Ok(entries) = std::fs::read_dir(&target_wasm) {
20322109
for entry in entries.flatten() {
2033-
if entry.path().extension().map(|e| e == "wasm").unwrap_or(false) {
2110+
if entry
2111+
.path()
2112+
.extension()
2113+
.map(|e| e == "wasm")
2114+
.unwrap_or(false)
2115+
{
20342116
std::fs::copy(entry.path(), &wasm_path)?;
20352117
break;
20362118
}
@@ -2047,10 +2129,12 @@ async fn run_publish(args: PluginPublishArgs) -> Result<()> {
20472129
println!("\nStep 3: Creating distribution package...");
20482130

20492131
let tarball_name = format!("{}-{}.tar.gz", plugin_id, plugin_version);
2050-
let tarball_path = args.output.unwrap_or_else(|| plugin_dir.join(&tarball_name));
2132+
let tarball_path = args
2133+
.output
2134+
.unwrap_or_else(|| plugin_dir.join(&tarball_name));
20512135

2052-
let tarball_file = std::fs::File::create(&tarball_path)
2053-
.context("Failed to create tarball file")?;
2136+
let tarball_file =
2137+
std::fs::File::create(&tarball_path).context("Failed to create tarball file")?;
20542138

20552139
let encoder = flate2::write::GzEncoder::new(tarball_file, flate2::Compression::default());
20562140
let mut archive = tar::Builder::new(encoder);

src/cortex-engine/src/plugin/integration.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,9 @@ impl PluginIntegrationBuilder {
493493
///
494494
/// If no registry was provided, creates a new empty registry.
495495
pub fn build(self) -> PluginIntegration {
496-
let registry = self.registry.unwrap_or_else(|| Arc::new(HookRegistry::new()));
496+
let registry = self
497+
.registry
498+
.unwrap_or_else(|| Arc::new(HookRegistry::new()));
497499
PluginIntegration::new(registry)
498500
}
499501
}

0 commit comments

Comments
 (0)