Skip to main content

nichlink_plugin_host/
admission.rs

1//! Host-side plugin admission: from the lock a host writes to a verified artifact.
2//! 宿主侧插件准入:从宿主写下的锁,到已验证的工件。
3//!
4//! The kernel owns every rule of plugin selection and verification, and the
5//! adapters own execution — but nothing walked the path between them. A host had
6//! to read `.nichlink/plugins/*.lock` itself, build a [`PluginCatalog`] itself,
7//! remember to call [`PluginPolicy::decision`] *with* that catalog, then pick
8//! between `verify_signed` and `verify_artifact`, and finally guess which lane
9//! the result qualifies for. Because that path was nobody's job, the Official
10//! lane was unreachable in practice: no in-tree code ever called `verify_signed`,
11//! and a lock written by the host's own plugin UI could not even match an
12//! official manifest (the record carries no signature, the manifest must).
13//! 内核拥有插件筛选与校验的每条规则,适配器拥有执行——但两者之间的路没有人走。宿主必须自己读
14//! `.nichlink/plugins/*.lock`、自己构造 [`PluginCatalog`]、记得把该目录传给
15//! [`PluginPolicy::decision`]、再在 `verify_signed` 与 `verify_artifact` 之间选择,最后还要猜
16//! 结果够得上哪条通道。因为这段路不属于任何人,Official 通道在实践中不可达:树内没有任何代码调用过
17//! `verify_signed`,而宿主自己的插件界面写下的锁甚至无法匹配官方 manifest(记录不带签名,
18//! 而 manifest 必须带)。
19//!
20//! This module is that path, and it is deliberately thin: selection, then the
21//! strongest verification the artifact's source allows, then the lane the earned
22//! assurance buys. It reads files, so it lives in an execution surface and not in
23//! the kernel.
24//! 本模块就是这段路,且刻意很薄:先筛选,再按工件来源做它能做的最强校验,最后按换来的保证等级
25//! 定通道。它读文件,因此住在执行面而不是内核。
26
27use std::path::Path;
28
29use nichlink_run_method::{
30    PluginArtifact, PluginAssurance, PluginCatalog, PluginChannel, PluginDecision, PluginPolicy,
31    PluginSignatureVerifier, PluginSource, PluginTrustPolicy, VerifiedPluginArtifact,
32};
33
34use crate::HostError;
35
36/// Directory, under a package root, that holds the plugin locks.
37/// 包根之下存放插件锁的目录。
38pub const PLUGIN_LOCK_DIRECTORY: &str = ".nichlink/plugins";
39
40/// Lock file a publisher maintains for the official lane.
41/// 发布者为官方通道维护的锁文件。
42pub const OFFICIAL_LOCK: &str = "official.lock";
43
44/// Lock file a host writes for user plugins.
45/// 宿主为用户插件写下的锁文件。
46pub const USER_LOCK: &str = "user.lock";
47
48/// Read both plugin locks under `package_root` into one catalogue.
49/// 把 `package_root` 下的两份插件锁读成一份目录。
50///
51/// A missing directory or a missing file is an empty catalogue, not a failure: a
52/// package that links no plugin has no lock, and that absence is safe because the
53/// lock check is what refuses an *unlisted official* plugin — the digest and
54/// signature checks still run for everything the host does admit.
55/// 目录或文件缺失都是空目录,而不是失败:不链接任何插件的包没有锁,而这种缺失是安全的,因为拒绝
56/// **未登记的官方**插件靠的正是锁检查——宿主真正准入的每个工件仍然要过摘要与签名检查。
57pub fn plugin_catalog(package_root: &Path) -> Result<PluginCatalog, HostError> {
58    let directory = package_root.join(PLUGIN_LOCK_DIRECTORY);
59    let mut lock = String::new();
60    for name in [OFFICIAL_LOCK, USER_LOCK] {
61        let path = directory.join(name);
62        match std::fs::read_to_string(&path) {
63            Ok(text) => {
64                lock.push_str(&text);
65                lock.push('\n');
66            }
67            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
68            Err(error) => {
69                return Err(HostError::Policy(format!(
70                    "cannot read {}: {error}",
71                    path.display()
72                )));
73            }
74        }
75    }
76    PluginCatalog::parse(&lock).map_err(|error| {
77        HostError::Policy(format!(
78            "invalid plugin lock under {}: {error}",
79            directory.display()
80        ))
81    })
82}
83
84/// The lane an admitted artifact may enter, from the assurance it earned.
85/// 已准入工件可进入的通道,取决于它换来的保证等级。
86///
87/// A verified signature is the only way into [`PluginChannel::Official`], and the
88/// digest path is the user lane. `Community` and `Local` carry the same rule in
89/// the kernel, so a host that declares `Local` may remap this answer.
90/// 已验证的签名是进入 [`PluginChannel::Official`] 的唯一途径,纯摘要路径则是用户通道。
91/// `Community` 与 `Local` 在内核中规则相同,因此声明 `Local` 的宿主可以改写这个答案。
92pub fn lane_for(artifact: &VerifiedPluginArtifact) -> PluginChannel {
93    match artifact.assurance() {
94        PluginAssurance::Signature => PluginChannel::Official,
95        PluginAssurance::Digest => PluginChannel::Community,
96    }
97}
98
99/// Selection policy, trust root, catalogue, and verifier in one admission step.
100/// 把筛选策略、信任根、目录与验证器合成一次准入。
101pub struct PluginAdmission<V> {
102    policy: PluginPolicy,
103    trust: PluginTrustPolicy,
104    catalog: PluginCatalog,
105    verifier: V,
106}
107
108impl<V: PluginSignatureVerifier> PluginAdmission<V> {
109    /// Build one admission from an already-parsed catalogue.
110    /// 用一份已解析的目录构造准入。
111    pub fn new(
112        policy: PluginPolicy,
113        trust: PluginTrustPolicy,
114        catalog: PluginCatalog,
115        verifier: V,
116    ) -> Self {
117        Self {
118            policy,
119            trust,
120            catalog,
121            verifier,
122        }
123    }
124
125    /// Build one admission from the plugin locks under `package_root`.
126    /// 从 `package_root` 下的插件锁构造准入。
127    pub fn from_package_root(
128        package_root: &Path,
129        policy: PluginPolicy,
130        trust: PluginTrustPolicy,
131        verifier: V,
132    ) -> Result<Self, HostError> {
133        Ok(Self::new(
134            policy,
135            trust,
136            plugin_catalog(package_root)?,
137            verifier,
138        ))
139    }
140
141    /// The catalogue this admission consults.
142    /// 本准入所咨询的目录。
143    pub fn catalog(&self) -> &PluginCatalog {
144        &self.catalog
145    }
146
147    /// Admit one raw artifact: selection first, then verification.
148    /// 准入一个原始工件:先筛选,后校验。
149    ///
150    /// The order matters and is the kernel's: revocation and the lock are
151    /// consulted before the signature, so a revoked or unlisted version is
152    /// refused whatever the signature says. An official manifest must pass
153    /// `verify_signed`, which requires a trust root — a host that configures none
154    /// gets `MissingOfficialKey` instead of a silent downgrade to checksums; a
155    /// user manifest takes the digest path, because no verifier is consulted for
156    /// that source.
157    /// 顺序很重要,而且由内核决定:撤销与锁在签名之前被检查,因此已吊销或未登记的版本无论签名说什么
158    /// 都被拒绝。官方 manifest 必须过 `verify_signed`,而它要求信任根——没有配置信任根的宿主会拿到
159    /// `MissingOfficialKey`,而不是被静默降级为纯摘要;用户 manifest 走纯摘要路径,因为该来源不会
160    /// 咨询验证器。
161    pub fn admit(&self, artifact: PluginArtifact) -> Result<VerifiedPluginArtifact, HostError> {
162        let manifest = artifact
163            .registration
164            .plugin
165            .ok_or_else(|| HostError::Policy("plugin artifact has no manifest".to_owned()))?;
166        match self.policy.decision(manifest, Some(&self.catalog)) {
167            PluginDecision::Accepted => {}
168            PluginDecision::Rejected(reason) => {
169                return Err(HostError::Policy(format!(
170                    "plugin rejected by the selection policy: {reason}"
171                )));
172            }
173        }
174        let verified = match manifest.source {
175            PluginSource::Official => artifact.verify_signed(self.trust, &self.verifier),
176            PluginSource::User => artifact.verify_artifact(self.trust),
177        };
178        verified.map_err(|error| {
179            HostError::Policy(format!("plugin refused by the trust policy: {error}"))
180        })
181    }
182}
183
184#[cfg(feature = "wasm")]
185impl<V: PluginSignatureVerifier> PluginAdmission<V> {
186    /// Admit an artifact and install it into a Wasm slot.
187    /// 准入一个工件并把它安装进 Wasm 槽。
188    ///
189    /// The lane comes from the assurance the artifact earned, and the slot's own
190    /// contract, framework, and channel list are checked by `install` before
191    /// anything is queued.
192    /// 通道来自工件换来的保证等级;槽自身的合同、框架与通道列表由 `install` 在挂起任何东西之前
193    /// 检查。
194    pub fn install(
195        &self,
196        table: &crate::WasmPluginTable,
197        slot: &str,
198        artifact: PluginArtifact,
199    ) -> Result<u64, HostError> {
200        let verified = self.admit(artifact)?;
201        let channel = lane_for(&verified);
202        table.install(slot, channel, verified)
203    }
204}
205
206#[cfg(feature = "process-tools")]
207impl<V: PluginSignatureVerifier> PluginAdmission<V> {
208    /// Admit an artifact and stage it for process execution.
209    /// 准入一个工件并为进程执行暂存它。
210    pub fn load_process(
211        &self,
212        backend: &crate::ProcessBackend,
213        program: crate::ProcessProgram,
214        artifact: PluginArtifact,
215    ) -> Result<crate::ProcessInstance, HostError> {
216        let verified = self.admit(artifact)?;
217        backend.load(verified, program)
218    }
219}