Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/target
pipelit-client/target
.sisyphus/

.env
.omc/
13 changes: 12 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,21 @@ RUN ARCH=$(uname -m) && \
mv "/root/.config/plit/dragonfly-${ARCH}" /root/.config/plit/dragonfly && \
chmod +x /root/.config/plit/dragonfly

# Download agentgateway binary
RUN ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && \
curl -fSL "https://github.com/agentgateway/agentgateway/releases/download/v1.0.1/agentgateway-linux-${ARCH}" \
-o /usr/local/bin/agentgateway && \
chmod +x /usr/local/bin/agentgateway

# Install yq for config assembly
RUN curl -fSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" \
-o /usr/local/bin/yq && \
chmod +x /usr/local/bin/yq

COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 8080 8000
EXPOSE 8080 8000 4000 3000 15000

VOLUME ["/root/.local/share/plit", "/root/.config/pipelit/workspaces"]

Expand Down
79 changes: 79 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,84 @@ if [ ! -f "$CONFIG_FILE" ]; then
eval plit init $INIT_ARGS
fi

# Start agentgateway if configured
# Check both plit .env and pipelit .env for AGENTGATEWAY_DIR
AGW_DIR=""
PLIT_ENV="/root/.config/plit/.env"
PIPELIT_ENV="/root/.local/share/plit/pipelit/.env"
for envfile in "$PLIT_ENV" "$PIPELIT_ENV"; do
if [ -f "$envfile" ] && [ -z "$AGW_DIR" ]; then
AGW_DIR=$(grep "^AGENTGATEWAY_DIR=" "$envfile" | cut -d= -f2 | tr -d '"')
fi
done

# Copy agentgateway env vars to Pipelit's .env if not already there
if [ -n "$AGW_DIR" ] && [ -f "$PIPELIT_ENV" ]; then
for var in AGENTGATEWAY_ENABLED AGENTGATEWAY_URL AGENTGATEWAY_DIR JWT_PRIVATE_KEY; do
if grep -q "^${var}=" "$PLIT_ENV" 2>/dev/null && ! grep -q "^${var}=" "$PIPELIT_ENV" 2>/dev/null; then
grep "^${var}=" "$PLIT_ENV" >> "$PIPELIT_ENV"
fi
done
# JWT_PRIVATE_KEY may be multiline — handle separately
if grep -q "^JWT_PRIVATE_KEY=" "$PLIT_ENV" 2>/dev/null && ! grep -q "^JWT_PRIVATE_KEY=" "$PIPELIT_ENV" 2>/dev/null; then
python3 -c "
import re
with open('$PLIT_ENV') as f: content = f.read()
m = re.search(r'(JWT_PRIVATE_KEY=\".*?\")', content, re.DOTALL)
if m:
with open('$PIPELIT_ENV', 'a') as f: f.write('\n' + m.group(1) + '\n')
"
fi
fi

if [ -n "$AGW_DIR" ] && [ -d "$AGW_DIR" ]; then
echo "Starting agentgateway..."

export AGENTGATEWAY_DIR="$AGW_DIR"
export AGENTGATEWAY_URL="${AGENTGATEWAY_URL:-http://localhost:4000}"

# Ensure binary is in place
if [ ! -f "$AGW_DIR/bin/agentgateway" ]; then
mkdir -p "$AGW_DIR/bin"
cp /usr/local/bin/agentgateway "$AGW_DIR/bin/agentgateway"
fi

# Get encryption key for key decryption (check both .env files)
FIELD_ENC_KEY=""
for envfile in "$PIPELIT_ENV" "$PLIT_ENV"; do
if [ -f "$envfile" ] && [ -z "$FIELD_ENC_KEY" ]; then
FIELD_ENC_KEY=$(grep "^FIELD_ENCRYPTION_KEY=" "$envfile" | head -1 | cut -d= -f2 | tr -d '"')
fi
done

# Start agentgateway in background
(
cd "$AGW_DIR" && \
FIELD_ENCRYPTION_KEY="$FIELD_ENC_KEY" \
PYTHON=/root/.local/share/plit/venv/bin/python3 \
YQ=yq \
./start.sh > /tmp/agw.log 2>&1
) &

# Block until agentgateway is ready. agentgateway is a hard boot
# dependency — if it never comes up, plit must not start serving.
AGW_READY=false
AGW_READY_TIMEOUT=45
for i in $(seq 1 "$AGW_READY_TIMEOUT"); do
if curl -s -o /dev/null "$AGENTGATEWAY_URL/" 2>/dev/null; then
echo "agentgateway ready"
AGW_READY=true
break
fi
sleep 1
done

if [ "$AGW_READY" != true ]; then
echo "FATAL: agentgateway did not become ready within ${AGW_READY_TIMEOUT}s (${AGENTGATEWAY_URL}) — refusing to start plit without it" >&2
cat /tmp/agw.log >&2 2>/dev/null || true
exit 1
fi
fi

echo "Starting plit stack..."
exec plit start --foreground
194 changes: 194 additions & 0 deletions src/commands/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use anyhow::{Context, Result};
use clap::Subcommand;
use pipelit_client::apis::{configuration::Configuration, workflows_api};
use pipelit_client::models::ValidateDslIn;

use super::auth::pipelit_config;

#[derive(Subcommand)]
pub enum ApiCommands {
#[command(subcommand)]
Workflow(WorkflowCommands),

NodeTypes,
}

#[derive(Subcommand)]
pub enum WorkflowCommands {
List {
#[arg(long, default_value = "50")]
limit: i32,
},

Get {
slug: String,
},

Validate {
#[arg(long)]
yaml: String,
},

Delete {
slug: String,

#[arg(long)]
force: bool,
},
}

pub async fn run(cmd: ApiCommands, json_output: bool) -> Result<()> {
let config = pipelit_config()?;

match cmd {
ApiCommands::Workflow(wf_cmd) => run_workflow(wf_cmd, &config, json_output).await,
ApiCommands::NodeTypes => run_node_types(&config, json_output).await,
}
}

async fn run_workflow(
cmd: WorkflowCommands,
config: &Configuration,
json_output: bool,
) -> Result<()> {
match cmd {
WorkflowCommands::List { limit } => {
let result =
workflows_api::list_workflows_api_v1_workflows_get(config, Some(limit), Some(0))
.await
.context("Failed to list workflows")?;

if json_output {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
if let Some(arr) = result.as_array() {
println!("{:<8} {:<30} {:<20}", "ID", "NAME", "SLUG");
println!("{}", "-".repeat(60));
for wf in arr {
println!(
"{:<8} {:<30} {:<20}",
wf["id"].as_i64().unwrap_or(0),
truncate(wf["name"].as_str().unwrap_or("-"), 28),
wf["slug"].as_str().unwrap_or("-"),
);
}
println!("\nTotal: {} workflows", arr.len());
}
}
Ok(())
}

WorkflowCommands::Get { slug } => {
let result =
workflows_api::get_workflow_detail_api_v1_workflows_slug_get(config, &slug)
.await
.context("Failed to get workflow")?;

if json_output {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
println!("Workflow: {}", result.name);
println!("Slug: {}", result.slug);
println!("ID: {}", result.id);
println!("Description: {}", result.description);
if let Some(Some(tags)) = &result.tags {
println!("Tags: {}", tags.join(", "));
}
if let Some(nodes) = &result.nodes {
println!("\nNodes: {}", nodes.len());
for node in nodes {
println!(" - {} ({:?})", node.node_id, node.component_type);
}
}
if let Some(edges) = &result.edges {
println!("\nEdges: {}", edges.len());
}
}
Ok(())
}

WorkflowCommands::Validate { yaml } => {
let yaml_content = std::fs::read_to_string(&yaml)
.with_context(|| format!("Failed to read {}", yaml))?;

let req = ValidateDslIn::new(yaml_content);

let result = workflows_api::validate_dsl_endpoint_api_v1_workflows_validate_dsl_post(
config, req,
)
.await
.context("Failed to validate DSL")?;

if json_output {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
let valid = result["valid"].as_bool().unwrap_or(false);
if valid {
println!("Valid DSL");
println!(" Nodes: {}", result["node_count"].as_i64().unwrap_or(0));
println!(" Edges: {}", result["edge_count"].as_i64().unwrap_or(0));
} else {
println!("Invalid DSL");
if let Some(errors) = result["errors"].as_array() {
for err in errors {
println!(" - {}", err.as_str().unwrap_or("?"));
}
}
}
}
Ok(())
}

WorkflowCommands::Delete { slug, force } => {
if !force {
eprint!("Delete workflow '{}'? [y/N] ", slug);
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled");
return Ok(());
}
}

workflows_api::delete_workflow_api_v1_workflows_slug_delete(config, &slug)
.await
.context("Failed to delete workflow")?;

println!("Deleted workflow: {}", slug);
Ok(())
}
}
}

async fn run_node_types(config: &Configuration, json_output: bool) -> Result<()> {
let result = workflows_api::list_node_types_api_v1_workflows_node_types_get(config)
.await
.context("Failed to list node types")?;

if json_output {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
if let Some(obj) = result.as_object() {
println!("{:<25} {:<15} DESCRIPTION", "TYPE", "CATEGORY");
println!("{}", "-".repeat(80));
for (type_name, spec) in obj {
println!(
"{:<25} {:<15} {}",
type_name,
spec["category"].as_str().unwrap_or("-"),
truncate(spec["description"].as_str().unwrap_or("-"), 40),
);
}
println!("\nTotal: {} node types", obj.len());
}
}
Ok(())
}

fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("{}...", &s[..max - 3])
}
}
Loading
Loading