feat(vars): VarsService + vars:: SDK + config capabilities

Scripts can now read group-inherited config via vars::get(key) / vars::all().

- shared: VarsService trait (read-only get/all) + NoopVarsService; added as
  the 14th field on the Services bundle.
- manager-core: VarsServiceImpl resolves the calling app's config via
  config_resolver (derives app_id from cx.app_id; AppVarsRead-gated for
  authed principals, script-as-gate for anon).
- executor-core: vars:: Rhai bridge (get/all), registered in the SDK.
- authz: AppVarsRead/Write, GroupVarsRead/Write, GroupSecretsRead/Write
  capabilities (group caps resolve via the Phase-2 ancestor walk; the
  GroupSecretsRead human-read gate is group_admin-only, distinct from an
  app's runtime injection).
- wired VarsServiceImpl into the picloud binary; updated the executor-core
  test Services::new call sites.

386 manager-core lib tests green; clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-24 20:59:23 +02:00
parent 35dbd9f368
commit 343f6d3b4d
22 changed files with 258 additions and 9 deletions

View File

@@ -26,6 +26,7 @@ pub mod retry;
pub mod secrets;
pub mod stdlib;
pub mod users;
pub mod vars;
pub use bridge::{dynamic_to_json, json_to_dynamic};
pub use cx::SdkCallCx;
@@ -62,6 +63,7 @@ pub fn register_all(
queue::register(engine, services, cx.clone());
retry::register(engine, services, cx.clone());
secrets::register(engine, services, cx.clone());
vars::register(engine, services, cx.clone());
email::register(engine, services, cx.clone());
users::register(engine, services, cx.clone());
invoke::register(engine, services, cx, limits, self_engine);

View File

@@ -0,0 +1,57 @@
//! `vars::` Rhai bridge — read-only access to the app's resolved,
//! group-inherited config (Phase 3).
//!
//! ```rhai
//! let region = vars::get("region"); // value or ()
//! let all = vars::all(); // #{ key: value, ... }
//! ```
//!
//! Values are inherited down the group tree and env-filtered (§3); the
//! resolution happens server-side in manager-core. Writes go through the
//! admin API, not the SDK. `app_id` is derived from `cx.app_id` in the
//! service — never a script argument — preserving cross-app isolation.
use std::sync::Arc;
use picloud_shared::{SdkCallCx, Services};
use rhai::{Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};
use super::bridge::{block_on, json_to_dynamic};
pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
let svc = services.vars.clone();
let mut module = Module::new();
// vars::get(key) — resolved value, or () if no level defines it.
{
let svc = svc.clone();
let cx = cx.clone();
module.set_native_fn(
"get",
move |key: &str| -> Result<Dynamic, Box<EvalAltResult>> {
let svc = svc.clone();
let cx = cx.clone();
let opt = block_on("vars", async move { svc.get(&cx, key).await })?;
Ok(opt.map_or(Dynamic::UNIT, json_to_dynamic))
},
);
}
// vars::all() — the fully-resolved config map.
{
let svc = svc.clone();
let cx = cx.clone();
module.set_native_fn("all", move || -> Result<Map, Box<EvalAltResult>> {
let svc = svc.clone();
let cx = cx.clone();
let resolved = block_on("vars", async move { svc.all(&cx).await })?;
let mut m = Map::new();
for (k, v) in resolved {
m.insert(k.into(), json_to_dynamic(v));
}
Ok(m)
});
}
engine.register_static_module("vars", module.into());
}