fix: release Dever 0.1.6 with shared runtime assets and unbounded workers

This commit is contained in:
Codex committed 2026-10-12 00:13:27 +00:00
1 parent a900d7e8db
commit 5589b657db
17 files changed
+357 -49

No files matched your search

@@ -36,6 +36,8 @@ Applies to AST/HIR contract metadata, typed LLVM/resource/coroutine lowering, pr
## 3. Contracts
External Worker operations have no implicit execution deadline. `protocol_timeout_ms` bounds startup and protocol writes only; cancellation, peer failure and shutdown remain supervised and bounded. Explicit Task deadlines remain caller-owned, and `external command` retains its separate configured process limits. Neither a `long` entry nor this policy creates a container or an inner sandbox. Keep native and LLVM/ABI definitions aligned, and verify an actual call exceeding 30 seconds alongside cancellation and cleanup regressions.
The current LLVM kernel accepts ordinary checked functions with supported value types, including Decimal, Text/Id, Bytes standard operations, List/Map and nested Record/Choice/Nullable, File/Stream, Task/Group/Channel, AsyncStream, TCP Socket/Listener and HTTP/TLS/WebSocket resources. Time/calendar, process arguments, stdout/logging, Uuid and Secret/crypto use the same typed runtime bridge. List/Bytes each/filter/find/reduce/reduce_until/sum and Stream/AsyncStream traversal use static typed handlers, including the existing checked suspending-handler contracts, never a generic Value interpreter. `dever_entry(ptr out, ptr fault) -> i32` initializes both slots, returns 0 on success and 1 on checked runtime or business failure; outputs are read only after success. Fault is `{i32 code, i32 origin, i32 depth, [capacity x i32] caller_locations, {ptr, i64} message, i32 business_type, i32 business_variant, [words x i64] business_payload}` with separate source-span metadata. Keep fields 0..4 stable; the aligned payload holds the complete concrete Choice and is sized for the largest reachable error type. Named fault codes (overflow=1, division-by-zero=2, RuntimeAbi=3, Business=4) are not the runtime C ABI status. RuntimeAbi retains original owned diagnostic bytes; Business retains nominal identity (`type_id+1`, 0 means absent), variant and typed payload. Callers release root outputs and both fault payloads through `dever_outputs_release` and `dever_fault_release`, including zero-initialized outputs after failure. Application entries and unsupported application intrinsics remain source-located rejections.
File create uses create_new and never truncates an existing path; clone/close preserve shared owner semantics. File chunks yield a shared synchronous Stream cursor, with one terminal Failed before End. Source traversal uses only its existing each/reduce/reduce_until contracts; stream_pull is internal ABI, not a new source pull/first(Stream). Preserve aliases and unvisited rows on early stop, and release resources on both typed business and runtime faults.
@@ -91,6 +91,8 @@ Authenticate the catalog before extraction. Zstandard is a single checksummed fr
Shared extensions live in service-writable `cache/extensions/<version>/<catalog-sha>/<id>`, outside native cache eviction and immutable core directories. IPC accepts only version and typed ID for an already authenticated installed catalog, never caller URLs/paths/keys. Prepare under the persistent install lock in private staging, verify complete bytes then publish readonly with NOREPLACE; corrupted reuse is an error. `SignedResources` owns verified read/list/root selection for Lib, daemon runtime verification and the actual compile worker. `dever target add linux-aarch64` and explicit dependency preparation may ensure resources; ordinary run/build/read cannot. Update prepares precisely the old active version's installed extension selection before switching the one core/skill active journal; any failure preserves the old selection. Stable launcher/daemon protocol changes require matching trusted bootstrap provisioning. Do not rewrite users' skill directories or add an independently switched skill pointer.
Client Lib reads select immutable base artifacts locally and obtain extension bytes through the authenticated daemon; they never probe or mount its private cache. Resource requests bind the installed version, signed catalog digest, typed extension ID and exact signed byte receipt, not filesystem paths or URLs. The daemon verifies the declared local resource and the client verifies the response receipt and payload digest. Catalog-only enumeration must be followed by a verified read of every consumed artifact; daemon compilation keeps full local pack validation. Reading never installs a missing extension or falls back to another source.
The root first-install script embeds its independently provisioned public key, verifies the full package before executing bootstrap, and generates only an owned temporary installer config. Its Bash entry passes the embedded Python source through a quoted heredoc to `/usr/bin/python3 -I -`; parse the complete function before invoking it. Non-root installation uses `/usr/bin/sudo`; help must neither elevate nor download. Missing Python 3.9/OpenSSL, unsupported OS/ABI, unavailable package or invalid signature is an explicit failure. Current x86_64 host packages require glibc 2.39; ARM application support does not establish an ARM compiler-host release. The installer never reads business configuration. Real-root installation explicitly starts the machine service unless --no-start is selected; image-root verification must not touch host services. Python/OpenSSL are installer prerequisites, not application runtime dependencies. `sdk/release-assets.py` uses an explicitly supplied author zstd executable and publishes deterministic base/extension/raw assets with Linux NOREPLACE directory rename; it cannot clean another writer's public output.
The root script is served at `/shemic/dever/raw/branch/main/install`; Release metadata and attachments must also allow anonymous HTTPS reads. Redirects are restricted to HTTPS `git.dever.cc` with no credentials, non-443 port or fragment. Complete all assets before publishing a release. Source pushes and local transport tests do not prove public download readiness. Changing the download protocol requires rebuilding the stable launcher/daemon and provisioning them once through the new installer; ordinary update activates version payloads but does not replace that bootstrap pair. Never relabel old binaries as a new release. Focused Rust/Python tests assert latest/exact selection, signature and length rejection, missing attachments and preservation of active versions. Correct: reuse one resolved release snapshot for the base and installed extensions. Wrong: reopen latest between those downloads or guess an attachment path from its name.
Generated
+6 -6
View File
@@ -414,7 +414,7 @@ dependencies = [
[[package]]
name = "dever-backend-bridge"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"cc",
"dever-core",
@@ -427,7 +427,7 @@ dependencies = [
[[package]]
name = "dever-cli"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"base64 0.22.1",
"csv",
@@ -455,7 +455,7 @@ dependencies = [
[[package]]
name = "dever-core"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"dever-runtime",
"pulldown-cmark",
@@ -465,7 +465,7 @@ dependencies = [
[[package]]
name = "dever-process"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"goblin",
"rustix",
@@ -473,7 +473,7 @@ dependencies = [
[[package]]
name = "dever-runtime"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"argon2",
"base64 0.22.1",
@@ -506,7 +506,7 @@ dependencies = [
[[package]]
name = "dever-tests"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"bytes",
"dever-core",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/dever-core", "crates/dever-runtime", "crates/dever-cli", "cra
resolver = "3"
[workspace.package]
version = "0.1.5"
version = "0.1.6"
edition = "2024"
rust-version = "1.95"
publish = false
+2
View File
@@ -169,6 +169,8 @@ entry 是所属领域目录内的可移植相对路径;上例构建时收集 `
`external` 只在 Adapter role 中有意义,且一个文件只能包含该声明和至多一个 `setting`。它完整实现且只能实现同领域一个 Port identity;不能同时写 Dever Adapter 函数或 helper。应用测试仍必须提供 case-local Dever Port fake,不启动生产 Worker,也不读取生产 Adapter setting。reference 后端明确不执行外部程序,正式执行只走 native 后端。
Worker 操作不设隐式执行期限,不会因执行超过 30 秒而自动失败;等待结果直到调用方主动取消、Worker 异常或应用停止。启动握手、协议写入和退出清理仍有独立短限时。`long` 只控制入口事务边界,不改变这些生命周期规则;业务确需期限时使用现有 Task 超时能力,不增加另一套 Worker 超时配置。`external command` 的显式命令限额不属于 Worker 协议,本次不改变其合同。
生态 Adapter 复用同一 Port 合同,声明中的 Lib 不带重复的生态前缀:
```dever
@@ -541,7 +541,7 @@ typedef struct {
uint64_t capability_count;
const DeverRtWireName *operations;
uint64_t operation_count;
uint64_t timeout_ms;
uint64_t protocol_timeout_ms;
} DeverRtExternalDefinition;
typedef struct {
DeverRtWireName path;
+2 -2
View File
@@ -4373,7 +4373,7 @@ mod runtime {
capability_count: u64,
operations: *const Name,
operation_count: u64,
timeout_ms: u64,
protocol_timeout_ms: u64,
}
#[repr(C)]
@@ -4490,7 +4490,7 @@ mod runtime {
} else {
Some(unsafe { managed::borrowed::<String>(setting) }?.clone())
},
timeout_ms: row.timeout_ms,
protocol_timeout_ms: row.protocol_timeout_ms,
})
})(
);
+2 -2
View File
@@ -858,7 +858,7 @@ impl InstalledRegistry {
fn signed_artifact(&self, relative: &Path) -> Result<Vec<u8>, String> {
let name = relative.to_str().ok_or("runtime path is not UTF-8")?;
self.resources.read(name)
self.resources.read_shared(name)
}
fn process_runtime_resources(
@@ -868,7 +868,7 @@ impl InstalledRegistry {
let prefix = format!("process-runtime/{}/", target.platform());
let mut resources = Vec::new();
let mut files = Vec::new();
for artifact in self.resources.artifacts(&prefix)? {
for artifact in self.resources.catalog_artifacts(&prefix) {
let Some(relative) = artifact.path.strip_prefix(&prefix) else {
continue;
};
@@ -270,6 +270,92 @@ impl SignedResources {
Ok(bytes)
}
pub fn read_shared(&self, logical_path: &str) -> Result<Vec<u8>, String> {
if self
.manifest
.artifacts
.iter()
.any(|artifact| artifact.path == logical_path)
{
return self.read(logical_path);
}
let artifact = self
.manifest
.artifacts
.iter()
.chain(
self.manifest
.extensions
.iter()
.flat_map(|extension| &extension.artifacts),
)
.find(|artifact| artifact.path == logical_path)
.ok_or_else(|| format!("signed release does not declare resource '{logical_path}'"))?;
super::service::resource_get(
&self.layout,
&self.manifest.version,
&self.catalog,
self.manifest
.extensions
.iter()
.find(|extension| {
extension
.artifacts
.iter()
.any(|artifact| artifact.path == logical_path)
})
.map(Extension::id),
&super::ArtifactReceipt {
sha256: artifact.sha256.clone(),
bytes: artifact.bytes,
},
)
}
pub(super) fn open_requested(
&self,
catalog: &str,
extension: Option<&ExtensionId>,
expected: &super::ArtifactReceipt,
) -> Result<fs::File, String> {
if catalog != self.catalog {
return Err("signed resource catalog differs from the installed release".into());
}
let artifacts = match extension {
Some(id) => &self.extension(id)?.artifacts,
None => &self.manifest.artifacts,
};
let artifact = artifacts
.iter()
.find(|artifact| artifact.sha256 == expected.sha256 && artifact.bytes == expected.bytes)
.ok_or("signed release does not declare the requested resource identity")?;
let path = self.path(&artifact.path)?;
let file = fs::File::open(path)
.map_err(|error| format!("cannot open signed resource: {error}"))?;
let metadata = file
.metadata()
.map_err(|error| format!("cannot inspect signed resource: {error}"))?;
if !metadata.is_file() || metadata.len() != artifact.bytes {
return Err("signed resource changed while opening".into());
}
Ok(file)
}
pub fn catalog_artifacts(&self, prefix: &str) -> Vec<Artifact> {
self.manifest
.artifacts
.iter()
.chain(
self.manifest
.extensions
.iter()
.flat_map(|extension| &extension.artifacts),
)
.filter(|artifact| artifact.path.starts_with(prefix))
.cloned()
.collect()
}
pub fn artifacts(&self, prefix: &str) -> Result<Vec<Artifact>, String> {
let mut artifacts = Vec::new();
for artifact in self
+90 -8
View File
@@ -105,6 +105,12 @@ enum Operation {
version: Version,
extension: super::ExtensionId,
},
ResourceGet {
version: Version,
catalog: String,
extension: Option<super::ExtensionId>,
artifact: ArtifactReceipt,
},
Compile {
bytes: u64,
},
@@ -361,14 +367,7 @@ pub fn artifact_get_optional(
return Ok(None);
}
require_artifact(response, &expected)?;
let mut bytes = vec![0; expected.bytes as usize];
stream
.read_exact(&mut bytes)
.map_err(|error| format!("deverd artifact download is incomplete: {error}"))?;
if hex(&Sha256::digest(&bytes)) != expected.sha256 {
return Err("deverd artifact download failed SHA-256 verification".into());
}
Ok(Some(bytes))
read_artifact(&mut stream, &expected).map(Some)
}
#[cfg(not(unix))]
{
@@ -401,6 +400,59 @@ pub fn ensure_extension(
}
}
pub(super) fn resource_get(
layout: &Layout,
version: &Version,
catalog: &str,
extension: Option<super::ExtensionId>,
expected: &ArtifactReceipt,
) -> Result<Vec<u8>, String> {
#[cfg(unix)]
{
let (mut stream, token) = connect(layout)?;
stream
.set_read_timeout(Some(std::time::Duration::from_secs(30)))
.map_err(|error| format!("cannot configure signed resource IPC timeout: {error}"))?;
write_frame(
&mut stream,
&Request {
token,
operation: Operation::ResourceGet {
version: version.clone(),
catalog: catalog.into(),
extension,
artifact: expected.clone(),
},
installed: Vec::new(),
},
)?;
require_artifact(read_frame(&mut stream)?, expected)?;
read_artifact(&mut stream, expected)
}
#[cfg(not(unix))]
{
let _ = (layout, version, catalog, extension, expected);
Err("deverd signed resource IPC is not implemented for this platform".into())
}
}
#[cfg(unix)]
fn read_artifact(
stream: &mut std::os::unix::net::UnixStream,
expected: &ArtifactReceipt,
) -> Result<Vec<u8>, String> {
let length = usize::try_from(expected.bytes)
.map_err(|_| "deverd artifact is too large for this platform")?;
let mut bytes = vec![0; length];
stream
.read_exact(&mut bytes)
.map_err(|error| format!("deverd artifact download is incomplete: {error}"))?;
if hex(&Sha256::digest(&bytes)) != expected.sha256 {
return Err("deverd artifact download failed SHA-256 verification".into());
}
Ok(bytes)
}
fn require_artifact(response: Response, expected: &ArtifactReceipt) -> Result<(), String> {
if !response.ok {
return Err(response
@@ -597,6 +649,36 @@ fn handle(
Err(error) => failure(error),
};
}
Operation::ResourceGet {
version,
catalog,
extension,
artifact,
} => {
let file = match super::SignedResources::load(layout, &version).and_then(|resources| {
resources.open_requested(&catalog, extension.as_ref(), &artifact)
}) {
Ok(file) => file,
Err(error) => return failure(error),
};
let length = artifact.bytes;
if let Err(error) = write_frame(stream, &artifact_response(artifact)) {
return failure(error);
}
match std::io::copy(&mut file.take(length), stream) {
Ok(copied) if copied == length => {}
Ok(_) => return failure("deverd signed resource transfer is truncated".into()),
Err(error) => {
return failure(format!("deverd signed resource transfer failed: {error}"));
}
}
return Response {
ok: true,
status: None,
artifact: None,
error: None,
};
}
Operation::Compile { bytes } => {
return match compile::serve(layout, stream, bytes) {
Ok(()) => Response {
+1 -1
View File
@@ -222,7 +222,7 @@ pub(super) fn initialize(
} else {
"None"
};
writeln!(output, "dever_runtime::component::start(dever_runtime::component::Definition {{ key: {}.into(), ecosystem: {}.into(), entry: {}.into(), port: {}.into(), adapter: {}.into(), schema: {}.into(), capabilities: vec![{capabilities}], operations: vec![{operations}], setting: {setting}, timeout_ms: 30_000 }}).await?;", rust_string(&external_key(adapter)), rust_string(external.ecosystem.keyword()), rust_string(&external.entry), rust_string(&adapter.identity), rust_string(&adapter.name), rust_string(&external.schema)).expect("string formatting");
writeln!(output, "dever_runtime::component::start(dever_runtime::component::Definition {{ key: {}.into(), ecosystem: {}.into(), entry: {}.into(), port: {}.into(), adapter: {}.into(), schema: {}.into(), capabilities: vec![{capabilities}], operations: vec![{operations}], setting: {setting}, protocol_timeout_ms: 30_000 }}).await?;", rust_string(&external_key(adapter)), rust_string(external.ecosystem.keyword()), rust_string(&external.entry), rust_string(&adapter.identity), rust_string(&adapter.name), rust_string(&external.schema)).expect("string formatting");
}
for (port, implementation) in &adapter.operations {
if instances.iter().any(|instance| instance.function == *port) {
+4 -11
View File
@@ -33,7 +33,7 @@ pub struct Definition {
pub capabilities: Vec<String>,
pub operations: Vec<String>,
pub setting: Option<String>,
pub timeout_ms: u64,
pub protocol_timeout_ms: u64,
}
#[derive(Debug)]
@@ -318,7 +318,7 @@ fn validate_definition(definition: &Definition) -> Result<(), String> {
|| definition.adapter.is_empty()
|| definition.schema.is_empty()
|| definition.operations.is_empty()
|| definition.timeout_ms == 0
|| definition.protocol_timeout_ms == 0
{
return Err("incomplete external Adapter definition".into());
}
@@ -468,10 +468,8 @@ async fn protocol_session(
let _ = request.response.send(Err("external Adapter worker stopped".into()));
return Err("external Adapter worker stopped while writing a call".into());
}
result = write_frame_timeout(input, &message, Duration::from_millis(definition.timeout_ms)) => result?,
result = write_frame_timeout(input, &message, Duration::from_millis(definition.protocol_timeout_ms)) => result?,
}
let timeout = tokio::time::sleep(Duration::from_millis(definition.timeout_ms));
tokio::pin!(timeout);
let result = tokio::select! {
biased;
_ = &mut *stopping => {
@@ -484,11 +482,6 @@ async fn protocol_session(
cancel_request(input, output, id).await?;
continue;
}
_ = &mut timeout => {
let _ = request.response.send(Err("external Adapter call timed out".into()));
cancel_request(input, output, id).await?;
continue;
}
};
match result {
Ok(reply) => {
@@ -564,7 +557,7 @@ async fn handshake(
input: &mut ChildStdin,
output: &mut ChildStdout,
) -> Result<(), String> {
let timeout = Duration::from_millis(definition.timeout_ms);
let timeout = Duration::from_millis(definition.protocol_timeout_ms);
write_frame_timeout(input, &hello_message(definition)?, timeout).await?;
let ready = read_frame_timeout(output, timeout).await?;
validate_ready(definition, &ready)?;
+3 -1
View File
@@ -17,6 +17,8 @@ SDK 用于把 Python、JavaScript、Go 或 C ABI 能力接入 Dever。应用先
处理函数返回能用 JSON 表达的值,业务失败返回已声明的 `BusinessError`。不要往 stdout 写日志;stdout 用于 Worker 通信,日志写 stderr。
Worker 调用不设固定执行倒计时,耗时推理可以持续等待。启动握手、协议写入和终止清理仍有短限时;调用方主动取消或应用停止时仍走既有取消和进程回收机制。SDK 不为每次调用创建容器或内层沙箱。
Python/JavaScript/Go 的协作取消会产生一次 `dever.cancelled` 错误,已取消调用的结果会被丢弃。如果处理函数一秒内不响应取消,Worker 失败并停止接收新调用。Dever 管理进程与 stdin;自行在 Go 进程内调用 `Serve` 时,调用方负责关闭或解除自己的 `io.Reader` 阻塞。C ABI 的同步调用终止边界见后文。
## SDK 维护与验证
@@ -45,7 +47,7 @@ SDK 不依赖第三方包,也不读取环境变量或 PATH。托管 Worker 只
- `operation` 是借用的操作名字节;`setting`、`payload` 是借用的 JSON 切片,均不保证 NUL 结尾,只在当前回调有效,不得留存指针。
- 输出缓冲由 SDK 分配,shim 只写 `data[0..capacity]` 并设置 `size`,不得替换指针/容量、释放缓冲或越界。业务值使用 Port 的 wire JSON;Runtime 按已编译 Port 校验实际输出类型、声明错误及深度/大小,不接受动态接口签名。
- 返回 `DEVER_FFI_OK` 时输出为 JSON 结果;`DEVER_FFI_ERROR` 时输出为 `{"error":"已声明错误身份","payload":...}`;其他状态关闭通道并报告运行故障。源码中的 Secret 不得写 stdout/stderr,stdout 仅作协议通信。
- v1 同步原生调用不承诺协作取消;调用卡死、崩溃或超过 supervisor 期限时由现有进程监管终止整个 Worker,不在主进程强杀线程,不自动重放失败调用。强制终止不保证执行 `close`,外部副作用应幂等。
- v1 同步原生调用不承诺协作取消,也没有隐式执行期限;调用方取消或应用退出时,清理宽限结束后由现有进程监管终止未退出的 Worker。崩溃或协议故障直接报错,不在主进程强杀线程,不自动重放失败调用。强制终止不保证执行 `close`,外部副作用应幂等。
静态 ELF 可直接打包;动态依赖放在入口旁的 `lib/`,只接收正常文件,不接收软链接。构建验证目标、SONAME、递归依赖和 ABI 版本;非系统依赖来自该目录;Linux 的解释器/基础系统 ABI 由运行容器提供并须匹配构建目标,不扫描宿主 Python、Node、Go 或编译器。例如 `worker/native` 对应 `worker/lib/libengine.so`。编译器同时打包唯一的 `contract.json`,不手写第二份 Port hash。C++ STL、跨语言对象、回调指针、可变参数及跨进程零拷贝不属于 v1 接口;需要时在 shim 内转为明确的 C 数据与 JSON 值。
@@ -144,6 +144,116 @@ fn signed_extension_preparation_is_shared_immutable_and_concurrent() {
assert_eq!(source.requests.load(Ordering::Relaxed), 1);
}
#[test]
fn signed_resources_cross_machine_roots_without_sharing_the_cache() {
let (root, layout, manager, key) = machine();
release(&layout, &key, "1.0.0");
let version = manager.install_requested("1.0.0").unwrap();
let mut daemon = TestDaemon::start(&layout);
let client = Layout::new(root.path().join("client"));
client.initialize().unwrap();
fs::copy(
layout.state().join("trusted-release-key"),
client.state().join("trusted-release-key"),
)
.unwrap();
let source = layout.versions().join(version.as_str());
let manifest: ReleaseManifest =
serde_json::from_slice(&fs::read(source.join("manifest.json")).unwrap()).unwrap();
for path in manifest
.artifacts
.iter()
.map(|artifact| artifact.path.as_str())
.chain(["manifest.json", "manifest.sig"])
{
let destination = client.downloads().join(version.as_str()).join(path);
fs::create_dir_all(destination.parent().unwrap()).unwrap();
fs::copy(source.join(path), destination).unwrap();
}
MachineManager::new(client.clone())
.install_requested(version.as_str())
.unwrap();
fs::hard_link(layout.service_socket(), client.service_socket()).unwrap();
fs::copy(layout.service_token(), client.service_token()).unwrap();
let resources = SignedResources::load(&client, &version).unwrap();
let extension = id(Ecosystem::Pip);
let logical = logical_path(&extension);
assert!(
resources
.read_shared(&logical)
.unwrap_err()
.contains("not installed")
);
assert!(!layout.extensions().exists());
let source = Source::new(false);
prepare_extension_with(&layout, &version, &extension, &source).unwrap();
assert_eq!(resources.read_shared(&logical).unwrap(), b"1.0.0");
assert_eq!(resources.read_shared(&logical).unwrap(), b"1.0.0");
assert_eq!(source.requests.load(Ordering::Relaxed), 1);
assert!(!client.extensions().exists());
assert!(
resources
.read(&logical)
.unwrap_err()
.contains("not installed")
);
assert!(
resources
.read_shared(&logical_path(&id(Ecosystem::Npm)))
.unwrap_err()
.contains("not installed")
);
assert!(
resources
.read_shared("../../state/service-token")
.unwrap_err()
.contains("does not declare")
);
let path = SignedResources::load(&layout, &version)
.unwrap()
.path(&logical)
.unwrap();
fs::write(&path, b"other").unwrap();
assert!(
resources
.read_shared(&logical)
.unwrap_err()
.contains("integrity")
);
daemon.stop();
assert!(resources.read_shared(&logical).is_err());
}
#[test]
fn signed_resource_ipc_rejects_unbound_receipts_catalogs_and_paths() {
let (_root, layout, manager, key) = machine();
release(&layout, &key, "1.0.0");
let version = manager.install_requested("1.0.0").unwrap();
prepare_extension_with(&layout, &version, &id(Ecosystem::Pip), &Source::new(false)).unwrap();
let _daemon = TestDaemon::start(&layout);
let token = fs::read_to_string(layout.service_token()).unwrap();
let catalog = hex(&Sha256::digest(
fs::read(layout.versions().join("1.0.0/manifest.json")).unwrap(),
));
let request = serde_json::json!({"version":"1.0.0", "catalog":catalog,
"extension":id(Ecosystem::Pip), "artifact":{"sha256":hex(&Sha256::digest(b"1.0.0")), "bytes":5}});
for field in ["catalog", "artifact", "extension", "path"] {
let mut invalid = request.clone();
invalid[field] = match field {
"catalog" => serde_json::json!("0".repeat(64)),
"artifact" => serde_json::json!({"sha256":"0".repeat(64),"bytes":u64::MAX}),
"extension" => serde_json::Value::Null,
_ => serde_json::json!("../../state/service-token"),
};
let result = raw_operation(
&layout,
token.trim(),
serde_json::json!({"resource_get":invalid}),
);
assert_eq!(result["ok"], false, "{field}: {result}");
}
}
#[test]
fn update_prepares_only_installed_extensions_and_preserves_active_on_failure() {
let (_root, layout, manager, key) = machine();
@@ -135,13 +135,14 @@ fn handle_call(call: &Value, mode: &str, hello: &Value) -> Result<(), String> {
"payload": {},
}))
}
"timeout" => {
let cancel = read_json()?;
require_kind(&cancel, "cancel")?;
if field(&cancel, "id")? != id {
return Err("cancel id does not match timed out call".into());
}
write_json(&json!({"kind": "result", "id": id, "payload": {}}))
"delayed" => {
let delay = call
.pointer("/payload/value")
.and_then(Value::as_u64)
.filter(|delay| *delay <= 35_000)
.ok_or("invalid fixture delay")?;
std::thread::sleep(std::time::Duration::from_millis(delay));
write_json(&json!({"kind": "result", "id": id, "payload": field(call, "payload")?}))
}
"crash" => std::process::exit(7),
"duplicate" => write_raw(&format!(
+1 -1
View File
@@ -87,7 +87,7 @@ impl Fixture {
capabilities: vec!["file".into(), "process".into()],
operations: vec!["execute".into()],
setting: None,
timeout_ms: 30_000,
protocol_timeout_ms: 30_000,
})
.await
}
+36 -8
View File
@@ -14,7 +14,7 @@ const OPERATIONS: &[&str] = &[
"echo",
"fail",
"wait",
"timeout",
"delayed",
"crash",
"duplicate",
"out_of_order",
@@ -23,6 +23,28 @@ const OPERATIONS: &[&str] = &[
"truncated",
];
#[test]
fn component_call_can_run_longer_than_thirty_seconds() {
let fixture = install_fixture();
dever_runtime::task::run_entry(async {
fixture.prepare()?;
component::start(definition("long_call", fixture.entry, "normal", 2_000)).await?;
let result = tokio::time::timeout(
std::time::Duration::from_secs(40),
component::call("long_call", "delayed", &payload(31_000)),
)
.await
.map_err(|_| "long-call fixture did not finish")??;
match result {
Reply::Result(body) => assert_eq!(body, r#"{"value":31000}"#),
Reply::Error { .. } => return Err("long call returned an error".into()),
}
component::shutdown().await
})
.expect("long external invocation has no implicit execution deadline");
assert!(fixture.path.with_extension("shutdown").is_file());
}
#[test]
fn component_protocol_and_lifecycle_are_bounded() {
let fixture = install_fixture();
@@ -159,14 +181,20 @@ async fn normal_lifecycle(entry: &'static str) -> Result<(), String> {
.await
.map_err(|error| format!("cancel wait: {error}"))?;
let timeout = component::call("normal", "timeout", &payload(0))
.await
.expect_err("timeout operation must reach the deadline");
assert_eq!(timeout, "external Adapter call timed out");
match tokio::time::timeout(
std::time::Duration::from_secs(2),
component::call("normal", "delayed", &payload(150)),
)
.await
.expect("fixture call must finish")?
{
Reply::Result(body) => assert_eq!(body, r#"{"value":150}"#),
Reply::Error { .. } => return Err("delayed call returned a business error".into()),
}
assert!(matches!(
component::call("normal", "echo", &payload(3))
.await
.map_err(|error| format!("echo after timeout: {error}"))?,
.map_err(|error| format!("echo after delayed call: {error}"))?,
Reply::Result(_)
));
@@ -248,7 +276,7 @@ fn definition(
key: &'static str,
entry: &'static str,
mode: &'static str,
timeout_ms: u64,
protocol_timeout_ms: u64,
) -> Definition {
let (_, setting) = component::adapter_settings()
.unwrap()
@@ -266,7 +294,7 @@ fn definition(
capabilities: vec!["network".into(), "file".into()],
operations: OPERATIONS.iter().map(|name| (*name).into()).collect(),
setting: Some(setting.to_string()),
timeout_ms,
protocol_timeout_ms,
}
}