Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,49 @@ libxpkg/
- [完整设计方案](.agents/plans/2026-03-01-libxpkg-design.md)
- [mcpp-style-ref | 现代C++编码/项目风格参考](https://github.com/mcpp-community/mcpp-style-ref)
- [xim-pkgindex | 包索引仓库](https://github.com/d2learn/xim-pkgindex)

## 外置 Hook 执行边界

需要限制 recipe 权限的消费者可以使用 `create_executor(path, ExecutionBoundary)`。
这个重载先发出 `load` 请求,不在调用进程创建 Lua state、加载标准库或执行
recipe 顶层代码。边界为空、初始化失败、传输失败时返回错误,不会回退到本地执行。
不传边界的原有 factory 和 `ExecutionContext` 保持原来的行为。

`HookInvocation` 和 `HookResponse` 是不依赖 JSON 库的协议数据。消费者负责
序列化、传输、进程隔离和返回效果的校验;`kHookBoundaryProtocol` 是协议版本。
请求的动作是 `load`、`run_hook`、`run_script`、`elfpatch` 和 `set_log_level`。
`load` 返回五个 hook 的存在标记,后续请求返回 `HookResult` 以及累计的
`XvmOp`、`InstallRequest` 快照。快照与原有 accessor 的语义相同,不能把每次
响应当作仅包含新增效果的列表再次全部应用。

消费者在受限进程内创建一个 `HookWorker`,经 `dispatch()` 处理请求。一个
worker 只加载一个 package,并在 install、config 等调用之间保持同一个
`PackageExecutor`。这样 recipe 的 Lua 全局状态不会丢失,每次 hook 的上下文
仍会更新;hook log、`system.exec(..., { tty = true })` 和旧 recipe 保持原来的
运行语义。`apply_install_stamp_if_empty()` 是消费者的派生数据记账,不执行
recipe Lua,仍由消费者在所有安装路径完成后调用。

这套接口本身不提供沙箱。worker 必须由消费者在权限已经限制的进程里启动,
且限制必须覆盖加载 recipe、整个 Lua hook、`io.open`、`io.popen`、原生
`os.execute` 和它们的后代,不能仅拦截 `system.exec`。控制协议应使用单独的
socket 或文件描述符:顶层 recipe 和 script 可以输出到 stdout,把 stdout
同时当作 NDJSON 协议会让 recipe 输出混入控制响应。

metadata loader 和 `build_index()` 是另一条执行路径。当前 loader 会执行
recipe 顶层 Lua,index builder 会执行 `pkgindex-build.lua`;其中的
`register_loader_sandbox` 是兼容 stub,不是 OS 权限边界。只接入外置 hook
factory 并不代表 metadata、index builder 或所有 recipe Lua 都已隔离。

消费者还可以通过 `load_package(path, LoaderContext, MetadataBoundary)` 接入
受限 metadata worker。这个重载只转交 `MetadataInvocation` 并接收 `Package`,
不会先在宿主执行一次 Lua;边界为空、失败或抛异常均返回错误。worker 内使用
原有 loader,因此仍只有一套 recipe 解析器。

重建 index 时使用带 `LoaderBoundaries` 的 `build_index()` 重载。metadata 和
`pkgindex-build.lua` 分别跨越 `metadata`、`index_build` 边界;存在 build script
却没有它的边界时直接拒绝,不在宿主运行 build script 或其 git checkout。
与旧 builder 跳过损坏 recipe 的行为不同,外置边界失败会使整次重建失败,
不能把权限拒绝视为成功生成一个空 index。消费者需要让 metadata worker
只读 recipe/index,把 builder 的写权限限于 index 的派生输出,同时校验返回
metadata 与效果。旧签名保持旧行为;只有实际接入这些 overload,才覆盖了
metadata 与 index build 两条路径。
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "xpkg"
version = "0.0.60"
version = "0.0.61"
description = "C++23 reference implementation of the xpkg V2 spec (multi-arch)"
license = "Apache-2.0"
repo = "https://github.com/openxlings/libxpkg"
Expand Down
109 changes: 109 additions & 0 deletions src/xpkg-boundary.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
module mcpplibs.xpkg.executor;

import std;

namespace mcpplibs::xpkg {

std::string_view hook_action_name(HookAction action) {
switch (action) {
case HookAction::Load: return "load";
case HookAction::RunHook: return "run_hook";
case HookAction::RunScript: return "run_script";
case HookAction::Elfpatch: return "elfpatch";
case HookAction::SetLogLevel: return "set_log_level";
}
return "";
}

std::optional<HookAction> hook_action_from_string(std::string_view name) {
for (auto action : {HookAction::Load, HookAction::RunHook, HookAction::RunScript,
HookAction::Elfpatch, HookAction::SetLogLevel})
if (hook_action_name(action) == name) return action;
return std::nullopt;
}

PackageExecutor::PackageExecutor(fs::path pkg, ExecutionBoundary boundary, HookResponse loaded)
: pkg_(std::move(pkg)), boundary_(std::move(boundary)), boundaryResponse_(std::move(loaded)) {}

HookResult PackageExecutor::execute_external_(HookAction action, const ExecutionContext& ctx,
HookType hook, std::string_view logLevel) {
if (!boundaryError_.empty()) return {.success = false, .error = boundaryError_};
boundaryContext_ = ctx;
try {
auto response = boundary_({.action = action, .package = pkg_, .hook = hook,
.context = ctx, .log_level = std::string(logLevel)});
if (!response) return {.success = false, .error = "hook boundary: " + response.error()};
// Load is the only source of capabilities: a hook cannot change what
// the host believes the package supplies by returning different flags.
response->hooks = boundaryResponse_.hooks;
boundaryResponse_ = std::move(*response);
return boundaryResponse_.result;
} catch (const std::exception& error) {
return {.success = false, .error = "hook boundary: " + std::string(error.what())};
} catch (...) {
return {.success = false, .error = "hook boundary failed"};
}
}

std::expected<PackageExecutor, std::string>
create_executor(const fs::path& pkg_path, ExecutionBoundary boundary) {
if (!boundary) return std::unexpected("hook execution boundary is empty");
try {
auto loaded = boundary({.action = HookAction::Load, .package = pkg_path});
if (!loaded) return std::unexpected("hook boundary: " + loaded.error());
if (!loaded->result.success)
return std::unexpected("hook boundary load failed: " + loaded->result.error);
return PackageExecutor(pkg_path, std::move(boundary), std::move(*loaded));
} catch (const std::exception& error) {
return std::unexpected("hook boundary: " + std::string(error.what()));
} catch (...) {
return std::unexpected("hook boundary load failed");
}
}

std::expected<HookResponse, std::string> HookWorker::dispatch(const HookInvocation& invocation) {
if (invocation.action == HookAction::Load) {
if (executor_) return std::unexpected("hook worker already has a package; start a new worker");
auto loaded = create_executor(invocation.package);
if (!loaded) return std::unexpected(loaded.error());
package_ = invocation.package;
executor_.emplace(std::move(*loaded));
} else if (!executor_) {
return std::unexpected("hook worker must load a package before executing it");
} else if (invocation.package != package_) {
return std::unexpected("hook worker refuses a different package");
}
HookResponse response;
switch (invocation.action) {
case HookAction::Load:
response.result.success = true;
for (auto hook : {HookType::Installed, HookType::Build, HookType::Install, HookType::Config, HookType::Uninstall})
response.hooks[static_cast<std::size_t>(hook)] = executor_->has_hook(hook);
break;
case HookAction::RunHook:
if (static_cast<std::size_t>(invocation.hook) >= response.hooks.size())
return std::unexpected("hook worker refuses an unknown hook");
response.result = executor_->run_hook(invocation.hook, invocation.context);
break;
case HookAction::RunScript:
response.result = executor_->run_script(invocation.context);
break;
case HookAction::Elfpatch:
response.result = executor_->apply_elfpatch_auto();
break;
case HookAction::SetLogLevel:
if (invocation.log_level != "debug" && invocation.log_level != "info" && invocation.log_level != "warn"
&& invocation.log_level != "error" && invocation.log_level != "silent")
return std::unexpected("hook worker refuses an unknown log level");
executor_->set_log_level(invocation.log_level);
response.result.success = true;
break;
default:
return std::unexpected("hook worker refuses an unknown action");
}
response.xvm_ops = executor_->xvm_operations();
response.install_requests = executor_->install_requests();
return response;
}

} // namespace mcpplibs::xpkg
70 changes: 70 additions & 0 deletions src/xpkg-executor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,33 @@ struct InstallRequest {

enum class HookType { Installed, Build, Install, Config, Uninstall };

inline constexpr int kHookBoundaryProtocol = 1;
enum class HookAction { Load, RunHook, RunScript, Elfpatch, SetLogLevel };
std::string_view hook_action_name(HookAction action);
std::optional<HookAction> hook_action_from_string(std::string_view name);

// Plain data for an external worker protocol. The consumer supplies transport,
// process isolation and validation of returned effects; this library supplies
// neither a sandbox nor an alternate Lua executor.
struct HookInvocation {
HookAction action { HookAction::Load };
fs::path package;
HookType hook { HookType::Installed };
ExecutionContext context;
std::string log_level;
};

struct HookResponse {
HookResult result;
std::array<bool, 5> hooks{}; // indexed by HookType, only Load publishes them
// Cumulative snapshots, exactly as the local executor's accessors return.
std::vector<XvmOp> xvm_ops;
std::vector<InstallRequest> install_requests;
};

using ExecutionBoundary = std::function<std::expected<HookResponse, std::string>(const HookInvocation&)>;


} // export namespace mcpplibs::xpkg

// Implementation detail (not exported)
Expand Down Expand Up @@ -1402,6 +1429,12 @@ export namespace mcpplibs::xpkg {
class PackageExecutor {
lua::State* L_ = nullptr;
fs::path pkg_ ;
ExecutionBoundary boundary_;
HookResponse boundaryResponse_;
ExecutionContext boundaryContext_;
std::string boundaryError_;
HookResult execute_external_(HookAction action, const ExecutionContext& ctx,
HookType hook = HookType::Installed, std::string_view logLevel = {});
std::unique_ptr<detail::HookOutput> hookOutput_ =
std::make_unique<detail::HookOutput>();
std::unique_ptr<detail::HookLog> hookLog_ =
Expand All @@ -1411,6 +1444,8 @@ public:
explicit PackageExecutor(lua::State* L, fs::path pkg)
: L_(L), pkg_(std::move(pkg)) {}

PackageExecutor(fs::path pkg, ExecutionBoundary boundary, HookResponse loaded);

~PackageExecutor() {
if (L_) { lua::close(L_); L_ = nullptr; }
}
Expand All @@ -1421,6 +1456,10 @@ public:
PackageExecutor(PackageExecutor&& o) noexcept
: L_(std::exchange(o.L_, nullptr)),
pkg_(std::move(o.pkg_)),
boundary_(std::move(o.boundary_)),
boundaryResponse_(std::move(o.boundaryResponse_)),
boundaryContext_(std::move(o.boundaryContext_)),
boundaryError_(std::move(o.boundaryError_)),
hookOutput_(std::move(o.hookOutput_)),
hookLog_(std::move(o.hookLog_)) {}

Expand All @@ -1429,13 +1468,18 @@ public:
if (L_) lua::close(L_);
L_ = std::exchange(o.L_, nullptr);
pkg_ = std::move(o.pkg_);
boundary_ = std::move(o.boundary_);
boundaryResponse_ = std::move(o.boundaryResponse_);
boundaryContext_ = std::move(o.boundaryContext_);
boundaryError_ = std::move(o.boundaryError_);
hookOutput_ = std::move(o.hookOutput_);
hookLog_ = std::move(o.hookLog_);
}
return *this;
}

bool has_hook(HookType hook) const {
if (boundary_) return boundaryResponse_.hooks.at(static_cast<std::size_t>(hook));
auto name = detail::hook_name(hook);
lua::getglobal(L_, std::string(name).c_str());
bool found = (lua::type(L_, -1) == lua::TFUNCTION);
Expand All @@ -1444,6 +1488,7 @@ public:
}

HookResult run_hook(HookType hook, const ExecutionContext& ctx) {
if (boundary_) return execute_external_(HookAction::RunHook, ctx, hook);
// Inject context before each hook call
detail::inject_context(L_, ctx);

Expand Down Expand Up @@ -1552,6 +1597,7 @@ public:
// Run elfpatch.apply_auto() if the install hook set elfpatch_auto flag.
// Returns {scanned, patched, failed} counts. Safe to call unconditionally.
HookResult apply_elfpatch_auto() {
if (boundary_) return execute_external_(HookAction::Elfpatch, boundaryContext_);
constexpr const char* script = R"__LUA__(
local ep = _LIBXPKG_MODULES and _LIBXPKG_MODULES["elfpatch"]
if not ep then return "no-ep 0 0 0" end
Expand Down Expand Up @@ -1579,6 +1625,7 @@ public:
}

std::vector<XvmOp> xvm_operations() {
if (boundary_) return boundaryResponse_.xvm_ops;
std::vector<XvmOp> ops;
lua::getglobal(L_, "_XVM_OPS");
if (lua::type(L_, -1) != lua::TTABLE) {
Expand Down Expand Up @@ -1654,6 +1701,7 @@ public:

// Run the script's xpkg_main() function with arguments.
HookResult run_script(const ExecutionContext& ctx) {
if (boundary_) return execute_external_(HookAction::RunScript, ctx);
detail::inject_context(L_, ctx);

lua::newtable(L_);
Expand Down Expand Up @@ -1686,6 +1734,11 @@ public:

// Set log level for Lua scripts: "debug", "info", "warn", "error", "silent"
void set_log_level(std::string_view level) {
if (boundary_) {
auto result = execute_external_(HookAction::SetLogLevel, boundaryContext_, HookType::Installed, level);
if (!result.success) boundaryError_ = result.error;
return;
}
std::string script = std::string("local log = _LIBXPKG_MODULES and _LIBXPKG_MODULES['log']; ")
+ "if log and log.set_level then log.set_level('" + std::string(level) + "') end";
if (lua::L_loadstring(L_, script.c_str()) == lua::OK) {
Expand All @@ -1696,6 +1749,7 @@ public:
}

std::vector<InstallRequest> install_requests() {
if (boundary_) return boundaryResponse_.install_requests;
std::vector<InstallRequest> reqs;
lua::getglobal(L_, "_INSTALL_REQUESTS");
if (lua::type(L_, -1) != lua::TTABLE) {
Expand Down Expand Up @@ -1760,4 +1814,20 @@ create_executor(const fs::path& pkg_path) {
return PackageExecutor(L, pkg_path);
}


// The external boundary runs even the package's top-level Lua in the worker.
// An empty or failing boundary is an error; it never falls back to local Lua.
std::expected<PackageExecutor, std::string>
create_executor(const fs::path& pkg_path, ExecutionBoundary boundary);

// Use only INSIDE the consumer's isolated worker. One instance is kept for
// one package, so recipe globals and cumulative effects survive hook calls.
class HookWorker {
std::optional<PackageExecutor> executor_;
fs::path package_;

public:
std::expected<HookResponse, std::string> dispatch(const HookInvocation& invocation);
};

} // export namespace mcpplibs::xpkg
Loading
Loading