feat(sandbox): 从许可派生启动数据与限定范围的密钥
This commit is contained in:
@@ -408,3 +408,14 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
|||||||
- 实际 Windows 测试确认绑定路径为卷 GUID 路径、内容对应已持有文件、缺失/越界入口拒绝、父目录重命名失败;全部所有者释放后重命名成功。真实 AppContainer 原生参数/环境探针通过 create_bound 启动成功,网络/取消/短期限探针也使用持续持有的绑定路径运行。
|
- 实际 Windows 测试确认绑定路径为卷 GUID 路径、内容对应已持有文件、缺失/越界入口拒绝、父目录重命名失败;全部所有者释放后重命名成功。真实 AppContainer 原生参数/环境探针通过 create_bound 启动成功,网络/取消/短期限探针也使用持续持有的绑定路径运行。
|
||||||
- 51 项扩展回归通过,4 项 ignored 为三个由父测试实际驱动的 Job 辅助入口及需显式执行的 60 秒验收,本轮未重跑长时项。Clippy 首次指出冗余 Ok/?,修正后全目标 -D warnings 通过。日志 `.build/extension-entry-binding-tests.log`、`.build/extension-entry-binding-clippy.log`。API 依据 [Microsoft GetFinalPathNameByHandleW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew)。
|
- 51 项扩展回归通过,4 项 ignored 为三个由父测试实际驱动的 Job 辅助入口及需显式执行的 60 秒验收,本轮未重跑长时项。Clippy 首次指出冗余 Ok/?,修正后全目标 -D warnings 通过。日志 `.build/extension-entry-binding-tests.log`、`.build/extension-entry-binding-clippy.log`。API 依据 [Microsoft GetFinalPathNameByHandleW](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew)。
|
||||||
- 这补上了普通祖先路径替换的绑定原语与实测,仍未完成并发新增未知文件、已有可写映射、完整攻击矩阵、许可原子核验或所有目标系统验证,不能据此认定 C-02 或整体生产化完成。
|
- 这补上了普通祖先路径替换的绑定原语与实测,仍未完成并发新增未知文件、已有可写映射、完整攻击矩阵、许可原子核验或所有目标系统验证,不能据此认定 C-02 或整体生产化完成。
|
||||||
|
|
||||||
|
|
||||||
|
## 增量:从执行许可构造参数、环境及包域凭据
|
||||||
|
|
||||||
|
- Claims 增加 Plugin/MCP 执行类型并纳入 HMAC 摘要,防止类型切换后沿用原许可。现有逐字段篡改测试覆盖该字段;许可仍为 Host 内存态能力,未增加旧许可兼容或 renderer 签发入口。
|
||||||
|
- 新增 extension_launch_authorization::Context::build,先核验许可有效期/HMAC、Host 当前 Vault/策略/平台,以及 BoundEntry 相对入口与展开树摘要,再从已签发声明构造参数和环境,不接受独立的替换参数或环境值。
|
||||||
|
- CredentialScope 的值解释为包域内不透明凭据引用。凭据归属由 HTTPS 来源、命名空间、包 ID 的带长度摘要派生,再按已绑定执行类型进入 Plugin 或 MCP 域;不接受调用者另报凭据域,也不回退 Provider。版本未加入凭据归属摘要,保留同包升级后的引用稳定性;新执行仍须独立完成信任和权限确认。
|
||||||
|
- 环境秘密通过现有 Stronghold broker 解密,拒绝缺失、锁定、无效引用和非 UTF-8 值;解析前后核对锁定代次。临时环境值由清理所有者清零,原始解密字节及最终 UTF-16 启动缓冲继续使用各自清理机制;错误只返回稳定代码,不包含值。
|
||||||
|
- 实际 Stronghold 测试验证 Provider 同名凭据不可见、正确 MCP 包域可用、重新签发后改变类型/来源/命名空间/包 ID 仍不能读取原凭据,以及入口/树/Vault/策略不匹配、参数篡改、过期、锁定和编码错误拒绝。真实 AppContainer 原生探针通过该准备链取得虚构测试凭据后启动,完整 argv/环境核对通过,未使用任何用户真实秘密。
|
||||||
|
- 52 项扩展回归通过,4 项 ignored 为三个由父测试驱动的 Job 辅助入口和单独执行的 60 秒验收;全目标 Clippy -D warnings 通过。日志 `.build/extension-launch-authorization-tests.log`、`.build/extension-launch-authorization-clippy.log`。
|
||||||
|
- 该组件只完成准备阶段,不是可直接恢复执行的授权租约;调用方仍须在 resume 前复核在线信任、活动安装身份、许可、Vault 和锁定代次,并完成运行中撤销/停止、broker 和完整资源限制。目前未开放第三方执行,整体生产化继续进行。
|
||||||
|
|||||||
@@ -702,6 +702,7 @@ mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let folder = profile.folder().unwrap();
|
let folder = profile.folder().unwrap();
|
||||||
let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap());
|
let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap());
|
||||||
|
#[cfg(not(feature = "desktop"))]
|
||||||
let data = crate::extension_launch_data::LaunchData::new(
|
let data = crate::extension_launch_data::LaunchData::new(
|
||||||
&executable,
|
&executable,
|
||||||
&args,
|
&args,
|
||||||
@@ -715,6 +716,57 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
{
|
{
|
||||||
|
use crate::extension_launch_authorization::{credential_id, Context};
|
||||||
|
use crate::extension_permit::{Authority, Claims, Environment, ExecutionKind};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
let credential_directory = tempfile::tempdir().unwrap();
|
||||||
|
let mut broker = crate::credentials::CredentialBroker::new(
|
||||||
|
credential_directory.path().join("credentials.v1"),
|
||||||
|
);
|
||||||
|
broker
|
||||||
|
.unlock(Zeroizing::new(b"native fixture passphrase".to_vec()))
|
||||||
|
.unwrap();
|
||||||
|
let claims = Claims {
|
||||||
|
kind: ExecutionKind::Mcp,
|
||||||
|
source: "https://catalog.example/".into(),
|
||||||
|
namespace: "examples".into(),
|
||||||
|
package_id: "native-probe".into(),
|
||||||
|
version: "1.0.0".into(),
|
||||||
|
archive_sha256: "a".repeat(64),
|
||||||
|
tree_sha256: bound_entry.tree_sha256().into(),
|
||||||
|
signer_sha256: "b".repeat(64),
|
||||||
|
entry: bound_entry.relative_name().into(),
|
||||||
|
arguments: args,
|
||||||
|
environment: [(
|
||||||
|
"CUSTOM".into(),
|
||||||
|
Environment::CredentialScope("custom".into()),
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
permissions: Default::default(),
|
||||||
|
vault_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
platform: "windows".into(),
|
||||||
|
policy_version: "1".into(),
|
||||||
|
expires_at_ms: 1000,
|
||||||
|
};
|
||||||
|
broker
|
||||||
|
.put(
|
||||||
|
&credential_id(&claims, "custom").unwrap(),
|
||||||
|
Zeroizing::new("declared=值".as_bytes().to_vec()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let authority = Authority::default();
|
||||||
|
let permit = authority.issue(&claims, 1).unwrap();
|
||||||
|
let context = Context {
|
||||||
|
vault_id: &claims.vault_id,
|
||||||
|
policy_version: "1",
|
||||||
|
system_root: &system,
|
||||||
|
container_data: &folder,
|
||||||
|
scratch: &folder.join("Temp"),
|
||||||
|
};
|
||||||
|
let data = context
|
||||||
|
.build(&authority, &permit, &claims, &bound_entry, &broker, 2)
|
||||||
|
.unwrap();
|
||||||
let suspended =
|
let suspended =
|
||||||
crate::extension_process::Suspended::create_bound(&profile, &bound_entry, data)
|
crate::extension_process::Suspended::create_bound(&profile, &bound_entry, data)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
//! Derive launch bytes from a verified permit and Host-bound entry/context.
|
||||||
|
//! This preparation step does not authorize resume or establish sandbox readiness.
|
||||||
|
use crate::{
|
||||||
|
credentials::{CredentialBroker, CredentialId, Scope},
|
||||||
|
extension_launch_data::LaunchData,
|
||||||
|
extension_permit::{Authority, Claims, Environment, ExecutionKind, Permit},
|
||||||
|
extension_pinned::BoundEntry,
|
||||||
|
workspace::{HostError, Result},
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{collections::BTreeMap, path::Path, sync::atomic::Ordering};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
/// Only the Host's selected workspace, policy and container supply these values.
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub vault_id: &'a str,
|
||||||
|
pub policy_version: &'a str,
|
||||||
|
pub system_root: &'a Path,
|
||||||
|
pub container_data: &'a Path,
|
||||||
|
pub scratch: &'a Path,
|
||||||
|
}
|
||||||
|
struct EnvironmentValues(BTreeMap<String, String>);
|
||||||
|
impl Drop for EnvironmentValues {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for value in self.0.values_mut() {
|
||||||
|
value.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Credential setup and execution must use the same derived identity. The
|
||||||
|
/// reference is an opaque ID inside this package's domain, never a caller scope.
|
||||||
|
pub fn credential_id(claims: &Claims, reference: &str) -> Result<CredentialId> {
|
||||||
|
if reference.is_empty()
|
||||||
|
|| reference.len() > 128
|
||||||
|
|| !reference
|
||||||
|
.bytes()
|
||||||
|
.all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
|
||||||
|
{
|
||||||
|
return Err(HostError::new("CREDENTIAL_ID_INVALID"));
|
||||||
|
}
|
||||||
|
let source = reqwest::Url::parse(&claims.source)
|
||||||
|
.map_err(|_| HostError::new("EXTENSION_PERMIT_INVALID"))?;
|
||||||
|
if source.scheme() != "https"
|
||||||
|
|| source.host_str().is_none()
|
||||||
|
|| !source.username().is_empty()
|
||||||
|
|| source.password().is_some()
|
||||||
|
|| source.query().is_some()
|
||||||
|
|| source.fragment().is_some()
|
||||||
|
{
|
||||||
|
return Err(HostError::new("EXTENSION_PERMIT_INVALID"));
|
||||||
|
}
|
||||||
|
let mut digest = Sha256::new();
|
||||||
|
digest.update(b"OpenNexus extension credential owner v1\0");
|
||||||
|
for value in [source.as_str(), &claims.namespace, &claims.package_id] {
|
||||||
|
digest.update((value.len() as u64).to_be_bytes());
|
||||||
|
digest.update(value.as_bytes());
|
||||||
|
}
|
||||||
|
let owner = format!("ext.{:x}", digest.finalize());
|
||||||
|
let scope = match claims.kind {
|
||||||
|
ExecutionKind::Plugin => Scope::Plugin(owner),
|
||||||
|
ExecutionKind::Mcp => Scope::Mcp(owner),
|
||||||
|
};
|
||||||
|
Ok(CredentialId {
|
||||||
|
scope,
|
||||||
|
id: reference.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
impl Context<'_> {
|
||||||
|
/// Caller must still recheck live trust/permit/session state immediately
|
||||||
|
/// before resume; returning encoded data is not an execution lease.
|
||||||
|
pub fn build(
|
||||||
|
&self,
|
||||||
|
authority: &Authority,
|
||||||
|
permit: &Permit,
|
||||||
|
claims: &Claims,
|
||||||
|
entry: &BoundEntry<'_>,
|
||||||
|
broker: &CredentialBroker,
|
||||||
|
now_ms: u64,
|
||||||
|
) -> Result<LaunchData> {
|
||||||
|
authority.verify(permit, claims, now_ms)?;
|
||||||
|
if claims.vault_id != self.vault_id
|
||||||
|
|| claims.policy_version != self.policy_version
|
||||||
|
|| claims.platform != std::env::consts::OS
|
||||||
|
{
|
||||||
|
return Err(HostError::new("EXTENSION_EXECUTION_CONTEXT_CHANGED"));
|
||||||
|
}
|
||||||
|
if claims.entry != entry.relative_name() || claims.tree_sha256 != entry.tree_sha256() {
|
||||||
|
return Err(HostError::new("EXTENSION_ENTRY_PERMIT_MISMATCH"));
|
||||||
|
}
|
||||||
|
let signal = broker.lock_signal();
|
||||||
|
let epoch = signal.load(Ordering::SeqCst);
|
||||||
|
let mut values = EnvironmentValues(BTreeMap::new());
|
||||||
|
let mut used_secret = false;
|
||||||
|
for (name, declaration) in &claims.environment {
|
||||||
|
match declaration {
|
||||||
|
Environment::Literal(value) => {
|
||||||
|
values.0.insert(name.clone(), value.clone());
|
||||||
|
}
|
||||||
|
Environment::CredentialScope(reference) => {
|
||||||
|
used_secret = true;
|
||||||
|
let id = credential_id(claims, reference)?;
|
||||||
|
let value = broker
|
||||||
|
.resolve(&id.scope, &id)
|
||||||
|
.map_err(|code| {
|
||||||
|
HostError::new(match code.as_str() {
|
||||||
|
"CREDENTIALS_LOCKED" => "CREDENTIALS_LOCKED",
|
||||||
|
"CREDENTIAL_SCOPE_DENIED" => "CREDENTIAL_SCOPE_DENIED",
|
||||||
|
"CREDENTIAL_ID_INVALID" => "CREDENTIAL_ID_INVALID",
|
||||||
|
_ => "EXTENSION_CREDENTIAL_UNAVAILABLE",
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| HostError::new("EXTENSION_CREDENTIAL_MISSING"))?;
|
||||||
|
let value = std::str::from_utf8(&value)
|
||||||
|
.map_err(|_| HostError::new("EXTENSION_CREDENTIAL_ENCODING_INVALID"))?;
|
||||||
|
// Insert directly into the cleaning owner, never an error or log.
|
||||||
|
values.0.insert(name.clone(), value.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let data = LaunchData::new(
|
||||||
|
entry.path(),
|
||||||
|
&claims.arguments,
|
||||||
|
self.system_root,
|
||||||
|
self.container_data,
|
||||||
|
self.scratch,
|
||||||
|
&values.0,
|
||||||
|
)?;
|
||||||
|
if used_secret && (broker.is_locked() || signal.load(Ordering::SeqCst) != epoch) {
|
||||||
|
return Err(HostError::new("CREDENTIALS_LOCKED"));
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{extension_package::Inventory, extension_pinned::PinnedPackage};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
#[test]
|
||||||
|
fn permit_entry_context_and_package_scoped_secrets_drive_launch_data() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let package = temp.path().join("package");
|
||||||
|
std::fs::create_dir(&package).unwrap();
|
||||||
|
std::fs::write(package.join("entry.exe"), b"verified").unwrap();
|
||||||
|
let dir =
|
||||||
|
cap_std::fs::Dir::open_ambient_dir(&package, cap_std::ambient_authority()).unwrap();
|
||||||
|
let inventory = Inventory {
|
||||||
|
files: [(
|
||||||
|
"entry.exe".into(),
|
||||||
|
format!("{:x}", Sha256::digest(b"verified")),
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
expanded_size: 8,
|
||||||
|
manifest: "entry.exe".into(),
|
||||||
|
};
|
||||||
|
let tree = crate::extension_unpack::verify_tree(&dir, &inventory).unwrap();
|
||||||
|
let pinned = PinnedPackage::open(&dir, &inventory, &tree).unwrap();
|
||||||
|
let entry = pinned.bind_entry("entry.exe").unwrap();
|
||||||
|
let claims = Claims {
|
||||||
|
kind: ExecutionKind::Mcp,
|
||||||
|
source: "https://catalog.example/".into(),
|
||||||
|
namespace: "examples".into(),
|
||||||
|
package_id: "reviewer".into(),
|
||||||
|
version: "1.0.0".into(),
|
||||||
|
archive_sha256: "a".repeat(64),
|
||||||
|
tree_sha256: tree,
|
||||||
|
signer_sha256: "b".repeat(64),
|
||||||
|
entry: "entry.exe".into(),
|
||||||
|
arguments: vec!["--stdio".into()],
|
||||||
|
environment: [("TOKEN".into(), Environment::CredentialScope("token".into()))]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
permissions: ["notes.read".into()].into_iter().collect(),
|
||||||
|
vault_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
platform: "windows".into(),
|
||||||
|
policy_version: "1".into(),
|
||||||
|
expires_at_ms: 1000,
|
||||||
|
};
|
||||||
|
let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap());
|
||||||
|
let context = Context {
|
||||||
|
vault_id: &claims.vault_id,
|
||||||
|
policy_version: "1",
|
||||||
|
system_root: &system,
|
||||||
|
container_data: temp.path(),
|
||||||
|
scratch: temp.path(),
|
||||||
|
};
|
||||||
|
let authority = Authority::default();
|
||||||
|
let permit = authority.issue(&claims, 1).unwrap();
|
||||||
|
let mut broker = CredentialBroker::new(temp.path().join("credentials.v1"));
|
||||||
|
broker
|
||||||
|
.unlock(Zeroizing::new(b"fixture passphrase 123".to_vec()))
|
||||||
|
.unwrap();
|
||||||
|
broker
|
||||||
|
.put(
|
||||||
|
&CredentialId {
|
||||||
|
scope: Scope::Provider,
|
||||||
|
id: "token".into(),
|
||||||
|
},
|
||||||
|
Zeroizing::new(b"provider-fixture-secret".to_vec()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.build(&authority, &permit, &claims, &entry, &broker, 2)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_CREDENTIAL_MISSING"
|
||||||
|
);
|
||||||
|
let id = credential_id(&claims, "token").unwrap();
|
||||||
|
broker
|
||||||
|
.put(&id, Zeroizing::new(b"mcp-fixture-secret".to_vec()))
|
||||||
|
.unwrap();
|
||||||
|
let data = context
|
||||||
|
.build(&authority, &permit, &claims, &entry, &broker, 2)
|
||||||
|
.unwrap();
|
||||||
|
let environment = Zeroizing::new(String::from_utf16(data.environment()).unwrap());
|
||||||
|
assert!(environment.contains("TOKEN=mcp-fixture-secret\0"));
|
||||||
|
assert!(!environment.contains("provider-fixture-secret"));
|
||||||
|
drop(data);
|
||||||
|
let mut changed = claims.clone();
|
||||||
|
changed.arguments.push("different".into());
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.build(&authority, &permit, &changed, &entry, &broker, 2)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_PERMIT_MISMATCH"
|
||||||
|
);
|
||||||
|
for field in ["kind", "source", "namespace", "package_id"] {
|
||||||
|
let mut changed = claims.clone();
|
||||||
|
match field {
|
||||||
|
"kind" => changed.kind = ExecutionKind::Plugin,
|
||||||
|
"source" => changed.source = "https://other.example/".into(),
|
||||||
|
"namespace" => changed.namespace = "others".into(),
|
||||||
|
_ => changed.package_id = "another".into(),
|
||||||
|
}
|
||||||
|
let fresh = authority.issue(&changed, 1).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.build(&authority, &fresh, &changed, &entry, &broker, 2)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_CREDENTIAL_MISSING"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for field in ["entry", "tree", "vault", "policy"] {
|
||||||
|
let mut changed = claims.clone();
|
||||||
|
match field {
|
||||||
|
"entry" => changed.entry = "another.exe".into(),
|
||||||
|
"tree" => changed.tree_sha256 = "c".repeat(64),
|
||||||
|
"vault" => changed.vault_id = uuid::Uuid::new_v4().to_string(),
|
||||||
|
_ => changed.policy_version = "2".into(),
|
||||||
|
}
|
||||||
|
let fresh = authority.issue(&changed, 1).unwrap();
|
||||||
|
let code = context
|
||||||
|
.build(&authority, &fresh, &changed, &entry, &broker, 2)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code;
|
||||||
|
assert_eq!(
|
||||||
|
code,
|
||||||
|
if field == "entry" || field == "tree" {
|
||||||
|
"EXTENSION_ENTRY_PERMIT_MISMATCH"
|
||||||
|
} else {
|
||||||
|
"EXTENSION_EXECUTION_CONTEXT_CHANGED"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
broker.put(&id, Zeroizing::new(vec![0xff])).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.build(&authority, &permit, &claims, &entry, &broker, 2)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_CREDENTIAL_ENCODING_INVALID"
|
||||||
|
);
|
||||||
|
broker.lock();
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.build(&authority, &permit, &claims, &entry, &broker, 2)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"CREDENTIALS_LOCKED"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.build(&authority, &permit, &claims, &entry, &broker, 1000)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_PERMIT_EXPIRED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,21 @@ use zeroize::Zeroize;
|
|||||||
#[serde(tag = "kind", content = "value", deny_unknown_fields)]
|
#[serde(tag = "kind", content = "value", deny_unknown_fields)]
|
||||||
pub enum Environment {
|
pub enum Environment {
|
||||||
Literal(String),
|
Literal(String),
|
||||||
// A scoped credential declaration, never resolved secret plaintext.
|
// An opaque credential reference in the Host-derived package scope, never plaintext.
|
||||||
CredentialScope(String),
|
CredentialScope(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ExecutionKind {
|
||||||
|
Plugin,
|
||||||
|
Mcp,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
|
pub kind: ExecutionKind,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
pub package_id: String,
|
pub package_id: String,
|
||||||
@@ -176,6 +184,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
fn claims() -> Claims {
|
fn claims() -> Claims {
|
||||||
Claims {
|
Claims {
|
||||||
|
kind: ExecutionKind::Mcp,
|
||||||
source: "https://catalog.example/".into(),
|
source: "https://catalog.example/".into(),
|
||||||
namespace: "examples".into(),
|
namespace: "examples".into(),
|
||||||
package_id: "note-reviewer".into(),
|
package_id: "note-reviewer".into(),
|
||||||
@@ -205,6 +214,7 @@ mod tests {
|
|||||||
match &mut changed[field] {
|
match &mut changed[field] {
|
||||||
serde_json::Value::String(s) => {
|
serde_json::Value::String(s) => {
|
||||||
*s = match field.as_str() {
|
*s = match field.as_str() {
|
||||||
|
"kind" => "plugin".into(),
|
||||||
"source" => "https://other.example/".into(),
|
"source" => "https://other.example/".into(),
|
||||||
"version" => "1.0.1".into(),
|
"version" => "1.0.1".into(),
|
||||||
"archive_sha256" | "tree_sha256" | "signer_sha256" => "d".repeat(64),
|
"archive_sha256" | "tree_sha256" | "signer_sha256" => "d".repeat(64),
|
||||||
|
|||||||
@@ -18,11 +18,18 @@ pub struct PinnedPackage {
|
|||||||
/// The package borrow and all ancestor handles must outlive the process using
|
/// The package borrow and all ancestor handles must outlive the process using
|
||||||
/// this path. Only files present in the verified package can produce this guard.
|
/// this path. Only files present in the verified package can produce this guard.
|
||||||
pub struct BoundEntry<'a> {
|
pub struct BoundEntry<'a> {
|
||||||
|
name: String,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
_ancestors: Vec<File>,
|
_ancestors: Vec<File>,
|
||||||
_package: &'a PinnedPackage,
|
_package: &'a PinnedPackage,
|
||||||
}
|
}
|
||||||
impl BoundEntry<'_> {
|
impl BoundEntry<'_> {
|
||||||
|
pub fn relative_name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
pub fn tree_sha256(&self) -> &str {
|
||||||
|
self._package.tree_sha256()
|
||||||
|
}
|
||||||
pub fn path(&self) -> &std::path::Path {
|
pub fn path(&self) -> &std::path::Path {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
@@ -228,6 +235,7 @@ impl PinnedPackage {
|
|||||||
return Err(bad());
|
return Err(bad());
|
||||||
}
|
}
|
||||||
Ok(BoundEntry {
|
Ok(BoundEntry {
|
||||||
|
name: name.to_owned(),
|
||||||
path: current,
|
path: current,
|
||||||
_ancestors: handles,
|
_ancestors: handles,
|
||||||
_package: self,
|
_package: self,
|
||||||
|
|||||||
@@ -69,3 +69,6 @@ pub mod extension_deadline;
|
|||||||
|
|
||||||
#[cfg(all(windows, feature = "desktop"))]
|
#[cfg(all(windows, feature = "desktop"))]
|
||||||
pub mod extension_pinned;
|
pub mod extension_pinned;
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "desktop"))]
|
||||||
|
pub mod extension_launch_authorization;
|
||||||
|
|||||||
Reference in New Issue
Block a user