feat(modules): owner-aware module lookup — lexical chain walk (Phase 4b C1)
Make the module-loading contract origin-aware so group-owned modules become resolvable and imports can resolve lexically (§5.5). - `ModuleScript` gains a polymorphic owner (`app_id`/`group_id` + `owner()`), mirroring `Script`. - `ModuleSource::lookup(&cx, name)` -> `resolve(origin: ScriptOwner, name)`: resolution walks the group chain rooted at the importing script's defining node (nearest-owner-wins), not the inheriting app's view. - `PostgresModuleSource::resolve` branches on origin: app-rooted (`CHAIN_LEVELS_CTE`) or a new group-rooted `GROUP_CHAIN_LEVELS_CTE`, joining `kind='module'` rows. The executor resolver passes a temporary `App(cx.app_id)` origin (behaviour- preserving for app scripts; true lexical origin lands in C3). Group scripts still can't carry imports until C4, so no trust-inversion window opens here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,8 +319,19 @@ impl ModuleResolver for PicloudModuleResolver {
|
||||
))
|
||||
})?;
|
||||
|
||||
// C1 (Phase 4b): resolve via the owner-aware `resolve`. Until C3
|
||||
// threads the importing script's true defining node through, the
|
||||
// origin is the calling app — behaviour-preserving for app-owned
|
||||
// scripts (group scripts can't yet carry imports). The app-rooted
|
||||
// chain walk additionally lets an app import an ancestor group's
|
||||
// modules, which is the intended Phase 4b capability.
|
||||
let lookup_result: Result<Option<picloud_shared::ModuleScript>, ModuleSourceError> =
|
||||
tokio::task::block_in_place(|| handle.block_on(self.source.lookup(&self.cx, path)));
|
||||
tokio::task::block_in_place(|| {
|
||||
handle.block_on(
|
||||
self.source
|
||||
.resolve(picloud_shared::ScriptOwner::App(self.cx.app_id), path),
|
||||
)
|
||||
});
|
||||
|
||||
let module_row = match lookup_result {
|
||||
Ok(Some(m)) => m,
|
||||
|
||||
@@ -16,7 +16,7 @@ use picloud_executor_core::{Engine, ExecRequest, InvocationType, Limits};
|
||||
use picloud_shared::{
|
||||
AppId, ExecutionId, ModuleScript, ModuleSource, ModuleSourceError, NoopDeadLetterService,
|
||||
NoopDocsService, NoopEventEmitter, NoopHttpService, NoopKvService, RequestId, ScriptId,
|
||||
ScriptSandbox, SdkCallCx, Services,
|
||||
ScriptOwner, ScriptSandbox, Services,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
@@ -27,9 +27,9 @@ struct FailingSource;
|
||||
|
||||
#[async_trait]
|
||||
impl ModuleSource for FailingSource {
|
||||
async fn lookup(
|
||||
async fn resolve(
|
||||
&self,
|
||||
_cx: &SdkCallCx,
|
||||
_origin: ScriptOwner,
|
||||
_name: &str,
|
||||
) -> Result<Option<ModuleScript>, ModuleSourceError> {
|
||||
Err(ModuleSourceError::Backend(SENTINEL.to_string()))
|
||||
|
||||
@@ -18,7 +18,7 @@ use picloud_executor_core::{Engine, ExecRequest, InvocationType, Limits};
|
||||
use picloud_shared::{
|
||||
AppId, ExecutionId, ModuleScript, ModuleSource, ModuleSourceError, NoopDeadLetterService,
|
||||
NoopDocsService, NoopEventEmitter, NoopHttpService, NoopKvService, RequestId, ScriptId,
|
||||
ScriptSandbox, SdkCallCx, Services,
|
||||
ScriptOwner, ScriptSandbox, Services,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -55,7 +55,8 @@ impl CountingModuleSource {
|
||||
(app_id, name.to_string()),
|
||||
ModuleScript {
|
||||
script_id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
name: name.to_string(),
|
||||
source: source.to_string(),
|
||||
updated_at,
|
||||
@@ -71,20 +72,27 @@ impl CountingModuleSource {
|
||||
|
||||
#[async_trait]
|
||||
impl ModuleSource for CountingModuleSource {
|
||||
async fn lookup(
|
||||
async fn resolve(
|
||||
&self,
|
||||
cx: &SdkCallCx,
|
||||
origin: ScriptOwner,
|
||||
name: &str,
|
||||
) -> Result<Option<ModuleScript>, ModuleSourceError> {
|
||||
self.lookups.fetch_add(1, Ordering::SeqCst);
|
||||
if let Some(err) = self.fail_with.lock().await.as_ref() {
|
||||
return Err(ModuleSourceError::Backend(err.clone()));
|
||||
}
|
||||
// This fake is flat/app-scoped — the inheritance + lexical
|
||||
// resolution semantics are covered by the CLI journey tests
|
||||
// against real Postgres. A group origin has no entries here.
|
||||
let app_id = match origin {
|
||||
ScriptOwner::App(a) => a,
|
||||
ScriptOwner::Group(_) => return Ok(None),
|
||||
};
|
||||
Ok(self
|
||||
.table
|
||||
.lock()
|
||||
.await
|
||||
.get(&(cx.app_id, name.to_string()))
|
||||
.get(&(app_id, name.to_string()))
|
||||
.cloned())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user