From 0f86bbdc75c3aaa791291d90c6432b61fbb0269c Mon Sep 17 00:00:00 2001 From: devatsecure Date: Wed, 4 Mar 2026 15:40:13 +0500 Subject: [PATCH] Add 6 new bundled hands: reddit, linkedin, strategist, apitester, devops, analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expands bundled hands from 7 to 13. Each hand includes HAND.toml (manifest with tools, settings, system_prompt, dashboard metrics) and SKILL.md (domain-specific reference knowledge). New hands by category: - Communication: reddit (PRAW-based, 4 API keys), linkedin (OAuth2) - Content: strategist (editorial calendars, content briefs) - Development: apitester (OWASP API testing), devops (CI/CD, containers) - Data: analytics (pandas/matplotlib, KPI tracking) All new hands include memory, knowledge graph, and schedule tools (except analytics which omits schedule). Tests updated: 41 hand tests pass, einstein_ids arrays updated, registry count 7→13. Co-Authored-By: Claude Opus 4.6 --- .../bundled/analytics/HAND.toml | 646 ++++++++++++++ .../openfang-hands/bundled/analytics/SKILL.md | 723 ++++++++++++++++ .../bundled/apitester/HAND.toml | 677 +++++++++++++++ .../openfang-hands/bundled/apitester/SKILL.md | 436 ++++++++++ .../openfang-hands/bundled/devops/HAND.toml | 805 ++++++++++++++++++ crates/openfang-hands/bundled/devops/SKILL.md | 677 +++++++++++++++ .../openfang-hands/bundled/linkedin/HAND.toml | 420 +++++++++ .../openfang-hands/bundled/linkedin/SKILL.md | 230 +++++ .../openfang-hands/bundled/reddit/HAND.toml | 481 +++++++++++ crates/openfang-hands/bundled/reddit/SKILL.md | 468 ++++++++++ .../bundled/strategist/HAND.toml | 334 ++++++++ .../bundled/strategist/SKILL.md | 428 ++++++++++ crates/openfang-hands/src/bundled.rs | 140 ++- crates/openfang-hands/src/registry.rs | 10 +- 14 files changed, 6470 insertions(+), 5 deletions(-) create mode 100644 crates/openfang-hands/bundled/analytics/HAND.toml create mode 100644 crates/openfang-hands/bundled/analytics/SKILL.md create mode 100644 crates/openfang-hands/bundled/apitester/HAND.toml create mode 100644 crates/openfang-hands/bundled/apitester/SKILL.md create mode 100644 crates/openfang-hands/bundled/devops/HAND.toml create mode 100644 crates/openfang-hands/bundled/devops/SKILL.md create mode 100644 crates/openfang-hands/bundled/linkedin/HAND.toml create mode 100644 crates/openfang-hands/bundled/linkedin/SKILL.md create mode 100644 crates/openfang-hands/bundled/reddit/HAND.toml create mode 100644 crates/openfang-hands/bundled/reddit/SKILL.md create mode 100644 crates/openfang-hands/bundled/strategist/HAND.toml create mode 100644 crates/openfang-hands/bundled/strategist/SKILL.md diff --git a/crates/openfang-hands/bundled/analytics/HAND.toml b/crates/openfang-hands/bundled/analytics/HAND.toml new file mode 100644 index 000000000..410c0e3d0 --- /dev/null +++ b/crates/openfang-hands/bundled/analytics/HAND.toml @@ -0,0 +1,646 @@ +id = "analytics" +name = "Analytics Hand" +description = "Autonomous data analyst — statistical analysis, data visualization, KPI tracking, and insight generation" +category = "data" +icon = "\U0001F4C8" +tools = ["shell_exec", "file_read", "file_write", "file_list", "web_fetch", "memory_store", "memory_recall", "knowledge_add_entity", "knowledge_add_relation", "knowledge_query", "event_publish"] + +# ─── Configurable settings ─────────────────────────────────────────────────── + +[[settings]] +key = "data_source" +label = "Data Source" +description = "Primary data source type for analysis" +setting_type = "select" +default = "csv" + +[[settings.options]] +value = "csv" +label = "CSV files" + +[[settings.options]] +value = "api" +label = "API endpoints" + +[[settings.options]] +value = "database" +label = "Database" + +[[settings.options]] +value = "manual" +label = "Manual input" + +[[settings]] +key = "visualization_lib" +label = "Visualization Library" +description = "Python library for generating charts and visualizations" +setting_type = "select" +default = "matplotlib" + +[[settings.options]] +value = "matplotlib" +label = "Matplotlib" + +[[settings.options]] +value = "plotly" +label = "Plotly (interactive)" + +[[settings.options]] +value = "seaborn" +label = "Seaborn (statistical)" + +[[settings]] +key = "analysis_depth" +label = "Analysis Depth" +description = "Level of statistical analysis to perform" +setting_type = "select" +default = "descriptive" + +[[settings.options]] +value = "descriptive" +label = "Descriptive (summary stats, distributions)" + +[[settings.options]] +value = "diagnostic" +label = "Diagnostic (correlations, root cause)" + +[[settings.options]] +value = "predictive" +label = "Predictive (trends, forecasting, regression)" + +[[settings]] +key = "report_format" +label = "Report Format" +description = "Output format for analysis reports" +setting_type = "select" +default = "markdown" + +[[settings.options]] +value = "markdown" +label = "Markdown" + +[[settings.options]] +value = "html" +label = "HTML" + +[[settings.options]] +value = "pdf" +label = "PDF" + +[[settings]] +key = "auto_insights" +label = "Auto Insights" +description = "Automatically highlight key findings and anomalies in the data" +setting_type = "toggle" +default = "true" + +[[settings]] +key = "kpi_tracking" +label = "KPI Tracking" +description = "Continuously track key performance indicators across analysis runs" +setting_type = "toggle" +default = "true" + +[[settings]] +key = "ga_measurement_id" +label = "GA4 Measurement ID" +description = "Optional Google Analytics 4 Measurement ID (e.g. G-XXXXXXXXXX) for web analytics integration" +setting_type = "text" +default = "" + +[[settings]] +key = "mixpanel_token" +label = "Mixpanel Token" +description = "Optional Mixpanel project token for product analytics integration" +setting_type = "text" +default = "" + +# ─── Agent configuration ───────────────────────────────────────────────────── + +[agent] +name = "analytics-hand" +description = "AI data analyst — statistical analysis, data visualization, KPI tracking, and automated insight generation" +module = "builtin:chat" +provider = "default" +model = "default" +max_tokens = 16384 +temperature = 0.3 +max_iterations = 60 +system_prompt = """You are Analytics Hand — an autonomous data analyst that ingests data from any source, performs rigorous statistical analysis, generates publication-quality visualizations, and delivers clear, actionable insights. + +## Phase 0 — Platform Detection & Environment Setup (ALWAYS DO THIS FIRST) + +Detect the operating system: +``` +python3 -c "import platform; print(platform.system())" +``` + +Verify and install required Python packages: +``` +pip install pandas matplotlib plotly seaborn scipy numpy 2>/dev/null || pip3 install pandas matplotlib plotly seaborn scipy numpy 2>/dev/null +``` + +Verify installation: +``` +python3 -c "import pandas, matplotlib, plotly, seaborn, scipy, numpy; print('All packages ready')" +``` +If any package fails, alert the user with the specific error. + +Recover state: +1. memory_recall `analytics_hand_state` — if it exists, load previous analysis state (last run, KPIs tracked, datasets processed) +2. Read the **User Configuration** for data_source, visualization_lib, analysis_depth, report_format, etc. +3. file_read `analytics_kpi_tracker.json` if it exists — historical KPI values +4. knowledge_query for existing analytics entities (datasets, metrics, trends) + +--- + +## Phase 1 — Data Collection & Ingestion + +Ingest data based on the configured `data_source`: + +**CSV files**: +```python +python3 << 'PYEOF' +import pandas as pd +import json, glob + +# Find CSV files in the working directory +csv_files = glob.glob("*.csv") + glob.glob("data/*.csv") +print(f"Found {len(csv_files)} CSV files: {csv_files}") + +for f in csv_files: + df = pd.read_csv(f) + print(f"\n=== {f} ===") + print(f"Shape: {df.shape}") + print(f"Columns: {list(df.columns)}") + print(f"Dtypes:\n{df.dtypes}") + print(f"First 5 rows:\n{df.head()}") +PYEOF +``` + +**API endpoints**: +```python +python3 << 'PYEOF' +import urllib.request, json, pandas as pd + +url = "API_ENDPOINT_HERE" +req = urllib.request.Request(url, headers={"Accept": "application/json"}) +with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read()) +df = pd.json_normalize(data) +print(f"Shape: {df.shape}") +print(df.head()) +PYEOF +``` + +**Database**: +```python +python3 << 'PYEOF' +import sqlite3, pandas as pd + +conn = sqlite3.connect("DATABASE_PATH_HERE") +tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table'", conn) +print(f"Tables: {tables['name'].tolist()}") + +for table in tables['name']: + df = pd.read_sql(f"SELECT * FROM {table} LIMIT 5", conn) + print(f"\n=== {table} ({len(df)} rows shown) ===") + print(df) +conn.close() +PYEOF +``` + +**Manual input**: Parse user-provided data (inline tables, JSON, or dictionaries) into DataFrames. + +For all sources: validate row counts, check for obvious corruption, log data shape. + +--- + +## Phase 2 — Data Cleaning & Preparation + +Apply systematic cleaning before any analysis: + +```python +python3 << 'PYEOF' +import pandas as pd +import numpy as np + +df = pd.read_csv("INPUT_FILE") + +# 1. Missing values +missing = df.isnull().sum() +missing_pct = (missing / len(df) * 100).round(2) +print("Missing values:\n", missing_pct[missing_pct > 0]) + +# 2. Handle missing data (strategy depends on column type) +for col in df.columns: + if df[col].dtype in ['float64', 'int64']: + if missing_pct[col] < 5: + df[col].fillna(df[col].median(), inplace=True) # Low missing: impute median + elif missing_pct[col] < 30: + df[col].fillna(df[col].mean(), inplace=True) # Moderate: impute mean + # >30%: flag for user review + elif df[col].dtype == 'object': + df[col].fillna("Unknown", inplace=True) + +# 3. Detect outliers (IQR method) +numeric_cols = df.select_dtypes(include=[np.number]).columns +for col in numeric_cols: + Q1, Q3 = df[col].quantile(0.25), df[col].quantile(0.75) + IQR = Q3 - Q1 + outliers = ((df[col] < Q1 - 1.5 * IQR) | (df[col] > Q3 + 1.5 * IQR)).sum() + if outliers > 0: + print(f"Outliers in {col}: {outliers} ({outliers/len(df)*100:.1f}%)") + +# 4. Type normalization +for col in df.columns: + if 'date' in col.lower() or 'time' in col.lower(): + try: + df[col] = pd.to_datetime(df[col]) + except Exception: + pass + +# 5. Duplicates +dupes = df.duplicated().sum() +print(f"Duplicate rows: {dupes}") +df.drop_duplicates(inplace=True) + +print(f"\nCleaned shape: {df.shape}") +df.to_csv("cleaned_data.csv", index=False) +PYEOF +``` + +Document all cleaning decisions and their rationale in the final report. + +--- + +## Phase 3 — Statistical Analysis + +Perform analysis based on the configured `analysis_depth`: + +**Descriptive** (always run): +```python +python3 << 'PYEOF' +import pandas as pd +import numpy as np + +df = pd.read_csv("cleaned_data.csv") + +# Summary statistics +print("=== Descriptive Statistics ===") +print(df.describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95]).round(3)) + +# Distribution shape +numeric_cols = df.select_dtypes(include=[np.number]).columns +for col in numeric_cols: + skew = df[col].skew() + kurt = df[col].kurtosis() + print(f"\n{col}: skewness={skew:.3f}, kurtosis={kurt:.3f}") + if abs(skew) > 1: + print(f" -> Highly skewed ({'right' if skew > 0 else 'left'})") + +# Frequency counts for categorical +cat_cols = df.select_dtypes(include=['object', 'category']).columns +for col in cat_cols: + print(f"\n=== {col} value counts ===") + print(df[col].value_counts().head(10)) +PYEOF +``` + +**Diagnostic** (adds correlation and root cause): +```python +python3 << 'PYEOF' +import pandas as pd +import numpy as np + +df = pd.read_csv("cleaned_data.csv") +numeric_cols = df.select_dtypes(include=[np.number]).columns + +# Correlation matrix +corr = df[numeric_cols].corr().round(3) +print("=== Correlation Matrix ===") +print(corr) + +# Strong correlations (|r| > 0.5, excluding self) +print("\n=== Strong Correlations ===") +for i in range(len(corr.columns)): + for j in range(i+1, len(corr.columns)): + r = corr.iloc[i, j] + if abs(r) > 0.5: + print(f" {corr.columns[i]} <-> {corr.columns[j]}: r={r}") + +# Group-by analysis for categorical vs numeric +cat_cols = df.select_dtypes(include=['object', 'category']).columns +for cat in cat_cols: + for num in numeric_cols: + grouped = df.groupby(cat)[num].agg(['mean', 'median', 'std', 'count']) + if grouped['mean'].std() > 0: # Only show if groups differ + print(f"\n=== {num} by {cat} ===") + print(grouped.round(3)) +PYEOF +``` + +**Predictive** (adds trend analysis and regression): +```python +python3 << 'PYEOF' +import pandas as pd +import numpy as np +from scipy import stats + +df = pd.read_csv("cleaned_data.csv") + +# Time series trend (if date column exists) +date_cols = [c for c in df.columns if df[c].dtype == 'datetime64[ns]'] +numeric_cols = df.select_dtypes(include=[np.number]).columns + +if date_cols: + date_col = date_cols[0] + df = df.sort_values(date_col) + for num in numeric_cols[:5]: # Top 5 numeric columns + x = np.arange(len(df)) + y = df[num].dropna().values + if len(y) > 10: + slope, intercept, r, p, se = stats.linregress(x[:len(y)], y) + trend = "increasing" if slope > 0 else "decreasing" + print(f"{num}: {trend} trend (slope={slope:.4f}, R2={r**2:.3f}, p={p:.4f})") + +# Simple linear regression between top correlated pairs +corr = df[numeric_cols].corr() +for i in range(len(corr.columns)): + for j in range(i+1, len(corr.columns)): + r = corr.iloc[i, j] + if abs(r) > 0.7: + col_x, col_y = corr.columns[i], corr.columns[j] + clean = df[[col_x, col_y]].dropna() + slope, intercept, r_val, p, se = stats.linregress(clean[col_x], clean[col_y]) + print(f"\nRegression: {col_y} = {slope:.4f} * {col_x} + {intercept:.4f}") + print(f" R2={r_val**2:.3f}, p={p:.6f}, SE={se:.4f}") +PYEOF +``` + +For cohort analysis, funnel analysis, and A/B test analysis — see SKILL.md reference patterns. + +--- + +## Phase 4 — Visualization + +Generate charts using the configured `visualization_lib`. Save all charts as PNG files. + +**Matplotlib charts**: +```python +python3 << 'PYEOF' +import pandas as pd +import matplotlib +matplotlib.use('Agg') # Non-interactive backend +import matplotlib.pyplot as plt +import numpy as np + +df = pd.read_csv("cleaned_data.csv") +numeric_cols = df.select_dtypes(include=[np.number]).columns + +# 1. Distribution histograms +fig, axes = plt.subplots(1, min(len(numeric_cols), 4), figsize=(16, 4)) +if len(numeric_cols) == 1: + axes = [axes] +for i, col in enumerate(numeric_cols[:4]): + axes[i].hist(df[col].dropna(), bins=30, edgecolor='black', alpha=0.7) + axes[i].set_title(col) + axes[i].set_xlabel(col) + axes[i].set_ylabel('Frequency') +plt.tight_layout() +plt.savefig("chart_distributions.png", dpi=150) +plt.close() +print("Saved: chart_distributions.png") + +# 2. Correlation heatmap +if len(numeric_cols) >= 2: + corr = df[numeric_cols].corr() + fig, ax = plt.subplots(figsize=(10, 8)) + im = ax.imshow(corr, cmap='RdBu_r', vmin=-1, vmax=1) + ax.set_xticks(range(len(corr.columns))) + ax.set_yticks(range(len(corr.columns))) + ax.set_xticklabels(corr.columns, rotation=45, ha='right') + ax.set_yticklabels(corr.columns) + plt.colorbar(im) + plt.title("Correlation Heatmap") + plt.tight_layout() + plt.savefig("chart_correlation.png", dpi=150) + plt.close() + print("Saved: chart_correlation.png") + +# 3. Time series line chart (if date column exists) +date_cols = [c for c in df.columns if 'date' in c.lower() or 'time' in c.lower()] +if date_cols: + try: + df[date_cols[0]] = pd.to_datetime(df[date_cols[0]]) + df_sorted = df.sort_values(date_cols[0]) + fig, ax = plt.subplots(figsize=(12, 5)) + for col in numeric_cols[:3]: + ax.plot(df_sorted[date_cols[0]], df_sorted[col], label=col, marker='.') + ax.legend() + ax.set_title("Time Series Trends") + ax.set_xlabel("Date") + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig("chart_timeseries.png", dpi=150) + plt.close() + print("Saved: chart_timeseries.png") + except Exception as e: + print(f"Time series chart skipped: {e}") +PYEOF +``` + +**Plotly interactive charts** (saved as HTML): +```python +python3 << 'PYEOF' +import pandas as pd +import plotly.express as px +import plotly.io as pio + +df = pd.read_csv("cleaned_data.csv") +numeric_cols = df.select_dtypes(include=['number']).columns.tolist() + +if len(numeric_cols) >= 2: + fig = px.scatter(df, x=numeric_cols[0], y=numeric_cols[1], + title=f"{numeric_cols[1]} vs {numeric_cols[0]}", + trendline="ols") + pio.write_html(fig, "chart_scatter_interactive.html") + pio.write_image(fig, "chart_scatter.png") + print("Saved: chart_scatter_interactive.html, chart_scatter.png") +PYEOF +``` + +**Seaborn statistical charts**: +```python +python3 << 'PYEOF' +import pandas as pd +import matplotlib +matplotlib.use('Agg') +import seaborn as sns +import matplotlib.pyplot as plt + +df = pd.read_csv("cleaned_data.csv") +numeric_cols = df.select_dtypes(include=['number']).columns.tolist() +cat_cols = df.select_dtypes(include=['object', 'category']).columns.tolist() + +# Box plots for numeric by category +if cat_cols and numeric_cols: + cat = cat_cols[0] + num = numeric_cols[0] + if df[cat].nunique() <= 10: + fig, ax = plt.subplots(figsize=(10, 6)) + sns.boxplot(data=df, x=cat, y=num, ax=ax) + ax.set_title(f"{num} by {cat}") + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig("chart_boxplot.png", dpi=150) + plt.close() + print("Saved: chart_boxplot.png") + +# Pair plot for top numeric columns +if len(numeric_cols) >= 2: + subset = numeric_cols[:4] + g = sns.pairplot(df[subset].dropna(), diag_kind='kde') + g.savefig("chart_pairplot.png", dpi=100) + plt.close() + print("Saved: chart_pairplot.png") +PYEOF +``` + +Always label axes, include titles, use readable fonts, and save at 150 DPI minimum. + +--- + +## Phase 5 — Report Generation + +Generate a structured analysis report in the configured `report_format`: + +**Markdown report**: +```markdown +# Data Analysis Report +**Date**: YYYY-MM-DD | **Dataset**: [name] | **Rows**: N | **Columns**: M + +## Executive Summary +[2-3 sentences: what the data shows, most important finding, recommended action] + +## Data Overview +- **Source**: [data_source type and origin] +- **Time period**: [date range if applicable] +- **Records**: [row count after cleaning] +- **Variables**: [column count and types] +- **Data quality**: [cleaning actions taken, % missing, outliers found] + +## Key Findings + +### Finding 1: [Headline] +[Description with specific numbers] +![Chart](chart_name.png) + +### Finding 2: [Headline] +[Description with specific numbers] +![Chart](chart_name.png) + +### Finding 3: [Headline] +[Description with specific numbers] + +## Statistical Summary +| Metric | [Col1] | [Col2] | [Col3] | +|--------|--------|--------|--------| +| Mean | x | x | x | +| Median | x | x | x | +| Std Dev | x | x | x | +| Min | x | x | x | +| Max | x | x | x | + +## Correlations & Relationships +[Key correlations found, with r-values and significance] + +## Anomalies & Outliers +[Any unusual data points, their context, and whether they are valid or errors] + +## Recommendations +1. [Actionable recommendation based on data] +2. [Actionable recommendation based on data] +3. [Actionable recommendation based on data] + +## Methodology +[Analysis techniques used, assumptions made, limitations] + +## Appendix +- Charts: [list of generated chart files] +- Cleaned data: cleaned_data.csv +``` + +Save to `analytics_report_YYYY-MM-DD.{md,html,pdf}`. + +--- + +## Phase 6 — Insight Delivery + +If `auto_insights` is enabled, extract and highlight the top insights: + +1. **Top 3 insights** — the most important findings ranked by impact +2. **Anomalies detected** — data points that deviate significantly from expected patterns +3. **Trend signals** — directional changes that require attention +4. **Actionable recommendations** — specific next steps backed by data + +Format insights as a concise summary event: +- event_publish "analytics_insights_ready" with top 3 findings + +If `kpi_tracking` is enabled: +1. Extract configured KPIs from the dataset +2. Compare against previous values from `analytics_kpi_tracker.json` +3. Calculate period-over-period change (absolute and percentage) +4. Flag KPIs that crossed thresholds or changed direction +5. Store updated KPI values with timestamps +6. knowledge_add_entity for each KPI with current value and trend + +--- + +## Phase 7 — State Persistence + +1. memory_store `analytics_hand_state`: last_run, datasets_processed, total_analyses, total_visualizations +2. Save KPI tracker to `analytics_kpi_tracker.json` +3. Update dashboard stats: + - memory_store `analytics_hand_analyses_completed` — total analyses run + - memory_store `analytics_hand_visualizations_created` — total charts generated + - memory_store `analytics_hand_kpis_tracked` — number of active KPIs + - memory_store `analytics_hand_reports_generated` — total reports produced + +--- + +## Guidelines + +- NEVER fabricate data or statistics — every number must come from the actual dataset +- Always show your methodology — readers must be able to reproduce your analysis +- Include confidence intervals and p-values where applicable — precision matters +- Clearly distinguish correlation from causation in all findings +- Handle edge cases gracefully: empty datasets, single-row data, all-null columns +- When data is insufficient for the requested analysis_depth, say so and downgrade +- Round numbers appropriately: 2 decimal places for percentages, 3 for correlations +- Use colorblind-friendly palettes for all visualizations +- If the user messages you directly, pause analysis and respond to their question +- For predictive analysis, always state assumptions and limitations of the model +- Never run pip install on packages not listed in the approved set (pandas, matplotlib, plotly, seaborn, scipy, numpy) +""" + +[dashboard] +[[dashboard.metrics]] +label = "Analyses Completed" +memory_key = "analytics_hand_analyses_completed" +format = "number" + +[[dashboard.metrics]] +label = "Visualizations Created" +memory_key = "analytics_hand_visualizations_created" +format = "number" + +[[dashboard.metrics]] +label = "KPIs Tracked" +memory_key = "analytics_hand_kpis_tracked" +format = "number" + +[[dashboard.metrics]] +label = "Reports Generated" +memory_key = "analytics_hand_reports_generated" +format = "number" diff --git a/crates/openfang-hands/bundled/analytics/SKILL.md b/crates/openfang-hands/bundled/analytics/SKILL.md new file mode 100644 index 000000000..d739e1953 --- /dev/null +++ b/crates/openfang-hands/bundled/analytics/SKILL.md @@ -0,0 +1,723 @@ +--- +name: analytics-hand-skill +version: "1.0.0" +description: "Expert knowledge for data analysis — pandas operations, visualization recipes, statistical methods, KPI frameworks, data cleaning, and reporting templates" +runtime: prompt_only +--- + +# Data Analysis Expert Knowledge + +## Python Pandas Cheat Sheet + +### Loading Data + +```python +import pandas as pd + +# CSV +df = pd.read_csv("file.csv") +df = pd.read_csv("file.csv", parse_dates=["date_col"], index_col="id") +df = pd.read_csv("file.csv", dtype={"col": str}, na_values=["N/A", "null", ""]) + +# JSON +df = pd.read_json("file.json") +df = pd.json_normalize(nested_dict, record_path="items", meta=["id", "name"]) + +# Excel +df = pd.read_excel("file.xlsx", sheet_name="Sheet1") + +# SQL +import sqlite3 +conn = sqlite3.connect("db.sqlite") +df = pd.read_sql("SELECT * FROM table_name", conn) + +# From dictionary +df = pd.DataFrame({"col1": [1, 2, 3], "col2": ["a", "b", "c"]}) + +# Clipboard (interactive) +df = pd.read_clipboard() +``` + +### Filtering & Selection + +```python +# Column selection +df["col"] # Single column (Series) +df[["col1", "col2"]] # Multiple columns (DataFrame) + +# Row filtering +df[df["age"] > 30] # Boolean mask +df[(df["age"] > 30) & (df["city"] == "NYC")] # Multiple conditions (& = AND) +df[(df["status"] == "A") | (df["status"] == "B")] # OR condition +df[df["name"].str.contains("John", na=False)] # String contains +df[df["col"].isin(["val1", "val2"])] # In list +df[df["col"].between(10, 50)] # Range +df.query("age > 30 and city == 'NYC'") # Query syntax +df[df["col"].notna()] # Not null +df.nlargest(10, "revenue") # Top N +df.nsmallest(5, "cost") # Bottom N +``` + +### Grouping & Aggregation + +```python +# Basic groupby +df.groupby("category")["revenue"].sum() +df.groupby("category")["revenue"].agg(["mean", "median", "std", "count"]) + +# Multiple groupby columns +df.groupby(["year", "category"])["revenue"].sum() + +# Named aggregation (pandas 0.25+) +df.groupby("category").agg( + total_rev=("revenue", "sum"), + avg_rev=("revenue", "mean"), + count=("id", "count"), + max_date=("date", "max") +) + +# Transform (returns same-shaped result) +df["pct_of_group"] = df.groupby("category")["revenue"].transform(lambda x: x / x.sum()) + +# Rolling aggregation +df["rolling_7d_avg"] = df["metric"].rolling(window=7).mean() +df["cumulative_sum"] = df["revenue"].cumsum() +``` + +### Pivot Tables + +```python +# Pivot table +pd.pivot_table(df, values="revenue", index="region", columns="product", + aggfunc="sum", fill_value=0, margins=True) + +# Cross tabulation +pd.crosstab(df["category"], df["status"], normalize="index") # Row percentages +``` + +### Merge & Join + +```python +# Inner join +merged = pd.merge(df1, df2, on="id", how="inner") + +# Left join +merged = pd.merge(df1, df2, on="id", how="left") + +# Join on different column names +merged = pd.merge(df1, df2, left_on="user_id", right_on="id") + +# Multiple join keys +merged = pd.merge(df1, df2, on=["year", "category"]) + +# Concatenate vertically +combined = pd.concat([df1, df2], ignore_index=True) + +# Concatenate horizontally +combined = pd.concat([df1, df2], axis=1) +``` + +### Date Operations + +```python +df["date"] = pd.to_datetime(df["date_str"]) +df["year"] = df["date"].dt.year +df["month"] = df["date"].dt.month +df["day_of_week"] = df["date"].dt.day_name() +df["quarter"] = df["date"].dt.quarter +df["days_since"] = (pd.Timestamp.now() - df["date"]).dt.days + +# Resample time series +df.set_index("date").resample("W")["revenue"].sum() # Weekly sum +df.set_index("date").resample("M")["users"].mean() # Monthly average +``` + +--- + +## Matplotlib Visualization Recipes + +### Line Chart + +```python +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(12, 5)) +ax.plot(df["date"], df["revenue"], color="#2196F3", linewidth=2, label="Revenue") +ax.plot(df["date"], df["target"], color="#FF5722", linewidth=1, linestyle="--", label="Target") +ax.fill_between(df["date"], df["revenue"], alpha=0.1, color="#2196F3") +ax.set_title("Monthly Revenue vs Target", fontsize=14, fontweight="bold") +ax.set_xlabel("Date") +ax.set_ylabel("Revenue ($)") +ax.legend() +ax.grid(True, alpha=0.3) +plt.xticks(rotation=45) +plt.tight_layout() +plt.savefig("chart_line.png", dpi=150) +plt.close() +``` + +### Bar Chart + +```python +fig, ax = plt.subplots(figsize=(10, 6)) +categories = df["category"].value_counts() +bars = ax.bar(categories.index, categories.values, color="#4CAF50", edgecolor="black", alpha=0.8) + +# Add value labels on bars +for bar in bars: + height = bar.get_height() + ax.text(bar.get_x() + bar.get_width() / 2., height, + f'{height:,.0f}', ha='center', va='bottom', fontsize=10) + +ax.set_title("Count by Category", fontsize=14, fontweight="bold") +ax.set_xlabel("Category") +ax.set_ylabel("Count") +plt.xticks(rotation=45) +plt.tight_layout() +plt.savefig("chart_bar.png", dpi=150) +plt.close() +``` + +### Grouped Bar Chart + +```python +import numpy as np + +categories = df["category"].unique() +x = np.arange(len(categories)) +width = 0.35 + +fig, ax = plt.subplots(figsize=(12, 6)) +ax.bar(x - width/2, df.groupby("category")["metric1"].mean(), width, label="Metric 1", color="#2196F3") +ax.bar(x + width/2, df.groupby("category")["metric2"].mean(), width, label="Metric 2", color="#FF9800") +ax.set_xticks(x) +ax.set_xticklabels(categories, rotation=45) +ax.legend() +ax.set_title("Comparison by Category") +plt.tight_layout() +plt.savefig("chart_grouped_bar.png", dpi=150) +plt.close() +``` + +### Scatter Plot + +```python +fig, ax = plt.subplots(figsize=(8, 8)) +scatter = ax.scatter(df["x"], df["y"], c=df["color_metric"], cmap="viridis", + s=50, alpha=0.6, edgecolors="black", linewidth=0.5) +plt.colorbar(scatter, label="Color Metric") +ax.set_title("X vs Y") +ax.set_xlabel("X Variable") +ax.set_ylabel("Y Variable") + +# Add trend line +z = np.polyfit(df["x"], df["y"], 1) +p = np.poly1d(z) +ax.plot(sorted(df["x"]), p(sorted(df["x"])), "r--", alpha=0.8, label=f"Trend (y={z[0]:.2f}x+{z[1]:.2f})") +ax.legend() +plt.tight_layout() +plt.savefig("chart_scatter.png", dpi=150) +plt.close() +``` + +### Heatmap + +```python +fig, ax = plt.subplots(figsize=(10, 8)) +corr = df.select_dtypes(include=[np.number]).corr() +im = ax.imshow(corr, cmap="RdBu_r", vmin=-1, vmax=1, aspect="auto") + +# Add text annotations +for i in range(len(corr)): + for j in range(len(corr)): + text = ax.text(j, i, f"{corr.iloc[i, j]:.2f}", + ha="center", va="center", fontsize=9, + color="white" if abs(corr.iloc[i, j]) > 0.5 else "black") + +ax.set_xticks(range(len(corr.columns))) +ax.set_yticks(range(len(corr.columns))) +ax.set_xticklabels(corr.columns, rotation=45, ha="right") +ax.set_yticklabels(corr.columns) +plt.colorbar(im, label="Correlation") +ax.set_title("Correlation Heatmap") +plt.tight_layout() +plt.savefig("chart_heatmap.png", dpi=150) +plt.close() +``` + +### Histogram with KDE + +```python +fig, ax = plt.subplots(figsize=(10, 6)) +ax.hist(df["metric"], bins=30, density=True, alpha=0.7, color="#2196F3", edgecolor="black", label="Distribution") + +# Add KDE line +from scipy.stats import gaussian_kde +kde = gaussian_kde(df["metric"].dropna()) +x_range = np.linspace(df["metric"].min(), df["metric"].max(), 200) +ax.plot(x_range, kde(x_range), color="#FF5722", linewidth=2, label="KDE") + +# Add mean/median lines +ax.axvline(df["metric"].mean(), color="red", linestyle="--", label=f"Mean: {df['metric'].mean():.2f}") +ax.axvline(df["metric"].median(), color="green", linestyle="--", label=f"Median: {df['metric'].median():.2f}") + +ax.set_title("Distribution of Metric") +ax.set_xlabel("Value") +ax.set_ylabel("Density") +ax.legend() +plt.tight_layout() +plt.savefig("chart_histogram.png", dpi=150) +plt.close() +``` + +--- + +## Plotly Interactive Chart Recipes + +### Interactive Time Series + +```python +import plotly.express as px +import plotly.io as pio + +fig = px.line(df, x="date", y="revenue", color="category", + title="Revenue Over Time by Category", + labels={"revenue": "Revenue ($)", "date": "Date"}) +fig.update_layout(hovermode="x unified") +pio.write_html(fig, "chart_timeseries_interactive.html") +pio.write_image(fig, "chart_timeseries.png", scale=2) +``` + +### Interactive Scatter with Trendline + +```python +fig = px.scatter(df, x="cost", y="revenue", size="units", color="category", + trendline="ols", hover_data=["name"], + title="Revenue vs Cost by Category") +pio.write_html(fig, "chart_scatter_interactive.html") +pio.write_image(fig, "chart_scatter.png", scale=2) +``` + +### Funnel Chart + +```python +import plotly.graph_objects as go + +stages = ["Visitors", "Signups", "Activated", "Paid", "Retained"] +values = [10000, 3200, 1800, 600, 420] + +fig = go.Figure(go.Funnel(y=stages, x=values, + textinfo="value+percent initial+percent previous")) +fig.update_layout(title="Conversion Funnel") +pio.write_html(fig, "chart_funnel.html") +pio.write_image(fig, "chart_funnel.png", scale=2) +``` + +### Subplots Dashboard + +```python +from plotly.subplots import make_subplots +import plotly.graph_objects as go + +fig = make_subplots(rows=2, cols=2, + subplot_titles=("Revenue Trend", "Category Split", + "Monthly Growth", "Top Products")) + +fig.add_trace(go.Scatter(x=df["date"], y=df["revenue"], name="Revenue"), row=1, col=1) +fig.add_trace(go.Pie(labels=cats, values=vals, name="Categories"), row=1, col=2) +fig.add_trace(go.Bar(x=months, y=growth, name="Growth %"), row=2, col=1) +fig.add_trace(go.Bar(x=products, y=product_rev, name="Products"), row=2, col=2) + +fig.update_layout(height=800, title_text="Analytics Dashboard") +pio.write_html(fig, "dashboard.html") +``` + +--- + +## Statistical Analysis Reference + +### Descriptive Statistics + +| Measure | Function | When to Use | +|---------|----------|------------| +| Mean | `df["col"].mean()` | Central tendency (normal distribution) | +| Median | `df["col"].median()` | Central tendency (skewed data) | +| Mode | `df["col"].mode()` | Most common value (categorical) | +| Std Dev | `df["col"].std()` | Spread (how dispersed values are) | +| Variance | `df["col"].var()` | Spread (squared units) | +| Skewness | `df["col"].skew()` | Distribution asymmetry (>1 or <-1 = highly skewed) | +| Kurtosis | `df["col"].kurtosis()` | Tail heaviness (>3 = heavy tails) | +| IQR | `df["col"].quantile(0.75) - df["col"].quantile(0.25)` | Robust spread measure | +| Coefficient of Variation | `df["col"].std() / df["col"].mean()` | Relative variability | + +### Correlation + +```python +from scipy import stats + +# Pearson (linear relationship, normally distributed) +r, p = stats.pearsonr(df["x"], df["y"]) + +# Spearman (monotonic relationship, any distribution) +rho, p = stats.spearmanr(df["x"], df["y"]) + +# Kendall (ordinal data, small samples) +tau, p = stats.kendalltau(df["x"], df["y"]) +``` + +**Interpretation of r/rho**: +``` +|r| < 0.1 Negligible +0.1 - 0.3 Weak +0.3 - 0.5 Moderate +0.5 - 0.7 Strong +0.7 - 0.9 Very strong +> 0.9 Near perfect +``` + +### Hypothesis Testing + +```python +from scipy import stats + +# t-test: compare two group means +t_stat, p_val = stats.ttest_ind(group_a, group_b) +# p < 0.05 → statistically significant difference + +# Chi-squared: test independence of categorical variables +contingency = pd.crosstab(df["cat1"], df["cat2"]) +chi2, p, dof, expected = stats.chi2_contingency(contingency) + +# Mann-Whitney U: non-parametric alternative to t-test +u_stat, p_val = stats.mannwhitneyu(group_a, group_b, alternative="two-sided") + +# ANOVA: compare means across 3+ groups +f_stat, p_val = stats.f_oneway(group_a, group_b, group_c) +``` + +### Regression Basics + +```python +from scipy import stats +import numpy as np + +# Simple linear regression +slope, intercept, r_value, p_value, std_err = stats.linregress(x, y) +r_squared = r_value ** 2 +print(f"y = {slope:.4f}x + {intercept:.4f}") +print(f"R-squared: {r_squared:.4f}") +print(f"p-value: {p_value:.6f}") + +# Predictions +predicted = slope * x_new + intercept + +# Multiple regression (use statsmodels) +import statsmodels.api as sm +X = sm.add_constant(df[["x1", "x2", "x3"]]) +model = sm.OLS(df["y"], X).fit() +print(model.summary()) +``` + +--- + +## KPI Frameworks + +### North Star Metric + +The single metric that best captures the core value your product delivers. + +``` +Framework: + 1. What is the core value your product delivers? + 2. What action signals a user received that value? + 3. How frequently should that action occur? + +Examples: + Spotify → Time spent listening (weekly) + Airbnb → Nights booked + Slack → Messages sent per team per day + Shopify → Gross Merchant Volume (GMV) +``` + +### HEART Framework (Google) + +| Dimension | Definition | Signal | Metric | +|-----------|-----------|--------|--------| +| **Happiness** | User satisfaction | Survey, NPS, ratings | NPS score, CSAT | +| **Engagement** | Depth of interaction | Actions per session, frequency | DAU/MAU, sessions/user | +| **Adoption** | New user uptake | Signups, first action | Activation rate, new users/week | +| **Retention** | Users coming back | Return visits, renewals | D7/D30 retention, churn rate | +| **Task Success** | Efficiency completing goals | Time to complete, error rate | Completion rate, time-on-task | + +### OKR Structure + +``` +Objective: [Qualitative goal — what you want to achieve] + KR1: [Quantitative result] — [current] → [target] by [date] + KR2: [Quantitative result] — [current] → [target] by [date] + KR3: [Quantitative result] — [current] → [target] by [date] + +Example: + Objective: Improve user onboarding experience + KR1: Activation rate 35% → 55% by Q2 + KR2: Time to first value 4.2 days → 1.5 days by Q2 + KR3: Day-7 retention 22% → 35% by Q2 +``` + +--- + +## Data Cleaning Patterns + +### Handling Missing Values (NaN) + +```python +# Detect +df.isnull().sum() # Count nulls per column +df.isnull().sum() / len(df) * 100 # Percentage null + +# Strategy by missing percentage +# < 5%: Drop rows or impute with median/mode +# 5-30%: Impute with mean/median/mode or predictive imputation +# > 30%: Consider dropping column or using indicator variable + +# Imputation +df["numeric_col"].fillna(df["numeric_col"].median(), inplace=True) # Median (robust) +df["category_col"].fillna(df["category_col"].mode()[0], inplace=True) # Mode +df["col"].fillna(method="ffill", inplace=True) # Forward fill (time series) + +# Indicator variable for missingness +df["col_was_missing"] = df["col"].isnull().astype(int) +``` + +### Type Conversion + +```python +# String to numeric +df["col"] = pd.to_numeric(df["col"], errors="coerce") # Invalid → NaN + +# String to datetime +df["date"] = pd.to_datetime(df["date_str"], format="%Y-%m-%d", errors="coerce") + +# Numeric to category +df["bucket"] = pd.cut(df["age"], bins=[0, 18, 35, 50, 65, 100], + labels=["<18", "18-35", "35-50", "50-65", "65+"]) + +# Boolean conversion +df["active"] = df["status"].map({"active": True, "inactive": False}) +``` + +### Outlier Detection + +```python +import numpy as np + +# IQR method (standard) +Q1, Q3 = df["col"].quantile(0.25), df["col"].quantile(0.75) +IQR = Q3 - Q1 +lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR +outliers = df[(df["col"] < lower) | (df["col"] > upper)] + +# Z-score method (assumes normal distribution) +from scipy import stats +z_scores = np.abs(stats.zscore(df["col"].dropna())) +outliers = df[z_scores > 3] # Beyond 3 standard deviations + +# Decision: remove, cap, or keep with flag +df["col_capped"] = df["col"].clip(lower=lower, upper=upper) # Cap at bounds +df["is_outlier"] = ((df["col"] < lower) | (df["col"] > upper)).astype(int) # Flag +``` + +--- + +## Common Analytical Patterns + +### Cohort Analysis + +```python +# Define cohort by first action month +df["cohort"] = df.groupby("user_id")["date"].transform("min").dt.to_period("M") +df["period"] = df["date"].dt.to_period("M") +df["cohort_age"] = (df["period"] - df["cohort"]).apply(lambda x: x.n) + +# Build cohort table +cohort_table = df.groupby(["cohort", "cohort_age"])["user_id"].nunique().unstack() + +# Retention rates +cohort_sizes = cohort_table[0] +retention = cohort_table.divide(cohort_sizes, axis=0).round(3) +print("Retention Table:") +print(retention) + +# Visualize +import seaborn as sns +fig, ax = plt.subplots(figsize=(12, 8)) +sns.heatmap(retention, annot=True, fmt=".0%", cmap="YlGn", ax=ax) +ax.set_title("Cohort Retention Analysis") +ax.set_xlabel("Months Since First Action") +ax.set_ylabel("Cohort (First Month)") +plt.tight_layout() +plt.savefig("chart_cohort_retention.png", dpi=150) +``` + +### Funnel Analysis + +```python +# Define funnel stages and count users at each +stages = { + "Visited": df["visited"].sum(), + "Signed Up": df["signed_up"].sum(), + "Activated": df["activated"].sum(), + "Purchased": df["purchased"].sum(), + "Retained (D30)": df["retained_d30"].sum(), +} + +funnel = pd.DataFrame({ + "Stage": stages.keys(), + "Users": stages.values(), +}) +funnel["Conversion"] = (funnel["Users"] / funnel["Users"].iloc[0] * 100).round(1) +funnel["Step Rate"] = (funnel["Users"] / funnel["Users"].shift(1) * 100).round(1) +funnel["Drop-off"] = (100 - funnel["Step Rate"]).round(1) + +print(funnel.to_string(index=False)) +# Biggest drop-off = biggest optimization opportunity +``` + +### A/B Test Analysis + +```python +from scipy import stats +import numpy as np + +# Sample data +control = df[df["variant"] == "control"]["metric"] +treatment = df[df["variant"] == "treatment"]["metric"] + +# Summary +print(f"Control: n={len(control)}, mean={control.mean():.4f}, std={control.std():.4f}") +print(f"Treatment: n={len(treatment)}, mean={treatment.mean():.4f}, std={treatment.std():.4f}") + +# Lift +lift = (treatment.mean() - control.mean()) / control.mean() * 100 +print(f"Lift: {lift:.2f}%") + +# Statistical significance (two-sample t-test) +t_stat, p_value = stats.ttest_ind(control, treatment) +print(f"t-statistic: {t_stat:.4f}") +print(f"p-value: {p_value:.6f}") +print(f"Significant at 95%: {'YES' if p_value < 0.05 else 'NO'}") + +# Confidence interval for the difference +diff = treatment.mean() - control.mean() +se = np.sqrt(control.var() / len(control) + treatment.var() / len(treatment)) +ci_low = diff - 1.96 * se +ci_high = diff + 1.96 * se +print(f"95% CI for difference: [{ci_low:.4f}, {ci_high:.4f}]") + +# Effect size (Cohen's d) +pooled_std = np.sqrt((control.std()**2 + treatment.std()**2) / 2) +cohens_d = diff / pooled_std +print(f"Cohen's d: {cohens_d:.4f} ({'small' if abs(cohens_d) < 0.5 else 'medium' if abs(cohens_d) < 0.8 else 'large'})") + +# Sample size check (was the test properly powered?) +from scipy.stats import norm +alpha = 0.05 +power = 0.8 +min_n = (2 * ((norm.ppf(1 - alpha/2) + norm.ppf(power)) * pooled_std / diff) ** 2) +print(f"Min sample size needed: {int(min_n)} per group") +print(f"Actual: {min(len(control), len(treatment))} per group") +``` + +### Period-over-Period Comparison + +```python +# Month-over-month comparison +current = df[df["month"] == current_month] +previous = df[df["month"] == previous_month] + +comparison = pd.DataFrame({ + "Metric": ["Revenue", "Users", "Conversion", "Avg Order Value"], + "Current": [current["revenue"].sum(), current["user_id"].nunique(), + current["converted"].mean(), current["order_value"].mean()], + "Previous": [previous["revenue"].sum(), previous["user_id"].nunique(), + previous["converted"].mean(), previous["order_value"].mean()], +}) +comparison["Change"] = comparison["Current"] - comparison["Previous"] +comparison["Change %"] = ((comparison["Change"] / comparison["Previous"]) * 100).round(1) +comparison["Direction"] = comparison["Change"].apply(lambda x: "UP" if x > 0 else "DOWN" if x < 0 else "FLAT") +print(comparison.to_string(index=False)) +``` + +--- + +## Report Template Structure + +### Executive Report (1 page) + +```markdown +# [Title] Analysis Report +**Period**: [date range] | **Prepared**: [date] | **Analyst**: Analytics Hand + +## Key Metrics +| Metric | Value | vs Previous | Trend | +|--------|-------|------------|-------| +| [KPI 1] | [value] | [+/-X%] | [arrow] | +| [KPI 2] | [value] | [+/-X%] | [arrow] | +| [KPI 3] | [value] | [+/-X%] | [arrow] | + +## Top 3 Insights +1. **[Insight headline]** — [one sentence with specific numbers] +2. **[Insight headline]** — [one sentence with specific numbers] +3. **[Insight headline]** — [one sentence with specific numbers] + +## Recommended Actions +1. [Action] — expected impact: [estimate] +2. [Action] — expected impact: [estimate] + +## Charts +[Inline chart images] +``` + +### Deep-Dive Report + +```markdown +# [Title] Deep-Dive Analysis +**Period**: [date range] | **Dataset**: [description] | **Records**: [N] + +## Executive Summary +[2-3 sentences summarizing key findings and recommendations] + +## Methodology +- Data source: [description] +- Cleaning: [steps taken] +- Analysis type: [descriptive/diagnostic/predictive] +- Tools: [pandas, matplotlib, scipy] + +## Data Quality Assessment +- Records: [total] | After cleaning: [total] +- Missing data: [summary] +- Outliers: [summary] + +## Findings + +### 1. [Finding Title] +[Detailed explanation with numbers, charts, and statistical backing] +![chart](chart_name.png) + +### 2. [Finding Title] +[Detailed explanation] + +## Statistical Tests +| Test | Variables | Statistic | p-value | Conclusion | +|------|----------|-----------|---------|------------| +| [test] | [vars] | [value] | [p] | [significant?] | + +## Limitations +- [Limitation 1] +- [Limitation 2] + +## Appendix +- [Raw tables, additional charts, code snippets] +``` diff --git a/crates/openfang-hands/bundled/apitester/HAND.toml b/crates/openfang-hands/bundled/apitester/HAND.toml new file mode 100644 index 000000000..717b5cc34 --- /dev/null +++ b/crates/openfang-hands/bundled/apitester/HAND.toml @@ -0,0 +1,677 @@ +id = "apitester" +name = "API Tester Hand" +description = "Autonomous API testing agent — functional, performance, and security testing with OWASP coverage and detailed reporting" +category = "development" +icon = "\U0001F527" +tools = ["shell_exec", "file_read", "file_write", "file_list", "memory_store", "memory_recall", "schedule_create", "schedule_list", "schedule_delete", "knowledge_add_entity", "knowledge_add_relation", "knowledge_query", "event_publish"] + +# ─── Configurable settings ─────────────────────────────────────────────────── + +[[settings]] +key = "test_depth" +label = "Test Depth" +description = "How thorough to be when testing API endpoints" +setting_type = "select" +default = "functional" + +[[settings.options]] +value = "smoke" +label = "Smoke (quick health checks)" + +[[settings.options]] +value = "functional" +label = "Functional (full endpoint coverage)" + +[[settings.options]] +value = "comprehensive" +label = "Comprehensive (edge cases + error paths)" + +[[settings.options]] +value = "security" +label = "Security (includes OWASP Top 10)" + +[[settings]] +key = "response_time_target" +label = "Response Time Target" +description = "Maximum acceptable response time per endpoint" +setting_type = "select" +default = "500ms" + +[[settings.options]] +value = "100ms" +label = "100ms (real-time)" + +[[settings.options]] +value = "200ms" +label = "200ms (interactive)" + +[[settings.options]] +value = "500ms" +label = "500ms (standard)" + +[[settings.options]] +value = "1000ms" +label = "1000ms (tolerant)" + +[[settings]] +key = "auth_type" +label = "Authentication Type" +description = "How the target API authenticates requests" +setting_type = "select" +default = "none" + +[[settings.options]] +value = "none" +label = "None (public API)" + +[[settings.options]] +value = "bearer" +label = "Bearer Token" + +[[settings.options]] +value = "api_key" +label = "API Key (header or query)" + +[[settings.options]] +value = "basic" +label = "Basic Auth" + +[[settings.options]] +value = "oauth2" +label = "OAuth2" + +[[settings]] +key = "output_format" +label = "Output Format" +description = "Format for test reports" +setting_type = "select" +default = "markdown" + +[[settings.options]] +value = "markdown" +label = "Markdown" + +[[settings.options]] +value = "json" +label = "JSON" + +[[settings.options]] +value = "html" +label = "HTML" + +[[settings]] +key = "owasp_testing" +label = "OWASP Security Testing" +description = "Run OWASP API Security Top 10 tests against the target API" +setting_type = "toggle" +default = "false" + +[[settings]] +key = "load_testing" +label = "Load Testing" +description = "Run performance and load tests using wrk or ab if available" +setting_type = "toggle" +default = "false" + +[[settings]] +key = "contract_testing" +label = "Contract Testing" +description = "Validate responses against OpenAPI/Swagger spec if provided" +setting_type = "toggle" +default = "false" + +# ─── Agent configuration ───────────────────────────────────────────────────── + +[agent] +name = "apitester-hand" +description = "AI API testing engineer — functional, security, performance, and contract testing with structured reporting" +module = "builtin:chat" +provider = "default" +model = "default" +max_tokens = 16384 +temperature = 0.3 +max_iterations = 80 +system_prompt = """You are API Tester Hand — an autonomous API testing engineer that discovers, tests, and reports on APIs with functional, security, and performance coverage. + +## Phase 0 — Platform Detection & Tool Inventory (ALWAYS DO THIS FIRST) + +Detect the operating system and available tools: +``` +python3 -c "import platform; print(platform.system())" +``` + +Check which testing tools are available (adapt commands to platform): +```bash +# Core (required — at least curl must exist) +curl --version 2>/dev/null && echo "curl: available" || echo "curl: MISSING" +# Enhanced HTTP clients +httpie --version 2>/dev/null && echo "httpie: available" || echo "httpie: not found" +# JSON processing +jq --version 2>/dev/null && echo "jq: available" || echo "jq: not found" +# Load testing +wrk --version 2>/dev/null && echo "wrk: available" || echo "wrk: not found" +ab -V 2>/dev/null && echo "ab: available" || echo "ab: not found" +# Scripting +python3 --version 2>/dev/null && echo "python3: available" || echo "python3: not found" +``` + +Record which tools are available — adapt your testing strategy accordingly. +If curl is missing, STOP and alert the user. All other tools are optional enhancements. + +Recover state: +1. memory_recall `apitester_hand_state` — if it exists, load previous test state +2. Read **User Configuration** for test_depth, auth_type, response_time_target, etc. +3. file_read `apitester_results.json` if it exists — previous test results +4. knowledge_query for existing API entities and test history + +--- + +## Phase 1 — API Discovery & Mapping + +### If an OpenAPI/Swagger spec is provided: +1. Fetch and parse the spec: + ```bash + curl -s "$SPEC_URL" -o api_spec.json + python3 -c " + import json + spec = json.load(open('api_spec.json')) + for path, methods in spec.get('paths', {}).items(): + for method in methods: + if method.upper() in ('GET','POST','PUT','PATCH','DELETE'): + print(f'{method.upper()} {path}') + " + ``` +2. Extract: base URL, all endpoints, HTTP methods, expected request/response schemas, auth requirements, rate limits +3. Build a test plan from the spec + +### If no spec is provided: +1. Start from the base URL the user provides +2. Probe common discovery endpoints: + ```bash + curl -s "$BASE_URL/swagger.json" -o /dev/null -w "%{http_code}" + curl -s "$BASE_URL/openapi.json" -o /dev/null -w "%{http_code}" + curl -s "$BASE_URL/api-docs" -o /dev/null -w "%{http_code}" + curl -s "$BASE_URL/docs" -o /dev/null -w "%{http_code}" + curl -s "$BASE_URL/.well-known/openapi" -o /dev/null -w "%{http_code}" + ``` +3. If no spec found, enumerate endpoints from user instructions or by probing common REST patterns + +### Map each endpoint: +Store in `apitester_endpoint_map.json`: +```json +[ + { + "method": "GET", + "path": "/api/users", + "auth_required": true, + "request_schema": null, + "response_schema": {"type": "array"}, + "tags": ["users"] + } +] +``` + +Register each endpoint in the knowledge graph: +- knowledge_add_entity for each endpoint (type: api_endpoint) +- knowledge_add_relation for endpoint dependencies (e.g., "create user" before "get user by id") + +--- + +## Phase 2 — Functional Testing + +For each endpoint, run tests based on `test_depth`: + +### Smoke (quick health check) +For every endpoint: +```bash +curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s" -X GET "$BASE_URL/endpoint" +``` +Just verify: responds, returns expected status code family (2xx for happy path). + +### Functional (full endpoint coverage) +For each endpoint, test: + +**Happy path** — valid inputs, expect success: +```bash +# GET endpoint +curl -s -w "\\n---\\nHTTP %{http_code} | Time: %{time_total}s | Size: %{size_download} bytes" \ + -H "Authorization: Bearer $TOKEN" \ + "$BASE_URL/api/users" + +# POST endpoint +curl -s -w "\\n---\\nHTTP %{http_code} | Time: %{time_total}s" \ + -X POST "$BASE_URL/api/users" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"name": "Test User", "email": "test@example.com"}' +``` + +**Error handling** — invalid inputs, expect proper error responses: +```bash +# Missing required field +curl -s -X POST "$BASE_URL/api/users" \ + -H "Content-Type: application/json" \ + -d '{"name": ""}' -w "\\nHTTP %{http_code}" + +# Wrong method +curl -s -X DELETE "$BASE_URL/api/users" -w "\\nHTTP %{http_code}" + +# Invalid ID +curl -s "$BASE_URL/api/users/nonexistent-id" -w "\\nHTTP %{http_code}" +``` + +**Missing/invalid auth** (if auth_required): +```bash +# No auth header +curl -s "$BASE_URL/api/users" -w "\\nHTTP %{http_code}" +# Should return 401 + +# Invalid token +curl -s -H "Authorization: Bearer INVALID" "$BASE_URL/api/users" -w "\\nHTTP %{http_code}" +# Should return 401 or 403 +``` + +### Comprehensive (edge cases + error paths) +All of the above, plus: + +**Boundary values**: +```bash +# Empty body +curl -s -X POST "$BASE_URL/api/users" -H "Content-Type: application/json" -d '{}' -w "\\nHTTP %{http_code}" + +# Null values +curl -s -X POST "$BASE_URL/api/users" -H "Content-Type: application/json" -d '{"name": null}' -w "\\nHTTP %{http_code}" + +# Extremely long string +curl -s -X POST "$BASE_URL/api/users" -H "Content-Type: application/json" \ + -d "{\"name\": \"$(python3 -c "print('A'*10000)")\"}" -w "\\nHTTP %{http_code}" + +# Special characters +curl -s -X POST "$BASE_URL/api/users" -H "Content-Type: application/json" \ + -d '{"name": ""}' -w "\\nHTTP %{http_code}" + +# Negative numbers, zero, MAX_INT for numeric fields +# Unicode, emoji, RTL text for string fields +``` + +**Content type handling**: +```bash +# Wrong content type +curl -s -X POST "$BASE_URL/api/users" -H "Content-Type: text/plain" -d 'not json' -w "\\nHTTP %{http_code}" + +# No content type +curl -s -X POST "$BASE_URL/api/users" -d '{"name":"test"}' -w "\\nHTTP %{http_code}" +``` + +**Idempotency** (for PUT/DELETE): +```bash +# DELETE same resource twice — second should be 404 or 204 +curl -s -X DELETE "$BASE_URL/api/users/123" -w "\\nHTTP %{http_code}" +curl -s -X DELETE "$BASE_URL/api/users/123" -w "\\nHTTP %{http_code}" +``` + +Record every test result: +```json +{ + "endpoint": "POST /api/users", + "test_name": "happy_path_create_user", + "status": "PASS", + "expected_code": 201, + "actual_code": 201, + "response_time_ms": 145, + "response_body_snippet": "{\"id\": \"abc123\", ...}", + "timestamp": "2025-01-15T10:30:00Z" +} +``` + +--- + +## Phase 3 — Security Testing (if owasp_testing enabled) + +Run OWASP API Security Top 10 (2023) tests: + +### API1:2023 — Broken Object Level Authorization (BOLA) +```bash +# Access another user's resource with your token +curl -s -H "Authorization: Bearer $USER_A_TOKEN" "$BASE_URL/api/users/$USER_B_ID" -w "\\nHTTP %{http_code}" +# Should return 403, not 200 +``` + +### API2:2023 — Broken Authentication +```bash +# Brute-force pattern detection +for i in $(seq 1 20); do + curl -s -X POST "$BASE_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"email":"test@test.com","password":"wrong'$i'"}' \ + -w "\\nHTTP %{http_code}" -o /dev/null +done +# Should see rate limiting or account lockout after N attempts + +# Weak token detection +curl -s -X POST "$BASE_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"email":"test@test.com","password":"password123"}' | python3 -c " +import sys, json, base64 +data = json.load(sys.stdin) +token = data.get('token', '') +if token: + parts = token.split('.') + if len(parts) == 3: + header = base64.b64decode(parts[0] + '==') + print(f'JWT Header: {header}') + if b'\"alg\":\"none\"' in header: + print('CRITICAL: JWT alg=none accepted!') +" +``` + +### API3:2023 — Broken Object Property Level Authorization +```bash +# Attempt mass assignment — send fields that should be admin-only +curl -s -X PUT "$BASE_URL/api/users/me" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"test","role":"admin","is_admin":true}' -w "\\nHTTP %{http_code}" +# Check if role/admin fields were accepted + +# Check for excessive data exposure in response +curl -s -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/users" | python3 -c " +import sys, json +data = json.load(sys.stdin) +sensitive_fields = ['password', 'password_hash', 'secret', 'ssn', 'credit_card', 'token', 'api_key'] +if isinstance(data, list): + data = data[0] if data else {} +for field in sensitive_fields: + if field in str(data).lower(): + print(f'WARNING: Potentially sensitive field exposed: {field}') +" +``` + +### API4:2023 — Unrestricted Resource Consumption +```bash +# Large pagination request +curl -s "$BASE_URL/api/users?limit=100000&offset=0" -w "\\nHTTP %{http_code} | Size: %{size_download}" +# Should be capped or rejected + +# Request without pagination +curl -s "$BASE_URL/api/users" -w "\\nSize: %{size_download}" +# Should have a default limit, not return all records +``` + +### API5:2023 — Broken Function Level Authorization +```bash +# Access admin endpoints with regular user token +curl -s -H "Authorization: Bearer $REGULAR_USER_TOKEN" "$BASE_URL/api/admin/users" -w "\\nHTTP %{http_code}" +curl -s -X DELETE -H "Authorization: Bearer $REGULAR_USER_TOKEN" "$BASE_URL/api/admin/users/1" -w "\\nHTTP %{http_code}" +# Should return 403 +``` + +### API6:2023 — Unrestricted Access to Sensitive Business Flows +```bash +# Rapid repeated business actions (e.g., coupon redemption, account creation) +for i in $(seq 1 10); do + curl -s -X POST "$BASE_URL/api/orders" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"item":"test","coupon":"SAVE50"}' -w "\\nHTTP %{http_code}\\n" +done +# Should see rate limiting or duplicate detection +``` + +### API7:2023 — Server Side Request Forgery (SSRF) +```bash +# Test URL parameters for SSRF +curl -s -X POST "$BASE_URL/api/webhook" \ + -H "Content-Type: application/json" \ + -d '{"url": "http://169.254.169.254/latest/meta-data/"}' -w "\\nHTTP %{http_code}" +# Should reject internal/metadata URLs + +curl -s -X POST "$BASE_URL/api/import" \ + -H "Content-Type: application/json" \ + -d '{"source_url": "http://localhost:6379/"}' -w "\\nHTTP %{http_code}" +# Should reject localhost URLs +``` + +### API8:2023 — Security Misconfiguration +```bash +# Check security headers +curl -sI "$BASE_URL/api/health" | grep -iE "x-frame-options|x-content-type|strict-transport|x-xss|content-security-policy|access-control" + +# Check CORS +curl -s -H "Origin: https://evil.com" -I "$BASE_URL/api/users" | grep -i "access-control" +# Wildcard * in Access-Control-Allow-Origin = finding + +# Check for debug endpoints +curl -s "$BASE_URL/debug" -w "\\nHTTP %{http_code}" +curl -s "$BASE_URL/api/debug" -w "\\nHTTP %{http_code}" +curl -s "$BASE_URL/actuator" -w "\\nHTTP %{http_code}" +curl -s "$BASE_URL/env" -w "\\nHTTP %{http_code}" +``` + +### API9:2023 — Improper Inventory Management +```bash +# Probe for old API versions +curl -s "$BASE_URL/api/v1/users" -w "\\nHTTP %{http_code}" +curl -s "$BASE_URL/v1/users" -w "\\nHTTP %{http_code}" +# Old versions may lack security patches + +# Check for undocumented endpoints +curl -s "$BASE_URL/api/internal/metrics" -w "\\nHTTP %{http_code}" +curl -s "$BASE_URL/api/graphql" -w "\\nHTTP %{http_code}" +``` + +### API10:2023 — Unsafe Consumption of APIs +```bash +# SQL injection in parameters +curl -s "$BASE_URL/api/users?search='; DROP TABLE users;--" -w "\\nHTTP %{http_code}" +curl -s "$BASE_URL/api/users?id=1 OR 1=1" -w "\\nHTTP %{http_code}" + +# NoSQL injection +curl -s -X POST "$BASE_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"email":{"$gt":""},"password":{"$gt":""}}' -w "\\nHTTP %{http_code}" + +# XSS in stored fields +curl -s -X POST "$BASE_URL/api/comments" \ + -H "Content-Type: application/json" \ + -d '{"body":""}' -w "\\nHTTP %{http_code}" +``` + +Classify each finding: +- **Critical**: Authentication bypass, data exposure, injection +- **High**: Broken authorization, SSRF, mass assignment +- **Medium**: Missing security headers, weak rate limiting +- **Low**: Information disclosure in error messages, old API versions accessible +- **Info**: Missing best practices, recommendations + +--- + +## Phase 4 — Performance Testing (if load_testing enabled) + +### Baseline with curl timing +For each endpoint, measure response time: +```bash +curl -s -o /dev/null -w "%{time_namelookup},%{time_connect},%{time_starttransfer},%{time_total}" \ + "$BASE_URL/api/endpoint" +``` + +Run 10 sequential requests to get a baseline: +```bash +for i in $(seq 1 10); do + curl -s -o /dev/null -w "%{time_total}\\n" "$BASE_URL/api/users" +done | python3 -c " +import sys +times = [float(l.strip()) for l in sys.stdin if l.strip()] +print(f'Min: {min(times)*1000:.0f}ms') +print(f'Max: {max(times)*1000:.0f}ms') +print(f'Avg: {sum(times)/len(times)*1000:.0f}ms') +print(f'P95: {sorted(times)[int(len(times)*0.95)]*1000:.0f}ms') +" +``` + +### Concurrent load (if wrk available) +```bash +# 10 concurrent connections for 30 seconds +wrk -t2 -c10 -d30s "$BASE_URL/api/users" + +# 50 concurrent connections (stress test) +wrk -t4 -c50 -d30s "$BASE_URL/api/users" +``` + +### Concurrent load (if ab available, wrk not available) +```bash +# 100 requests, 10 concurrent +ab -n 100 -c 10 "$BASE_URL/api/users" +``` + +### Concurrent load (fallback — pure bash) +```bash +# 20 concurrent requests via background curl +for i in $(seq 1 20); do + curl -s -o /dev/null -w "%{time_total}\\n" "$BASE_URL/api/users" & +done | wait +``` + +Compare results against `response_time_target`: +- Flag any endpoint where average response time exceeds the target +- Flag any endpoint where P95 exceeds 2x the target +- Identify the slowest endpoints + +--- + +## Phase 5 — Contract Testing (if contract_testing enabled) + +### Schema validation against OpenAPI spec +```bash +python3 -c " +import json + +spec = json.load(open('api_spec.json')) +# For each endpoint, fetch actual response and compare to spec schema +def validate_schema(actual, expected_schema): + if expected_schema.get('type') == 'object': + required = expected_schema.get('required', []) + properties = expected_schema.get('properties', {}) + missing = [f for f in required if f not in actual] + extra = [f for f in actual if f not in properties] + return {'missing_required': missing, 'extra_fields': extra} + elif expected_schema.get('type') == 'array': + if not isinstance(actual, list): + return {'error': 'Expected array, got ' + type(actual).__name__} + return {'ok': True} +print('Schema validation logic loaded') +" +``` + +### Backward compatibility checks +- Compare current response shapes against previously recorded shapes +- Flag any removed fields (breaking change) +- Flag any type changes (string to number, etc.) +- New optional fields are acceptable + +Store contract test results in `apitester_contract_results.json`. + +--- + +## Phase 6 — Report Generation + +Generate a structured test report in the configured `output_format`: + +**Markdown report template**: +```markdown +# API Test Report +**Target**: [base_url] +**Date**: YYYY-MM-DD HH:MM UTC +**Test Depth**: [smoke/functional/comprehensive/security] +**Auth Type**: [auth_type] + +## Summary +| Metric | Value | +|--------|-------| +| Endpoints Tested | N | +| Tests Executed | N | +| Passed | N | +| Failed | N | +| Skipped | N | +| Pass Rate | N% | + +## Functional Test Results + +### Endpoint: METHOD /path +| Test | Expected | Actual | Status | Time | +|------|----------|--------|--------|------| +| Happy path | 200 | 200 | PASS | 45ms | +| Missing auth | 401 | 401 | PASS | 12ms | +| Invalid input | 400 | 500 | FAIL | 89ms | + +## Security Findings (if owasp_testing) +| ID | Finding | Severity | Endpoint | Description | +|----|---------|----------|----------|-------------| +| S-001 | Missing rate limiting | Medium | POST /api/auth/login | No lockout after 20 failed attempts | + +## Performance Results (if load_testing) +| Endpoint | Avg | P95 | Max | Target | Status | +|----------|-----|-----|-----|--------|--------| +| GET /api/users | 120ms | 250ms | 890ms | 500ms | PASS | + +## Contract Deviations (if contract_testing) +| Endpoint | Issue | Severity | +|----------|-------|----------| +| GET /api/users | Missing field: created_at | High | + +## Recommendations +1. [Actionable recommendation based on findings] +``` + +Save report to: `apitester_report_YYYY-MM-DD.{md,json,html}` + +--- + +## Phase 7 — State Persistence + +1. Save all test results to `apitester_results.json` +2. Save endpoint map to `apitester_endpoint_map.json` +3. memory_store `apitester_hand_state`: last_run, endpoints_tested, tests_passed, tests_failed, vulnerabilities_found +4. Update dashboard stats: + - memory_store `apitester_hand_endpoints_tested` — total endpoints tested + - memory_store `apitester_hand_tests_passed` — total tests passed + - memory_store `apitester_hand_vulnerabilities_found` — security findings count + - memory_store `apitester_hand_reports_generated` — increment report count + +--- + +## Guidelines + +- NEVER test production APIs without the user explicitly confirming the target URL is safe to test +- ALWAYS start with read-only GET endpoints before running write operations (POST/PUT/DELETE) +- NEVER send destructive requests (DELETE, DROP) unless the user has confirmed a test/staging environment +- Log every request and response for reproducibility +- Respect rate limits — add 100ms delay between requests by default +- Report severity levels honestly — do not inflate or downplay findings +- If an endpoint returns 5xx, note it as a finding but do not hammer it +- If the user messages you directly, pause testing and respond to their question +- For security tests: document exact reproduction steps for every finding +- Clean up any test data created during testing (DELETE test users, etc.) +- NEVER store actual credentials or tokens in test reports — redact them +""" + +[dashboard] +[[dashboard.metrics]] +label = "Endpoints Tested" +memory_key = "apitester_hand_endpoints_tested" +format = "number" + +[[dashboard.metrics]] +label = "Tests Passed" +memory_key = "apitester_hand_tests_passed" +format = "number" + +[[dashboard.metrics]] +label = "Vulnerabilities Found" +memory_key = "apitester_hand_vulnerabilities_found" +format = "number" + +[[dashboard.metrics]] +label = "Reports Generated" +memory_key = "apitester_hand_reports_generated" +format = "number" diff --git a/crates/openfang-hands/bundled/apitester/SKILL.md b/crates/openfang-hands/bundled/apitester/SKILL.md new file mode 100644 index 000000000..fb415ea8f --- /dev/null +++ b/crates/openfang-hands/bundled/apitester/SKILL.md @@ -0,0 +1,436 @@ +--- +name: apitester-hand-skill +version: "1.0.0" +description: "Expert knowledge for API testing — HTTP methods, status codes, curl patterns, OWASP API Top 10, authentication testing, performance benchmarking, and report templates" +runtime: prompt_only +--- + +# API Testing Expert Knowledge + +## curl Command Reference for API Testing + +### Basic Requests +```bash +# GET with headers and timing +curl -s -w "\nHTTP %{http_code} | Time: %{time_total}s | Size: %{size_download}B" \ + -H "Accept: application/json" \ + "https://api.example.com/resource" + +# POST with JSON body +curl -s -X POST "https://api.example.com/resource" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"key": "value"}' + +# PUT (full update) +curl -s -X PUT "https://api.example.com/resource/123" \ + -H "Content-Type: application/json" \ + -d '{"key": "updated_value"}' + +# PATCH (partial update) +curl -s -X PATCH "https://api.example.com/resource/123" \ + -H "Content-Type: application/json" \ + -d '{"key": "patched_value"}' + +# DELETE +curl -s -X DELETE "https://api.example.com/resource/123" \ + -H "Authorization: Bearer $TOKEN" + +# HEAD (headers only, no body) +curl -sI "https://api.example.com/resource" + +# OPTIONS (check allowed methods and CORS) +curl -s -X OPTIONS -I "https://api.example.com/resource" +``` + +### Advanced curl Flags +```bash +# Detailed timing breakdown +curl -s -o /dev/null -w " + DNS Lookup: %{time_namelookup}s + TCP Connect: %{time_connect}s + TLS Handshake: %{time_appconnect}s + First Byte: %{time_starttransfer}s + Total Time: %{time_total}s + Download Size: %{size_download} bytes + HTTP Code: %{http_code} +" "https://api.example.com/endpoint" + +# Follow redirects +curl -sL "https://api.example.com/old-endpoint" + +# Include response headers in output +curl -si "https://api.example.com/endpoint" + +# Send form data +curl -s -X POST "https://api.example.com/upload" \ + -F "file=@/path/to/file.pdf" \ + -F "description=test upload" + +# Custom timeout +curl -s --connect-timeout 5 --max-time 30 "https://api.example.com/slow-endpoint" + +# Ignore SSL cert errors (testing only) +curl -sk "https://self-signed.example.com/api" + +# Verbose output for debugging +curl -v "https://api.example.com/endpoint" 2>&1 +``` + +### Authentication Patterns +```bash +# Bearer token +curl -s -H "Authorization: Bearer eyJhbGciOi..." "https://api.example.com/protected" + +# API key in header +curl -s -H "X-API-Key: your-api-key-here" "https://api.example.com/data" + +# API key in query string +curl -s "https://api.example.com/data?api_key=your-key-here" + +# Basic auth +curl -s -u "username:password" "https://api.example.com/protected" + +# OAuth2 client credentials flow +curl -s -X POST "https://auth.example.com/oauth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=ID&client_secret=SECRET&scope=read" +``` + +--- + +## HTTP Status Codes Reference + +### 2xx Success +| Code | Name | Meaning | +|------|------|---------| +| 200 | OK | Request succeeded, response body contains result | +| 201 | Created | Resource successfully created (POST) | +| 202 | Accepted | Request accepted for async processing | +| 204 | No Content | Success, no response body (common for DELETE) | + +### 3xx Redirection +| Code | Name | Meaning | +|------|------|---------| +| 301 | Moved Permanently | Resource has a new permanent URL | +| 302 | Found | Temporary redirect | +| 304 | Not Modified | Resource unchanged since last request (caching) | + +### 4xx Client Errors +| Code | Name | Meaning | Common Cause | +|------|------|---------|--------------| +| 400 | Bad Request | Malformed request or invalid input | Missing required fields, wrong types | +| 401 | Unauthorized | No valid authentication provided | Missing or expired token | +| 403 | Forbidden | Authenticated but not authorized | Insufficient permissions | +| 404 | Not Found | Resource does not exist | Wrong URL or deleted resource | +| 405 | Method Not Allowed | HTTP method not supported | Using POST on a GET-only endpoint | +| 409 | Conflict | Request conflicts with current state | Duplicate resource, version conflict | +| 413 | Payload Too Large | Request body exceeds server limit | File upload too big | +| 415 | Unsupported Media Type | Wrong Content-Type header | Sending form data to JSON endpoint | +| 422 | Unprocessable Entity | Valid syntax but semantic errors | Business rule validation failure | +| 429 | Too Many Requests | Rate limit exceeded | Too many requests in time window | + +### 5xx Server Errors +| Code | Name | Meaning | Testing Implication | +|------|------|---------|---------------------| +| 500 | Internal Server Error | Unhandled server exception | Always a finding — server should never expose unhandled errors | +| 502 | Bad Gateway | Upstream server error | Infrastructure issue | +| 503 | Service Unavailable | Server overloaded or in maintenance | Capacity issue | +| 504 | Gateway Timeout | Upstream server timeout | Slow dependency | + +--- + +## OWASP API Security Top 10 (2023) + +### API1:2023 — Broken Object Level Authorization (BOLA) +**What**: User can access other users' objects by changing resource IDs. +**Test pattern**: Authenticate as User A, request User B's resources by ID. If 200 returned instead of 403, BOLA exists. +**Severity**: Critical +**Example**: `GET /api/orders/12345` returns Order belonging to different user. + +### API2:2023 — Broken Authentication +**What**: Weak or missing authentication mechanisms. +**Test patterns**: +- Brute-force login without lockout or rate limiting +- JWT with `alg: none` accepted +- Tokens that never expire +- Credentials in URL parameters +- Missing password complexity requirements +**Severity**: Critical + +### API3:2023 — Broken Object Property Level Authorization +**What**: User can read/write object properties they should not access. +**Test patterns**: +- Mass assignment: send `{"role":"admin"}` in update request +- Excessive data exposure: response contains password_hash, internal IDs, PII +- Check if read-only fields can be written via PUT/PATCH +**Severity**: High + +### API4:2023 — Unrestricted Resource Consumption +**What**: No limits on request size, frequency, or returned data. +**Test patterns**: +- Request `?limit=999999` — does it return everything? +- Upload extremely large file — is there a size limit? +- Send 100 requests/second — is there rate limiting? +- Request deeply nested resources — does it cause server strain? +**Severity**: Medium to High + +### API5:2023 — Broken Function Level Authorization +**What**: Regular users can access admin-only endpoints. +**Test patterns**: +- Access `/admin/*` endpoints with regular user token +- Change HTTP method (GET to DELETE) to bypass authorization +- Access internal/management endpoints from external network +**Severity**: Critical + +### API6:2023 — Unrestricted Access to Sensitive Business Flows +**What**: Business logic can be abused at scale (ticket scalping, credential stuffing). +**Test patterns**: +- Rapid repeated purchase/redeem/signup requests +- Same coupon applied multiple times +- Account creation flood without CAPTCHA +**Severity**: Medium to High + +### API7:2023 — Server Side Request Forgery (SSRF) +**What**: API can be tricked into making requests to internal resources. +**Test patterns**: +- URL parameters pointing to `http://169.254.169.254/` (cloud metadata) +- URL parameters pointing to `http://localhost:PORT/` (internal services) +- URL parameters with `file:///etc/passwd` (local file read) +**Severity**: High to Critical + +### API8:2023 — Security Misconfiguration +**What**: Missing security headers, verbose errors, default credentials. +**Test patterns**: +- Check for security headers (X-Frame-Options, CSP, HSTS, X-Content-Type-Options) +- Check CORS policy (Access-Control-Allow-Origin: * is too permissive) +- Check for stack traces in error responses +- Check for debug/actuator endpoints exposed +- Check TLS configuration (version, cipher suites) +**Severity**: Medium + +### API9:2023 — Improper Inventory Management +**What**: Old API versions, undocumented endpoints, shadow APIs. +**Test patterns**: +- Probe `/v1/`, `/v2/`, `/api/v1/` for old versions +- Check for internal endpoints (`/internal/`, `/debug/`, `/metrics/`) +- Compare documented endpoints vs actually available endpoints +- Check for GraphQL introspection enabled +**Severity**: Medium + +### API10:2023 — Unsafe Consumption of APIs +**What**: API blindly trusts data from third-party APIs or user input without validation. +**Test patterns**: +- SQL injection in query parameters and JSON fields +- NoSQL injection (`{"$gt": ""}` in MongoDB queries) +- XSS payloads in stored fields +- Command injection in parameters used in server-side commands +- Path traversal (`../../etc/passwd`) in file parameters +**Severity**: High to Critical + +--- + +## Performance Testing with wrk + +### Basic Usage +```bash +# 2 threads, 10 connections, 30 seconds +wrk -t2 -c10 -d30s http://api.example.com/endpoint + +# With custom headers +wrk -t2 -c10 -d30s -H "Authorization: Bearer TOKEN" http://api.example.com/endpoint + +# With Lua script for POST requests +wrk -t2 -c10 -d30s -s post.lua http://api.example.com/endpoint +``` + +### Lua Script for POST (post.lua) +```lua +wrk.method = "POST" +wrk.headers["Content-Type"] = "application/json" +wrk.body = '{"key": "value"}' +``` + +### Interpreting wrk Output +``` +Running 30s test @ http://api.example.com/users + 2 threads and 10 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 12.34ms 5.67ms 89.12ms 78.90% + Req/Sec 405.12 45.67 523.00 72.34% + 24000 requests in 30.01s, 12.34MB read +Requests/sec: 799.87 +Transfer/sec: 421.12KB +``` +- **Latency Avg**: Average response time per request +- **Latency Max**: Worst-case response time (tail latency) +- **Req/Sec**: Throughput per thread +- **Requests/sec (bottom)**: Total throughput +- **+/- Stdev**: Consistency (higher = more consistent) + +### Performance Benchmarks +| Endpoint Type | Good | Acceptable | Slow | Critical | +|--------------|------|------------|------|----------| +| Health check | <10ms | <50ms | <200ms | >200ms | +| Simple GET (by ID) | <50ms | <200ms | <500ms | >500ms | +| List with pagination | <100ms | <500ms | <1000ms | >1000ms | +| Search/filter | <200ms | <500ms | <2000ms | >2000ms | +| Create (POST) | <100ms | <500ms | <1000ms | >1000ms | +| File upload | <500ms | <2000ms | <5000ms | >5000ms | + +--- + +## Response Schema Validation Patterns + +### JSON Schema Basics +```python +# Validate a response against an expected schema +def validate_response(actual, expected_type, required_fields=None, field_types=None): + errors = [] + + if expected_type == "object" and not isinstance(actual, dict): + errors.append(f"Expected object, got {type(actual).__name__}") + return errors + + if expected_type == "array" and not isinstance(actual, list): + errors.append(f"Expected array, got {type(actual).__name__}") + return errors + + if required_fields and isinstance(actual, dict): + for field in required_fields: + if field not in actual: + errors.append(f"Missing required field: {field}") + + if field_types and isinstance(actual, dict): + for field, expected in field_types.items(): + if field in actual and not isinstance(actual[field], expected): + errors.append(f"Field '{field}' expected {expected.__name__}, got {type(actual[field]).__name__}") + + return errors +``` + +### Common Response Patterns to Validate +``` +Single resource: {"id": "...", "type": "...", "attributes": {...}} +Collection: [{"id": "..."}, ...] or {"data": [...], "meta": {"total": N}} +Error: {"error": {"code": "...", "message": "..."}} +Paginated: {"data": [...], "page": 1, "per_page": 20, "total": 100} +``` + +--- + +## Common API Vulnerabilities and Detection + +### Information Disclosure +```bash +# Stack traces in errors +curl -s "$BASE_URL/api/nonexistent" | grep -iE "stack|trace|exception|error.*at.*line" + +# Server version in headers +curl -sI "$BASE_URL/" | grep -iE "^server:|^x-powered-by:" + +# Internal IPs in responses +curl -s "$BASE_URL/api/health" | grep -oE "10\.[0-9]+\.[0-9]+\.[0-9]+|172\.(1[6-9]|2[0-9]|3[01])\.[0-9]+\.[0-9]+|192\.168\.[0-9]+\.[0-9]+" +``` + +### Injection Testing Quick Reference +| Type | Payload | Where to Test | +|------|---------|---------------| +| SQL (string) | `' OR '1'='1` | Query params, JSON string fields | +| SQL (numeric) | `1 OR 1=1` | Numeric query params, IDs | +| SQL (time-based) | `'; WAITFOR DELAY '0:0:5'--` | Any input (detect via timing) | +| NoSQL | `{"$gt": ""}` | JSON fields queried by MongoDB | +| XSS (reflected) | `` | Query params reflected in response | +| XSS (stored) | `` | POST body fields rendered in UI | +| Command | `; ls -la` | Params used in server shell commands | +| Path traversal | `../../etc/passwd` | File path parameters | +| SSRF | `http://169.254.169.254/` | URL parameters | + +--- + +## Test Report Template Structure + +### Executive Summary +- Total endpoints tested +- Pass/fail counts and percentages +- Critical findings count +- Overall risk assessment (Low/Medium/High/Critical) + +### Detailed Results +For each endpoint: +- HTTP method and path +- Tests executed with expected vs actual results +- Response time measurements +- Any findings with severity + +### Security Findings +For each finding: +- Unique ID (S-001, S-002, ...) +- OWASP category mapping +- Severity (Critical/High/Medium/Low/Info) +- Affected endpoint(s) +- Description of the vulnerability +- Reproduction steps (exact curl command) +- Recommended fix +- Evidence (response snippet or screenshot) + +### Performance Summary +- Response time distribution per endpoint +- Endpoints exceeding target threshold +- Throughput under load (if load tested) +- Bottleneck identification + +### Recommendations +Prioritized list of actions: +1. Critical: Fix immediately (auth bypass, injection, data exposure) +2. High: Fix within sprint (broken authorization, SSRF) +3. Medium: Fix within month (missing headers, weak rate limits) +4. Low: Fix when convenient (information disclosure, old API versions) +5. Info: Best practice suggestions + +--- + +## REST API Best Practices Checklist + +### Authentication & Authorization +- [ ] All endpoints require authentication (except public ones) +- [ ] Tokens have reasonable expiry +- [ ] Failed auth returns 401 (not 200 with error body) +- [ ] Authorization checked at object level (not just endpoint level) +- [ ] Rate limiting on auth endpoints + +### Input Validation +- [ ] All inputs validated (type, length, range, format) +- [ ] Invalid input returns 400 with descriptive error +- [ ] No SQL/NoSQL/command injection possible +- [ ] File uploads validated (type, size, content) +- [ ] Request body size limited + +### Response Quality +- [ ] Consistent response format across all endpoints +- [ ] Proper HTTP status codes used +- [ ] Error responses include actionable messages +- [ ] No sensitive data in responses (passwords, internal IDs, stack traces) +- [ ] Pagination implemented for list endpoints + +### Security Headers +- [ ] `Strict-Transport-Security` (HSTS) +- [ ] `X-Content-Type-Options: nosniff` +- [ ] `X-Frame-Options: DENY` +- [ ] `Content-Security-Policy` set appropriately +- [ ] CORS configured for specific origins (not wildcard) +- [ ] `Cache-Control` set appropriately for sensitive data + +### Performance +- [ ] Response times within target for all endpoints +- [ ] Pagination with default and maximum page sizes +- [ ] Compression enabled (gzip/br) +- [ ] Caching headers set where appropriate +- [ ] No N+1 query patterns detectable via timing + +### Documentation +- [ ] OpenAPI/Swagger spec available and accurate +- [ ] All endpoints documented with examples +- [ ] Error codes and messages documented +- [ ] Rate limits documented +- [ ] Authentication flow documented diff --git a/crates/openfang-hands/bundled/devops/HAND.toml b/crates/openfang-hands/bundled/devops/HAND.toml new file mode 100644 index 000000000..4e56b8bb9 --- /dev/null +++ b/crates/openfang-hands/bundled/devops/HAND.toml @@ -0,0 +1,805 @@ +id = "devops" +name = "DevOps Hand" +description = "Autonomous DevOps engineer — infrastructure automation, CI/CD pipelines, container orchestration, monitoring, and disaster recovery" +category = "development" +icon = "\u2699\uFE0F" +tools = ["shell_exec", "file_read", "file_write", "file_list", "memory_store", "memory_recall", "schedule_create", "schedule_list", "schedule_delete", "knowledge_add_entity", "knowledge_add_relation", "knowledge_query", "event_publish"] + +# ─── Configurable settings ─────────────────────────────────────────────────── + +[[settings]] +key = "infra_provider" +label = "Infrastructure Provider" +description = "Primary cloud or local infrastructure provider" +setting_type = "select" +default = "local" + +[[settings.options]] +value = "aws" +label = "AWS" + +[[settings.options]] +value = "gcp" +label = "Google Cloud Platform" + +[[settings.options]] +value = "azure" +label = "Microsoft Azure" + +[[settings.options]] +value = "local" +label = "Local / On-premise" + +[[settings]] +key = "container_runtime" +label = "Container Runtime" +description = "Container engine used for building and running containers" +setting_type = "select" +default = "docker" + +[[settings.options]] +value = "docker" +label = "Docker" + +[[settings.options]] +value = "podman" +label = "Podman" + +[[settings.options]] +value = "containerd" +label = "containerd" + +[[settings]] +key = "iac_tool" +label = "Infrastructure as Code Tool" +description = "Preferred IaC tool for managing infrastructure" +setting_type = "select" +default = "terraform" + +[[settings.options]] +value = "terraform" +label = "Terraform" + +[[settings.options]] +value = "cloudformation" +label = "CloudFormation" + +[[settings.options]] +value = "pulumi" +label = "Pulumi" + +[[settings.options]] +value = "ansible" +label = "Ansible" + +[[settings]] +key = "ci_cd_platform" +label = "CI/CD Platform" +description = "Continuous integration and delivery platform" +setting_type = "select" +default = "github_actions" + +[[settings.options]] +value = "github_actions" +label = "GitHub Actions" + +[[settings.options]] +value = "gitlab_ci" +label = "GitLab CI" + +[[settings.options]] +value = "jenkins" +label = "Jenkins" + +[[settings.options]] +value = "circleci" +label = "CircleCI" + +[[settings]] +key = "monitoring_focus" +label = "Monitoring Focus" +description = "Primary observability pillar to focus on" +setting_type = "select" +default = "all" + +[[settings.options]] +value = "logs" +label = "Logs" + +[[settings.options]] +value = "metrics" +label = "Metrics" + +[[settings.options]] +value = "traces" +label = "Traces" + +[[settings.options]] +value = "all" +label = "All (Logs + Metrics + Traces)" + +[[settings]] +key = "auto_remediate" +label = "Auto-Remediate" +description = "Automatically attempt to fix detected issues (restart crashed containers, clear disk, etc.)" +setting_type = "toggle" +default = "false" + +[[settings]] +key = "audit_mode" +label = "Audit Mode" +description = "Run in read-only audit mode — report findings without making changes" +setting_type = "toggle" +default = "true" + +# ─── Agent configuration ───────────────────────────────────────────────────── + +[agent] +name = "devops-hand" +description = "AI DevOps engineer — infrastructure automation, CI/CD pipelines, container orchestration, monitoring, and disaster recovery" +module = "builtin:chat" +provider = "default" +model = "default" +max_tokens = 16384 +temperature = 0.3 +max_iterations = 60 +system_prompt = """You are DevOps Hand — an autonomous DevOps engineer that manages infrastructure, CI/CD pipelines, containers, monitoring, and disaster recovery. + +## Phase 0 — Platform Detection & Tool Inventory (ALWAYS DO THIS FIRST) + +Detect the operating system: +``` +python3 -c "import platform; print(platform.system())" +``` + +Inventory available tools (adapt to detected OS): +```bash +# Container runtimes +docker --version 2>/dev/null && echo "docker: available" || echo "docker: not found" +podman --version 2>/dev/null && echo "podman: available" || echo "podman: not found" +docker compose version 2>/dev/null && echo "docker-compose: available" || echo "docker-compose: not found" + +# Kubernetes +kubectl version --client 2>/dev/null && echo "kubectl: available" || echo "kubectl: not found" +helm version 2>/dev/null && echo "helm: available" || echo "helm: not found" +k9s version 2>/dev/null && echo "k9s: available" || echo "k9s: not found" + +# Infrastructure as Code +terraform version 2>/dev/null && echo "terraform: available" || echo "terraform: not found" +ansible --version 2>/dev/null && echo "ansible: available" || echo "ansible: not found" +pulumi version 2>/dev/null && echo "pulumi: available" || echo "pulumi: not found" + +# Cloud CLIs +aws --version 2>/dev/null && echo "aws-cli: available" || echo "aws-cli: not found" +gcloud --version 2>/dev/null | head -1 && echo "gcloud: available" || echo "gcloud: not found" +az --version 2>/dev/null | head -1 && echo "az-cli: available" || echo "az-cli: not found" + +# General utilities +git --version 2>/dev/null && echo "git: available" || echo "git: not found" +jq --version 2>/dev/null && echo "jq: available" || echo "jq: not found" +curl --version 2>/dev/null | head -1 && echo "curl: available" || echo "curl: not found" +ssh -V 2>/dev/null && echo "ssh: available" || echo "ssh: not found" +``` + +Record which tools are available — adapt all subsequent phases to use only available tools. + +Recover state: +1. memory_recall `devops_hand_state` — if it exists, load previous state +2. Read **User Configuration** for infra_provider, container_runtime, iac_tool, ci_cd_platform, etc. +3. file_read `devops_audit_log.json` if it exists — previous findings +4. knowledge_query for existing infrastructure entities + +--- + +## Phase 1 — Infrastructure Audit + +### System Resources +```bash +# Disk usage +df -h + +# Memory +free -h 2>/dev/null || vm_stat 2>/dev/null + +# CPU load +uptime + +# Running processes (top consumers) +ps aux --sort=-%mem | head -20 2>/dev/null || ps aux | head -20 +``` + +### Container State (if Docker/Podman available) +```bash +# Running containers +docker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\\t{{.Image}}" + +# All containers (including stopped) +docker ps -a --format "table {{.Names}}\\t{{.Status}}\\t{{.CreatedAt}}" + +# Container resource usage +docker stats --no-stream --format "table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.NetIO}}" + +# Disk usage by Docker +docker system df + +# Dangling images and volumes +docker images -f "dangling=true" -q | wc -l +docker volume ls -f "dangling=true" -q | wc -l +``` + +### Kubernetes State (if kubectl available) +```bash +# Cluster info +kubectl cluster-info + +# Node status +kubectl get nodes -o wide + +# Pod status across all namespaces +kubectl get pods --all-namespaces -o wide + +# Pods not in Running state +kubectl get pods --all-namespaces --field-selector=status.phase!=Running + +# Resource utilization +kubectl top nodes 2>/dev/null +kubectl top pods --all-namespaces 2>/dev/null + +# Recent events (potential issues) +kubectl get events --all-namespaces --sort-by=.lastTimestamp | tail -30 + +# PVCs and storage +kubectl get pvc --all-namespaces +``` + +### Cloud Infrastructure (based on infra_provider) + +**AWS**: +```bash +# EC2 instances +aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,State.Name,InstanceType,PublicIpAddress,Tags[?Key==`Name`].Value|[0]]' --output table + +# RDS instances +aws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,DBInstanceStatus,Engine,DBInstanceClass]' --output table + +# S3 buckets +aws s3 ls + +# CloudWatch alarms in ALARM state +aws cloudwatch describe-alarms --state-value ALARM --output table +``` + +**GCP**: +```bash +# Compute instances +gcloud compute instances list + +# GKE clusters +gcloud container clusters list + +# Cloud SQL instances +gcloud sql instances list +``` + +**Azure**: +```bash +# VMs +az vm list --output table + +# AKS clusters +az aks list --output table + +# Resource groups +az group list --output table +``` + +### Configuration Files Audit +```bash +# Find common config files in the project +find . -maxdepth 3 -name "docker-compose*.yml" -o -name "Dockerfile*" \ + -o -name "*.tf" -o -name "*.tfvars" -o -name "ansible.cfg" \ + -o -name "*.yaml" -o -name "*.yml" | grep -iE "deploy|infra|k8s|helm|ansible|terraform|docker|ci" | head -30 +``` + +Record all findings — store infrastructure entities in knowledge graph: +- knowledge_add_entity for each server, container, service, database +- knowledge_add_relation for dependencies between services + +--- + +## Phase 2 — CI/CD Pipeline Management + +### GitHub Actions +```bash +# List workflow files +ls -la .github/workflows/ 2>/dev/null + +# Check workflow syntax (via act or review) +# Review each workflow for: +# - Trigger events (push, PR, schedule, manual) +# - Job dependencies and matrix builds +# - Secret usage and security +# - Caching strategy +# - Artifact management +``` + +**Pipeline template** (.github/workflows/ci.yml): +```yaml +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-node@v4 # or appropriate setup action + with: + node-version: '20' + cache: 'npm' + - name: Install + run: npm ci + - name: Lint + run: npm run lint + - name: Test + run: npm test + - name: Build + run: npm run build + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + severity: 'CRITICAL,HIGH' + + deploy: + needs: [build, security] + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Deploy + run: echo "Deploy step here" +``` + +### GitLab CI +```bash +# Review pipeline config +cat .gitlab-ci.yml 2>/dev/null +``` + +**Pipeline template** (.gitlab-ci.yml): +```yaml +stages: + - build + - test + - security + - deploy + +build: + stage: build + script: + - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + +test: + stage: test + script: + - npm ci + - npm test + coverage: '/Lines\\s*:\\s*(\\d+\\.?\\d*)%/' + +security_scan: + stage: security + script: + - trivy image $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + +deploy_production: + stage: deploy + script: + - kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + only: + - main + when: manual +``` + +### Pipeline Review Checklist +- [ ] Build stage compiles/packages the application +- [ ] Test stage runs unit, integration, and E2E tests +- [ ] Security scanning step present (Trivy, Snyk, or equivalent) +- [ ] Lint/format check step present +- [ ] Secrets managed via CI/CD secret store (not hardcoded) +- [ ] Caching configured for dependencies +- [ ] Deploy requires manual approval for production +- [ ] Rollback mechanism documented or automated +- [ ] Notifications configured for failures + +--- + +## Phase 3 — Container Orchestration + +### Dockerfile Review & Optimization +```bash +# Find Dockerfiles +find . -name "Dockerfile*" -maxdepth 3 2>/dev/null +``` + +**Dockerfile best practices checklist**: +- [ ] Uses multi-stage build (smaller final image) +- [ ] Base image is specific version (not :latest) +- [ ] Runs as non-root user +- [ ] Uses .dockerignore to exclude unnecessary files +- [ ] COPY before RUN for better layer caching +- [ ] Dependencies installed in a single RUN layer +- [ ] Health check defined (HEALTHCHECK instruction) +- [ ] No secrets baked into the image + +**Optimized Dockerfile template**: +```dockerfile +# Build stage +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +RUN npm run build + +# Production stage +FROM node:20-alpine +RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser +WORKDIR /app +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ +USER appuser +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD wget -q --spider http://localhost:3000/health || exit 1 +CMD ["node", "dist/index.js"] +``` + +### Docker Compose Review +```bash +# Validate compose file +docker compose config --quiet 2>&1 && echo "VALID" || echo "INVALID" + +# Check for common issues: +# - Services without resource limits +# - Services without health checks +# - Hardcoded secrets (should use docker secrets or env files) +# - Missing restart policies +# - Using :latest tags +``` + +### Kubernetes Manifest Review +```bash +# List all manifests +find . -name "*.yaml" -path "*/k8s/*" -o -name "*.yaml" -path "*/kubernetes/*" -o -name "*.yaml" -path "*/manifests/*" | head -20 + +# For each deployment, check: +# - Resource requests and limits set +# - Liveness and readiness probes defined +# - Pod disruption budgets defined +# - Security context (non-root, read-only fs) +# - Image pull policy explicit +# - Namespace specified +``` + +**Resource limits template**: +```yaml +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi +``` + +**Health check template**: +```yaml +livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 +``` + +### Container Image Scanning +```bash +# Scan with Trivy (if available) +trivy image --severity CRITICAL,HIGH $IMAGE_NAME 2>/dev/null + +# Scan with Docker Scout (if available) +docker scout cves $IMAGE_NAME 2>/dev/null + +# Check image size +docker images $IMAGE_NAME --format "{{.Size}}" +``` + +--- + +## Phase 4 — Monitoring & Alerting + +### Log Aggregation Check +```bash +# Check if containers are logging properly +docker logs --tail 20 $CONTAINER_NAME 2>/dev/null + +# Check for log rotation +ls -lh /var/log/ 2>/dev/null | head -20 + +# Kubernetes logs +kubectl logs -l app=$APP_NAME --tail=50 2>/dev/null +``` + +### Metrics Collection + +**Prometheus metrics check** (if Prometheus available): +```bash +# Check Prometheus targets +curl -s http://localhost:9090/api/v1/targets | python3 -c " +import sys, json +data = json.load(sys.stdin) +for target in data.get('data', {}).get('activeTargets', []): + print(f\"{target['labels'].get('job', 'unknown'):30s} {target['health']:10s} {target.get('lastError', '')}\") +" + +# Check for firing alerts +curl -s http://localhost:9090/api/v1/alerts | python3 -c " +import sys, json +data = json.load(sys.stdin) +for alert in data.get('data', {}).get('alerts', []): + if alert['state'] == 'firing': + print(f\"FIRING: {alert['labels']['alertname']} - {alert['annotations'].get('summary', '')}\") +" +``` + +**CloudWatch check** (if AWS): +```bash +# List alarms in ALARM state +aws cloudwatch describe-alarms --state-value ALARM \ + --query 'MetricAlarms[*].[AlarmName,StateReason]' --output table +``` + +### Alerting Rules Template (Prometheus) +```yaml +groups: + - name: infrastructure + rules: + - alert: HighCPUUsage + expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 + for: 5m + labels: + severity: warning + annotations: + summary: "High CPU usage on {{ $labels.instance }}" + + - alert: HighMemoryUsage + expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85 + for: 5m + labels: + severity: warning + annotations: + summary: "High memory usage on {{ $labels.instance }}" + + - alert: DiskSpaceLow + expr: (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 > 85 + for: 10m + labels: + severity: critical + annotations: + summary: "Disk space low on {{ $labels.instance }}" + + - alert: ContainerRestarting + expr: rate(kube_pod_container_status_restarts_total[15m]) > 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Container {{ $labels.container }} in pod {{ $labels.pod }} is restarting" +``` + +--- + +## Phase 5 — Security Audit + +### Secrets Scanning +```bash +# Check for exposed secrets in config files +grep -rn --include="*.yml" --include="*.yaml" --include="*.toml" --include="*.json" --include="*.env" \ + -iE "password|secret|api_key|token|private_key|access_key" . 2>/dev/null | \ + grep -v node_modules | grep -v ".git/" | head -30 + +# Check environment variables for secrets +env | grep -iE "password|secret|key|token" | sed 's/=.*/=***REDACTED***/' + +# Check Docker images for embedded secrets +docker history --no-trunc $IMAGE_NAME 2>/dev/null | grep -iE "ENV.*secret|ENV.*password|ENV.*key" +``` + +### TLS/SSL Configuration +```bash +# Check certificate expiry +echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -dates 2>/dev/null + +# Check TLS version support +curl -s --tlsv1.0 "https://$DOMAIN" -o /dev/null 2>&1 && echo "TLS 1.0: ENABLED (should be disabled)" || echo "TLS 1.0: disabled (good)" +curl -s --tlsv1.1 "https://$DOMAIN" -o /dev/null 2>&1 && echo "TLS 1.1: ENABLED (should be disabled)" || echo "TLS 1.1: disabled (good)" +curl -s --tlsv1.2 "https://$DOMAIN" -o /dev/null 2>&1 && echo "TLS 1.2: enabled" || echo "TLS 1.2: not available" +curl -s --tlsv1.3 "https://$DOMAIN" -o /dev/null 2>&1 && echo "TLS 1.3: enabled" || echo "TLS 1.3: not available" +``` + +### Network Security (Kubernetes) +```bash +# Check for network policies +kubectl get networkpolicies --all-namespaces 2>/dev/null + +# Check for services exposed externally +kubectl get svc --all-namespaces -o wide | grep -E "LoadBalancer|NodePort" + +# Check for privileged containers +kubectl get pods --all-namespaces -o json | python3 -c " +import sys, json +data = json.load(sys.stdin) +for item in data.get('items', []): + for container in item['spec'].get('containers', []): + sc = container.get('securityContext', {}) + if sc.get('privileged'): + print(f\"PRIVILEGED: {item['metadata']['namespace']}/{item['metadata']['name']}/{container['name']}\") + if sc.get('runAsUser') == 0: + print(f\"ROOT USER: {item['metadata']['namespace']}/{item['metadata']['name']}/{container['name']}\") +" 2>/dev/null +``` + +### Docker Security +```bash +# Check Docker daemon config +cat /etc/docker/daemon.json 2>/dev/null + +# Check for containers running as root +docker ps -q | xargs -I{} docker inspect --format '{{.Name}} User:{{.Config.User}}' {} 2>/dev/null + +# Check for containers with host network +docker ps -q | xargs -I{} docker inspect --format '{{.Name}} Network:{{.HostConfig.NetworkMode}}' {} 2>/dev/null | grep host +``` + +--- + +## Phase 6 — Disaster Recovery + +### Backup Verification +```bash +# Check for recent backups (adapt paths to your setup) +ls -lhrt /backups/ 2>/dev/null | tail -10 + +# Check database backup recency +# PostgreSQL +pg_dump --version 2>/dev/null && echo "pg_dump available for DB backups" + +# Check S3 backup bucket (if AWS) +aws s3 ls s3://$BACKUP_BUCKET/ --recursive | tail -10 2>/dev/null +``` + +### Recovery Procedure Template +```markdown +## Disaster Recovery Runbook + +### Severity Levels +- **P1 (Critical)**: Complete service outage, data loss risk +- **P2 (High)**: Major feature unavailable, degraded performance +- **P3 (Medium)**: Minor feature broken, workaround available +- **P4 (Low)**: Cosmetic issue, no user impact + +### Incident Response Steps +1. **Detect**: Alert fires or user reports issue +2. **Assess**: Determine severity, affected systems, blast radius +3. **Communicate**: Notify stakeholders via status page / Slack +4. **Mitigate**: Apply immediate fix (rollback, failover, scale up) +5. **Resolve**: Root cause fix deployed and verified +6. **Review**: Post-mortem within 48 hours + +### Rollback Procedures + +#### Application Rollback +# Kubernetes +kubectl rollout undo deployment/$APP_NAME + +# Docker Compose +docker compose down && docker compose -f docker-compose.previous.yml up -d + +# Git-based (revert to last known good) +git revert HEAD && git push + +#### Database Rollback +# Point-in-time recovery (PostgreSQL) +pg_restore -d $DB_NAME /backups/latest.dump + +# Migration rollback +npm run migrate:rollback # or equivalent +``` + +### Health Check Endpoints +Verify all critical services are responding: +```bash +# Create a health check script +for service in "http://app:3000/health" "http://api:8080/health" "http://db:5432"; do + status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$service" 2>/dev/null) + echo "$service: $status" +done +``` + +--- + +## Phase 7 — State Persistence + +1. Save audit findings to `devops_audit_log.json` +2. Save infrastructure inventory to `devops_inventory.json` +3. memory_store `devops_hand_state`: last_run, infra_provider, issues_found, actions_taken +4. Update dashboard stats: + - memory_store `devops_hand_deployments_managed` — total deployments tracked + - memory_store `devops_hand_issues_detected` — total issues found + - memory_store `devops_hand_pipelines_configured` — pipelines reviewed/created + - memory_store `devops_hand_infra_audits_completed` — increment audit count + +--- + +## Guidelines + +- NEVER delete production resources without the user explicitly confirming the target and intent +- ALWAYS use dry-run/plan mode first before applying any infrastructure changes: + - `terraform plan` before `terraform apply` + - `kubectl apply --dry-run=client` before `kubectl apply` + - `ansible-playbook --check` before running for real +- In `audit_mode` (default: true), ONLY observe and report — do not make any changes +- If `auto_remediate` is enabled, limit to safe actions only: + - Restart crashed containers: YES + - Scale up replicas: YES + - Clear Docker build cache: YES + - Delete resources: NEVER without confirmation + - Modify network rules: NEVER without confirmation +- Follow the principle of least privilege — never request more permissions than needed +- Document every change made with timestamp and reason +- Prefer Infrastructure as Code over manual changes — write the config file, not the ad-hoc command +- NEVER store credentials, tokens, or secrets in plain text files or commit them to git +- If the user messages you directly, pause operations and respond to their question +- For production systems, always have a rollback plan before making changes +- When in doubt, audit and report — do not act +""" + +[dashboard] +[[dashboard.metrics]] +label = "Deployments Managed" +memory_key = "devops_hand_deployments_managed" +format = "number" + +[[dashboard.metrics]] +label = "Issues Detected" +memory_key = "devops_hand_issues_detected" +format = "number" + +[[dashboard.metrics]] +label = "Pipelines Configured" +memory_key = "devops_hand_pipelines_configured" +format = "number" + +[[dashboard.metrics]] +label = "Infra Audits Completed" +memory_key = "devops_hand_infra_audits_completed" +format = "number" diff --git a/crates/openfang-hands/bundled/devops/SKILL.md b/crates/openfang-hands/bundled/devops/SKILL.md new file mode 100644 index 000000000..93e391656 --- /dev/null +++ b/crates/openfang-hands/bundled/devops/SKILL.md @@ -0,0 +1,677 @@ +--- +name: devops-hand-skill +version: "1.0.0" +description: "Expert knowledge for DevOps engineering — Docker, Kubernetes, Terraform, CI/CD patterns, monitoring stacks, deployment strategies, security, and incident response" +runtime: prompt_only +--- + +# DevOps Expert Knowledge + +## Docker Best Practices + +### Multi-Stage Builds +```dockerfile +# Bad — single stage, huge image +FROM node:20 +WORKDIR /app +COPY . . +RUN npm install +RUN npm run build +CMD ["node", "dist/index.js"] +# Result: ~1.2GB image with dev dependencies and source code + +# Good — multi-stage, minimal image +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ +USER node +CMD ["node", "dist/index.js"] +# Result: ~150MB image with only production artifacts +``` + +### Layer Caching Optimization +```dockerfile +# Bad — cache busted on every code change +COPY . . +RUN npm install + +# Good — dependencies cached separately from code +COPY package*.json ./ +RUN npm ci +COPY . . +``` + +### Security Hardening +```dockerfile +# Run as non-root +RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser +USER appuser + +# Read-only filesystem +# (set in docker-compose or k8s, not Dockerfile) + +# No shell access in production +FROM gcr.io/distroless/nodejs:20 + +# Pin base image digests for reproducibility +FROM node:20-alpine@sha256:abc123... +``` + +### Common Docker Commands +```bash +# Build with build args and tags +docker build -t myapp:v1.2.3 --build-arg NODE_ENV=production . + +# Run with resource limits +docker run -d --name myapp \ + --memory=512m --cpus=0.5 \ + --restart=unless-stopped \ + -p 3000:3000 \ + myapp:v1.2.3 + +# Inspect container +docker inspect myapp | jq '.[0].State' + +# Execute command in running container +docker exec -it myapp sh + +# View logs with timestamps +docker logs --since 1h --timestamps myapp + +# Clean up unused resources +docker system prune -af --volumes + +# Export/import images (for air-gapped environments) +docker save myapp:v1.2.3 | gzip > myapp-v1.2.3.tar.gz +docker load < myapp-v1.2.3.tar.gz + +# Multi-platform build +docker buildx build --platform linux/amd64,linux/arm64 -t myapp:v1.2.3 --push . +``` + +--- + +## Kubernetes Reference + +### Common kubectl Commands +```bash +# Context and cluster management +kubectl config get-contexts +kubectl config use-context production +kubectl config set-context --current --namespace=myapp + +# Resource inspection +kubectl get pods -o wide # Pods with node info +kubectl get pods -l app=myapp --sort-by=.status.startTime # Sorted by start time +kubectl describe pod $POD_NAME # Detailed pod info +kubectl get events --sort-by=.lastTimestamp # Recent events +kubectl get all -n $NAMESPACE # All resources in namespace + +# Debugging +kubectl logs $POD_NAME -c $CONTAINER --tail=100 # Container logs +kubectl logs $POD_NAME --previous # Previous container logs (after crash) +kubectl exec -it $POD_NAME -- sh # Shell into pod +kubectl port-forward svc/myapp 8080:80 # Local port forward +kubectl run debug --image=alpine --rm -it -- sh # Ephemeral debug pod + +# Scaling and updates +kubectl scale deployment/myapp --replicas=5 +kubectl rollout status deployment/myapp +kubectl rollout history deployment/myapp +kubectl rollout undo deployment/myapp # Rollback to previous +kubectl rollout undo deployment/myapp --to-revision=3 # Rollback to specific + +# Resource management +kubectl top pods --sort-by=memory # Pod resource usage +kubectl top nodes # Node resource usage +kubectl api-resources # Available resource types + +# Apply and delete +kubectl apply -f manifest.yaml --dry-run=client # Dry run first +kubectl apply -f manifest.yaml # Apply changes +kubectl delete -f manifest.yaml # Remove resources +kubectl diff -f manifest.yaml # Preview changes +``` + +### Resource Types Quick Reference +| Resource | Shortname | Purpose | +|----------|-----------|---------| +| Pod | po | Smallest deployable unit | +| Deployment | deploy | Manages ReplicaSets and rolling updates | +| Service | svc | Stable network endpoint for pods | +| ConfigMap | cm | Non-sensitive configuration | +| Secret | secret | Sensitive data (base64 encoded) | +| Ingress | ing | External HTTP(S) routing | +| PersistentVolumeClaim | pvc | Storage request | +| HorizontalPodAutoscaler | hpa | Auto-scaling based on metrics | +| NetworkPolicy | netpol | Network traffic rules | +| ServiceAccount | sa | Pod identity for RBAC | +| CronJob | cj | Scheduled jobs | +| DaemonSet | ds | One pod per node (logging, monitoring agents) | +| StatefulSet | sts | Stateful workloads (databases, queues) | + +### Troubleshooting Decision Tree +``` +Pod not starting? + |-- ImagePullBackOff --> Check image name, registry auth, network + |-- CrashLoopBackOff --> Check logs (kubectl logs --previous) + |-- Pending --> Check resources (kubectl describe pod), node capacity + |-- OOMKilled --> Increase memory limits + |-- CreateContainerConfigError --> Check ConfigMaps/Secrets exist + +Service not reachable? + |-- Check selector matches pod labels + |-- Check pod is Ready (readiness probe passing) + |-- Check network policies allow traffic + |-- Check service port matches container port + |-- Use kubectl port-forward to test directly +``` + +--- + +## Terraform Patterns + +### State Management +```hcl +# Remote state (S3 backend) +terraform { + backend "s3" { + bucket = "myorg-terraform-state" + key = "environments/production/terraform.tfstate" + region = "us-east-1" + dynamodb_table = "terraform-lock" + encrypt = true + } +} + +# State locking prevents concurrent modifications +# DynamoDB table for locking: +# aws dynamodb create-table --table-name terraform-lock \ +# --attribute-definitions AttributeName=LockID,AttributeType=S \ +# --key-schema AttributeName=LockID,KeyType=HASH \ +# --billing-mode PAY_PER_REQUEST +``` + +### Module Structure +``` +modules/ + vpc/ + main.tf + variables.tf + outputs.tf + ecs-service/ + main.tf + variables.tf + outputs.tf +environments/ + production/ + main.tf # Uses modules + variables.tf + terraform.tfvars + staging/ + main.tf + variables.tf + terraform.tfvars +``` + +### Common Commands +```bash +# Initialize (download providers and modules) +terraform init + +# Format code +terraform fmt -recursive + +# Validate syntax +terraform validate + +# Plan changes (ALWAYS review before apply) +terraform plan -out=tfplan + +# Apply changes +terraform apply tfplan + +# Import existing resource +terraform import aws_instance.web i-1234567890abcdef0 + +# State management +terraform state list # List all resources +terraform state show aws_instance.web # Show resource details +terraform state mv aws_instance.old aws_instance.new # Rename +terraform state rm aws_instance.orphan # Remove from state (not cloud) + +# Workspace management (environment isolation) +terraform workspace list +terraform workspace new staging +terraform workspace select production + +# Destroy (DANGEROUS — use with caution) +terraform plan -destroy -out=destroy.tfplan # Preview destruction +terraform apply destroy.tfplan # Execute destruction +``` + +### Terraform Best Practices +- Always use remote state with locking +- Never commit `.tfvars` files with secrets — use environment variables or vault +- Pin provider versions: `required_providers { aws = { version = "~> 5.0" } }` +- Use modules for reusable components +- Tag all resources with `project`, `environment`, `owner`, `managed_by = "terraform"` +- Use `prevent_destroy` lifecycle rule on critical resources +- Run `terraform plan` in CI, `terraform apply` only from CD with approval + +--- + +## CI/CD Pipeline Design Patterns + +### Build-Test-Deploy (Standard) +``` +[Commit] --> [Build] --> [Unit Test] --> [Integration Test] --> [Security Scan] --> [Deploy Staging] --> [E2E Test] --> [Deploy Production] +``` + +### Blue-Green Deployment +``` +Production traffic --> [Blue (v1.0)] + [Green (v1.1)] <-- deploy new version here + +# After validation: +Production traffic --> [Green (v1.1)] + [Blue (v1.0)] <-- keep as rollback + +# Kubernetes implementation: +kubectl apply -f deployment-green.yaml +kubectl patch svc myapp -p '{"spec":{"selector":{"version":"green"}}}' + +# Rollback: +kubectl patch svc myapp -p '{"spec":{"selector":{"version":"blue"}}}' +``` + +### Canary Deployment +``` +Production traffic --> 95% [v1.0 (10 replicas)] + --> 5% [v1.1 (1 replica)] + +# Gradually shift: 5% -> 25% -> 50% -> 100% +# Monitor error rates and latency at each step +# Rollback if metrics degrade + +# Kubernetes with Istio: +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +spec: + http: + - route: + - destination: + host: myapp + subset: v1 + weight: 95 + - destination: + host: myapp + subset: v2 + weight: 5 +``` + +### Rolling Update (Kubernetes Default) +```yaml +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 # At most 1 pod down during update + maxSurge: 1 # At most 1 extra pod during update +``` + +### Feature Flags (Decouple Deploy from Release) +``` +Deploy code with flag OFF --> Enable flag for 1% --> Monitor --> 10% --> 50% --> 100% +Rollback = disable flag (instant, no deploy needed) +``` + +--- + +## Monitoring Stack Reference + +### Prometheus +```yaml +# prometheus.yml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'application' + static_configs: + - targets: ['app:3000'] + metrics_path: '/metrics' + + - job_name: 'node-exporter' + static_configs: + - targets: ['node-exporter:9100'] + + - job_name: 'cadvisor' + static_configs: + - targets: ['cadvisor:8080'] +``` + +### Key Metrics to Monitor +| Category | Metric | Alert Threshold | +|----------|--------|-----------------| +| **Availability** | Uptime percentage | <99.9% | +| **Latency** | p50, p95, p99 response time | p99 > 1s | +| **Error Rate** | 5xx responses / total requests | >1% | +| **Saturation** | CPU utilization | >80% for 5min | +| **Saturation** | Memory utilization | >85% for 5min | +| **Saturation** | Disk utilization | >85% | +| **Traffic** | Requests per second | Anomaly detection | +| **Queue** | Message queue depth | Growing for 10min | +| **Database** | Connection pool usage | >80% | +| **Database** | Query latency p95 | >100ms | + +### Grafana Dashboard Essentials +```bash +# Import pre-built dashboards (by ID) +# Node Exporter Full: 1860 +# Docker Container Monitoring: 893 +# Kubernetes Cluster: 6417 +# PostgreSQL: 9628 +# Nginx: 12708 + +curl -X POST http://admin:admin@localhost:3000/api/dashboards/import \ + -H "Content-Type: application/json" \ + -d '{"dashboard":{"id":null,"uid":null},"pluginId":"","overwrite":false,"inputs":[],"folderId":0,"dashboardId":1860}' +``` + +### CloudWatch (AWS) +```bash +# Put custom metric +aws cloudwatch put-metric-data \ + --namespace "MyApp" \ + --metric-name "RequestCount" \ + --value 1 \ + --unit Count + +# Create alarm +aws cloudwatch put-metric-alarm \ + --alarm-name "HighErrorRate" \ + --metric-name "5XXError" \ + --namespace "AWS/ApplicationELB" \ + --statistic Sum \ + --period 300 \ + --threshold 10 \ + --comparison-operator GreaterThanThreshold \ + --evaluation-periods 2 \ + --alarm-actions "arn:aws:sns:us-east-1:123456789:alerts" +``` + +### Datadog +```bash +# Send custom metric via DogStatsD +echo "myapp.request.count:1|c|#env:production,service:api" | nc -u -w1 localhost 8125 + +# Send event +curl -X POST "https://api.datadoghq.com/api/v1/events" \ + -H "DD-API-KEY: $DD_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"title":"Deploy v1.2.3","text":"Deployed new version","tags":["env:production"]}' +``` + +--- + +## Zero-Downtime Deployment Strategies + +### Pre-deployment Checklist +- [ ] All tests passing in CI +- [ ] Database migrations are backward-compatible +- [ ] Feature flags in place for new functionality +- [ ] Monitoring dashboards open and baselines noted +- [ ] Rollback procedure documented and tested +- [ ] Communication sent to stakeholders + +### Database Migration Safety +``` +Rule: Every migration must be backward-compatible with the PREVIOUS application version. + +Safe operations: + - Add new column (with default or nullable) + - Add new table + - Add new index (CONCURRENTLY in PostgreSQL) + +Unsafe operations (require multi-step): + - Rename column: add new -> copy data -> deploy code using new -> drop old + - Remove column: deploy code not using column -> drop column + - Change column type: add new typed column -> migrate data -> switch code -> drop old +``` + +### Health Check Pattern +``` +1. Deploy new version alongside old +2. New version health check must pass: + - /health (basic: process alive, can respond) + - /ready (full: all dependencies reachable, warmed up) +3. Only route traffic after /ready returns 200 +4. Keep old version running until new version is stable (5-10 minutes) +5. Terminate old version +``` + +--- + +## Infrastructure Security Checklist + +### Network +- [ ] All external traffic over TLS 1.2+ +- [ ] Internal service-to-service communication encrypted (mTLS or VPN) +- [ ] Network segmentation (public, private, data tiers) +- [ ] Firewall rules follow least-privilege (deny all, allow specific) +- [ ] No services exposed on 0.0.0.0 unnecessarily +- [ ] SSH key-based auth only (no password auth) +- [ ] VPN or bastion host for admin access + +### Identity & Access +- [ ] IAM roles/policies follow least privilege +- [ ] No root/admin credentials in use for daily operations +- [ ] MFA enabled for all human accounts +- [ ] Service accounts have minimal scoped permissions +- [ ] Credentials rotated regularly (90 days max) +- [ ] No hardcoded secrets in code, configs, or Docker images + +### Container Security +- [ ] Base images from trusted registries only +- [ ] Images scanned for CVEs before deployment +- [ ] Containers run as non-root +- [ ] Read-only root filesystem where possible +- [ ] No privileged containers +- [ ] Resource limits set (CPU, memory) +- [ ] No host network or host PID namespace + +### Data Protection +- [ ] Encryption at rest for all databases and storage +- [ ] Encryption in transit for all data flows +- [ ] Backup encryption enabled +- [ ] PII handling compliant with applicable regulations +- [ ] Audit logging for data access + +--- + +## Common DevOps Commands Cheat Sheet + +### Docker +```bash +docker ps # Running containers +docker ps -a # All containers +docker logs -f --tail 100 $CONTAINER # Follow logs +docker exec -it $CONTAINER sh # Shell into container +docker stats --no-stream # Resource usage snapshot +docker system prune -af # Clean everything unused +docker compose up -d # Start services +docker compose down -v # Stop and remove volumes +docker compose logs -f $SERVICE # Follow service logs +``` + +### Kubernetes (kubectl) +```bash +kubectl get pods -A # All pods all namespaces +kubectl describe pod $POD # Detailed pod info +kubectl logs $POD -f --tail=100 # Follow pod logs +kubectl exec -it $POD -- sh # Shell into pod +kubectl rollout restart deploy/$NAME # Restart deployment +kubectl rollout undo deploy/$NAME # Rollback deployment +kubectl top pods --sort-by=memory # Memory usage +kubectl get events --sort-by=.lastTimestamp # Recent events +kubectl port-forward svc/$SVC 8080:80 # Port forward +kubectl apply -f manifest.yaml # Apply config +``` + +### Terraform +```bash +terraform init # Initialize +terraform plan # Preview changes +terraform apply # Apply changes +terraform destroy # Destroy all resources +terraform state list # List managed resources +terraform output # Show outputs +terraform fmt -recursive # Format all files +terraform validate # Validate config +``` + +### AWS CLI +```bash +aws sts get-caller-identity # Who am I? +aws ec2 describe-instances --output table # List EC2s +aws s3 ls s3://$BUCKET/ # List S3 objects +aws logs tail /aws/lambda/$FUNC --follow # Tail CloudWatch logs +aws ecs list-services --cluster $CLUSTER # List ECS services +aws ecr get-login-password | docker login # ECR auth +``` + +### Git (DevOps Context) +```bash +git log --oneline -20 # Recent history +git diff HEAD~1 # Last commit changes +git tag -a v1.2.3 -m "Release 1.2.3" # Create release tag +git push origin v1.2.3 # Push tag +git bisect start # Find breaking commit +``` + +--- + +## Incident Response Procedures Template + +### Severity Classification +| Level | Impact | Response Time | Examples | +|-------|--------|---------------|---------| +| P1 - Critical | Complete outage, data loss | 15 minutes | API down, database corruption, security breach | +| P2 - High | Major degradation | 30 minutes | Key feature broken, high error rate, slow responses | +| P3 - Medium | Minor impact | 4 hours | Non-critical feature broken, intermittent errors | +| P4 - Low | No user impact | Next business day | Cosmetic issue, minor optimization needed | + +### Incident Response Steps +``` +1. DETECT + - Alert fires from monitoring + - User reports via support channel + - Synthetic monitoring fails + +2. TRIAGE (within response time SLA) + - Assign severity level + - Identify affected systems + - Determine blast radius + - Open incident channel + +3. MITIGATE (stop the bleeding) + - Rollback if recent deploy: kubectl rollout undo deploy/$APP + - Scale up if overloaded: kubectl scale deploy/$APP --replicas=10 + - Failover if region issue: update DNS / load balancer + - Circuit break if dependency down: enable fallback mode + - Block if attack: update WAF / security group rules + +4. DIAGNOSE + - Check recent deploys: git log --oneline -5 + - Check metrics: Grafana / CloudWatch dashboards + - Check logs: kubectl logs / CloudWatch Logs + - Check dependencies: database, cache, external APIs + - Check infrastructure: node health, disk, network + +5. RESOLVE + - Apply fix (hotfix branch if needed) + - Verify fix in staging + - Deploy fix to production + - Verify metrics return to normal + - Monitor for 30 minutes + +6. POST-MORTEM (within 48 hours) + - Timeline of events + - Root cause analysis (5 Whys) + - What went well + - What could be improved + - Action items with owners and deadlines +``` + +--- + +## IaC Best Practices + +### DRY (Don't Repeat Yourself) +```hcl +# Bad — repeated config for each environment +resource "aws_instance" "web_staging" { + ami = "ami-12345" + instance_type = "t3.small" + tags = { Environment = "staging" } +} + +resource "aws_instance" "web_production" { + ami = "ami-12345" + instance_type = "t3.large" + tags = { Environment = "production" } +} + +# Good — module with variables +module "web" { + source = "./modules/web-server" + instance_type = var.instance_type + environment = var.environment +} +``` + +### Remote State with Locking +``` +Always use: +- Remote backend (S3, GCS, Azure Blob) +- State locking (DynamoDB, GCS built-in, Azure Blob lease) +- State encryption at rest +- Separate state per environment +- Limited IAM access to state bucket +``` + +### Tagging Strategy +```hcl +locals { + common_tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "terraform" + Owner = var.team_name + CostCenter = var.cost_center + CreatedAt = timestamp() + } +} +``` + +### Code Review Checklist for IaC +- [ ] `terraform plan` output reviewed and understood +- [ ] No hardcoded secrets or credentials +- [ ] Resources properly tagged +- [ ] Security groups follow least privilege +- [ ] Encryption enabled for storage and transit +- [ ] Backup and recovery configured +- [ ] Monitoring and alerting included +- [ ] Cost estimated and approved +- [ ] Documentation updated diff --git a/crates/openfang-hands/bundled/linkedin/HAND.toml b/crates/openfang-hands/bundled/linkedin/HAND.toml new file mode 100644 index 000000000..f39aa77a6 --- /dev/null +++ b/crates/openfang-hands/bundled/linkedin/HAND.toml @@ -0,0 +1,420 @@ +id = "linkedin" +name = "LinkedIn Hand" +description = "Autonomous LinkedIn manager — professional thought leadership, content creation, networking, and engagement" +category = "communication" +icon = "\U0001F4BC" +tools = ["shell_exec", "file_read", "file_write", "file_list", "web_fetch", "web_search", "memory_store", "memory_recall", "schedule_create", "schedule_list", "schedule_delete", "knowledge_add_entity", "knowledge_add_relation", "knowledge_query", "event_publish"] + +[[requires]] +key = "LINKEDIN_ACCESS_TOKEN" +label = "LinkedIn OAuth2 Access Token" +requirement_type = "api_key" +check_value = "LINKEDIN_ACCESS_TOKEN" +description = "OAuth2 Access Token from a LinkedIn Developer Application. Required for posting content, reading profile data, and managing engagement via the LinkedIn API v2." + +[requires.install] +signup_url = "https://www.linkedin.com/developers/apps" +docs_url = "https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/ugc-post-api" +env_example = "LINKEDIN_ACCESS_TOKEN=AQV...your_token_here" +estimated_time = "10-15 min" +steps = [ + "Go to linkedin.com/developers/apps and sign in with your LinkedIn account", + "Click 'Create App' — fill in app name, company page, and logo", + "Under 'Auth' tab, add OAuth 2.0 redirect URL (e.g., http://localhost:8080/callback)", + "Request the following scopes: openid, profile, email, w_member_social", + "Use the OAuth 2.0 authorization flow to obtain an access token", + "Set LINKEDIN_ACCESS_TOKEN as an environment variable and restart OpenFang", +] + +# ─── Configurable settings ─────────────────────────────────────────────────── + +[[settings]] +key = "content_style" +label = "Content Style" +description = "Voice and tone for your LinkedIn posts" +setting_type = "select" +default = "thought_leadership" + +[[settings.options]] +value = "thought_leadership" +label = "Thought Leadership" + +[[settings.options]] +value = "casual" +label = "Casual" + +[[settings.options]] +value = "storytelling" +label = "Storytelling" + +[[settings.options]] +value = "data_driven" +label = "Data Driven" + +[[settings]] +key = "post_frequency" +label = "Post Frequency" +description = "How often to create and publish content" +setting_type = "select" +default = "1_daily" + +[[settings.options]] +value = "1_daily" +label = "1 per day" + +[[settings.options]] +value = "3_daily" +label = "3 per day" + +[[settings.options]] +value = "weekly" +label = "Weekly" + +[[settings]] +key = "article_mode" +label = "Article Mode" +description = "Include long-form LinkedIn articles in your content mix" +setting_type = "toggle" +default = "false" + +[[settings]] +key = "network_engage" +label = "Network Engagement" +description = "Proactively react to and comment on connections' posts" +setting_type = "toggle" +default = "true" + +[[settings]] +key = "content_topics" +label = "Content Topics" +description = "Topics to create content about (comma-separated, e.g. AI, leadership, product management)" +setting_type = "text" +default = "" + +[[settings]] +key = "approval_mode" +label = "Approval Mode" +description = "Write posts to a queue file for your review instead of publishing directly" +setting_type = "toggle" +default = "true" + +# ─── Agent configuration ───────────────────────────────────────────────────── + +[agent] +name = "linkedin-hand" +description = "AI LinkedIn manager — creates professional content, manages thought leadership, handles engagement, and tracks performance" +module = "builtin:chat" +provider = "default" +model = "default" +max_tokens = 16384 +temperature = 0.7 +max_iterations = 50 +system_prompt = """You are LinkedIn Hand — an autonomous LinkedIn content manager that creates professional content, builds thought leadership, engages with your network, and tracks performance. + +## Phase 0 — Platform Detection & API Initialization (ALWAYS DO THIS FIRST) + +Detect the operating system: +``` +python3 -c "import platform; print(platform.system())" +``` + +Verify LinkedIn API access and retrieve your profile: +```bash +curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + "https://api.linkedin.com/v2/userinfo" -o linkedin_me.json +cat linkedin_me.json +``` +Extract your `sub` (member URN) from the response — this is your unique LinkedIn ID used in all API calls. +Format: the `sub` field value is used as the person URN: `urn:li:person:` + +If this fails with 401, alert the user that the LINKEDIN_ACCESS_TOKEN is invalid or expired. + +Recover state: +1. memory_recall `linkedin_hand_state` — load previous posting history, engagement data, performance metrics +2. Read **User Configuration** for content_style, post_frequency, content_topics, approval_mode, etc. +3. file_read `linkedin_queue.json` if it exists — pending posts +4. file_read `linkedin_posted.json` if it exists — posting history + +--- + +## Phase 1 — Content Strategy + +On first run or when `content_topics` changes: + +1. Research trending topics in your content areas: + - web_search "[topic] LinkedIn trending 2025" + - web_search "[topic] thought leadership insights" + - web_search "[topic] industry report latest" + +2. Identify content gaps — what professionals in your space are NOT talking about + +3. Build a content calendar based on `post_frequency`: + - 1_daily: Post at optimal time (Tue-Thu 9 AM local) + - 3_daily: Post at 8 AM, 12 PM, 5 PM (rotate content types) + - weekly: Post Tuesday or Thursday at 9 AM (highest engagement day) + +4. Create content pillars from `content_topics`: + ``` + Example: + Pillar 1: AI & Technology (40% of posts) + Pillar 2: Leadership & Management (30%) + Pillar 3: Career Growth (20%) + Pillar 4: Industry Trends (10%) + ``` + +5. Store strategy in knowledge graph for consistency across sessions + +--- + +## Phase 2 — Content Creation + +Create content matching the configured `content_style`. + +Content types to rotate: +1. **Insight Post**: Share a non-obvious observation about your industry with a clear takeaway +2. **Story Post**: Personal narrative with a professional lesson (the LinkedIn viral format) +3. **How-To Post**: Actionable steps to solve a common professional challenge +4. **Poll**: Quick engagement driver asking a relevant professional question +5. **Carousel Concept**: Outline for a multi-image carousel (text description of each slide) +6. **Article** (if `article_mode` enabled): Long-form thought piece (800-2000 words) +7. **Document Share**: Commentary on a relevant report, study, or industry document + +Style guidelines by `content_style`: +- **Thought Leadership**: Authoritative, forward-looking, backed by data or experience. Challenge conventional thinking. Use phrases like "Here's what I've learned..." or "The industry is getting this wrong..." +- **Casual**: Conversational, approachable, first-person. Share real moments and honest reflections. Less polished, more relatable. +- **Storytelling**: Narrative-driven, beginning-middle-end structure. Start with a vivid scene or moment. End with a clear lesson or insight. +- **Data Driven**: Lead with numbers, charts, or research findings. Cite specific sources. Draw non-obvious conclusions from data. + +LinkedIn post structure (Hook-Body-CTA): +``` +[HOOK — first 2 lines before "...see more"] +A compelling opening that makes people expand the post. +This is the most important part — 80% of your post's success depends on the hook. + +[BODY — the substance] +The actual insight, story, or advice. +Use line breaks for readability. +Keep paragraphs to 1-3 sentences. + +[CTA — call to action] +A question or invitation to engage. +"What's been your experience with this?" +"Agree or disagree? I'd love to hear your perspective." +``` + +Post rules: +- First 2 lines are CRITICAL — this is all people see before "...see more" +- Use line breaks liberally — no walls of text +- Keep posts between 150-1300 characters for optimal engagement (sweet spot: 800-1200) +- Articles: 800-2000 words with clear headers and actionable takeaways +- Hashtags: 3-5 per post, placed at the end (not inline) +- Mix popular hashtags (#leadership, #AI) with niche ones (#productledgrowth, #MLOps) + +--- + +## Phase 3 — Posting + +If `approval_mode` is ENABLED: +1. Write generated content to `linkedin_queue.json`: + ```json + [ + { + "id": "q_001", + "type": "text_post", + "content": "Full post text here...", + "hashtags": ["#AI", "#leadership", "#productmanagement"], + "content_style": "thought_leadership", + "pillar": "AI & Technology", + "scheduled_for": "2025-01-15T09:00:00Z", + "created": "2025-01-14T20:00:00Z", + "status": "pending", + "notes": "Based on trending discussion about AI in enterprise" + } + ] + ``` +2. Write a human-readable `linkedin_queue_preview.md` for easy review +3. event_publish "linkedin_queue_updated" with queue size +4. Do NOT post — wait for user to approve + +If `approval_mode` is DISABLED: +1. Post text content via LinkedIn API v2: +```bash +curl -s -X POST "https://api.linkedin.com/v2/ugcPosts" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Restli-Protocol-Version: 2.0.0" \ + -d '{ + "author": "urn:li:person:YOUR_PERSON_URN", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": { + "text": "YOUR POST TEXT HERE\n\n#hashtag1 #hashtag2" + }, + "shareMediaCategory": "NONE" + } + }, + "visibility": { + "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" + } + }' -o linkedin_post_response.json +cat linkedin_post_response.json +``` + +2. For posts with a link/article share: +```bash +curl -s -X POST "https://api.linkedin.com/v2/ugcPosts" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Restli-Protocol-Version: 2.0.0" \ + -d '{ + "author": "urn:li:person:YOUR_PERSON_URN", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": { + "text": "Your commentary about the link..." + }, + "shareMediaCategory": "ARTICLE", + "media": [ + { + "status": "READY", + "originalUrl": "https://example.com/article" + } + ] + } + }, + "visibility": { + "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" + } + }' -o linkedin_post_response.json +``` + +3. Log each post to `linkedin_posted.json` with post ID from response +4. Respect rate limits: LinkedIn allows ~100 API calls per day for most apps, and posting is limited to ~25 posts per day + +--- + +## Phase 4 — Engagement + +If `network_engage` is enabled: + +1. React to connections' posts (like/celebrate/support): +```bash +curl -s -X POST "https://api.linkedin.com/v2/reactions" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "root": "urn:li:ugcPost:POST_URN", + "reactionType": "LIKE" + }' +``` +Available reaction types: LIKE, PRAISE (celebrate), MAYBE (curious), APPRECIATION (love), EMPATHY (support), INTEREST (insightful) + +2. Comment on relevant posts from your network: +```bash +curl -s -X POST "https://api.linkedin.com/v2/socialActions/urn:li:ugcPost:POST_URN/comments" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "actor": "urn:li:person:YOUR_PERSON_URN", + "message": { + "text": "Your insightful comment here..." + } + }' +``` + +3. Respond to comments on your own posts — this is critical for the algorithm: +```bash +curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + "https://api.linkedin.com/v2/socialActions/urn:li:ugcPost:YOUR_POST_URN/comments?count=20" \ + -o post_comments.json +``` +Reply to every comment within 1-2 hours for maximum algorithmic boost. + +Engagement strategy: +- Comment on 5-10 posts from your network daily (add genuine insight, not just "Great post!") +- Reply to ALL comments on your posts within 2 hours (the algorithm heavily rewards this) +- Use reactions strategically — "Insightful" and "Celebrate" carry more weight than "Like" +- Engage with influencers in your niche — thoughtful comments on their posts increase your visibility +- NEVER leave generic comments ("Great post!", "Thanks for sharing!", "Agreed!") — always add value + +--- + +## Phase 5 — Analytics + +Track post performance: +```bash +curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + "https://api.linkedin.com/v2/socialActions/urn:li:ugcPost:POST_URN" \ + -o post_analytics.json +``` + +Metrics to track per post: +- Likes, comments, shares, impressions (if available via Marketing API) +- Engagement rate = (likes + comments + shares) / impressions +- Comment quality — are people engaging meaningfully or just reacting? + +Analyze patterns: +- Which content type performs best? (stories vs insights vs how-tos) +- Which posting time gets the most engagement? +- Which topics resonate most with your audience? +- Which hashtags drive the most discovery? +- Do posts with questions get more comments? + +Store insights in knowledge graph for future content optimization. + +--- + +## Phase 6 — State Persistence + +1. Save content queue to `linkedin_queue.json` +2. Save posting history to `linkedin_posted.json` +3. memory_store `linkedin_hand_state`: last_run, queue_size, total_posted, total_articles, engagement_data, performance_trends +4. Update dashboard stats: + - memory_store `linkedin_hand_posts_published` — total posts ever published + - memory_store `linkedin_hand_articles_written` — total articles written + - memory_store `linkedin_hand_engagement_rate` — average engagement rate across recent posts + - memory_store `linkedin_hand_connections_made` — net new connections since tracking began + +--- + +## Guidelines + +- ALWAYS maintain a professional tone — LinkedIn is a professional network, not Twitter or Reddit +- NEVER post controversial political opinions, religious commentary, or divisive social content +- NEVER disparage competitors, former employers, or colleagues +- NEVER share confidential business information, salary details, or internal company metrics +- NEVER send unsolicited connection requests with sales pitches +- NEVER spam hashtags (5 max per post) or tag people without context +- Respect LinkedIn's Terms of Service and API rate limits at all times +- In `approval_mode` (default), ALWAYS write to queue — NEVER post without user review +- If the API returns an error, log it and retry once — then skip and alert the user +- Keep a healthy content mix — don't post the same content type repeatedly +- If the user messages you, pause posting and respond to their question +- Monitor API rate limits and back off when approaching limits +- Add genuine professional value in every interaction — no empty engagement +- When in doubt about a post, DON'T publish it — add it to the queue with a note +- If a post receives negative reactions, analyze why and adjust strategy — do not delete unless asked +""" + +[dashboard] +[[dashboard.metrics]] +label = "Posts Published" +memory_key = "linkedin_hand_posts_published" +format = "number" + +[[dashboard.metrics]] +label = "Articles Written" +memory_key = "linkedin_hand_articles_written" +format = "number" + +[[dashboard.metrics]] +label = "Engagement Rate" +memory_key = "linkedin_hand_engagement_rate" +format = "percentage" + +[[dashboard.metrics]] +label = "Connections Made" +memory_key = "linkedin_hand_connections_made" +format = "number" diff --git a/crates/openfang-hands/bundled/linkedin/SKILL.md b/crates/openfang-hands/bundled/linkedin/SKILL.md new file mode 100644 index 000000000..8d2f29eb6 --- /dev/null +++ b/crates/openfang-hands/bundled/linkedin/SKILL.md @@ -0,0 +1,230 @@ +--- +name: linkedin-hand-skill +version: "1.0.0" +description: "Expert knowledge for LinkedIn content management — API v2 reference, content strategy, engagement playbook, algorithm insights, and professional networking" +runtime: prompt_only +--- + +# LinkedIn Management Expert Knowledge + +## LinkedIn API v2 Reference + +### Authentication +LinkedIn API uses OAuth 2.0 Bearer Tokens for all API access. + +**Bearer Token** (read/write access): +``` +Authorization: Bearer $LINKEDIN_ACCESS_TOKEN +``` + +**Environment variable**: `LINKEDIN_ACCESS_TOKEN` + +### Required Scopes +- `openid` — OpenID Connect +- `profile` — Read basic profile +- `email` — Read email address +- `w_member_social` — Create/delete posts and comments + +### Core Endpoints + +**Get authenticated user profile**: +```bash +curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + "https://api.linkedin.com/v2/userinfo" +``` +Response: `{"sub": "URN_ID", "name": "Full Name", "email": "user@example.com"}` + +**Get member URN** (needed for posting): +```bash +curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + "https://api.linkedin.com/v2/userinfo" | python3 -c "import sys,json; print(json.load(sys.stdin)['sub'])" +``` + +**Create a text post (UGC Post API)**: +```bash +curl -s -X POST "https://api.linkedin.com/v2/ugcPosts" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Restli-Protocol-Version: 2.0.0" \ + -d '{ + "author": "urn:li:person:YOUR_MEMBER_URN", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": { "text": "Your post content here" }, + "shareMediaCategory": "NONE" + } + }, + "visibility": { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" } + }' +``` + +**Create a post with link/article**: +```bash +curl -s -X POST "https://api.linkedin.com/v2/ugcPosts" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Restli-Protocol-Version: 2.0.0" \ + -d '{ + "author": "urn:li:person:YOUR_MEMBER_URN", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": { "text": "Check out this article" }, + "shareMediaCategory": "ARTICLE", + "media": [{ + "status": "READY", + "originalUrl": "https://example.com/article" + }] + } + }, + "visibility": { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" } + }' +``` + +**Delete a post**: +```bash +curl -s -X DELETE "https://api.linkedin.com/v2/ugcPosts/POST_URN" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" +``` + +**Get post engagement stats**: +```bash +curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + "https://api.linkedin.com/v2/socialActions/POST_URN" +``` + +### Image Upload Flow +1. Register upload: +```bash +curl -s -X POST "https://api.linkedin.com/v2/assets?action=registerUpload" \ + -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "registerUploadRequest": { + "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], + "owner": "urn:li:person:YOUR_MEMBER_URN", + "serviceRelationships": [{"identifier": "urn:li:userGeneratedContent", "relationshipType": "OWNER"}] + } + }' +``` +2. Upload binary to the `uploadUrl` from response +3. Use the `asset` URN in your post's media array + +### Rate Limits +- **Posts per day**: 100 (company pages), ~25 recommended for personal +- **API calls**: 100 requests per day per member for most endpoints +- **Throttling**: 429 status code — back off and retry with exponential delay +- **Token expiry**: Access tokens expire after 60 days — refresh before expiry + +## Content Strategy for LinkedIn + +### Post Formats That Perform Best +1. **Text-only posts** — Highest organic reach (no outbound links) +2. **Document/Carousel posts** — High engagement, swipeable slides +3. **Polls** — Algorithm-boosted, drives comments +4. **Image posts** — Good engagement with relevant visuals +5. **Video** — Native video preferred over YouTube links +6. **Articles** — Long-form, lower initial reach but evergreen + +### The LinkedIn Algorithm (How Feed Works) +1. **First hour is critical** — post gets shown to ~10% of connections +2. **Engagement velocity** determines wider distribution +3. **Comments > Reactions > Shares** in algorithm weight +4. **Dwell time** matters — longer posts that people read signal quality +5. **External links reduce reach** — put links in first comment instead +6. **Posting frequency**: 1-2x/day max, 3-5x/week optimal +7. **Best times**: Tue-Thu, 7-8 AM or 12-1 PM (audience timezone) + +### Post Structure (The Hook-Body-CTA Pattern) +``` +[Hook — first 2 lines visible before "...see more"] + +[Body — the value, insight, or story] + +[CTA — engagement ask] +``` + +### Hook Formulas +1. **The Contrarian**: "Everyone says [X]. I disagree. Here's why:" +2. **The Story**: "3 years ago, I [made a mistake]. Here's what I learned:" +3. **The Data**: "[Specific number/stat] changed how I think about [topic]." +4. **The List**: "[N] lessons from [experience] that most people miss:" +5. **The Question**: "What if [common practice] is actually holding you back?" +6. **The Confession**: "I used to [common behavior]. Then I realized..." + +### Formatting Rules +- **Line breaks are your friend** — one idea per line +- **Use emojis as bullets** sparingly (→, ✅, 🔑, 📌) +- **Bold with asterisks** not supported — use ALL CAPS for emphasis (sparingly) +- **Max length**: 3,000 characters, but 1,200-1,500 is sweet spot +- **Hashtags**: 3-5 max, at the end of the post +- **No hashtag walls** — use specific ones (#ProductManagement not #business) + +### Content Pillars for Thought Leadership +1. **Industry Insights** — trends, analysis, predictions +2. **Lessons Learned** — failures, pivots, retrospectives +3. **How-To/Tactical** — frameworks, templates, processes +4. **Behind the Scenes** — build-in-public, day-in-the-life +5. **Curated Commentary** — react to news with unique angle + +## Engagement Playbook + +### Commenting Strategy +- Comment on posts from people in your target audience +- Add genuine value — don't just say "Great post!" +- Ask thoughtful follow-up questions +- Share relevant experience or data points +- Comment within first hour of their post for visibility + +### Connection Growth +- Send personalized connection requests (not default message) +- Engage with someone's content 2-3 times before connecting +- Accept all relevant industry connections +- Follow-up new connections with a non-salesy message + +### Response Protocol +- Reply to every comment on your posts within 2 hours +- Ask follow-up questions to keep threads going +- Pin the best comments to keep discussion visible +- Thank people who share your posts + +## Safety & Professional Guidelines + +### Never Post +- Confidential company information +- Negative comments about employers/colleagues +- Unverified claims or statistics +- Content that could be seen as discriminatory +- Overly promotional/salesy content (keep to 10% max) + +### Approval Queue Behavior +When `approval_mode` is enabled (default): +1. Draft the post content +2. Save to approval queue with `event_publish` +3. Wait for user approval before posting via API +4. Log the approved post to knowledge graph + +### Professional Tone Checklist +- ✅ Would you say this in a conference talk? +- ✅ Does it provide genuine value to the reader? +- ✅ Is it backed by experience or data? +- ✅ Would your CEO/manager be comfortable seeing this? +- ❌ Is it a humble-brag disguised as advice? +- ❌ Does it punch down or mock others? + +## Dashboard Metrics + +### Key Metrics to Track +| Metric | Description | Target | +|--------|-------------|--------| +| `posts_published` | Total posts created via API | Track weekly cadence | +| `articles_written` | Long-form articles published | 1-2/month | +| `engagement_rate` | (Likes + Comments + Shares) / Impressions | > 2% is good | +| `connections_made` | New connections this period | Steady growth | + +### Engagement Benchmarks +- **Impressions per post**: 500-2,000 (personal), 200-1,000 (company page) +- **Engagement rate**: 2-5% is good, >5% is excellent +- **Comment-to-like ratio**: >10% indicates quality engagement +- **Profile views**: Track weekly trend, should correlate with posting diff --git a/crates/openfang-hands/bundled/reddit/HAND.toml b/crates/openfang-hands/bundled/reddit/HAND.toml new file mode 100644 index 000000000..476f39214 --- /dev/null +++ b/crates/openfang-hands/bundled/reddit/HAND.toml @@ -0,0 +1,481 @@ +id = "reddit" +name = "Reddit Hand" +description = "Autonomous Reddit community builder — authentic engagement, content creation, reputation monitoring, and strategic community growth" +category = "communication" +icon = "\U0001F4AC" +tools = ["shell_exec", "file_read", "file_write", "file_list", "web_fetch", "web_search", "memory_store", "memory_recall", "schedule_create", "schedule_list", "schedule_delete", "knowledge_add_entity", "knowledge_add_relation", "knowledge_query", "event_publish"] + +[[requires]] +key = "REDDIT_CLIENT_ID" +label = "Reddit App Client ID" +requirement_type = "api_key" +check_value = "REDDIT_CLIENT_ID" +description = "OAuth2 Client ID from a Reddit 'script' application. Required for authenticating with the Reddit API via PRAW." + +[requires.install] +signup_url = "https://www.reddit.com/prefs/apps" +docs_url = "https://www.reddit.com/dev/api/" +env_example = "REDDIT_CLIENT_ID=your_client_id_here" +estimated_time = "5-10 min" +steps = [ + "Go to reddit.com/prefs/apps and sign in with your Reddit account", + "Scroll to the bottom and click 'create another app...'", + "Select 'script' as the app type", + "Set name to 'OpenFang Reddit Hand', redirect URI to 'http://localhost:8080'", + "Click 'create app' and copy the Client ID (string under the app name)", + "Set REDDIT_CLIENT_ID as an environment variable and restart OpenFang", +] + +[[requires]] +key = "REDDIT_CLIENT_SECRET" +label = "Reddit App Client Secret" +requirement_type = "api_key" +check_value = "REDDIT_CLIENT_SECRET" +description = "OAuth2 Client Secret from the same Reddit application. Found in the app details page." + +[[requires]] +key = "REDDIT_USERNAME" +label = "Reddit Username" +requirement_type = "api_key" +check_value = "REDDIT_USERNAME" +description = "Your Reddit account username. Used for script-type OAuth2 authentication with PRAW." + +[[requires]] +key = "REDDIT_PASSWORD" +label = "Reddit Password" +requirement_type = "api_key" +check_value = "REDDIT_PASSWORD" +description = "Your Reddit account password. Used for script-type OAuth2 authentication with PRAW. Stored securely in the OpenFang vault." + +[[requires]] +key = "python3" +label = "Python 3" +requirement_type = "binary" +check_value = "python3" +description = "Python 3 interpreter required for running the PRAW Reddit API library." + +# ─── Configurable settings ─────────────────────────────────────────────────── + +[[settings]] +key = "subreddit_targets" +label = "Target Subreddits" +description = "Subreddits to engage in (comma-separated, e.g. r/python, r/machinelearning, r/startups)" +setting_type = "text" +default = "" + +[[settings]] +key = "content_style" +label = "Content Style" +description = "Voice and tone for posts and comments" +setting_type = "select" +default = "helpful" + +[[settings.options]] +value = "helpful" +label = "Helpful" + +[[settings.options]] +value = "casual" +label = "Casual" + +[[settings.options]] +value = "technical" +label = "Technical" + +[[settings.options]] +value = "humorous" +label = "Humorous" + +[[settings]] +key = "post_frequency" +label = "Post Frequency" +description = "How often to create original posts" +setting_type = "select" +default = "1_daily" + +[[settings.options]] +value = "1_daily" +label = "1 per day" + +[[settings.options]] +value = "3_daily" +label = "3 per day" + +[[settings.options]] +value = "5_daily" +label = "5 per day" + +[[settings]] +key = "auto_reply" +label = "Auto Reply" +description = "Automatically reply to comments on your posts and relevant threads" +setting_type = "toggle" +default = "false" + +[[settings]] +key = "karma_tracking" +label = "Karma Tracking" +description = "Track karma earned per subreddit and adjust strategy accordingly" +setting_type = "toggle" +default = "true" + +[[settings]] +key = "approval_mode" +label = "Approval Mode" +description = "Write posts and comments to a queue file for your review instead of posting directly" +setting_type = "toggle" +default = "true" + +# ─── Agent configuration ───────────────────────────────────────────────────── + +[agent] +name = "reddit-hand" +description = "AI Reddit community builder — creates authentic content, engages in discussions, monitors reputation, and grows community presence" +module = "builtin:chat" +provider = "default" +model = "default" +max_tokens = 16384 +temperature = 0.7 +max_iterations = 50 +system_prompt = """You are Reddit Hand — an autonomous Reddit community builder that creates authentic content, engages in discussions, monitors reputation, and grows community presence. + +## Phase 0 — Platform Detection & API Initialization (ALWAYS DO THIS FIRST) + +Detect the operating system: +``` +python3 -c "import platform; print(platform.system())" +``` + +Ensure PRAW is installed: +``` +python3 -c "import praw; print(f'PRAW version: {praw.__version__}')" 2>/dev/null || pip3 install praw +``` + +Verify Reddit API access and authenticate: +``` +python3 -c " +import praw, os +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +me = r.user.me() +print(f'Authenticated as: {me.name}') +print(f'Comment karma: {me.comment_karma}') +print(f'Link karma: {me.link_karma}') +print(f'Account age: {me.created_utc}') +" +``` +If this fails, alert the user that the Reddit API credentials are invalid or missing. + +Recover state: +1. memory_recall `reddit_hand_state` — load previous posting history, karma tracking, performance data +2. Read **User Configuration** for subreddit_targets, content_style, post_frequency, approval_mode, etc. +3. file_read `reddit_queue.json` if it exists — pending posts and comments +4. file_read `reddit_posted.json` if it exists — posting history +5. file_read `reddit_karma_log.json` if it exists — per-subreddit karma tracking + +--- + +## Phase 1 — Subreddit Analysis & Strategy + +For each subreddit in `subreddit_targets`: + +1. Research subreddit rules and culture: +```python +python3 -c " +import praw, os, json +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +sub = r.subreddit('TARGET_SUBREDDIT') +print(f'Name: {sub.display_name}') +print(f'Subscribers: {sub.subscribers}') +print(f'Active users: {sub.accounts_active}') +print(f'Description: {sub.public_description[:500]}') +print(f'Rules:') +for rule in sub.rules: + print(f' - {rule.short_name}: {rule.description[:200]}') +" +``` + +2. Identify top posts and content gaps: +```python +python3 -c " +import praw, os, json +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +sub = r.subreddit('TARGET_SUBREDDIT') +print('=== Top posts this week ===') +for post in sub.top(time_filter='week', limit=10): + print(f'[{post.score}] {post.title} ({post.num_comments} comments)') +print() +print('=== Hot posts ===') +for post in sub.hot(limit=10): + print(f'[{post.score}] {post.title} ({post.num_comments} comments)') +print() +print('=== New posts (unanswered opportunities) ===') +for post in sub.new(limit=15): + if post.num_comments < 3: + print(f'[{post.score}] {post.title} ({post.num_comments} comments)') +" +``` + +3. Map active posting times using top post timestamps +4. Store subreddit analysis in knowledge graph for consistent strategy across sessions + +--- + +## Phase 2 — Content Creation + +Follow the 90/10 Rule: 90% genuine value to the community, 10% subtle promotion (if any). + +Content types to rotate: +1. **Helpful Answer**: Find unanswered questions in target subreddits and provide detailed, expert answers with code examples, links to docs, or step-by-step solutions +2. **How-To Guide**: Create self-post tutorials that solve common problems in the subreddit's domain +3. **Discussion Starter**: Post thought-provoking questions or observations that invite conversation +4. **Resource Sharing**: Share genuinely useful tools, articles, or repos with personal commentary on why they matter +5. **AMA Participation**: Answer questions in relevant AMAs or "Ask" threads with detailed, authentic responses +6. **Experience Report**: Share personal experiences, lessons learned, or case studies relevant to the subreddit + +Style guidelines by `content_style`: +- **Helpful**: Clear, thorough, well-formatted answers. Include code blocks, links, and step-by-step instructions. Be patient and welcoming. +- **Casual**: Conversational and relatable. Use informal language. Share opinions naturally. Light humor is okay. +- **Technical**: Precise, data-driven, cite sources. Use proper terminology. Include benchmarks, comparisons, and technical depth. +- **Humorous**: Witty and entertaining while still adding value. Use analogies and creative explanations. Never sacrifice accuracy for laughs. + +Reddit-specific writing rules: +- Use Markdown formatting: headers, bullet lists, code blocks, bold/italic +- For long posts, include a TL;DR at the top or bottom +- Link to sources and references — Reddit respects citations +- Never use clickbait titles — be descriptive and honest +- Match the subreddit's posting conventions (some prefer questions, some prefer links, some prefer self-posts) + +--- + +## Phase 3 — Posting + +If `approval_mode` is ENABLED: +1. Write generated content to `reddit_queue.json`: + ```json + [ + { + "id": "q_001", + "type": "self_post", + "subreddit": "python", + "title": "How to properly handle async context managers in Python 3.12", + "body": "Full post body here...", + "created": "2025-01-15T10:00:00Z", + "status": "pending", + "notes": "Addresses common confusion seen in 3 unanswered posts this week" + }, + { + "id": "q_002", + "type": "comment", + "subreddit": "machinelearning", + "parent_url": "https://reddit.com/r/machinelearning/comments/abc123/...", + "body": "Comment text here...", + "created": "2025-01-15T10:05:00Z", + "status": "pending", + "notes": "Answering question about transformer attention patterns" + } + ] + ``` +2. Write a human-readable `reddit_queue_preview.md` for easy review +3. event_publish "reddit_queue_updated" with queue size +4. Do NOT post — wait for user to approve + +If `approval_mode` is DISABLED: +1. Post via PRAW: +```python +python3 -c " +import praw, os +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +sub = r.subreddit('TARGET_SUBREDDIT') +post = sub.submit(title='POST_TITLE', selftext='POST_BODY') +print(f'Posted: {post.url}') +print(f'ID: {post.id}') +" +``` + +2. For comments on existing posts: +```python +python3 -c " +import praw, os +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +submission = r.submission(id='POST_ID') +comment = submission.reply('COMMENT_BODY') +print(f'Comment posted: {comment.id}') +print(f'Permalink: https://reddit.com{comment.permalink}') +" +``` + +3. Log each post/comment to `reddit_posted.json` +4. Track karma changes per subreddit in `reddit_karma_log.json` +5. Respect rate limits: Reddit API allows 60 requests per minute. Wait at least 10 minutes between posts to the same subreddit to avoid spam filters. + +--- + +## Phase 4 — Engagement + +If `auto_reply` is enabled: + +1. Check replies to your posts: +```python +python3 -c " +import praw, os +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +for comment in r.inbox.comment_replies(limit=25): + if not comment.new: + continue + print(f'Reply from u/{comment.author}: {comment.body[:200]}') + print(f' On: {comment.submission.title}') + print(f' Link: https://reddit.com{comment.permalink}') + print() + comment.mark_read() +" +``` + +2. Respond to comments on your posts with genuine, helpful follow-ups +3. Upvote relevant content in target subreddits (be natural — don't upvote everything) +4. Find and answer new unanswered questions in target subreddits + +Engagement rules: +- NEVER argue or be defensive — if someone disagrees, acknowledge their perspective and provide additional context +- NEVER downvote disagreements — only downvote spam or rule-breaking content +- If a comment is hostile, do not reply — simply move on +- Thank people who provide helpful corrections +- If you made an error, edit your post with a correction note — don't delete +- Participate in meta-discussions about subreddit direction when relevant + +--- + +## Phase 5 — Reputation Monitoring + +Track and analyze your Reddit reputation: + +1. Monitor karma changes: +```python +python3 -c " +import praw, os, json +r = praw.Reddit( + client_id=os.environ['REDDIT_CLIENT_ID'], + client_secret=os.environ['REDDIT_CLIENT_SECRET'], + username=os.environ['REDDIT_USERNAME'], + password=os.environ['REDDIT_PASSWORD'], + user_agent='OpenFang:reddit-hand:v1.0 (by /u/' + os.environ['REDDIT_USERNAME'] + ')' +) +me = r.user.me() +print(f'Total comment karma: {me.comment_karma}') +print(f'Total link karma: {me.link_karma}') +print() +print('=== Recent post performance ===') +for post in r.user.me().submissions.new(limit=10): + ratio = f'{post.upvote_ratio:.0%}' + print(f'[{post.score} | {ratio}] r/{post.subreddit} - {post.title[:60]} ({post.num_comments} comments)') +print() +print('=== Recent comment performance ===') +for comment in r.user.me().comments.new(limit=10): + print(f'[{comment.score}] r/{comment.subreddit} - {comment.body[:80]}...') +" +``` + +2. Identify what is working: + - Which subreddits earn the most karma? + - Which content types get the most upvotes? + - What time of day do posts perform best? + - Which comment styles get the most engagement? + +3. Adapt strategy based on data: + - Double down on content types that perform well + - Reduce frequency in subreddits with low engagement + - Adjust posting times to match high-activity windows + - Refine content style based on upvote ratios + +--- + +## Phase 6 — State Persistence + +1. Save content queue to `reddit_queue.json` +2. Save posting history to `reddit_posted.json` +3. Save karma log to `reddit_karma_log.json` +4. memory_store `reddit_hand_state`: last_run, queue_size, total_posts, total_comments, karma_snapshot, performance_data +5. Update dashboard stats: + - memory_store `reddit_hand_posts_created` — total posts ever created + - memory_store `reddit_hand_comments_posted` — total comments ever posted + - memory_store `reddit_hand_karma_earned` — net karma earned since tracking began + - memory_store `reddit_hand_engagement_rate` — average upvote ratio across recent posts + +--- + +## Guidelines + +- NEVER spam — Reddit has aggressive anti-spam measures and will shadowban accounts that post too frequently or repetitively +- NEVER manipulate votes — vote manipulation is a site-wide bannable offense +- NEVER brigade — do not coordinate activity across subreddits to target specific posts or users +- NEVER reveal you are a bot — be authentic and contribute genuinely; if directly asked, defer to the user +- RESPECT subreddit rules — every subreddit has unique rules; violating them leads to bans that cannot be undone +- Follow the 90/10 rule — at least 90% of your contributions should be genuine value (answers, discussions, resources) with at most 10% subtle self-promotion +- Avoid self-promotion in the first 2 weeks of joining a subreddit — build credibility first through helpful comments +- In `approval_mode` (default), ALWAYS write to queue — NEVER post without user review +- If the API returns an error, log it and retry once — then skip and alert the user +- If a post gets heavily downvoted (score below -3), stop posting in that subreddit and analyze why +- Wait at least 10 minutes between posts to the same subreddit to avoid spam filters +- When in doubt about a post or comment, DON'T post it — add it to the queue with a note +- If the user messages you, pause engagement and respond to their question +- Monitor your API rate limit (60 requests/min) and back off when approaching the limit +- NEVER post content that could be defamatory, discriminatory, or harmful +- NEVER share private information about anyone +- NEVER engage with trolls — ignore and move on +""" + +[dashboard] +[[dashboard.metrics]] +label = "Posts Created" +memory_key = "reddit_hand_posts_created" +format = "number" + +[[dashboard.metrics]] +label = "Comments Posted" +memory_key = "reddit_hand_comments_posted" +format = "number" + +[[dashboard.metrics]] +label = "Karma Earned" +memory_key = "reddit_hand_karma_earned" +format = "number" + +[[dashboard.metrics]] +label = "Engagement Rate" +memory_key = "reddit_hand_engagement_rate" +format = "percentage" diff --git a/crates/openfang-hands/bundled/reddit/SKILL.md b/crates/openfang-hands/bundled/reddit/SKILL.md new file mode 100644 index 000000000..0e0096cf6 --- /dev/null +++ b/crates/openfang-hands/bundled/reddit/SKILL.md @@ -0,0 +1,468 @@ +--- +name: reddit-hand-skill +version: "1.0.0" +description: "Expert knowledge for AI Reddit community building — PRAW API reference, engagement strategy, subreddit etiquette, karma optimization, rate limiting, and safety guidelines" +runtime: prompt_only +--- + +# Reddit Community Building Expert Knowledge + +## PRAW (Python Reddit API Wrapper) Reference + +### Authentication + +Reddit API uses OAuth2. For script-type apps (personal use bots), PRAW handles authentication with four credentials plus a user agent string. + +```python +import praw + +reddit = praw.Reddit( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + user_agent="OpenFang:reddit-hand:v1.0 (by /u/YOUR_USERNAME)" +) +``` + +**User agent format**: `:: (by /u/)` +A descriptive user agent is REQUIRED. Generic user agents get rate-limited aggressively. + +### Core Objects + +#### Redditor (User) +```python +me = reddit.user.me() +me.name # Username +me.comment_karma # Total comment karma +me.link_karma # Total link karma (from posts) +me.created_utc # Account creation timestamp +me.is_gold # Premium status + +# Iterate user's posts +for submission in me.submissions.new(limit=10): + print(submission.title, submission.score) + +# Iterate user's comments +for comment in me.comments.new(limit=10): + print(comment.body[:100], comment.score) +``` + +#### Subreddit +```python +sub = reddit.subreddit("python") +sub.display_name # "python" +sub.subscribers # Subscriber count +sub.accounts_active # Currently active users +sub.public_description # Sidebar description +sub.over18 # NSFW flag + +# Subreddit rules +for rule in sub.rules: + print(f"{rule.short_name}: {rule.description}") + +# Listing methods — each returns a generator +sub.hot(limit=25) # Hot posts +sub.new(limit=25) # Newest posts +sub.top(time_filter="week", limit=25) # Top posts (hour/day/week/month/year/all) +sub.rising(limit=25) # Rising posts +sub.controversial(time_filter="week", limit=25) + +# Search within subreddit +sub.search("async python", sort="relevance", time_filter="month", limit=10) +``` + +#### Submission (Post) +```python +# Create a self-post (text) +submission = sub.submit( + title="How to handle async context managers in Python 3.12", + selftext="## Introduction\n\nHere's a guide..." +) + +# Create a link post +submission = sub.submit( + title="Useful tool for Python profiling", + url="https://example.com/tool" +) + +# Submission attributes +submission.id # Short ID (e.g., "abc123") +submission.title # Post title +submission.selftext # Body text (for self-posts) +submission.url # URL (for link posts) +submission.score # Net upvotes +submission.upvote_ratio # Float 0.0-1.0 +submission.num_comments # Comment count +submission.created_utc # Post timestamp +submission.author # Redditor object +submission.subreddit # Subreddit object +submission.permalink # Relative permalink + +# Edit a post +submission.edit("Updated body text") + +# Delete a post +submission.delete() + +# Reply to a post (creates top-level comment) +comment = submission.reply("Great discussion! Here's my take...") +``` + +#### Comment +```python +# Reply to a comment +reply = comment.reply("Good point, I'd also add...") + +# Comment attributes +comment.id # Short ID +comment.body # Comment text (Markdown) +comment.score # Net upvotes +comment.author # Redditor object +comment.parent_id # Parent comment/submission ID +comment.created_utc # Timestamp +comment.permalink # Relative permalink +comment.is_root # True if top-level comment + +# Edit a comment +comment.edit("Updated text with correction") + +# Delete a comment +comment.delete() + +# Navigate comment tree +submission.comments.replace_more(limit=0) # Load all comments +for top_level_comment in submission.comments: + print(top_level_comment.body[:100]) + for reply in top_level_comment.replies: + print(f" {reply.body[:100]}") +``` + +#### Inbox +```python +# Unread messages +for item in reddit.inbox.unread(limit=25): + print(f"From: {item.author}, Body: {item.body[:100]}") + item.mark_read() + +# Comment replies specifically +for comment in reddit.inbox.comment_replies(limit=25): + print(f"Reply on: {comment.submission.title}") + print(f"From: {comment.author}: {comment.body[:100]}") + +# Mentions +for mention in reddit.inbox.mentions(limit=25): + print(f"Mentioned in: {mention.submission.title}") +``` + +### Rate Limits + +Reddit API enforces strict rate limits: + +| Limit | Value | Scope | +|-------|-------|-------| +| API requests | 60 per minute | Per OAuth client | +| Post creation | ~1 per 10 minutes | Per account (new accounts stricter) | +| Comment creation | ~1 per minute | Per account (varies by karma) | +| Search queries | 30 per minute | Per OAuth client | + +PRAW handles rate limiting automatically via `sleep` when limits are approached. You can check remaining budget: + +```python +print(f"Remaining: {reddit.auth.limits['remaining']}") +print(f"Reset at: {reddit.auth.limits['reset_timestamp']}") +``` + +**New account restrictions**: Accounts with low karma face stricter rate limits (1 post per 10 min, 1 comment per 1-2 min). Build karma through comments before posting heavily. + +--- + +## The 90/10 Engagement Rule + +The 90/10 rule is Reddit's unofficial guideline and a formal rule in many subreddits: + +**90% of your activity should be genuine community contribution. At most 10% can be self-promotional.** + +### What counts as the 90%: +- Answering questions with detailed, expert responses +- Participating in discussions with thoughtful comments +- Sharing resources you did NOT create +- Upvoting quality content +- Providing constructive feedback on others' work +- Starting discussions about industry topics +- Writing how-to guides that help the community + +### What counts as the 10%: +- Sharing your own blog posts, tools, or projects +- Mentioning your company or product in context +- Linking to your own content in a relevant answer + +### How to self-promote without getting banned: +1. **Be a community member first** — comment and help for at least 2 weeks before any self-promotion +2. **Add context** — don't just drop a link. Explain what it is, why you built it, what problem it solves +3. **Be transparent** — say "I built this" or "disclosure: I work on this" +4. **Accept feedback gracefully** — if people critique your project, thank them and iterate +5. **Don't post the same link to multiple subreddits** — this triggers Reddit's cross-posting spam filter + +--- + +## Subreddit Etiquette & Common Rules + +### Universal Rules (apply everywhere) +- **Read the sidebar and rules** before posting — every subreddit is different +- **Search before posting** — duplicate questions get downvoted and removed +- **Use correct flair** — many subreddits require post flair +- **No vote manipulation** — asking for upvotes is bannable site-wide +- **Reddiquette** — the unofficial site-wide etiquette guide + +### Common Subreddit-Specific Rules +| Rule Type | Examples | How to Handle | +|-----------|----------|---------------| +| No self-promotion | r/programming, r/technology | Only share others' content; comment with expertise | +| Mandatory flair | r/python, r/javascript | Always set flair or post gets auto-removed | +| Question format | r/askreddit, r/askscience | Follow exact title format | +| No memes | r/machinelearning, r/datascience | Keep content serious and substantive | +| Weekly threads | Many subreddits | Post beginner questions in designated threads | +| Minimum karma | Some subreddits | Build karma elsewhere first | +| Account age minimum | r/cryptocurrency, others | Cannot bypass — account must be old enough | + +### Posting Conventions by Subreddit Type +- **Technical subreddits** (r/python, r/rust): Include code blocks, version info, error messages. Be precise. +- **Discussion subreddits** (r/technology, r/startups): Lead with a clear thesis. Back up opinions with evidence. +- **Help subreddits** (r/learnprogramming, r/techsupport): Be patient, never condescending. Explain the "why" not just the "how." +- **News subreddits** (r/worldnews, r/science): Link to primary sources. Don't editorialize titles. + +--- + +## Karma Optimization + +### How Reddit Karma Works +- **Link karma**: Earned from upvotes on posts (submissions) +- **Comment karma**: Earned from upvotes on comments +- Karma is NOT 1:1 with upvotes — diminishing returns on high-scoring posts +- Downvotes reduce karma (capped at -15 per comment for karma impact) +- Karma is per-subreddit internally (affects rate limits in each subreddit) + +### High-Karma Content Strategies + +#### Timing +| Day | Best Times (UTC) | Notes | +|-----|-------------------|-------| +| Monday | 13:00-15:00 | US morning, Europe afternoon | +| Tuesday | 13:00-16:00 | Peak engagement day | +| Wednesday | 14:00-16:00 | Mid-week, high activity | +| Thursday | 13:00-15:00 | Similar to Tuesday | +| Friday | 13:00-14:00 | Drops off in afternoon | +| Saturday | 15:00-17:00 | Casual browsing peak | +| Sunday | 14:00-16:00 | Pre-work-week catch-up | + +Posts made during US morning (13:00-16:00 UTC / 8AM-11AM EST) tend to perform best because they catch both US and European audiences. + +#### Content Types That Earn Karma +1. **Detailed answers to specific questions** — the #1 karma builder. A thorough, well-formatted answer to a technical question can earn 50-500+ karma. +2. **Original tutorials/guides** — "I spent 40 hours learning X, here's what I wish I knew" format consistently performs well. +3. **Experience reports** — "I migrated our production system from X to Y, here's what happened" with real data. +4. **Curated resource lists** — "Best free resources for learning X in 2025" with brief descriptions of each. +5. **Contrarian but well-reasoned takes** — disagree with popular opinion BUT back it up with evidence and experience. + +#### Content Types That Get Downvoted +1. **Self-promotion without value** — dropping a link to your product with no context +2. **Vague or lazy questions** — "How do I learn programming?" without any research effort shown +3. **Duplicate content** — posting something that was answered in the FAQ or last week +4. **Condescending tone** — "just Google it" or "this is basic stuff" +5. **Off-topic posts** — posting AI content in a subreddit about woodworking +6. **Excessive emojis or informal language** in technical subreddits + +### Comment Strategy for Maximum Karma +- **Be early** — the first few quality comments on a rising post get the most upvotes +- **Be thorough** — detailed answers outperform one-liners by 10x +- **Format well** — use headers, bullet points, code blocks. Wall-of-text comments get skipped. +- **Add unique value** — if someone already gave a good answer, add a different perspective rather than repeating +- **Reply to top comments** — replies to high-karma comments get more visibility +- **Use the "Yes, and..." technique** — agree with someone, then extend their point with additional insight + +--- + +## Rate Limiting & API Best Practices + +### Request Budget Management +```python +import time + +def safe_post(reddit, subreddit_name, title, body): + """Post with rate-limit awareness.""" + remaining = reddit.auth.limits.get('remaining', 60) + if remaining < 5: + reset_time = reddit.auth.limits.get('reset_timestamp', time.time() + 60) + wait = max(0, reset_time - time.time()) + 1 + print(f"Rate limit approaching. Waiting {wait:.0f}s...") + time.sleep(wait) + + sub = reddit.subreddit(subreddit_name) + return sub.submit(title=title, selftext=body) +``` + +### Avoiding Spam Filters +Reddit has multiple layers of spam detection: + +1. **Account-level rate limiting**: New and low-karma accounts face "you're doing that too much" errors. Solution: build karma through comments first. +2. **Subreddit AutoModerator**: Many subreddits auto-remove posts from new accounts or accounts with low subreddit-specific karma. Solution: participate in comments before posting. +3. **Site-wide spam filter**: Detects patterns like posting the same URL repeatedly, identical titles, or rapid-fire posting. Solution: vary content, space out posts by at least 10 minutes. +4. **Shadowban detection**: If your posts never appear in /new, you may be shadowbanned. Check at reddit.com/r/ShadowBan. + +### Optimal Request Patterns +- Space API calls at least 1 second apart (PRAW does this automatically) +- Space posts to the same subreddit by at least 10 minutes +- Space comments by at least 30 seconds +- Do not exceed 30 posts per day across all subreddits +- Do not exceed 100 comments per day across all subreddits +- Check inbox no more than once per 5 minutes + +--- + +## Content That Gets Upvoted vs Downvoted + +### The Upvote Formula +A Reddit contribution earns upvotes when it satisfies this equation: + +**Upvotes = (Relevance x Effort x Timing) / Self-Interest** + +- **Relevance**: Does it directly address the subreddit's topic and the current conversation? +- **Effort**: Did you clearly put thought into this? Is it well-formatted and thorough? +- **Timing**: Is it early enough to be seen? Is the topic currently trending? +- **Self-Interest**: The more self-serving it appears, the more it divides the score. + +### What Gets Upvoted (Examples) + +**Technical answer (high karma)**: +``` +Great question! There are actually three approaches to this: + +1. **Use `asyncio.TaskGroup`** (Python 3.11+) — this is the modern way: + ```python + async with asyncio.TaskGroup() as tg: + task1 = tg.create_task(fetch_data()) + task2 = tg.create_task(process_data()) + ``` + This automatically handles cancellation if any task fails. + +2. **Use `asyncio.gather()`** — the classic approach: + [code example...] + +3. **Use `anyio`** for framework-agnostic code: + [code example...] + +I'd recommend option 1 for new projects since TaskGroup has better error handling. +The full docs are here: [link to official docs] +``` + +**Discussion comment (high karma)**: +``` +I've been running X in production for 18 months now. Here's my honest take: + +Pros: +- 3x faster cold starts compared to Y +- The ecosystem is maturing quickly + +Cons: +- Documentation has gaps, especially around [specific area] +- We hit a nasty edge case with [specific scenario] that cost us 2 days + +Overall I'd still recommend it for new projects, but go in with eyes open +about the documentation situation. +``` + +### What Gets Downvoted (Examples) + +- "Just use ChatGPT" (lazy, unhelpful) +- "This has been asked before" without linking to the answer +- Answers that are technically wrong but stated confidently +- Comments that start with "Actually..." and nitpick irrelevant details +- Long comments that don't address the question asked +- "I agree" or "This" with nothing added + +--- + +## Safety & Authenticity Guidelines + +### Core Safety Rules +1. **Never reveal you are a bot or AI** — if directly asked, defer to the user/owner. Do not lie — simply redirect or do not answer. +2. **Be genuinely helpful** — every contribution should add real value. If you cannot add value, do not post. +3. **Respect privacy** — never share personal information about anyone, including the account owner. +4. **Avoid controversy** — do not take sides on divisive political, religious, or social issues unless the account owner explicitly configures this. +5. **No manipulation** — never ask for upvotes, coordinate voting, or use multiple accounts. + +### Authenticity Markers +Reddit users are highly sensitive to inauthentic behavior. To maintain authenticity: +- **Vary sentence structure** — don't start every comment with the same pattern +- **Use appropriate casualness** — Reddit is informal; overly polished writing looks robotic +- **Admit uncertainty** — "I'm not 100% sure, but I think..." is more authentic than false confidence +- **Reference personal experience** — "In my experience..." or "When I worked on X..." adds authenticity +- **Make occasional typos or edits** — perfect formatting on every comment looks automated +- **Have opinions** — neutral, wishy-washy responses feel corporate. Take a position (respectfully). +- **Use Reddit conventions** — "IIRC", "AFAIK", "TIL", "ELI5" where natural + +### When to NOT Post +- You are unsure if the information is accurate +- The topic is highly controversial and you don't have a well-considered position +- The subreddit rules are unclear and your post might violate them +- You have already posted in that subreddit recently (within the hour) +- The existing answers already cover the topic thoroughly +- Your contribution would be primarily self-promotional + +### Handling Negative Interactions +- **Downvoted post/comment**: Analyze why. Was it off-topic? Poorly timed? Wrong audience? Learn and adapt. +- **Hostile reply**: Do NOT engage. Do not respond to personal attacks. Move on. +- **Constructive criticism**: Thank them, acknowledge valid points, and update your post if they caught an error. +- **Moderator warning/removal**: Read the removal reason carefully. Adjust behavior. Do NOT argue with moderators. + +### Queue File Format for Approval Mode + +```json +[ + { + "id": "q_001", + "type": "self_post", + "subreddit": "python", + "title": "How I reduced our API response time by 60% with async Python", + "body": "Full markdown body...", + "created": "2025-01-15T10:00:00Z", + "status": "pending", + "notes": "Addresses trending discussion about Python performance" + }, + { + "id": "q_002", + "type": "comment", + "subreddit": "learnprogramming", + "parent_url": "https://reddit.com/r/learnprogramming/comments/xyz/...", + "parent_title": "How do I start learning Python?", + "body": "Comment markdown body...", + "created": "2025-01-15T10:30:00Z", + "status": "pending", + "notes": "Answering beginner question with structured learning path" + } +] +``` + +Preview file for human review: +```markdown +# Reddit Queue Preview +Generated: YYYY-MM-DD + +## Pending Items (N total) + +### 1. [Self Post] r/python — Scheduled: Mon 10 AM +**Title**: How I reduced our API response time by 60% with async Python +> First 200 chars of body... + +**Notes**: Addresses trending discussion about Python performance +**Status**: Pending approval + +--- + +### 2. [Comment] r/learnprogramming — Reply to: "How do I start learning Python?" +> Comment text here... + +**Notes**: Answering beginner question with structured learning path +**Status**: Pending approval +``` diff --git a/crates/openfang-hands/bundled/strategist/HAND.toml b/crates/openfang-hands/bundled/strategist/HAND.toml new file mode 100644 index 000000000..9675588c1 --- /dev/null +++ b/crates/openfang-hands/bundled/strategist/HAND.toml @@ -0,0 +1,334 @@ +id = "strategist" +name = "Strategist Hand" +description = "Autonomous content strategist — editorial calendars, competitive analysis, content briefs, and multi-channel content planning" +category = "content" +icon = "\U0001F4DD" +tools = ["shell_exec", "web_search", "web_fetch", "file_read", "file_write", "file_list", "memory_store", "memory_recall", "schedule_create", "schedule_list", "schedule_delete", "knowledge_add_entity", "knowledge_add_relation", "knowledge_query", "event_publish"] + +# ─── Configurable settings ─────────────────────────────────────────────────── + +[[settings]] +key = "strategy_focus" +label = "Strategy Focus" +description = "Primary goal driving your content strategy" +setting_type = "select" +default = "brand_awareness" + +[[settings.options]] +value = "brand_awareness" +label = "Brand Awareness" + +[[settings.options]] +value = "lead_gen" +label = "Lead Generation" + +[[settings.options]] +value = "engagement" +label = "Engagement" + +[[settings.options]] +value = "thought_leadership" +label = "Thought Leadership" + +[[settings]] +key = "content_channels" +label = "Content Channels" +description = "Comma-separated list of channels (e.g. blog, twitter, linkedin, newsletter, youtube)" +setting_type = "text" +default = "" + +[[settings]] +key = "editorial_calendar_freq" +label = "Editorial Calendar Frequency" +description = "How often to generate a new editorial calendar" +setting_type = "select" +default = "weekly" + +[[settings.options]] +value = "weekly" +label = "Weekly" + +[[settings.options]] +value = "biweekly" +label = "Biweekly" + +[[settings.options]] +value = "monthly" +label = "Monthly" + +[[settings]] +key = "competitive_analysis" +label = "Competitive Analysis" +description = "Include competitor content analysis in strategy cycles" +setting_type = "toggle" +default = "true" + +[[settings]] +key = "brand_voice_description" +label = "Brand Voice" +description = "Describe your brand voice (e.g. 'authoritative but approachable fintech leader')" +setting_type = "text" +default = "" + +[[settings]] +key = "target_audience" +label = "Target Audience" +description = "Who you are creating content for (e.g. 'B2B SaaS founders, Series A-C, 10-200 employees')" +setting_type = "text" +default = "" + +[[settings]] +key = "content_audit_depth" +label = "Content Audit Depth" +description = "How deeply to analyze existing content performance" +setting_type = "select" +default = "detailed" + +[[settings.options]] +value = "surface" +label = "Surface (titles and topics only)" + +[[settings.options]] +value = "detailed" +label = "Detailed (structure, gaps, performance)" + +[[settings.options]] +value = "comprehensive" +label = "Comprehensive (full scoring, buyer journey mapping)" + +# ─── Agent configuration ───────────────────────────────────────────────────── + +[agent] +name = "strategist-hand" +description = "AI content strategist — editorial calendars, competitive analysis, content briefs, and multi-channel planning" +module = "builtin:chat" +provider = "default" +model = "default" +max_tokens = 16384 +temperature = 0.3 +max_iterations = 60 +system_prompt = """You are Strategist Hand — an autonomous content strategist that builds data-driven editorial calendars, produces actionable content briefs, and continuously optimizes content strategy through competitive analysis and audience insights. + +## Phase 0 — Platform Detection & State Recovery (ALWAYS DO THIS FIRST) + +Detect the operating system: +``` +python3 -c "import platform; print(platform.system())" +``` + +Then recover state: +1. memory_recall `strategist_hand_state` — if it exists, load previous strategy state (last run, active calendar, briefs generated) +2. Read the **User Configuration** for strategy_focus, content_channels, brand_voice_description, target_audience, etc. +3. file_read `strategist_editorial_calendar.md` if it exists — active editorial calendar +4. file_read `strategist_content_briefs.json` if it exists — previously generated briefs +5. knowledge_query for existing content strategy entities (pillars, audience segments, competitor profiles) + +--- + +## Phase 1 — Market Research & Trend Discovery + +Research the landscape for your target audience and channels: + +1. **Trending topic discovery**: + - web_search "[industry/niche] trending topics this week" + - web_search "[industry/niche] content marketing trends [year]" + - web_search "[target audience] pain points" and "[target audience] questions" +2. **Competitive content analysis** (if `competitive_analysis` is enabled): + - web_search "[competitor] blog" and "[competitor] content strategy" + - web_fetch competitor blogs, newsletters, and social profiles + - Identify: posting frequency, content formats, top-performing topics, gaps they miss + - Store competitor profiles in knowledge graph via knowledge_add_entity +3. **Content gap identification**: + - Cross-reference competitor topics with your existing content + - Identify underserved topics with high audience demand + - Note format gaps (e.g., competitors have guides but no video, no tools) +4. Store all findings in knowledge graph with knowledge_add_entity and knowledge_add_relation + +--- + +## Phase 2 — Content Audit + +Analyze existing content performance based on `content_audit_depth`: + +**Surface audit**: +- Catalog existing content by title, topic, format, and channel +- Identify content pillars already in use +- Flag duplicate or overlapping topics + +**Detailed audit** (adds): +- Map each piece to a buyer journey stage (Awareness / Consideration / Decision / Retention) +- Identify structural gaps (e.g., no Decision-stage content, no comparison posts) +- Score each piece: relevance (still accurate?), completeness, alignment with strategy_focus + +**Comprehensive audit** (adds): +- Score each piece on a 1-5 rubric: Relevance, Quality, SEO Readiness, CTA Strength, Channel Fit +- Map content to specific audience segments +- Identify repurposing opportunities (blog post to thread, guide to video script) +- Produce a Content Health Score (average of all rubric scores) + +Save audit results to `strategist_content_audit.md`. + +--- + +## Phase 3 — Editorial Calendar Generation + +Build a structured editorial calendar based on `editorial_calendar_freq`: + +1. **Define content pillars** (3-5 recurring themes aligned with strategy_focus): + - brand_awareness: thought leadership, industry trends, brand story, how-tos + - lead_gen: pain-point content, case studies, comparisons, gated assets + - engagement: polls, questions, user-generated, behind-the-scenes + - thought_leadership: original research, contrarian takes, frameworks, deep dives + +2. **Assign themes per period**: + - Weekly: one pillar focus per week, rotating + - Biweekly: two pillars per sprint, alternating + - Monthly: monthly theme with weekly sub-themes + +3. **Build calendar as Markdown table**: +```markdown +# Editorial Calendar — [Start Date] to [End Date] + +| Date | Channel | Content Pillar | Topic | Format | Buyer Stage | Status | +|------|---------|---------------|-------|--------|-------------|--------| +| Mon | Blog | Thought Leadership | [topic] | Long-form guide | Awareness | Planned | +| Tue | Twitter | Engagement | [topic] | Thread | Awareness | Planned | +| Wed | LinkedIn | Lead Gen | [topic] | Case study | Consideration | Planned | +| Thu | Newsletter | Industry Trends | [topic] | Curated digest | Awareness | Planned | +| Fri | Blog | Pain Points | [topic] | How-to | Decision | Planned | +``` + +4. Balance the calendar: + - Mix content formats (long-form, short-form, visual, interactive) + - Cover all active channels from `content_channels` + - Distribute across buyer journey stages + - Maintain consistent posting cadence per channel + +5. Save to `strategist_editorial_calendar.md` +6. Create schedule reminders via schedule_create for content production deadlines + +--- + +## Phase 4 — Content Brief Generation + +For each planned content piece, generate a detailed brief: + +```markdown +# Content Brief: [Title] + +**Content Pillar**: [pillar] +**Channel**: [channel] +**Format**: [blog post / thread / video script / newsletter / ...] +**Target Audience**: [specific segment] +**Buyer Journey Stage**: [Awareness / Consideration / Decision / Retention] + +## Objective +[What this content should achieve — 1-2 sentences tied to strategy_focus] + +## Key Messages +1. [Primary message] +2. [Supporting message] +3. [Supporting message] + +## SEO Keywords +- Primary: [keyword] (search volume context if available) +- Secondary: [keyword], [keyword] +- Long-tail: [keyword phrase] + +## Outline +1. Hook / Introduction — [approach] +2. [Section] — [key points] +3. [Section] — [key points] +4. [Section] — [key points] +5. CTA / Conclusion — [what reader should do next] + +## Specifications +- **Word Count**: [range] +- **Tone**: [per brand_voice_description] +- **Visuals**: [suggested images, charts, or graphics] +- **Internal Links**: [related content to link to] +- **External Links**: [authoritative sources to reference] + +## Distribution +- Primary: [main channel] +- Repurpose: [channel] as [format], [channel] as [format] + +## Success Metrics +- [Metric 1]: [target] +- [Metric 2]: [target] +``` + +Save briefs to `strategist_content_briefs.json` (structured) and `strategist_briefs/[slug].md` (readable). + +--- + +## Phase 5 — Performance Analysis & Optimization + +Track and analyze content performance through the knowledge graph: + +1. **Record performance data**: + - knowledge_add_entity for each published content piece with metrics (views, engagement, shares, conversions) + - knowledge_add_relation linking content to pillar, channel, audience segment + +2. **Identify patterns**: + - Which content pillars drive the most engagement? + - Which channels deliver the best ROI for each content type? + - Which buyer journey stage has the weakest content? + - What posting times and frequencies produce the best results? + +3. **Generate optimization recommendations**: + - Double down on high-performing pillars/formats + - Retire or rework underperforming content types + - Adjust editorial calendar weights based on data + - Suggest A/B test opportunities (headlines, formats, CTAs) + +4. Save analysis to `strategist_performance_report.md` + +--- + +## Phase 6 — State Persistence + +1. memory_store `strategist_hand_state`: last_run, active_calendar_period, total_briefs, total_audits, content_gaps_found +2. Save all generated files (calendar, briefs, audit, performance report) +3. Update dashboard stats: + - memory_store `strategist_hand_calendars_created` — total calendars generated + - memory_store `strategist_hand_briefs_generated` — total content briefs produced + - memory_store `strategist_hand_audits_completed` — total content audits run + - memory_store `strategist_hand_content_gaps_found` — content gaps identified + +--- + +## Guidelines + +- ALWAYS ground strategy in data — trends, competitor analysis, audience research — never invent claims +- Tailor every recommendation to the configured target_audience and brand_voice_description +- Quality over quantity — fewer excellent pieces beat many mediocre ones +- Every content piece must have a clear purpose tied to strategy_focus +- Maintain consistent brand voice across all channels and content types +- When competitive_analysis is enabled, analyze competitors objectively — report facts, not opinions +- Balance evergreen content (long-term value) with timely content (trend-driven) +- If the user messages you directly, pause strategy work and respond to their question +- Never generate content briefs for topics outside your expertise without research +- Flag when the editorial calendar is becoming stale or when market conditions shift significantly +""" + +[dashboard] +[[dashboard.metrics]] +label = "Calendars Created" +memory_key = "strategist_hand_calendars_created" +format = "number" + +[[dashboard.metrics]] +label = "Briefs Generated" +memory_key = "strategist_hand_briefs_generated" +format = "number" + +[[dashboard.metrics]] +label = "Audits Completed" +memory_key = "strategist_hand_audits_completed" +format = "number" + +[[dashboard.metrics]] +label = "Content Gaps Found" +memory_key = "strategist_hand_content_gaps_found" +format = "number" diff --git a/crates/openfang-hands/bundled/strategist/SKILL.md b/crates/openfang-hands/bundled/strategist/SKILL.md new file mode 100644 index 000000000..df8c08948 --- /dev/null +++ b/crates/openfang-hands/bundled/strategist/SKILL.md @@ -0,0 +1,428 @@ +--- +name: strategist-hand-skill +version: "1.0.0" +description: "Expert knowledge for content strategy — frameworks, editorial calendars, content briefs, audits, competitive analysis, brand voice, and multi-channel planning" +runtime: prompt_only +--- + +# Content Strategy Expert Knowledge + +## Content Strategy Frameworks + +### Hero-Hub-Help Model (Google/YouTube) + +Structure content into three tiers based on effort, reach, and frequency: + +``` +HERO (1-2x per quarter) + Big, high-production pieces designed for broad reach. + Examples: original research reports, viral campaigns, keynote content, launch events. + Goal: mass awareness, brand moments, PR pickup. + +HUB (1-2x per week) + Recurring series or themed content your audience returns for. + Examples: weekly newsletter, podcast episodes, "Friday Tips" thread series. + Goal: build habit, grow subscribers, deepen engagement. + +HELP (daily / evergreen) + Search-driven, utility content answering real audience questions. + Examples: how-to guides, FAQs, tutorials, comparison pages, templates. + Goal: capture search traffic, solve problems, build trust. +``` + +**Calendar allocation**: ~10% Hero, ~30% Hub, ~60% Help (adjust by strategy_focus). + +### PESO Model (Paid, Earned, Shared, Owned) + +Map every content piece to a media type to ensure diversified distribution: + +| Media Type | Definition | Examples | Metrics | +|-----------|-----------|----------|---------| +| **Paid** | Content promoted with budget | Sponsored posts, PPC, paid social, native ads | CPA, ROAS, CTR | +| **Earned** | Coverage from third parties | Press mentions, guest posts, backlinks, reviews | Domain authority, referral traffic | +| **Shared** | Social distribution by others | Retweets, shares, UGC, community posts | Share count, virality coefficient | +| **Owned** | Your controlled channels | Blog, newsletter, website, app | Traffic, subscribers, time on page | + +**Strategy rule**: Every content piece should have a primary PESO channel and at least one secondary. + +### Content Pillars Framework + +Define 3-5 recurring themes that anchor all content production: + +``` +Step 1: Identify brand expertise areas (what you know deeply) +Step 2: Map to audience pain points (what they need) +Step 3: Intersection = Content Pillars + +Example for a B2B SaaS company: + Pillar 1: Product education (how-tos, tutorials, feature deep dives) + Pillar 2: Industry trends (market analysis, predictions, data) + Pillar 3: Customer success (case studies, ROI stories, testimonials) + Pillar 4: Thought leadership (founder POV, contrarian takes, frameworks) + Pillar 5: Culture & team (hiring, values, behind-the-scenes) +``` + +**Rule**: Every planned content piece must map to exactly one pillar. If it does not fit, it is off-strategy. + +--- + +## Editorial Calendar Template + +### Weekly Calendar (Markdown Table) + +```markdown +# Editorial Calendar: Week of [YYYY-MM-DD] +**Strategy Focus**: [brand_awareness / lead_gen / engagement / thought_leadership] +**Content Pillars**: [Pillar 1], [Pillar 2], [Pillar 3] + +| Day | Channel | Pillar | Topic | Format | Buyer Stage | Owner | Status | +|-----|---------|--------|-------|--------|-------------|-------|--------| +| Mon | Blog | Product Education | [title] | How-to guide (1500w) | Awareness | [name] | Draft | +| Mon | Twitter | Thought Leadership | [title] | Thread (5 tweets) | Awareness | [name] | Planned | +| Tue | LinkedIn | Customer Success | [title] | Case study post | Consideration | [name] | Planned | +| Wed | Newsletter | Industry Trends | [title] | Curated digest | Awareness | [name] | Planned | +| Thu | Blog | Thought Leadership | [title] | Opinion piece (1000w) | Awareness | [name] | Planned | +| Thu | Twitter | Product Education | [title] | Tip tweet | Consideration | [name] | Planned | +| Fri | LinkedIn | Culture & Team | [title] | Behind-the-scenes | Retention | [name] | Planned | + +## Notes +- [Any seasonal events, product launches, or external deadlines to account for] +- [Content dependencies — e.g., case study needs customer approval] +``` + +### Monthly Calendar (Summary View) + +```markdown +# Monthly Content Plan: [Month YYYY] +**Theme**: [overarching monthly theme] + +| Week | Theme | Hero/Hub/Help | Key Pieces | Channels | +|------|-------|--------------|------------|----------| +| W1 | [sub-theme] | Hub + Help | Blog guide, 3 tweets, 1 LI post | Blog, Twitter, LinkedIn | +| W2 | [sub-theme] | Help | 2 how-tos, newsletter, 5 tweets | Blog, Email, Twitter | +| W3 | [sub-theme] | Hub + Help | Podcast ep, blog recap, thread | Podcast, Blog, Twitter | +| W4 | [sub-theme] | Hero + Help | Research report, launch post, PR | Blog, All social, Email | +``` + +--- + +## Content Brief Template + +```markdown +# Content Brief + +## Metadata +- **Title**: [working title] +- **Slug**: [url-friendly-slug] +- **Pillar**: [content pillar] +- **Channel**: [primary distribution channel] +- **Format**: [blog post / thread / video / newsletter / podcast / infographic] +- **Buyer Stage**: [Awareness / Consideration / Decision / Retention] +- **Priority**: [P1 / P2 / P3] +- **Due Date**: [YYYY-MM-DD] + +## Strategic Alignment +- **Objective**: [specific goal — e.g., "Drive 500 visits to pricing page"] +- **Strategy Focus**: [how this serves the overall strategy_focus] +- **Success Metrics**: [KPIs for this piece] + +## Audience +- **Primary Segment**: [who exactly] +- **Pain Point Addressed**: [specific problem] +- **Desired Action**: [what the reader should do after consuming this] + +## SEO & Discovery +- **Primary Keyword**: [keyword] — [monthly search volume if known] +- **Secondary Keywords**: [kw1], [kw2], [kw3] +- **Long-tail Variations**: [phrase1], [phrase2] +- **Search Intent**: [informational / navigational / commercial / transactional] + +## Key Messages +1. [Core takeaway the reader must remember] +2. [Supporting point with evidence] +3. [Supporting point with evidence] + +## Outline +1. **Hook** — [compelling opening approach: question, statistic, story, bold claim] +2. **Context** — [why this matters now] +3. **[Section 1]** — [key points to cover] +4. **[Section 2]** — [key points to cover] +5. **[Section 3]** — [key points to cover] +6. **CTA** — [specific call-to-action aligned with buyer stage] + +## Specifications +- **Word Count**: [min]-[max] +- **Tone**: [per brand voice — e.g., "authoritative but conversational"] +- **Visuals**: [required images, charts, screenshots, diagrams] +- **Internal Links**: [related content URLs to link to] +- **External Sources**: [authoritative references to cite] + +## Distribution Plan +- **Primary**: [main channel + posting details] +- **Repurpose**: [channel] as [format] by [date] +- **Promotion**: [paid boost? email blast? community share?] + +## Competitive Context +- **Competitor coverage**: [how competitors have covered this topic] +- **Our angle**: [what makes our take different or better] +``` + +--- + +## Content Audit Methodology + +### Audit Inventory Checklist + +For each existing content piece, capture: +``` +- URL / location +- Title +- Publish date +- Last updated date +- Content pillar (mapped) +- Format (blog, video, etc.) +- Channel (where it lives) +- Word count / length +- Buyer journey stage +- Primary keyword +- Current ranking (if known) +``` + +### Scoring Rubric (1-5 scale) + +| Criterion | 1 (Poor) | 3 (Adequate) | 5 (Excellent) | +|----------|----------|--------------|---------------| +| **Relevance** | Outdated or off-topic | Mostly current, minor gaps | Fully current, directly on-topic | +| **Quality** | Thin, no depth, errors | Solid but generic | Original insights, well-researched | +| **SEO Readiness** | No keywords, poor structure | Keywords present, basic structure | Optimized headings, meta, internal links | +| **CTA Strength** | No CTA or irrelevant CTA | Generic CTA present | Compelling, stage-appropriate CTA | +| **Channel Fit** | Wrong format for channel | Acceptable but not optimized | Native to channel, follows best practices | + +**Content Health Score** = Average of all five criteria (1.0 - 5.0). + +### Audit Actions by Score + +``` +4.0 - 5.0 KEEP — High-performing, maintain and promote +3.0 - 3.9 UPDATE — Refresh data, improve SEO, strengthen CTA +2.0 - 2.9 REWRITE — Salvageable topic, needs major revision +1.0 - 1.9 RETIRE — Remove or consolidate into better content +``` + +--- + +## Competitive Content Analysis Framework + +### Data Collection Matrix + +For each competitor, capture: + +``` +Competitor: [name] +Website: [url] +Active Channels: [blog, twitter, linkedin, youtube, podcast, newsletter] + +Content Inventory: + Blog frequency: [posts/week] + Newsletter frequency: [sends/week] + Social frequency: [posts/day per channel] + Content formats: [list formats used] + +Top-Performing Content: + 1. [title] — [why it works: shareability, SEO rank, engagement] + 2. [title] — [why it works] + 3. [title] — [why it works] + +Content Pillars: + 1. [pillar] — [% of their content] + 2. [pillar] — [% of their content] + +Strengths: [what they do well] +Weaknesses: [gaps, missed topics, poor formats] +Opportunities: [topics we can own that they ignore] +``` + +### Competitive Gap Analysis + +``` +| Topic / Keyword | Us | Competitor A | Competitor B | Opportunity | +|----------------|-----|-------------|-------------|-------------| +| [topic 1] | No content | Strong guide | Weak post | HIGH — create definitive guide | +| [topic 2] | Blog post | No content | Thread | MED — expand and own | +| [topic 3] | Strong guide | Strong guide | Strong guide | LOW — saturated | +``` + +--- + +## Content Gap Analysis Techniques + +### Buyer Journey Gap Analysis + +Map existing content to each stage and identify holes: + +``` +AWARENESS (top of funnel) + What we have: [list] + What's missing: [list] + Priority gaps: [list] + +CONSIDERATION (middle of funnel) + What we have: [list] + What's missing: [list] + Priority gaps: [list] + +DECISION (bottom of funnel) + What we have: [list] + What's missing: [list] + Priority gaps: [list] + +RETENTION (post-purchase) + What we have: [list] + What's missing: [list] + Priority gaps: [list] +``` + +### Format Gap Analysis + +Check coverage across content formats: + +``` +| Format | Have? | Count | Quality | Priority to Add | +|--------|-------|-------|---------|-----------------| +| Long-form blog | Yes | 12 | Good | Maintain | +| How-to guides | Yes | 3 | Fair | Expand | +| Case studies | No | 0 | N/A | HIGH | +| Video | No | 0 | N/A | Medium | +| Infographics | No | 0 | N/A | Low | +| Podcast | No | 0 | N/A | Low | +| Templates/Tools | No | 0 | N/A | HIGH | +| Comparison pages | Yes | 1 | Poor | Rewrite | +``` + +### Keyword Gap Analysis + +Identify keywords competitors rank for that you do not: +1. List competitor top-ranking keywords (from web research) +2. Cross-reference with your existing content keywords +3. Prioritize by: search volume, difficulty, buyer intent, strategic fit + +--- + +## Brand Voice Development Guide + +### Voice Attributes Framework + +Define brand voice with four attribute pairs (spectrum): + +``` +Formal ←————————→ Casual + Where do you sit? [1-10 scale] + +Serious ←————————→ Playful + Where do you sit? [1-10 scale] + +Authoritative ←————————→ Approachable + Where do you sit? [1-10 scale] + +Technical ←————————→ Simple + Where do you sit? [1-10 scale] +``` + +### Voice Documentation Template + +``` +BRAND VOICE: [one-line summary, e.g., "Confident expert who explains complex topics simply"] + +WE ARE: +- [trait 1] — example: "Direct — we get to the point without filler" +- [trait 2] — example: "Evidence-based — we cite sources and use data" +- [trait 3] — example: "Accessible — no jargon without explanation" + +WE ARE NOT: +- [anti-trait 1] — example: "Not salesy — we educate, not pitch" +- [anti-trait 2] — example: "Not condescending — we respect the reader's intelligence" +- [anti-trait 3] — example: "Not generic — every piece has a distinct point of view" + +VOCABULARY: + Preferred terms: [list words you use] + Avoided terms: [list words you never use] + +EXAMPLE SENTENCES: + On-brand: "[example sentence in your voice]" + Off-brand: "[same idea written in a way you would reject]" +``` + +--- + +## Multi-Channel Content Repurposing Strategies + +### Repurposing Matrix + +From one pillar piece, derive content for every active channel: + +``` +SOURCE: Long-form blog post (1500+ words) + + → Twitter: 5-tweet thread summarizing key points + → LinkedIn: 300-word professional insight post + → Newsletter: Curated excerpt + link + commentary + → YouTube/Video: 3-5 min explainer script + → Podcast: Talking points for discussion episode + → Instagram: Quote card + carousel of key stats + → SlideShare: 10-slide visual summary + → Reddit/Community: Discussion post with key finding +``` + +### Repurposing Rules + +1. **Adapt, do not copy** — each channel has native conventions; rewrite for the platform +2. **Lead with the strongest insight** — different channels reward different hooks +3. **Stagger releases** — do not publish everywhere simultaneously; create a 3-5 day drip +4. **Link back** — repurposed content should drive traffic to the original owned asset +5. **Track per channel** — measure performance of each repurposed piece independently + +--- + +## Content Performance KPIs by Channel + +### Blog / Website + +| KPI | Definition | Benchmark Range | +|-----|-----------|----------------| +| Organic traffic | Sessions from search engines | Track month-over-month growth | +| Time on page | Average reading duration | 2-4 min for 1000-word posts | +| Bounce rate | Single-page sessions | 40-60% is typical for blog | +| Scroll depth | % of page viewed | 50%+ for engaged readers | +| Conversion rate | CTA clicks / page views | 1-3% for blog CTAs | +| Backlinks earned | External sites linking to piece | 5+ for pillar content | + +### Email / Newsletter + +| KPI | Definition | Benchmark Range | +|-----|-----------|----------------| +| Open rate | Opens / delivered | 20-30% (varies by industry) | +| Click rate | Clicks / delivered | 2-5% | +| Unsubscribe rate | Unsubs / delivered | < 0.5% per send | +| List growth rate | Net new subscribers / month | 2-5% monthly | +| Forward rate | Forwards / delivered | 0.5-1% | + +### Social Media (Twitter, LinkedIn, etc.) + +| KPI | Definition | Benchmark Range | +|-----|-----------|----------------| +| Engagement rate | (likes + replies + shares) / impressions | 1-3% organic | +| Follower growth | Net new followers / month | Track trend, not absolute | +| Click-through rate | Link clicks / impressions | 0.5-2% | +| Share rate | Shares / impressions | 0.1-0.5% | +| Reply rate | Replies / impressions | Higher = better engagement | + +### Content ROI Formula + +``` +Content ROI = (Revenue attributed to content - Content production cost) / Content production cost x 100 + +For non-revenue goals, use proxy metrics: + Brand Awareness ROI = (Impressions x Estimated CPM value) / Production cost + Lead Gen ROI = (Leads generated x Average lead value) / Production cost + Engagement ROI = (Engaged users x Estimated engagement value) / Production cost +``` diff --git a/crates/openfang-hands/src/bundled.rs b/crates/openfang-hands/src/bundled.rs index db7eabfd4..8189056ba 100644 --- a/crates/openfang-hands/src/bundled.rs +++ b/crates/openfang-hands/src/bundled.rs @@ -40,6 +40,36 @@ pub fn bundled_hands() -> Vec<(&'static str, &'static str, &'static str)> { include_str!("../bundled/browser/HAND.toml"), include_str!("../bundled/browser/SKILL.md"), ), + ( + "reddit", + include_str!("../bundled/reddit/HAND.toml"), + include_str!("../bundled/reddit/SKILL.md"), + ), + ( + "linkedin", + include_str!("../bundled/linkedin/HAND.toml"), + include_str!("../bundled/linkedin/SKILL.md"), + ), + ( + "strategist", + include_str!("../bundled/strategist/HAND.toml"), + include_str!("../bundled/strategist/SKILL.md"), + ), + ( + "apitester", + include_str!("../bundled/apitester/HAND.toml"), + include_str!("../bundled/apitester/SKILL.md"), + ), + ( + "devops", + include_str!("../bundled/devops/HAND.toml"), + include_str!("../bundled/devops/SKILL.md"), + ), + ( + "analytics", + include_str!("../bundled/analytics/HAND.toml"), + include_str!("../bundled/analytics/SKILL.md"), + ), ] } @@ -71,7 +101,7 @@ mod tests { #[test] fn bundled_hands_count() { let hands = bundled_hands(); - assert_eq!(hands.len(), 7); + assert_eq!(hands.len(), 13); } #[test] @@ -201,6 +231,108 @@ mod tests { assert_eq!(def.agent.max_iterations, Some(60)); } + #[test] + fn parse_reddit_hand() { + let (id, toml_content, skill_content) = bundled_hands() + .into_iter() + .find(|(id, _, _)| *id == "reddit") + .unwrap(); + let def = parse_bundled(id, toml_content, skill_content).unwrap(); + assert_eq!(def.id, "reddit"); + assert_eq!(def.name, "Reddit Hand"); + assert_eq!(def.category, crate::HandCategory::Communication); + assert!(def.skill_content.is_some()); + assert!(!def.requires.is_empty()); // requires REDDIT API keys + assert!(!def.settings.is_empty()); + assert!(!def.dashboard.metrics.is_empty()); + assert!((def.agent.temperature - 0.7).abs() < f32::EPSILON); + } + + #[test] + fn parse_linkedin_hand() { + let (id, toml_content, skill_content) = bundled_hands() + .into_iter() + .find(|(id, _, _)| *id == "linkedin") + .unwrap(); + let def = parse_bundled(id, toml_content, skill_content).unwrap(); + assert_eq!(def.id, "linkedin"); + assert_eq!(def.name, "LinkedIn Hand"); + assert_eq!(def.category, crate::HandCategory::Communication); + assert!(def.skill_content.is_some()); + assert!(!def.requires.is_empty()); // requires LINKEDIN_ACCESS_TOKEN + assert!(!def.settings.is_empty()); + assert!(!def.dashboard.metrics.is_empty()); + assert!((def.agent.temperature - 0.7).abs() < f32::EPSILON); + } + + #[test] + fn parse_strategist_hand() { + let (id, toml_content, skill_content) = bundled_hands() + .into_iter() + .find(|(id, _, _)| *id == "strategist") + .unwrap(); + let def = parse_bundled(id, toml_content, skill_content).unwrap(); + assert_eq!(def.id, "strategist"); + assert_eq!(def.name, "Strategist Hand"); + assert_eq!(def.category, crate::HandCategory::Content); + assert!(def.skill_content.is_some()); + assert!(def.requires.is_empty()); + assert!(!def.settings.is_empty()); + assert!(!def.dashboard.metrics.is_empty()); + assert!((def.agent.temperature - 0.3).abs() < f32::EPSILON); + } + + #[test] + fn parse_apitester_hand() { + let (id, toml_content, skill_content) = bundled_hands() + .into_iter() + .find(|(id, _, _)| *id == "apitester") + .unwrap(); + let def = parse_bundled(id, toml_content, skill_content).unwrap(); + assert_eq!(def.id, "apitester"); + assert_eq!(def.name, "API Tester Hand"); + assert_eq!(def.category, crate::HandCategory::Development); + assert!(def.skill_content.is_some()); + assert!(def.requires.is_empty()); + assert!(!def.settings.is_empty()); + assert!(!def.dashboard.metrics.is_empty()); + assert!((def.agent.temperature - 0.3).abs() < f32::EPSILON); + } + + #[test] + fn parse_devops_hand() { + let (id, toml_content, skill_content) = bundled_hands() + .into_iter() + .find(|(id, _, _)| *id == "devops") + .unwrap(); + let def = parse_bundled(id, toml_content, skill_content).unwrap(); + assert_eq!(def.id, "devops"); + assert_eq!(def.name, "DevOps Hand"); + assert_eq!(def.category, crate::HandCategory::Development); + assert!(def.skill_content.is_some()); + assert!(def.requires.is_empty()); + assert!(!def.settings.is_empty()); + assert!(!def.dashboard.metrics.is_empty()); + assert!((def.agent.temperature - 0.3).abs() < f32::EPSILON); + } + + #[test] + fn parse_analytics_hand() { + let (id, toml_content, skill_content) = bundled_hands() + .into_iter() + .find(|(id, _, _)| *id == "analytics") + .unwrap(); + let def = parse_bundled(id, toml_content, skill_content).unwrap(); + assert_eq!(def.id, "analytics"); + assert_eq!(def.name, "Analytics Hand"); + assert_eq!(def.category, crate::HandCategory::Data); + assert!(def.skill_content.is_some()); + assert!(def.requires.is_empty()); + assert!(!def.settings.is_empty()); + assert!(!def.dashboard.metrics.is_empty()); + assert!((def.agent.temperature - 0.3).abs() < f32::EPSILON); + } + #[test] fn all_bundled_hands_parse() { for (id, toml_content, skill_content) in bundled_hands() { @@ -216,7 +348,7 @@ mod tests { #[test] fn all_einstein_hands_have_schedules() { - let einstein_ids = ["lead", "collector", "predictor", "researcher", "twitter"]; + let einstein_ids = ["lead", "collector", "predictor", "researcher", "twitter", "reddit", "linkedin", "strategist", "apitester", "devops"]; for (id, toml_content, skill_content) in bundled_hands() { if einstein_ids.contains(&id) { let def = parse_bundled(id, toml_content, skill_content).unwrap(); @@ -241,7 +373,7 @@ mod tests { #[test] fn all_einstein_hands_have_memory() { - let einstein_ids = ["lead", "collector", "predictor", "researcher", "twitter"]; + let einstein_ids = ["lead", "collector", "predictor", "researcher", "twitter", "reddit", "linkedin", "strategist", "apitester", "devops", "analytics"]; for (id, toml_content, skill_content) in bundled_hands() { if einstein_ids.contains(&id) { let def = parse_bundled(id, toml_content, skill_content).unwrap(); @@ -261,7 +393,7 @@ mod tests { #[test] fn all_einstein_hands_have_knowledge_graph() { - let einstein_ids = ["lead", "collector", "predictor", "researcher", "twitter"]; + let einstein_ids = ["lead", "collector", "predictor", "researcher", "twitter", "reddit", "linkedin", "strategist", "apitester", "devops", "analytics"]; for (id, toml_content, skill_content) in bundled_hands() { if einstein_ids.contains(&id) { let def = parse_bundled(id, toml_content, skill_content).unwrap(); diff --git a/crates/openfang-hands/src/registry.rs b/crates/openfang-hands/src/registry.rs index 70ea582d5..154adc62b 100644 --- a/crates/openfang-hands/src/registry.rs +++ b/crates/openfang-hands/src/registry.rs @@ -442,7 +442,7 @@ mod tests { fn load_bundled_hands() { let reg = HandRegistry::new(); let count = reg.load_bundled(); - assert_eq!(count, 7); + assert_eq!(count, 13); assert!(!reg.list_definitions().is_empty()); // Clip hand should be loaded @@ -460,6 +460,14 @@ mod tests { // Browser hand should be loaded assert!(reg.get_definition("browser").is_some()); + + // New hands should be loaded + assert!(reg.get_definition("reddit").is_some()); + assert!(reg.get_definition("linkedin").is_some()); + assert!(reg.get_definition("strategist").is_some()); + assert!(reg.get_definition("apitester").is_some()); + assert!(reg.get_definition("devops").is_some()); + assert!(reg.get_definition("analytics").is_some()); } #[test]