From d6f7bedf984fbd9edb4254069250cecfcbef7e07 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Wed, 14 Oct 2026 15:13:30 -0500 Subject: [PATCH 1/3] feat(id): add source-only world install policy --- .opencode/plans/phase-9-portable-platform.md | 18 ++- ...-05Z_feature_source_only_world_installs.md | 33 ++++++ pkgs/id/examples/roc-world/README.md | 19 +++ pkgs/id/src/cli.rs | 42 +++++++ pkgs/id/src/commands/serve.rs | 73 +++++++++--- pkgs/id/src/main.rs | 2 + pkgs/id/src/web/world_ws.rs | 3 +- pkgs/id/src/world_session.rs | 108 +++++++++++++++++- 8 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 doc/2026-10-14T20-00-05Z_feature_source_only_world_installs/2026-10-14T20-00-05Z_feature_source_only_world_installs.md diff --git a/.opencode/plans/phase-9-portable-platform.md b/.opencode/plans/phase-9-portable-platform.md index 3413152a6..ae7eab4e7 100644 --- a/.opencode/plans/phase-9-portable-platform.md +++ b/.opencode/plans/phase-9-portable-platform.md @@ -14,9 +14,14 @@ The immediate slice (P9.1) is a backwards-compatible, validated cell-grid presen 4. **P9.4 Standard libraries.** Add tested Roc JSON parse/encode primitives and migrate an app/example to structured host events, records, and presentation. Grow data, game, and communication helpers on stable capabilities rather than host-specific shortcuts. 5. **P9.5 Adapters/examples.** Provide a Rust/Ratatui buffer-to-screen adapter or documented capture interface, plus equivalent terminal and browser examples. Ratatui widgets alone are not an app runtime; applications still supply update state and event-loop logic. -## Current worktree scope +## Status and current worktree scope -Implement P9.1 only, as a stacked PR on `platform-roc-web`. Do not mix source-only policy, compiler pinning, generalized storage, or unconfined execution into this presentation change. Capture all of those follow-ups here and in the design record. +- **P9.1 complete:** portable surface contract and SSH/web presenters are committed on `platform-portable-ui` (PR #214, stacked on #209). +- **P9.3a complete in this worktree:** server-wide source-only install policy, stacked on #214. Source compilation remains enabled and raw artifacts are refused. Compiler provenance, per-world profiles, and broader allowed-function ceilings remain open. +- **P9.2 next:** viewport and typed interactive input needs transport/session design so resize is not accidentally journaled as shared durable application state. +- **P9.4/P9.5 remain planned:** Roc JSON/data helpers and Ratatui-shaped adapters/examples. + +“Direct control” is limited to explicit named capabilities under an admin-controlled ceiling. This plan does not enable unrestricted filesystem, network, or process access; native execution remains OS-sandboxed. ## Acceptance for P9.1 @@ -28,3 +33,12 @@ Implement P9.1 only, as a stacked PR on `platform-roc-web`. Do not mix source-on - Roc `Screen` can produce the DTO without asking app authors to hand-concatenate JSON. See [the design record](../../doc/2026-10-14T19-39-30Z_architecture_portable_world_surfaces/2026-10-14T19-39-30Z_architecture_portable_world_surfaces.md). + +P9.3a record: [source-only world installs](../../doc/2026-10-14T20-00-05Z_feature_source_only_world_installs/2026-10-14T20-00-05Z_feature_source_only_world_installs.md). + +## P9.3a acceptance + +- Source-only is an explicit server option, off by default, and requires an enabled world plus a configured server-side Roc compiler/platform. +- Raw upload is rejected before consuming chunks and raw installation is also refused through the service interface. +- Source compilation still validates and installs through the established module validator, persistence, and activation path. +- Native execution stays separately gated; trusted compiler provenance is not claimed until compiler identity is recorded. diff --git a/doc/2026-10-14T20-00-05Z_feature_source_only_world_installs/2026-10-14T20-00-05Z_feature_source_only_world_installs.md b/doc/2026-10-14T20-00-05Z_feature_source_only_world_installs/2026-10-14T20-00-05Z_feature_source_only_world_installs.md new file mode 100644 index 000000000..4630eb9dd --- /dev/null +++ b/doc/2026-10-14T20-00-05Z_feature_source_only_world_installs/2026-10-14T20-00-05Z_feature_source_only_world_installs.md @@ -0,0 +1,33 @@ +# Source-only world module installation + +## Intent + +### Request (verbatim) + +> continue with any missing features. build out the platforms, capabilities, standard libraries across the feature set. ensure that most any ratatui shaped ssh app could be built using this tool set, and that any tui/ssh interface can usually be presented in web, either a direct translation or the app offers multiple presentation interfaces. ensure security sanbdoxing and other limits are customizable, in case an admin wants to Enable direct control. a server can also control access by what functions it allows in the roc interface and by only accepting untrusted code in source form, compiling the roc code itself and only using trusted sources. continue to fully implement all features one would expect for a general platform for data games communication etc. + +This phase implements the source-only policy slice of that request; it does not declare the whole platform roadmap complete. + +Add an operator-controlled mode for Roc worlds that accepts new guest programs only as Roc source compiled by this server's configured compiler. The normal Wasm path remains bounded and import-free. Raw Wasm/native artifact installation is a trusted-artifact feature and must be disabled entirely when source-only mode is enabled, including direct library calls and the streaming upload protocol. + +This does not claim compiler supply-chain provenance: the executable is configured by the operator, but its version/digest is not currently recorded beside the active module. That remains a follow-up before compiler provenance can be claimed. Nor does source-only mean compilation is harmless; source size, compiler wall time and child process limits still apply. + +## Interface and behavior + +- Add `--world-source-only` (and `ID_WORLD_SOURCE_ONLY`) for `id serve --world`. +- Require a configured Roc compiler and reject `--world-module` at startup; those raw artifacts bypass the source path. +- Reject raw `install_wasm` and the WebSocket/Iroh streaming upload protocol before accepting bytes. +- Preserve `compile_and_install`: successful server-compiled output goes through the same hash validation, Wasm validator, module persistence, and atomic activation, with an internal marker distinguishing it from artifact upload. +- Do not implicitly enable native execution. If native compilation is requested, it still requires the separately opted-in OS-sandboxed native tier. +- Default remains backward compatible (`source-only = false`). + +## Validation + +Test both the CLI option constraints and `WorldService` behavior: raw artifacts are denied with/without admin auth; source compilation remains available when a compiler is configured; default mode still accepts an admin-authenticated Wasm module; and a source-only server cannot bootstrap from a raw `--world-module` file. + +## Follow-up gaps + +- Store compiler identity/version or digest with the installed module and expose it in module info. +- Add configurable compiler subprocess memory/CPU/file-size limits; current compiler limits include fixed time and output/source caps. +- Make resource profiles per-world and expose the implemented screen/payload limits as documented policy where appropriate. +- Keep native code isolated by Landlock/seccomp/rlimits; no unconfined host direct-control switch is introduced. diff --git a/pkgs/id/examples/roc-world/README.md b/pkgs/id/examples/roc-world/README.md index 7f0b1a5ae..060f3646e 100644 --- a/pkgs/id/examples/roc-world/README.md +++ b/pkgs/id/examples/roc-world/README.md @@ -70,6 +70,25 @@ the probe re-snapshots to identical text and publishes identical records. Both exports are optional at the Wasm level, so older modules keep working and keep their full journals. +## Source-only server policy + +For a server that accepts guest programs only as Roc source, enable +`id serve --world --world-admin-token --world-source-only` and configure +the trusted compiler/platform with `--roc-bin` and `--roc-platform` (or their +`ID_ROC_BIN` / `ID_ROC_PLATFORM` environment variables). The server refuses +startup if source-only mode has no compiler/platform, and the flag cannot be +combined with a raw `--world-module` bootstrap. It rejects raw Wasm/native +uploads before receiving upload chunks; authorized source compilation still +installs through the normal Wasm validation and execution bounds. The default +remains backward-compatible and accepts admin-authenticated raw modules. + +Source-only mode is not a compiler sandbox or compiler provenance guarantee: +the server operator must trust the configured compiler and platform. The +compiler version/digest is not yet recorded with each module; see the [source +policy design record](../../../../doc/2026-10-14T20-00-05Z_feature_source_only_world_installs/2026-10-14T20-00-05Z_feature_source_only_world_installs.md) +for tracked follow-ups. Native execution remains a separate opt-in and retains +OS sandboxing. + The host passes the id of the participant who sent an event as the second argument of `update`, so programs can implement turn-taking or per-player rules without the host knowing anything about the game. diff --git a/pkgs/id/src/cli.rs b/pkgs/id/src/cli.rs index 4cdc5a822..9ddfffa7e 100644 --- a/pkgs/id/src/cli.rs +++ b/pkgs/id/src/cli.rs @@ -421,6 +421,15 @@ pub enum Command { /// import-free format, so this tier is opt-in. #[arg(long, requires = "world")] world_native: bool, + /// Accept new world programs only as Roc source compiled by this server. + /// Disables raw Wasm/native module uploads and `--world-module`. + #[arg( + long, + env = "ID_WORLD_SOURCE_ONLY", + requires = "world", + conflicts_with = "world_module" + )] + world_source_only: bool, /// Execution bounds for world programs. #[command(flatten)] world_runtime: WorldRuntimeArgs, @@ -3369,6 +3378,39 @@ mod tests { ); } + #[test] + fn source_only_mode_is_opt_in_and_requires_world_mode() { + let parsed = Cli::try_parse_from([ + "id", + "serve", + "--world", + "--world-admin-token", + "admin", + "--world-source-only", + ]) + .unwrap(); + match parsed.command { + Some(Command::Serve { + world_source_only, .. + }) => assert!(world_source_only), + _ => panic!("Expected Serve command"), + } + assert!(Cli::try_parse_from(["id", "serve", "--world-source-only"]).is_err()); + assert!( + Cli::try_parse_from([ + "id", + "serve", + "--world", + "--world-admin-token", + "admin", + "--world-source-only", + "--world-module", + "trusted.wasm", + ]) + .is_err() + ); + } + #[test] fn test_cli_parse_web_cookie_secure() { match Cli::parse_from(["id", "serve", "--web", "--web-cookie-secure"]).command { diff --git a/pkgs/id/src/commands/serve.rs b/pkgs/id/src/commands/serve.rs index 9f7d3e71b..c0189cd9c 100644 --- a/pkgs/id/src/commands/serve.rs +++ b/pkgs/id/src/commands/serve.rs @@ -325,6 +325,8 @@ pub struct ServeOptions { pub world_ssh_port: Option, /// Accept native (ELF) module installs. pub world_native: bool, + /// Accept program changes only through server-side Roc compilation. + pub world_source_only: bool, /// Execution bounds for world programs. pub world_runtime: crate::cli::WorldRuntimeArgs, /// Nodes allowed to modify the store. @@ -339,6 +341,7 @@ fn validate_world_options( world_module: Option<&PathBuf>, world_name: &str, world_cap_policy: &str, + world_source_only: bool, ) -> Result<()> { ensure!( !world || admin_token.is_some_and(|token| !token.is_empty()), @@ -354,6 +357,14 @@ fn validate_world_options( world_cap_policy == "deny" || world_cap_policy == "grant-on-use", "--world-cap-policy must be `deny` or `grant-on-use`" ); + ensure!( + !world_source_only || world_module.is_none(), + "--world-source-only cannot be combined with --world-module" + ); + ensure!( + !world_source_only || world, + "--world-source-only requires --world" + ); #[cfg(not(feature = "world"))] let _ = world_name; Ok(()) @@ -474,6 +485,7 @@ struct ServeWorlds { grant_on_use: bool, compiler: Option, native: bool, + source_only: bool, runtime: crate::world_limits::RuntimeLimits, mail: Option>, } @@ -530,10 +542,12 @@ impl ServeWorlds { let admin_token = self.admin_token.clone(); let blobs = self.blobs.clone(); let mail = self.mail.clone(); + let source_only = self.source_only; move |handle| { let service = WorldService::new(handle, admin_token) .with_blob_store(blobs) - .with_runtime_limits(runtime); + .with_runtime_limits(runtime) + .with_source_only(source_only); match &mail { Some(sink) => service.with_mail(Arc::clone(sink)), None => service, @@ -697,6 +711,7 @@ pub async fn cmd_serve(opts: ServeOptions) -> Result<()> { roc_bin, roc_platform, world_native, + world_source_only, world_ssh_port, world_runtime, allow_node, @@ -708,6 +723,7 @@ pub async fn cmd_serve(opts: ServeOptions) -> Result<()> { world_module.as_ref(), &world_name, &world_cap_policy, + world_source_only, )?; let key = load_or_create_keypair(KEY_FILE).await?; let node_id: EndpointId = key.public(); @@ -797,6 +813,11 @@ pub async fn cmd_serve(opts: ServeOptions) -> Result<()> { let world_hub = if world { use crate::world_hub::{HubLimits, WorldHub}; let runtime = crate::world_limits::RuntimeLimits::from_args(&world_runtime)?; + let compiler = resolve_compiler(roc_bin, roc_platform)?; + ensure!( + !world_source_only || compiler.is_some(), + "--world-source-only requires a configured Roc compiler and platform (see --roc-bin and --roc-platform)" + ); let opener = Arc::new(ServeWorlds { ephemeral, admin_token: world_admin_token.clone(), @@ -806,8 +827,9 @@ pub async fn cmd_serve(opts: ServeOptions) -> Result<()> { default_world: world_name.clone(), default_module: world_module.clone(), grant_on_use: world_cap_policy == "grant-on-use", - compiler: resolve_compiler(roc_bin, roc_platform)?, + compiler, native: world_native, + source_only: world_source_only, runtime, mail: mail_sink( world_mail_outbox, @@ -1302,27 +1324,41 @@ mod tests { #[test] fn world_mode_requires_an_admin_token_but_not_web() { - assert!(validate_world_options(true, None, None, "lobby", "deny").is_err()); - assert!(validate_world_options(true, Some(""), None, "lobby", "deny").is_err()); - assert!(validate_world_options(true, Some("admin"), None, "lobby", "deny").is_ok()); - assert!(validate_world_options(false, None, None, "lobby", "deny").is_ok()); + assert!(validate_world_options(true, None, None, "lobby", "deny", false).is_err()); + assert!(validate_world_options(true, Some(""), None, "lobby", "deny", false).is_err()); + assert!(validate_world_options(true, Some("admin"), None, "lobby", "deny", false).is_ok()); + assert!(validate_world_options(false, None, None, "lobby", "deny", false).is_ok()); assert!( validate_world_options( true, Some("admin"), Some(&PathBuf::from("world.wasm")), "lobby", - "deny" + "deny", + false ) .is_ok() ); + assert!(validate_world_options(true, Some("admin"), None, "lobby", "deny", true).is_ok()); + assert!( + validate_world_options( + true, + Some("admin"), + Some(&PathBuf::from("world.wasm")), + "lobby", + "deny", + true + ) + .is_err() + ); assert!( validate_world_options( false, None, Some(&PathBuf::from("world.wasm")), "lobby", - "deny" + "deny", + false ) .is_err() ); @@ -1357,19 +1393,26 @@ mod tests { "wörld", ] { assert!( - validate_world_options(true, Some("admin"), None, bad, "deny").is_err(), + validate_world_options(true, Some("admin"), None, bad, "deny", false).is_err(), "{bad:?} must be refused" ); } - assert!(validate_world_options(true, Some("admin"), None, "tt-2_x", "deny").is_ok()); - assert!(validate_world_options(true, Some("admin"), None, &"a".repeat(64), "deny").is_ok()); + assert!(validate_world_options(true, Some("admin"), None, "tt-2_x", "deny", false).is_ok()); + assert!( + validate_world_options(true, Some("admin"), None, &"a".repeat(64), "deny", false) + .is_ok() + ); + assert!( + validate_world_options(true, Some("admin"), None, &"a".repeat(65), "deny", false) + .is_err() + ); + assert!(validate_world_options(true, Some("admin"), None, "lobby", "deny", false).is_ok()); assert!( - validate_world_options(true, Some("admin"), None, &"a".repeat(65), "deny").is_err() + validate_world_options(true, Some("admin"), None, "lobby", "grant-on-use", false) + .is_ok() ); - assert!(validate_world_options(true, Some("admin"), None, "lobby", "deny").is_ok()); - assert!(validate_world_options(true, Some("admin"), None, "lobby", "grant-on-use").is_ok()); assert!( - validate_world_options(true, Some("admin"), None, "lobby", "sometimes").is_err(), + validate_world_options(true, Some("admin"), None, "lobby", "sometimes", false).is_err(), "an unknown policy is refused" ); } diff --git a/pkgs/id/src/main.rs b/pkgs/id/src/main.rs index 489a59c02..8ccb69f83 100644 --- a/pkgs/id/src/main.rs +++ b/pkgs/id/src/main.rs @@ -183,6 +183,7 @@ async fn main() -> Result<()> { roc_bin, roc_platform, world_native, + world_source_only, world_runtime, allow_node, open_writes, @@ -224,6 +225,7 @@ async fn main() -> Result<()> { roc_bin, roc_platform, world_native, + world_source_only, world_runtime, allow_node, open_writes, diff --git a/pkgs/id/src/web/world_ws.rs b/pkgs/id/src/web/world_ws.rs index 985c4a913..eec614519 100644 --- a/pkgs/id/src/web/world_ws.rs +++ b/pkgs/id/src/web/world_ws.rs @@ -1176,7 +1176,8 @@ mod tests { let blobs: iroh_blobs::api::Store = MemStore::new().into(); let service = WorldService::new(world.clone(), Some("admin".to_owned())) .with_blob_store(blobs.clone()) - .with_compiler(compiler); + .with_compiler(compiler) + .with_source_only(true); let addr = serve(world_routes().with_state(WorldWebState { hub: Some(WorldHub::single(service)), cookie_secure: false, diff --git a/pkgs/id/src/world_session.rs b/pkgs/id/src/world_session.rs index f776c1117..2585a70b4 100644 --- a/pkgs/id/src/world_session.rs +++ b/pkgs/id/src/world_session.rs @@ -389,6 +389,8 @@ pub enum InstallError { Unauthorized, /// Size or content hash validation failed. InvalidUpload, + /// Raw artifacts are disabled by this world's source-only policy. + RawModuleDisabled, /// Wasmtime rejected or could not instantiate the module. InvalidModule, /// The blob could not be pinned or the world actor refused activation. @@ -416,6 +418,8 @@ pub struct WorldService { records: Option>, compiler: Option>, native: bool, + /// Raw artifact installs are refused; compiled source remains permitted. + source_only: bool, runtime: crate::world_limits::RuntimeLimits, mail: Option>, outbox: Option>>, @@ -440,6 +444,20 @@ impl WorldService { self } + /// Require new Wasm programs to be compiled from source by this server. + /// This disables raw artifact installation, but not the compiled output path. + #[must_use] + pub const fn with_source_only(mut self, enabled: bool) -> Self { + self.source_only = enabled; + self + } + + /// Whether raw Wasm/native uploads are disabled for this world. + #[must_use] + pub(crate) const fn source_only(&self) -> bool { + self.source_only + } + /// Read or change this world's directory as the caller. /// /// # Errors @@ -533,6 +551,7 @@ impl WorldService { records: None, compiler: None, native: false, + source_only: false, runtime: crate::world_limits::RuntimeLimits::default(), mail: None, outbox: None, @@ -593,8 +612,11 @@ impl WorldService { format!("compile failed: {error:#}") })?; let hash = module_hash(&wasm); + let permit = self + .try_reserve_module_upload() + .map_err(|error| format!("install failed: {error:?}"))?; let (_, sequence) = self - .install_wasm(supplied_admin, wasm, spec.seed, &hash) + .install_wasm_reserved(supplied_admin, wasm, spec.seed, &hash, permit, true) .await .map_err(|error| format!("install failed: {error:?}"))?; Ok((hash, sequence, diagnostics)) @@ -742,7 +764,7 @@ impl WorldService { claimed_hash: &str, ) -> Result<(String, u64), InstallError> { let permit = self.try_reserve_module_upload()?; - self.install_wasm_reserved(supplied_admin, wasm, seed, claimed_hash, permit) + self.install_wasm_reserved(supplied_admin, wasm, seed, claimed_hash, permit, false) .await } @@ -754,6 +776,7 @@ impl WorldService { seed: u64, claimed_hash: &str, _permit: OwnedSemaphorePermit, + compiled_from_source: bool, ) -> Result<(String, u64), InstallError> { self.authorize_admin(supplied_admin) .map_err(|error| match error { @@ -762,6 +785,9 @@ impl WorldService { InviteError::Unavailable => InstallError::Unavailable, InviteError::InvalidBounds(_) => InstallError::InvalidUpload, })?; + if self.source_only && !compiled_from_source { + return Err(InstallError::RawModuleDisabled); + } if wasm.is_empty() || wasm.len() > MAX_WORLD_MODULE_BYTES { return Err(InstallError::InvalidUpload); } @@ -1265,6 +1291,15 @@ async fn run_world_session(service: &WorldService, io: &mut I, fra module_hash, seed, } => { + if service.source_only() { + if service.authorize_admin(Some(&admin_token)).is_err() { + let _ = send_error(io, "module install denied").await; + return; + } + let _ = + send_error(io, "raw module uploads are disabled by source-only policy").await; + return; + } if let Err(message) = install_module_session( service, io, @@ -1687,12 +1722,22 @@ async fn install_module_session( return Err("module content hash mismatch"); } let (module_hash, sequence) = service - .install_wasm_reserved(Some(admin_token), bytes, seed, &actual_hash, upload_permit) + .install_wasm_reserved( + Some(admin_token), + bytes, + seed, + &actual_hash, + upload_permit, + false, + ) .await .map_err(|error| match error { InstallError::Disabled => "module installation is not enabled on this host", InstallError::Unauthorized => "module install denied", InstallError::InvalidUpload => "invalid module upload", + InstallError::RawModuleDisabled => { + "raw module uploads are disabled by source-only policy" + } InstallError::InvalidModule => "Wasm module failed validation or initialization", InstallError::Unavailable => "world could not activate the module", })?; @@ -3121,6 +3166,63 @@ mod tests { .expect("default limits install"); } + #[cfg(feature = "sandbox")] + #[tokio::test] + async fn source_only_policy_rejects_raw_modules_but_preserves_normal_mode() { + use iroh_blobs::store::mem::MemStore; + + let wasm = include_bytes!("../examples/roc-counter/counter.wasm").to_vec(); + let hash = module_hash(&wasm); + let world = WorldHandle::spawn(WorldCore::new("lobby", WorldLimits::default()).unwrap()); + let service = WorldService::new(world.clone(), Some("admin".to_owned())) + .with_blob_store(MemStore::new().into()) + .with_source_only(true); + + assert!(service.source_only()); + assert!(matches!( + service + .install_wasm(Some("wrong"), wasm.clone(), 1, &hash) + .await, + Err(InstallError::Unauthorized) + )); + assert!(matches!( + service + .install_wasm(Some("admin"), wasm.clone(), 1, &hash) + .await, + Err(InstallError::RawModuleDisabled) + )); + assert_eq!(service.active_module_hash().await, None); + + let normal = WorldService::new( + WorldHandle::spawn(WorldCore::new("lobby", WorldLimits::default()).unwrap()), + Some("admin".to_owned()), + ) + .with_blob_store(MemStore::new().into()); + normal + .install_wasm(Some("admin"), wasm, 1, &hash) + .await + .expect("raw modules stay supported by default"); + } + + #[cfg(feature = "sandbox")] + #[tokio::test] + async fn source_only_policy_rejects_streaming_upload_before_accepting_chunks() { + let world = WorldHandle::spawn(WorldCore::new("lobby", WorldLimits::default()).unwrap()); + let service = WorldService::new(world, Some("admin".to_owned())).with_source_only(true); + let mut peer = Peer::connect(service, SessionConfig::default()); + peer.say(serde_json::json!({ + "type": "install_begin", + "admin_token": "admin", + "total_bytes": 32, + "module_hash": null, + "seed": 1, + })) + .await; + let error = peer.next().await.unwrap(); + assert_eq!(error["type"], "error"); + assert!(error["message"].as_str().unwrap().contains("source-only")); + } + #[cfg(feature = "sandbox")] #[tokio::test] async fn bad_module_replacement_preserves_the_active_program() { From ecfb36fe8e5c587078fabe0ffc47ddd1a97b8960 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Wed, 14 Oct 2026 15:21:33 -0500 Subject: [PATCH 2/3] fix(id): avoid unwrap in save cooldown limiter --- pkgs/id/src/web/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/id/src/web/mod.rs b/pkgs/id/src/web/mod.rs index c6731d804..29a45e3b9 100644 --- a/pkgs/id/src/web/mod.rs +++ b/pkgs/id/src/web/mod.rs @@ -115,7 +115,7 @@ impl SaveRateLimiter { if let Some(last) = map.get(name) { let elapsed = last.elapsed(); if elapsed < self.cooldown { - return Err(self.cooldown.checked_sub(elapsed).unwrap()); + return Err(self.cooldown.saturating_sub(elapsed)); } } Ok(()) From ac642c389b27c771470cf70e034d30120b4df9bc Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Thu, 15 Oct 2026 04:23:08 -0500 Subject: [PATCH 3/3] test(id): assert structured Roc screen output --- pkgs/id/src/world_compile.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkgs/id/src/world_compile.rs b/pkgs/id/src/world_compile.rs index cf9c5d5e2..84009d60a 100644 --- a/pkgs/id/src/world_compile.rs +++ b/pkgs/id/src/world_compile.rs @@ -582,8 +582,17 @@ mod tests { ) .unwrap(); let frame = String::from_utf8(world.view().unwrap()).unwrap(); - let rows: Vec<&str> = frame.split('\n').collect(); - assert_eq!(rows.len(), 3); + let Some(crate::world_presentation::WorldSurface::Screen(screen)) = + crate::world_presentation::parse_surface(&frame).unwrap() + else { + panic!("Roc screen example must return a structured screen"); + }; + assert_eq!((screen.width, screen.height), (20, 3)); + let rows: Vec = screen + .cells + .chunks(usize::from(screen.width)) + .map(|row| row.iter().map(|cell| cell.symbol.as_str()).collect()) + .collect(); assert!(rows[0].starts_with("+-counter")); assert!(rows[1].starts_with("| count=1")); assert_eq!(rows[2], format!("+{}+", "-".repeat(18)));