progit-plugin-sdk 0.3.0

Plugin SDK for ProGit — sandboxed LuaJIT runtime with capability-based security. LSL-1.0 (file-level copyleft, proprietary plugins allowed via the commercial bridge).
Documentation
// SPDX-License-Identifier: LSL-1.0
// Copyright (c) 2026 Markus Maiwald

//! Async job execution contract
//!
//! Host-managed background jobs that plugins can spawn and monitor.
//! The plugin never owns the thread — it requests work from the host,
//! receives progress events, and may cancel jobs it owns.
//!
//! This preserves Doctrine 4 (trait firewall): plugins cannot spawn raw
//! threads, open arbitrary processes, or block the TUI render loop.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Opaque job identifier. Plugins treat this as an opaque string.
pub type JobId = String;

/// What the plugin wants the host to run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobSpawnRequest {
    /// Human label shown in TUI job lists.
    pub label: String,
    /// Command argv[0] — must be a binary the host already trusts
    /// (e.g. `citadel`, not an arbitrary path).
    pub binary: String,
    /// Arguments passed to the binary.
    pub args: Vec<String>,
    /// Working directory. Empty = repo root.
    #[serde(default)]
    pub cwd: Option<String>,
    /// Environment variables injected for this job.
    #[serde(default)]
    pub env: HashMap<String, String>,
    /// Max runtime before host auto-cancels (seconds). 0 = host default.
    #[serde(default)]
    pub timeout_secs: u64,
}

/// Events the host emits about a job's lifecycle.
///
/// Serialised as `{"type": "...", "job_id": "...", "data": {...}}`
/// so Lua receives ergonomic tables.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum JobEvent {
    /// Job has started on a host background thread.
    JobStarted {
        job_id: JobId,
        #[serde(flatten)]
        data: JobStartedData,
    },
    /// Incremental progress (0–100 or arbitrary units).
    JobProgress {
        job_id: JobId,
        #[serde(flatten)]
        data: JobProgressData,
    },
    /// One line of stdout/stderr from the job process.
    JobLogLine {
        job_id: JobId,
        #[serde(flatten)]
        data: JobLogLineData,
    },
    /// Raw output chunk (for non-line-oriented binaries).
    JobOutputChunk {
        job_id: JobId,
        #[serde(flatten)]
        data: JobOutputChunkData,
    },
    /// Job was cancelled by user or timeout.
    JobCancelled {
        job_id: JobId,
        #[serde(flatten)]
        data: JobCancelledData,
    },
    /// Job finished successfully.
    JobCompleted {
        job_id: JobId,
        #[serde(flatten)]
        data: JobCompletedData,
    },
    /// Job failed (non-zero exit, panic, or unhandled error).
    JobFailed {
        job_id: JobId,
        #[serde(flatten)]
        data: JobFailedData,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStartedData {
    pub pid: Option<u32>,
    pub started_at: String, // ISO 8601
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobProgressData {
    /// 0–100 when known, otherwise arbitrary step counter.
    pub percent: Option<u8>,
    /// Human step description (e.g. "Compiling module 3/7").
    pub step: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobLogLineData {
    pub line: String,
    /// `stdout` | `stderr` | `system`
    pub stream: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobOutputChunkData {
    pub bytes: Vec<u8>,
    /// `stdout` | `stderr`
    pub stream: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobCancelledData {
    pub reason: String,
    pub cancelled_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobCompletedData {
    pub exit_code: i32,
    pub completed_at: String,
    /// Truncated stdout tail (host decides limit, e.g. last 4 KB).
    pub output_tail: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobFailedData {
    pub exit_code: Option<i32>,
    pub error: String,
    pub failed_at: String,
}

/// Host-side trait for job orchestration.
///
/// The ProGit TUI implements this and injects it into the plugin runtime.
/// Plugins receive a `Box<dyn JobHost>` (or Lua equivalent) and call
/// `spawn`, `cancel`, and `status` — never `std::process::Command` directly.
pub trait JobHost: Send + Sync {
    /// Spawn a background job. Returns the opaque `JobId`.
    fn spawn(&self, req: JobSpawnRequest) -> JobId;

    /// Cancel a job the plugin previously spawned.
    /// Returns true if cancellation was sent; false if job already finished.
    fn cancel(&self, job_id: &JobId) -> bool;

    /// Poll the current status of a job.
    fn status(&self, job_id: &JobId) -> Option<JobStatus>;

    /// Drain events for jobs owned by this plugin.
    /// Non-blocking: returns events accumulated since last call.
    fn drain_events(&self) -> Vec<JobEvent>;
}

/// Lightweight status snapshot for UI lists.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobStatus {
    Pending,
    Running {
        started_at: String,
        percent: Option<u8>,
    },
    Cancelled {
        reason: String,
    },
    Completed {
        exit_code: i32,
    },
    Failed {
        exit_code: Option<i32>,
        error: String,
    },
}