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}