executor-core/sdk/invoke.rs adds `invoke()` and `invoke_async()` as
top-level Rhai helpers (no `::` namespace, mirroring the brief):
invoke(target, args) -> Dynamic (sync, returns callee's value)
invoke_async(target, args) -> String (fire-and-forget; returns execution_id)
Sync re-entry pattern:
- bridge captures Arc<Engine> via Engine::self_arc()
- calls engine.execute(&source, req) directly — same engine instance,
same Services, same Limits; only SdkCallCx changes per call
- runs inside the caller's spawn_blocking thread (no nested
spawn_blocking, no gate re-admission — the outer execution already
holds a permit)
Back-reference plumbing:
- Engine gains self_weak: OnceLock<Weak<Engine>> + set_self_weak() +
self_arc() accessor
- picloud binary calls engine.set_self_weak(Arc::downgrade(&engine))
right after construction
- register_all extended with limits + Option<Arc<Engine>> params
- Engine::execute_ast threads both through
Limits.trigger_depth_max added — mirrors TriggerConfig::max_trigger_depth.
Default 8; the picloud binary syncs from TriggerConfig::from_env() so
PICLOUD_MAX_TRIGGER_DEPTH governs both the dispatcher's fan-out cap and
the invoke depth bound.
Target parsing (string-first):
- "/api/foo" → InvokeTarget::Path
- 36-char UUID-like → InvokeTarget::Id (uuid::Uuid parse-validated)
- everything else → InvokeTarget::Name
Cross-app + depth + FnPtr guards:
- resolve() returns InvokeError::CrossApp if resolved.app_id != cx.app_id
- bridge throws "invoke: depth limit exceeded (max N)" when
cx.trigger_depth + 1 > limits.trigger_depth_max (checked BEFORE
resolve to avoid a wasted DB round-trip)
- args_to_json rejects FnPtr at any depth — closures don't survive
invoke boundaries
invoke_async path:
- calls services.invoke.enqueue_async() which writes an
OutboxSourceKind::Invoke row (commit 10 wires the dispatcher arm)
- returns the new ExecutionId as a string for caller tracking
Integration tests (sdk_invoke.rs, 6 binaries):
- sync invoke returns callee's value (script returns body.x + 1)
- cross-app invoke rejected (target in other app)
- depth limit exceeded (recursive script that calls itself; throws
before stack overflow)
- callee receives incremented depth in cx (smoke — strict assertion
needs ctx.trigger_depth surface, deferred to v1.2)
- args_to_json rejects FnPtr in payload
- invoke_async returns valid UUID string + queues args payload
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
99 lines
3.8 KiB
Rust
99 lines
3.8 KiB
Rust
use picloud_shared::ScriptSandbox;
|
|
|
|
/// Resource and capability limits applied to every script execution.
|
|
///
|
|
/// Defaults are conservative and safe to expose to untrusted Rhai sources.
|
|
/// Per-script overrides (via `Limits::with_overrides`) replace individual
|
|
/// fields; the manager clamps every override against the admin ceiling at
|
|
/// write time, so the executor trusts what arrives in `ExecRequest`.
|
|
#[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,
|
|
|
|
/// v1.1.3: hard ceiling on `import` chain depth (A→B→C→…). Independent
|
|
/// of cycle detection — guards against deep but acyclic graphs.
|
|
/// Not script-overridable (this is a platform-level guard, not a
|
|
/// per-script knob).
|
|
pub module_import_depth_max: u32,
|
|
|
|
/// v1.1.9: hard ceiling on `cx.trigger_depth` (shared between
|
|
/// trigger fan-out and `invoke()` re-entry). The dispatcher uses
|
|
/// `TriggerConfig::max_trigger_depth` for the SAME bound; the
|
|
/// invoke bridge uses this Limits-side mirror to short-circuit
|
|
/// before a DB round-trip. Defaults to 8 (matches
|
|
/// TriggerConfig::conservative); the picloud binary keeps the two
|
|
/// in sync by passing `TriggerConfig::from_env().max_trigger_depth`
|
|
/// through.
|
|
pub trigger_depth_max: u32,
|
|
}
|
|
|
|
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,
|
|
module_import_depth_max: 8,
|
|
trigger_depth_max: 8,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Limits {
|
|
/// Returns a new `Limits` with each field replaced by the matching
|
|
/// override if present, otherwise the existing field. Overrides
|
|
/// arrive as `u64` for JSONB round-tripping cleanliness; they're
|
|
/// narrowed to `usize` here, saturating on the unlikely overflow
|
|
/// (these caps come from admin-clamped writes, so the values are
|
|
/// always small).
|
|
#[must_use]
|
|
pub fn with_overrides(&self, overrides: &ScriptSandbox) -> Self {
|
|
Self {
|
|
max_operations: overrides.max_operations.unwrap_or(self.max_operations),
|
|
max_string_size: overrides
|
|
.max_string_size
|
|
.map_or(self.max_string_size, narrow_usize),
|
|
max_array_size: overrides
|
|
.max_array_size
|
|
.map_or(self.max_array_size, narrow_usize),
|
|
max_map_size: overrides
|
|
.max_map_size
|
|
.map_or(self.max_map_size, narrow_usize),
|
|
max_call_levels: overrides
|
|
.max_call_levels
|
|
.map_or(self.max_call_levels, narrow_usize),
|
|
max_expr_depth: overrides
|
|
.max_expr_depth
|
|
.map_or(self.max_expr_depth, narrow_usize),
|
|
// module_import_depth_max is platform-level — overrides
|
|
// never touch it. Carry through unchanged.
|
|
module_import_depth_max: self.module_import_depth_max,
|
|
// trigger_depth_max is also platform-level (v1.1.9 invoke
|
|
// depth bound mirrors the dispatcher's trigger-depth cap).
|
|
trigger_depth_max: self.trigger_depth_max,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn narrow_usize(v: u64) -> usize {
|
|
usize::try_from(v).unwrap_or(usize::MAX)
|
|
}
|