feat(interceptors): hook set_if (CAS) + pic interceptors ls (§9.4 M12)
Closes the §9.4 track:
- set_if (compare-and-swap), per-app and shared, now runs the same (kv, set)
before/after interceptor as set — otherwise CAS was a silent bypass of a set
policy. The before-hook can transform the new value; the after-hook sees the
swapped bool.
- read-only pic interceptors ls --app|--group: app shows the RESOLVED chain
(every marker guarding its writes, nearest-owner-wins), group its own markers.
New apply_service::interceptor_report + InterceptorInfo, /apps|groups/{id}/
interceptors routes (AppRead/GroupScriptsRead), client + cmd mirroring
extension-points.
CLAUDE.md updated: §9.4 service interceptors are now COMPLETE (M1-M12).
Pinned by a journey: set_if of a guarded key is denied while a free key swaps,
and interceptors ls lists the kv/set guard (12 interceptor journeys green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -215,12 +215,38 @@ fn register_set_if(engine: &mut RhaiEngine) {
|
||||
expected: Dynamic,
|
||||
new: Dynamic|
|
||||
-> Result<bool, Box<EvalAltResult>> {
|
||||
let h = handle.clone();
|
||||
let exp = expected_from_dynamic(&expected)?;
|
||||
let new = dynamic_to_json_capped(&new, MAX_JSON_MATERIALIZE_BYTES)?;
|
||||
block_on("kv", async move {
|
||||
h.service.set_if(&h.cx, &h.collection, key, exp, new).await
|
||||
})
|
||||
let json = dynamic_to_json_capped(&new, MAX_JSON_MATERIALIZE_BYTES)?;
|
||||
// §9.4 M12: CAS is a WRITE, so it runs the `(kv, set)` guard too —
|
||||
// otherwise `set_if` would be a silent bypass of a `set` policy.
|
||||
let write_val = super::interceptor::run_before(
|
||||
&handle.ictx,
|
||||
&handle.cx,
|
||||
"kv",
|
||||
"set",
|
||||
&handle.collection,
|
||||
key,
|
||||
Some(&json),
|
||||
)?
|
||||
.unwrap_or(json);
|
||||
let written = write_val.clone();
|
||||
let h = handle.clone();
|
||||
let swapped = block_on("kv", async move {
|
||||
h.service
|
||||
.set_if(&h.cx, &h.collection, key, exp, write_val)
|
||||
.await
|
||||
})?;
|
||||
super::interceptor::run_after(
|
||||
&handle.ictx,
|
||||
&handle.cx,
|
||||
"kv",
|
||||
"set",
|
||||
&handle.collection,
|
||||
key,
|
||||
Some(&written),
|
||||
serde_json::Value::Bool(swapped),
|
||||
)?;
|
||||
Ok(swapped)
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -395,12 +421,25 @@ fn register_group_set_if(engine: &mut RhaiEngine) {
|
||||
expected: Dynamic,
|
||||
new: Dynamic|
|
||||
-> Result<bool, Box<EvalAltResult>> {
|
||||
let h = handle.clone();
|
||||
let exp = expected_from_dynamic(&expected)?;
|
||||
let new = dynamic_to_json_capped(&new, MAX_JSON_MATERIALIZE_BYTES)?;
|
||||
block_on("kv", async move {
|
||||
h.service.set_if(&h.cx, &h.collection, key, exp, new).await
|
||||
})
|
||||
let json = dynamic_to_json_capped(&new, MAX_JSON_MATERIALIZE_BYTES)?;
|
||||
// §9.4 M12: CAS on a shared collection runs the `(kv, set)` guard too.
|
||||
let write_val = group_run_before(handle, "set", key, Some(&json))?.unwrap_or(json);
|
||||
let written = write_val.clone();
|
||||
let h = handle.clone();
|
||||
let swapped = block_on("kv", async move {
|
||||
h.service
|
||||
.set_if(&h.cx, &h.collection, key, exp, write_val)
|
||||
.await
|
||||
})?;
|
||||
group_run_after(
|
||||
handle,
|
||||
"set",
|
||||
key,
|
||||
Some(&written),
|
||||
serde_json::Value::Bool(swapped),
|
||||
)?;
|
||||
Ok(swapped)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user