feat(interceptors): extend before/after hooks to docs/files/queue/pubsub/http (§9.4 M7-M11)
Every non-KV mutating op now runs the same before/after interceptor machinery: - docs create/update/delete (+ group) — create/update honor the M4 data transform; - files create/update/delete (+ group) — allow/deny only (the blob is never surfaced to the hook, value = None); - queue enqueue (+ shared) — transform-capable; after reports the message id; - pubsub publish_durable (+ shared topic) — transform-capable; - http request — all verbs funnel through two svc.request sites; collection = method, key = url, body not surfaced. ictx threaded into the free-fn/handle paths that lacked it (http was _ictx; queue/pubsub per-app publish). validate_bundle_for generalized to a per-service allowed-ops map (kv set/delete; docs/files create/update/delete; queue enqueue; pubsub publish; http request) — unknown service/op still rejected. INVARIANT enforced + verified: every allowed (service, op) has a matching runtime hook, so no validated-but-unhooked fail-open. Pinned by a new docs-create deny journey (11 interceptor journeys green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,12 +43,11 @@ pub(super) fn register(
|
||||
{
|
||||
let svc = svc.clone();
|
||||
let cx = cx.clone();
|
||||
let ictx = ictx.clone();
|
||||
module.set_native_fn(
|
||||
"enqueue",
|
||||
move |name: &str, message: Dynamic| -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::enqueue")?;
|
||||
let json = message_to_json(&message)?;
|
||||
enqueue_blocking(&svc, &cx, name, json, EnqueueOpts::default())
|
||||
app_enqueue(&ictx, &svc, &cx, name, &message, EnqueueOpts::default())
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -58,13 +57,12 @@ pub(super) fn register(
|
||||
{
|
||||
let svc = svc.clone();
|
||||
let cx = cx.clone();
|
||||
let ictx = ictx.clone();
|
||||
module.set_native_fn(
|
||||
"enqueue",
|
||||
move |name: &str, message: Dynamic, opts: Map| -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::enqueue")?;
|
||||
let json = message_to_json(&message)?;
|
||||
let opts = parse_opts(&opts)?;
|
||||
enqueue_blocking(&svc, &cx, name, json, opts)
|
||||
app_enqueue(&ictx, &svc, &cx, name, &message, opts)
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -134,12 +132,42 @@ pub struct GroupQueueHandle {
|
||||
collection: String,
|
||||
service: Arc<dyn GroupQueueService>,
|
||||
cx: Arc<SdkCallCx>,
|
||||
// §9.4 M1 plumbing: carried so `enqueue` can run a before/after hook in a
|
||||
// later milestone (M10). Unused until then.
|
||||
#[allow(dead_code)]
|
||||
// §9.4: carried so shared `enqueue` runs the before/after hook too (M10).
|
||||
ictx: InterceptorCtx,
|
||||
}
|
||||
|
||||
/// Per-app enqueue with the §9.4 interceptor around it: charge the emission
|
||||
/// budget, run the before-hook (allow/deny + M4 data-transform), enqueue the
|
||||
/// (possibly rewritten) message, then run the after-hook with the new message
|
||||
/// id as the result. `key` is `""` (a queue has no key). `collection` = the
|
||||
/// queue name.
|
||||
fn app_enqueue(
|
||||
ictx: &InterceptorCtx,
|
||||
svc: &Arc<dyn QueueService>,
|
||||
cx: &Arc<SdkCallCx>,
|
||||
name: &str,
|
||||
message: &Dynamic,
|
||||
opts: EnqueueOpts,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::enqueue")?;
|
||||
let json = message_to_json(message)?;
|
||||
let write_val =
|
||||
super::interceptor::run_before(ictx, cx, "queue", "enqueue", name, "", Some(&json))?
|
||||
.unwrap_or(json);
|
||||
let written = write_val.clone();
|
||||
let id = enqueue_blocking(svc, cx, name, write_val, opts)?;
|
||||
super::interceptor::run_after(
|
||||
ictx,
|
||||
cx,
|
||||
"queue",
|
||||
"enqueue",
|
||||
name,
|
||||
"",
|
||||
Some(&written),
|
||||
Json::String(id.0.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
fn group_enqueue(
|
||||
h: &mut GroupQueueHandle,
|
||||
message: Dynamic,
|
||||
@@ -147,6 +175,18 @@ fn group_enqueue(
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
crate::sdk::emit_budget::charge_emission("queue::shared_collection")?;
|
||||
let json = message_to_json(&message)?;
|
||||
// §9.4 before-op interceptor also guards shared-queue enqueues.
|
||||
let write_val = super::interceptor::run_before(
|
||||
&h.ictx,
|
||||
&h.cx,
|
||||
"queue",
|
||||
"enqueue",
|
||||
&h.collection,
|
||||
"",
|
||||
Some(&json),
|
||||
)?
|
||||
.unwrap_or(json);
|
||||
let written = write_val.clone();
|
||||
let handle = TokioHandle::try_current().map_err(|e| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(
|
||||
format!("queue: no tokio runtime available: {e}").into(),
|
||||
@@ -157,12 +197,21 @@ fn group_enqueue(
|
||||
let service = h.service.clone();
|
||||
let cx = h.cx.clone();
|
||||
let collection = h.collection.clone();
|
||||
handle
|
||||
.block_on(async move { service.enqueue(&cx, &collection, json, opts).await })
|
||||
.map(|_id| ())
|
||||
let id = handle
|
||||
.block_on(async move { service.enqueue(&cx, &collection, write_val, opts).await })
|
||||
.map_err(|err| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(format!("queue: {err}").into(), rhai::Position::NONE).into()
|
||||
})
|
||||
})?;
|
||||
super::interceptor::run_after(
|
||||
&h.ictx,
|
||||
&h.cx,
|
||||
"queue",
|
||||
"enqueue",
|
||||
&h.collection,
|
||||
"",
|
||||
Some(&written),
|
||||
Json::String(id.0.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
fn group_depth<F, Fut>(h: &mut GroupQueueHandle, f: F) -> Result<i64, Box<EvalAltResult>>
|
||||
@@ -209,14 +258,15 @@ fn register_shared_queue_methods(engine: &mut RhaiEngine) {
|
||||
}
|
||||
|
||||
/// Run the async enqueue inside the synchronous Rhai context. Mirrors
|
||||
/// `pubsub::block_on` but for the queue's `Result<_, QueueError>`.
|
||||
/// `pubsub::block_on` but for the queue's `Result<_, QueueError>`. Returns the
|
||||
/// new message id (surfaced to the §9.4 after-hook, not to the script).
|
||||
fn enqueue_blocking(
|
||||
svc: &Arc<dyn QueueService>,
|
||||
cx: &Arc<SdkCallCx>,
|
||||
name: &str,
|
||||
payload: Json,
|
||||
opts: EnqueueOpts,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
) -> Result<picloud_shared::QueueMessageId, Box<EvalAltResult>> {
|
||||
let handle = TokioHandle::try_current().map_err(|e| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(
|
||||
format!("queue: no tokio runtime available: {e}").into(),
|
||||
@@ -229,7 +279,6 @@ fn enqueue_blocking(
|
||||
let name = name.to_string();
|
||||
handle
|
||||
.block_on(async move { svc.enqueue(&cx, &name, payload, opts).await })
|
||||
.map(|_id| ())
|
||||
.map_err(|err| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(format!("queue: {err}").into(), rhai::Position::NONE).into()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user