feat(interceptors): per-interceptor wall-clock timeout (§9.4 M5)
A [[interceptors]] marker can set timeout_ms (migration 0075, CHECK > 0); a
runaway guard (loop {}) is interrupted and the op DENIED (fail closed) within
budget rather than hanging the write path. The effective deadline is
min(caller-remaining, now + timeout) — a hook can only tighten, never extend,
its caller's deadline. NULL uses PICLOUD_INTERCEPTOR_TIMEOUT_MS (default 5s).
Wiring: run_resolved_blocking splits into a core + a _with_timeout variant that
computes the effective deadline from engine::ambient_deadline() and runs via
execute_ast_with_deadline; run_one_hook passes the marker's timeout_ms (or the
env default). timeout_ms threaded end to end — manifest, plan, BundleInterceptor,
the reconcile diff (part of the mutable body: a timeout change is an Update),
insert_interceptor_tx, resolve_chain/list_for_owner/list_on_app_chain +
SealedInterceptor/InterceptorMarker, and interceptor_service → ResolvedInterceptor.
Schema snapshot re-blessed. Pinned by a journey: a loop{} guard with
timeout_ms=100 is denied and its write does not persist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -179,12 +179,53 @@ fn invoke_blocking(
|
||||
/// response `body` JSON. Shared by `invoke()` and the §9.4 interceptor hook —
|
||||
/// the caller is responsible for the depth check (both do it before resolving).
|
||||
/// `label` prefixes any compile/execute error.
|
||||
/// Run a resolved script through the `invoke()` re-entry core, inheriting the
|
||||
/// caller's ambient execution deadline.
|
||||
pub(super) fn run_resolved_blocking(
|
||||
self_engine: &Arc<Engine>,
|
||||
cx: &Arc<SdkCallCx>,
|
||||
resolved: &picloud_shared::ResolvedScript,
|
||||
body_json: Json,
|
||||
label: &str,
|
||||
) -> Result<Json, Box<EvalAltResult>> {
|
||||
run_resolved_core(
|
||||
self_engine,
|
||||
cx,
|
||||
resolved,
|
||||
body_json,
|
||||
label,
|
||||
crate::engine::ambient_deadline(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`run_resolved_blocking`] but bounds the run by a per-hook `timeout`
|
||||
/// (§9.4 M5). The effective deadline is `min(ambient, now + timeout)` — a hook
|
||||
/// can only ever TIGHTEN, never extend, its caller's deadline.
|
||||
pub(super) fn run_resolved_blocking_with_timeout(
|
||||
self_engine: &Arc<Engine>,
|
||||
cx: &Arc<SdkCallCx>,
|
||||
resolved: &picloud_shared::ResolvedScript,
|
||||
body_json: Json,
|
||||
label: &str,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> Result<Json, Box<EvalAltResult>> {
|
||||
let ambient = crate::engine::ambient_deadline();
|
||||
let own = timeout.map(|d| std::time::Instant::now() + d);
|
||||
let effective = match (ambient, own) {
|
||||
(Some(a), Some(o)) => Some(a.min(o)),
|
||||
(a, None) => a,
|
||||
(None, o) => o,
|
||||
};
|
||||
run_resolved_core(self_engine, cx, resolved, body_json, label, effective)
|
||||
}
|
||||
|
||||
fn run_resolved_core(
|
||||
self_engine: &Arc<Engine>,
|
||||
cx: &Arc<SdkCallCx>,
|
||||
resolved: &picloud_shared::ResolvedScript,
|
||||
body_json: Json,
|
||||
label: &str,
|
||||
deadline: Option<std::time::Instant>,
|
||||
) -> Result<Json, Box<EvalAltResult>> {
|
||||
let req = ExecRequest {
|
||||
execution_id: ExecutionId::new(),
|
||||
@@ -218,7 +259,7 @@ pub(super) fn run_resolved_blocking(
|
||||
EvalAltResult::ErrorRuntime(format!("{label}: {e}").into(), rhai::Position::NONE).into()
|
||||
})?;
|
||||
let resp = self_engine
|
||||
.execute_ast(&ast, req)
|
||||
.execute_ast_with_deadline(&ast, req, deadline)
|
||||
.map_err(|e| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(format!("{label}: {e}").into(), rhai::Position::NONE).into()
|
||||
})?;
|
||||
|
||||
Reference in New Issue
Block a user