feat(interceptors): ordered before-chains + cycle guard + ctx plumbing (§9.4 M1+M2)
M1: introduces a clone-cheap InterceptorCtx { interceptors, self_engine,
limits } threaded into every SDK register fn (kv/docs/files/queue/pubsub/http)
so the non-KV services carry the hook seam (unused until M7-M11); KvHandle
collapses its three fields into one ictx.
M2: replaces the nearest-only resolver with ordered before/after chains. The
trait becomes resolve(cx, service, op) -> InterceptorChain { before, after }
(migration 0074 adds a phase column + phase-aware unique indexes). The before
-chain runs ancestor->app (depth DESC) so a group compliance guard can't be
bypassed by a descendant; single-marker behavior is byte-identical to before.
An identity cycle guard (thread-local visited-set keyed by script_id) denies a
detected cycle, alongside the existing binary re-entrancy break. Fail-closed
verdict preserved (allow only on #{ allowed: true }; a Dangling entry or a
missing engine back-ref denies); app_id still derives from cx.app_id only.
after-chains resolve but stay unused until M3. Pinned by two new interceptor
journeys (ancestor->app chain ordering; self-referential no-recurse).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,26 +30,23 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use picloud_shared::{GroupKvService, InterceptorService, KvService, SdkCallCx, Services};
|
||||
use picloud_shared::{GroupKvService, KvService, SdkCallCx, Services};
|
||||
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};
|
||||
|
||||
use super::bridge::{
|
||||
block_on, dynamic_to_json_capped, json_to_dynamic, JsonSizeError, MAX_JSON_MATERIALIZE_BYTES,
|
||||
};
|
||||
use crate::engine::Engine;
|
||||
use crate::sandbox::Limits;
|
||||
use super::interceptor::InterceptorCtx;
|
||||
|
||||
/// Per-call handle captured by the Rhai SDK. Cheap to clone (a few Arcs plus an
|
||||
/// owned string). Carries the §9.4 interceptor deps so `set`/`delete` can run a
|
||||
/// owned string). Carries the §9.4 interceptor ctx so `set`/`delete` can run a
|
||||
/// before-op allow/deny hook (the resolver + the `invoke()` re-entry engine).
|
||||
#[derive(Clone)]
|
||||
pub struct KvHandle {
|
||||
collection: String,
|
||||
service: Arc<dyn KvService>,
|
||||
cx: Arc<SdkCallCx>,
|
||||
interceptors: Arc<dyn InterceptorService>,
|
||||
self_engine: Option<Arc<Engine>>,
|
||||
limits: Limits,
|
||||
ictx: InterceptorCtx,
|
||||
}
|
||||
|
||||
/// §11.6 shared-collection handle, returned by `kv::shared_collection(name)`. A distinct
|
||||
@@ -57,28 +54,24 @@ pub struct KvHandle {
|
||||
/// collections never grow triggers) and a script's choice of private-vs-shared
|
||||
/// scope is explicit. Routes through the `GroupKvService`, which resolves the
|
||||
/// owning group from `cx.app_id` (the script never names a group). Carries the
|
||||
/// §9.4 interceptor deps too so a `(kv, set/delete)` guard covers shared-
|
||||
/// §9.4 interceptor ctx too so a `(kv, set/delete)` guard covers shared-
|
||||
/// collection writes — otherwise the shared handle would be a silent bypass.
|
||||
#[derive(Clone)]
|
||||
pub struct GroupKvHandle {
|
||||
collection: String,
|
||||
service: Arc<dyn GroupKvService>,
|
||||
cx: Arc<SdkCallCx>,
|
||||
interceptors: Arc<dyn InterceptorService>,
|
||||
self_engine: Option<Arc<Engine>>,
|
||||
limits: Limits,
|
||||
ictx: InterceptorCtx,
|
||||
}
|
||||
|
||||
pub(super) fn register(
|
||||
engine: &mut RhaiEngine,
|
||||
services: &Services,
|
||||
cx: Arc<SdkCallCx>,
|
||||
limits: Limits,
|
||||
self_engine: Option<Arc<Engine>>,
|
||||
ictx: InterceptorCtx,
|
||||
) {
|
||||
let kv_service = services.kv.clone();
|
||||
let group_kv_service = services.group_kv.clone();
|
||||
let interceptors = services.interceptors.clone();
|
||||
|
||||
// `kv::collection(name)` / `kv::shared_collection(name)` — both constructors live in
|
||||
// the `kv` static module so the script-visible calls are `kv::collection`
|
||||
@@ -87,8 +80,7 @@ pub(super) fn register(
|
||||
{
|
||||
let kv_service = kv_service.clone();
|
||||
let cx = cx.clone();
|
||||
let interceptors = interceptors.clone();
|
||||
let self_engine = self_engine.clone();
|
||||
let ictx = ictx.clone();
|
||||
module.set_native_fn(
|
||||
"collection",
|
||||
move |name: &str| -> Result<KvHandle, Box<EvalAltResult>> {
|
||||
@@ -99,9 +91,7 @@ pub(super) fn register(
|
||||
collection: name.to_string(),
|
||||
service: kv_service.clone(),
|
||||
cx: cx.clone(),
|
||||
interceptors: interceptors.clone(),
|
||||
self_engine: self_engine.clone(),
|
||||
limits,
|
||||
ictx: ictx.clone(),
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -109,8 +99,7 @@ pub(super) fn register(
|
||||
{
|
||||
let group_kv_service = group_kv_service.clone();
|
||||
let cx = cx.clone();
|
||||
let interceptors = interceptors.clone();
|
||||
let self_engine = self_engine.clone();
|
||||
let ictx = ictx.clone();
|
||||
module.set_native_fn(
|
||||
"shared_collection",
|
||||
move |name: &str| -> Result<GroupKvHandle, Box<EvalAltResult>> {
|
||||
@@ -121,9 +110,7 @@ pub(super) fn register(
|
||||
collection: name.to_string(),
|
||||
service: group_kv_service.clone(),
|
||||
cx: cx.clone(),
|
||||
interceptors: interceptors.clone(),
|
||||
self_engine: self_engine.clone(),
|
||||
limits,
|
||||
ictx: ictx.clone(),
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -188,10 +175,8 @@ fn register_set(engine: &mut RhaiEngine) {
|
||||
// §9.4 before-op interceptor (allow/deny). A denial errors here and
|
||||
// the write below never runs.
|
||||
super::interceptor::run_before(
|
||||
&handle.interceptors,
|
||||
handle.self_engine.as_ref(),
|
||||
&handle.ictx,
|
||||
&handle.cx,
|
||||
handle.limits,
|
||||
"kv",
|
||||
"set",
|
||||
&handle.collection,
|
||||
@@ -244,10 +229,8 @@ fn register_delete(engine: &mut RhaiEngine) {
|
||||
"delete",
|
||||
|handle: &mut KvHandle, key: &str| -> Result<bool, Box<EvalAltResult>> {
|
||||
super::interceptor::run_before(
|
||||
&handle.interceptors,
|
||||
handle.self_engine.as_ref(),
|
||||
&handle.ictx,
|
||||
&handle.cx,
|
||||
handle.limits,
|
||||
"kv",
|
||||
"delete",
|
||||
&handle.collection,
|
||||
@@ -271,10 +254,8 @@ fn group_run_before(
|
||||
value: Option<&serde_json::Value>,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
super::interceptor::run_before(
|
||||
&handle.interceptors,
|
||||
handle.self_engine.as_ref(),
|
||||
&handle.ictx,
|
||||
&handle.cx,
|
||||
handle.limits,
|
||||
"kv",
|
||||
op,
|
||||
&handle.collection,
|
||||
|
||||
Reference in New Issue
Block a user