chore: initial scaffold — workspace, docs, blueprint

Sets up the PiCloud monorepo as a Cargo workspace organised around the
three-service architecture (manager / orchestrator / executor), each
backed by a *-core library crate so the same logic powers both the MVP
all-in-one `picloud` binary and the future split-process cluster mode.

  * crates/shared, executor-core, orchestrator-core, manager-core
    define the library surface and trait seams between the three
    services (`ExecutorClient`, `ScriptResolver`, `ScriptRepository`).
  * crates/picloud is the MVP entrypoint; serves /healthz on 8080
    (override via PICLOUD_BIND).
  * crates/picloud-{manager,orchestrator,executor} are skeleton
    binaries that keep the crate boundaries honest until cluster
    mode is built out in v1.3+.
  * docs/git-workflow.md defines the trunk-based workflow:
    short-lived branches, Conventional Commits, separate hotfix
    flow with mandatory reproduction tests.
  * CLAUDE.md captures the working rules for future Claude sessions.

Workspace passes `cargo fmt`, `cargo clippy -D warnings` (with
pedantic enabled), and `cargo test --workspace`. The all-in-one
binary responds on `/healthz` and `/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-22 23:16:32 +02:00
commit b8b544816d
36 changed files with 5843 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
[package]
name = "picloud-executor-core"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
picloud-shared.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tracing.workspace = true
uuid.workspace = true
chrono.workspace = true
rhai.workspace = true

View File

@@ -0,0 +1,8 @@
//! The `ctx` object exposed to running Rhai scripts.
//!
//! MVP shape only: execution metadata + request data. The full SDK
//! (kv, docs, users, http, retry, invoke) lands in v1.1+ and will be
//! injected via a `ServiceProvider` trait rather than hardcoded here.
// Implementation lands together with the Rhai engine wiring; the file
// exists now so the module boundary is fixed in the public API.

View File

@@ -0,0 +1,48 @@
use crate::sandbox::Limits;
use crate::types::{ExecError, ExecRequest, ExecResponse};
/// Preconfigured Rhai engine with sandbox limits applied.
///
/// One `Engine` is constructed at process startup and reused across
/// invocations. `execute` is the only entry point — it owns the per-call
/// scope and log buffer, then returns a complete `ExecResponse`.
pub struct Engine {
limits: Limits,
// The actual `rhai::Engine` lands with the first execution implementation.
// Keep this opaque for now so callers don't bind to it.
}
impl Engine {
#[must_use]
pub fn new(limits: Limits) -> Self {
Self { limits }
}
#[must_use]
pub fn limits(&self) -> &Limits {
&self.limits
}
/// Parse-only validation, used by the manager at script-upload time
/// so syntax errors are surfaced before the first invocation.
pub fn validate(&self, _source: &str) -> Result<(), ExecError> {
// TODO(executor-core): wire `rhai::Engine::compile`
Ok(())
}
/// Execute `source` against `req` under the configured sandbox.
///
/// `async` is part of the contract: v1.1+ SDK calls (kv, docs, http)
/// will await injected service providers from inside this method.
#[allow(clippy::unused_async)]
pub async fn execute(
&self,
_source: &str,
_req: ExecRequest,
) -> Result<ExecResponse, ExecError> {
// TODO(executor-core): wire `rhai::Engine::eval_with_scope`
Err(ExecError::Runtime(
"executor-core::Engine::execute not yet implemented".into(),
))
}
}

View File

@@ -0,0 +1,17 @@
//! Rhai script execution: engine, sandbox, SDK bindings.
//!
//! This crate has no Postgres dependency and no awareness of HTTP. It is
//! used both by the orchestrator (in-process, via `LocalExecutorClient`) and
//! by the standalone executor binary (in cluster mode). Keep it that way.
pub mod context;
pub mod engine;
pub mod logging;
pub mod sandbox;
pub mod types;
pub use engine::Engine;
pub use sandbox::Limits;
pub use types::{
ExecError, ExecRequest, ExecResponse, ExecStats, InvocationType, LogEntry, LogLevel,
};

View File

@@ -0,0 +1,6 @@
//! `log.info / warn / error / debug` bindings exposed to Rhai scripts.
//!
//! Captures structured log entries into a per-execution buffer that is
//! attached to the `ExecResponse` and persisted by the manager.
// Implementation lands with the engine wiring.

View File

@@ -0,0 +1,37 @@
/// Resource and capability limits applied to every script execution.
///
/// Defaults are conservative and safe to expose to untrusted Rhai sources.
/// Per-script overrides (e.g. higher operation budgets) come from the
/// `Script` config and are clamped against these as upper bounds.
#[derive(Debug, Clone, Copy)]
pub struct Limits {
/// Hard cap on Rhai operations executed per invocation.
/// Doubles as a CPU-time proxy without needing real timers.
pub max_operations: u64,
/// Max length of any single string the script constructs.
pub max_string_size: usize,
/// Max number of elements in any array.
pub max_array_size: usize,
/// Max number of properties in any object/map.
pub max_map_size: usize,
/// Max call/expression nesting depth.
pub max_call_levels: usize,
pub max_expr_depth: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_operations: 1_000_000,
max_string_size: 64 * 1024,
max_array_size: 10_000,
max_map_size: 10_000,
max_call_levels: 64,
max_expr_depth: 64,
}
}
}

View File

@@ -0,0 +1,80 @@
use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use picloud_shared::{ExecutionId, RequestId, ScriptId};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum InvocationType {
Http,
Function,
Scheduled,
}
/// Everything the executor needs to run one invocation.
///
/// This type crosses process boundaries in cluster mode — keep it fully
/// serializable and don't add transient handles (DB pools, etc.).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecRequest {
pub execution_id: ExecutionId,
pub request_id: RequestId,
pub script_id: ScriptId,
pub script_name: String,
pub invocation_type: InvocationType,
pub path: String,
pub headers: BTreeMap<String, String>,
pub body: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecResponse {
pub status_code: u16,
pub headers: BTreeMap<String, String>,
pub body: serde_json::Value,
pub logs: Vec<LogEntry>,
pub stats: ExecStats,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub timestamp: DateTime<Utc>,
pub level: LogLevel,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct ExecStats {
pub duration_ms: u64,
pub operations: u64,
}
#[derive(Debug, Error)]
pub enum ExecError {
#[error("script failed to parse: {0}")]
Parse(String),
#[error("script returned an invalid response shape: {0}")]
InvalidResponse(String),
#[error("execution timed out after {0}s")]
Timeout(u32),
#[error("execution exceeded operation budget")]
OperationBudgetExceeded,
#[error("script runtime error: {0}")]
Runtime(String),
}