Skip to content

Add craft-plugin-sdk and craft-plugin-sdk-macros: the guest SDK for craft-abi 1 plugins - #455

Open
bigwolfeman wants to merge 62 commits into
storytold:mainfrom
bigwolfeman:pr/craft-plugin-sdk
Open

bigwolfeman wants to merge 62 commits into
storytold:mainfrom
bigwolfeman:pr/craft-plugin-sdk

Conversation

@bigwolfeman

@bigwolfeman bigwolfeman commented Oct 30, 2026 •

Copy link
Copy Markdown

Depends on #432. The branch sits on top of #431, #432 and #433, so the Files tab also shows their commits until they merge. Review two commits only: craft-plugin-sdk: guest SDK and #[craft_plugin] macro for craft-abi 1 plugins and craft-plugin-sdk: format test manifests request fs.picked (two test fixtures). This PR uses none of the code in #431 and #433.

Part of #414.

What

Two new crates. They are the guest side of craft-plugin-abi. A plugin crate is a cdylib that depends on craft-plugin-sdk and on nothing else.

craft-plugin-sdk (crates/plugin-sdk) has these modules:

  • host: call_json, the one function that talks to craft_host.call, and typed helpers: log, info, cancelled, progress, and the modules cmd, state, doc, clock, random and link. A reply that does not fit the buffer is retried once with the size the host asks for.
  • entry: request and response types for each contribution kind (command, hook, panel, format, and image bands for effect, filter, object-filter and live-effect).
  • rt: the runtime behind the exports. Its safe parts are plain Rust over slices and run natively. The wasm32 part turns the raw pointers of the ABI into those slices and owns craft_alloc and craft_free.
  • backend: the craft_host.call import on wasm32. On other targets a test installs a fake host. With no fake host, a call returns HostError::NoHost. It never returns a canned answer.
  • testing (native only): with_host, reply, status. Handlers run in a plain cargo test.
  • ui: a builder for panel widget trees. Widget::build rejects duplicate ids and trees over 2,000 nodes.

craft-plugin-sdk-macros (crates/plugin-sdk-macros) has one attribute, #[craft_plugin(manifest = "craft.toml")]. At compile time it reads the manifest, numbers the contributions, and checks the result with craft_plugin_abi::manifest::Manifest::parse, the parser the install gate uses. A manifest the gate would reject is a compile error. It also checks that each contribution has a handler and that ids are unique. It emits:

  • the manifest as the craft.manifest custom section (wasm32 only),
  • the exports craft_abi, craft_alloc, craft_free, and craft_invoke or craft_process when a contribution needs them (wasm32 only),
  • a craft_testing module with the manifest and the dispatch tables, for native tests.

Each crate has a README. The workflow .github/workflows/plugin-sdk.yml runs fmt, clippy, tests and the wasm32 build. The root README.md lists the crates.

Size:

  • craft-plugin-sdk: 8 files under src/, 1299 lines. 47 tests in 6 files under tests/ (1043 lines).
  • craft-plugin-sdk-macros: 1 file under src/, 401 lines, 6 unit tests. 13 manifest fixtures (97 lines).

Changes from our staging copy: comments that named an internal "contract" now point at the sections of crates/plugin-abi/ABI.md, test names that said "contract" now say "abi" or "wire", and the use super::* lines in host.rs and rt.rs are explicit, because the workspace denies wildcard_imports. Two behavior changes:

  • rt::wasm now refuses a non-empty range at address 0. A Rust slice may not start at a null pointer. A host that sent a descriptor with in_ptr = 0 got a slice over address 0 before. It now gets status 3 (BAD_POINTER).
  • tests/wasm_plugin.rs is new. See "Unsafe code" and "Checks".

Why

Plugin authors write Rust. The SDK and the macro hide the ABI, so an author writes handler functions and a manifest. The compile step checks the manifest, so a bad manifest fails before the package is built. This is PR 6 of the series in #414. It depends on craft-plugin-abi (#432) only, so it can merge before the plugin host.

How apps and plugin authors use it

Apps do not depend on this crate. Plugin authors do. A plugin is a craft.toml and an inline module of handler functions. This example was built for wasm32-unknown-unknown and run through craft_plugin_abi::check_package for this PR (187,170 bytes with opt-level = "s", no violations). Its native test also passes.

craft.toml:

id = "org.example.greeter"
name = "Greeter"
version = "0.1.0"

[requires]
ops = ["host.log", "state.get", "state.put"]

[permissions]
required = ["state.local"]

[[contributions]]
kind = "command"
id = "greet"
label = "Greet"
handler = "greet"

[[contributions]]
kind = "hook"
id = "no-delete"
phase = "before"
match = ["clip.delete"]
handler = "no_delete"

src/lib.rs:

use craft_plugin_sdk::prelude::*;

#[craft_plugin(manifest = "craft.toml")]
mod plugin {
    use craft_plugin_sdk::prelude::*;

    /// Count the calls in the plugin's own store and say hello.
    fn greet(call: CommandCall) -> Result<Value, String> {
        let name = call.params["name"].as_str().unwrap_or("world").to_owned();
        let count: u64 = state::get_as("count").map_err(|e| e.to_string())?.unwrap_or(0) + 1;
        state::put("count", &count).map_err(|e| e.to_string())?;
        host::info(&format!("hello {name}"));
        Ok(json!({ "text": format!("hello {name}"), "count": count }))
    }

    fn no_delete(_hook: HookCall) -> HookVerdict {
        HookVerdict::Veto("this project is read only".to_owned())
    }
}

Cargo.toml has crate-type = ["cdylib", "rlib"], the one dependency craft-plugin-sdk, and unsafe_code = "deny". The linker must write a memory maximum, because the gate rejects a memory with none: -C link-arg=--max-memory=67108864 for wasm32-unknown-unknown. The developer CLI sets this later. Until then it goes in .cargo/config.toml.

Unsafe code

The workspace sets unsafe_code = "forbid". craft-plugin-sdk sets deny in its own manifest (it cannot inherit [lints] workspace = true and override one lint, so it lists the other workspace lint levels itself). Guest glue needs unsafe in two modules, each with an explicit #[allow(unsafe_code)]. RFC question 4 asks whether this exception is acceptable. Both modules exist on wasm32 only.

Sites in the crates:

File What Why
crates/plugin-sdk/src/backend.rs, unsafe extern "C" block the craft_host.call import an import is unsafe to declare
backend.rs, one unsafe { craft_host_call(..) } the call pointers and lengths come from live slices
crates/plugin-sdk/src/rt.rs, slice slice::from_raw_parts turns a checked range of linear memory into &[u8]
rt.rs, slice_mut slice::from_raw_parts_mut the one &mut [u8] of craft_process, checked not to overlap the input, params or descriptor
rt.rs, alloc std::alloc::alloc craft_alloc
rt.rs, free std::alloc::dealloc craft_free
crates/plugin-sdk-macros/src/lib.rs, generated #[unsafe(no_mangle)] on craft_abi, craft_alloc, craft_free, craft_invoke, craft_process the ABI exports
same, generated #[unsafe(link_section = "craft.manifest")] the manifest section

That is 4 unsafe blocks and 1 unsafe extern block in the SDK, and 6 attributes in the macro output. Each block has a SAFETY comment. The macro output carries #[allow(unsafe_code)] on the export module and the manifest static, so a plugin crate that sets deny can use the macro. A crate that sets forbid cannot (E0453).

tests/wasm_plugin.rs builds a fixture plugin for wasm32 and runs it in wasmi 2.0.0 (a dev dependency) with a fake craft_host.call. It is the only test that runs the unsafe code. It checks craft_abi, craft_alloc alignment and failure, a command that logs through the host import, a hook, a bad entry, a bad JSON request, a request pointer outside memory, an output that overlaps the input, an output at address 0 and an output past the end of memory. The fixture crate is written from the test file into CARGO_TARGET_TMPDIR, so the repo holds no wasm file.

Not in this PR

  • The developer CLI (craft new, build, inspect, run, watch, dev, bindgen). It needs the plugin host crate and comes later.
  • craft-plugin-ui (panel tree renderer).
  • The example plugins. The example above is the one in this description.
  • The plugin host, engines and the host layer. The test engine here is a dev dependency, not a host.
  • craft_state_save and craft_state_load. The macro does not export them yet.
  • The audio-effect kind. The macro refuses it with a compile error that says so.
  • Signatures (craft.sig).

Dependencies

  • craft-plugin-abi (path, Add craft-plugin-abi: craft-abi 1 types, permission names and install gate #432).
  • serde and serde_json through [workspace.dependencies].
  • craft-plugin-sdk-macros: proc-macro2 1.0.107, quote 1.0.47, syn 2.0.119 (all MIT OR Apache-2.0, already in Cargo.lock) and toml 1.1.8 (MIT OR Apache-2.0, default features off, already in Cargo.lock; its entry gains one dependency line, toml_writer, which was already locked).
  • craft-plugin-sdk dev dependency: wasmi 2.0.0 (MIT/Apache-2.0; features std, validate, portable-dispatch, default features off). It brings wasmi_core, wasmi_ir, wasmi_collections (all MIT/Apache-2.0), spin 0.9.9 (MIT), string-interner 0.19.0 (MIT/Apache-2.0), hashbrown 0.15.5 (MIT OR Apache-2.0), foldhash 0.1.5 (Zlib) and a second wasmparser 0.228.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT). If maintainers do not want a dev engine in this PR, delete the four tests that use it. The other tests do not need it.
  • Licenses read from cargo metadata. The repo has no deny.toml, so no cargo-deny check exists.
  • Cargo.lock grows by 113 lines and drops 7. The 7 are wasmparser and foldhash entries that now carry a version suffix, because a second version exists.

Checks

Run on branch pr/craft-plugin-sdk, from a clean worktree, with CARGO_TARGET_DIR outside the repo.

  • cargo fmt --check: no output, exit 0.
  • cargo clippy -j4 --workspace --all-targets -- -D warnings: exit 0.
  • cargo clippy -j4 -p craft-plugin-sdk -p craft-plugin-sdk-macros --all-targets --locked --target <T> -- -D warnings: exit 0 for x86_64-unknown-linux-gnu, x86_64-pc-windows-gnu and aarch64-apple-darwin.
  • cargo clippy -j4 -p craft-plugin-sdk --locked --target wasm32-unknown-unknown -- -D warnings: exit 0 (the library; the integration tests use testing, which is native only, and the macro crate is a proc macro, so neither builds for wasm32).
  • cargo test -j4 -p craft-plugin-sdk -p craft-plugin-sdk-macros --locked: exit 0. SDK: 17 host_calls, 9 macro_plugin, 9 rt, 4 ui, 3 format_plugin, 5 wasm_plugin. Macros: 6. Total 53 passed, 0 failed, 2 ignored (the doc examples).
  • cargo test -j4 --workspace --locked --no-fail-fast with CARGO_PROFILE_DEV_DEBUG=0: exit 0. 2216 passed, 0 failed, 29 ignored.
  • cargo build -j4 --target wasm32-unknown-unknown -p craft-plugin-sdk --locked: finished.
  • RUSTDOCFLAGS="-D warnings" cargo doc -j4 -p craft-plugin-sdk -p craft-plugin-sdk-macros --no-deps --locked: finished.
  • The example plugin above: cargo test 1 passed. cargo build --release --target wasm32-unknown-unknown finished. check_package on the result gave no violations and the manifest org.example.greeter with 2 contributions.

Gated test: tests/wasm_plugin.rs needs the wasm32-unknown-unknown target and network access to the crates.io index on its first run (the fixture crate resolves its own dependencies, starting from the workspace Cargo.lock). When the target is missing, its five tests print "skipped" and pass. Every other test runs on a host with no extra setup.

Not run: the new workflow on GitHub, the Windows and macOS test runs (clippy ran for both targets, tests did not), fuzzing of the manifest macro or of the descriptor handling, and any plugin on a real host engine of an app. The wasmi test engine in wasm_plugin.rs is not the host that apps will use.

bigwolfeman and others added 7 commits October 29, 2026 17:46
…raft apps

Adds craft-command and its #[command] macro crate, craft-command-derive.

craft-command holds a registry of commands that can be added and removed at run
time, before/after hooks with per-owner time budgets, a change bus with a
Tracked<T> wrapper, a bounded observer queue, JSON Schema from params docs, a
JSON diff, and (feature testing) a conformance harness for apps.

Part of storytold#414.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Adds craft-ipc, the transport for a Craft app that talks to other Craft apps on
one machine. It is native only: interprocess has no wasm32 build.

- server and client: one JSON object per line, push events, a bulk connection
  for frames, token auth on every connection, connection and time limits.
- discovery: one record per running app in a per-user runtime directory, with a
  lock file that shows whether the owner is alive.
- launch, trust, cache: find an app binary, start it only after an executable
  check, keep caches in a private per-user directory.
- activate: focus hand-off between apps.
- moniker, record, payload, asset, refresh, fetch, watch: asset names, stored
  link records, open payloads, linked-file polling and a frame cache.
- PROTOCOL.md describes the wire protocol. The record schemas and fixtures are
  in the crate.

- The crate compiles and passes clippy -D warnings on x86_64-pc-windows-gnu and
  aarch64-apple-darwin. This is a compile and lint fix only. No one ran the
  Windows or macOS code paths, so this does not claim they work there.

Part of storytold#414.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… gate

Adds craft-plugin-abi. It describes a WebAssembly plugin package and decides
whether the package may load, without running guest code.

- manifest: the craft.manifest custom section, its parser and JSON Schema.
- gate: check_package, the install-time policy check (size, memory, tables,
  functions, start function, imports, export signatures, feature allow list).
- permissions, ops, descriptor, format, text: permission names, host call
  numbers and status codes, the craft_process descriptor, the format
  contribution shapes, and the rules for text a plugin supplies.
- ABI.md lists the rules the crate encodes.

Part of storytold#414.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
ActivationBackend gains can_apply_token (default false). On Linux a
token only counts when the backend can apply it to a live window.
Stock winit cannot, so the answer is false plus a request for user
attention. Wire values are unchanged. PROTOCOL.md says what each
value means.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… plugins

Adds craft-plugin-sdk and craft-plugin-sdk-macros. They are the guest side of
craft-plugin-abi.

- host: call_json and typed helpers over craft_host.call (log, progress, cmd,
  state, doc, clock, random, link).
- entry, ui: handler request and response types, and the panel tree builder.
- rt, backend: the runtime behind the exports. The wasm32 modules are the only
  code that uses unsafe, each behind an explicit allow. Everything else is
  denied.
- testing: a fake host, so handlers run in a plain cargo test.
- #[craft_plugin(manifest = "craft.toml")] checks the manifest with the install
  gate's parser at compile time and emits the five ABI exports on wasm32.

Part of storytold#414.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 152 files, which is 52 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 6ac2a6a and 6729c00.

Files ignored due to path filters: 1

  • Cargo.lock is excluded by !**/*.lock
Files selected for processing: 152

  • .github/workflows/command.yml
  • .github/workflows/ipc.yml
  • .github/workflows/plugin-abi.yml
  • .github/workflows/plugin-sdk.yml
  • Cargo.toml
  • README.md
  • crates/command-derive/Cargo.toml
  • crates/command-derive/README.md
  • crates/command-derive/src/lib.rs
  • crates/command-derive/tests/derive.rs
  • crates/command/Cargo.toml
  • crates/command/README.md
  • crates/command/benches/execute.rs
  • crates/command/src/change.rs
  • crates/command/src/conformance.rs
  • crates/command/src/hooks.rs
  • crates/command/src/json_diff.rs
  • crates/command/src/lib.rs
  • crates/command/src/observe.rs
  • crates/command/src/overlay.rs
  • crates/command/src/registry.rs
  • crates/command/src/schema.rs
  • crates/command/src/spec.rs
  • crates/command/src/tracked.rs
  • crates/command/tests/conformance.rs
  • crates/command/tests/fixtures/effectcraft.json
  • crates/command/tests/fixtures/filmcraft.json
  • crates/command/tests/gate_budget.rs
  • crates/command/tests/hooks.rs
  • crates/command/tests/json_diff.rs
  • crates/command/tests/registry.rs
  • crates/command/tests/schema.rs
  • crates/command/tests/tracked.rs
  • crates/ipc/Cargo.toml
  • crates/ipc/PROTOCOL.md
  • crates/ipc/README.md
  • crates/ipc/schemas/instance-record.schema.json
  • crates/ipc/schemas/link-record.schema.json
  • crates/ipc/src/activate.rs
  • crates/ipc/src/asset.rs
  • crates/ipc/src/auth.rs
  • crates/ipc/src/bulk.rs
  • crates/ipc/src/cache.rs
  • crates/ipc/src/client.rs
  • crates/ipc/src/discovery/dir.rs
  • crates/ipc/src/discovery/mod.rs
  • crates/ipc/src/discovery/record.rs
  • crates/ipc/src/error.rs
  • crates/ipc/src/fetch.rs
  • crates/ipc/src/fsopen.rs
  • crates/ipc/src/ids.rs
  • crates/ipc/src/launch.rs
  • crates/ipc/src/lib.rs
  • crates/ipc/src/moniker.rs
  • crates/ipc/src/payload.rs
  • crates/ipc/src/record.rs
  • crates/ipc/src/refresh.rs
  • crates/ipc/src/server/dispatch.rs
  • crates/ipc/src/server/events.rs
  • crates/ipc/src/server/listener.rs
  • crates/ipc/src/server/mod.rs
  • crates/ipc/src/server/provider.rs
  • crates/ipc/src/server/tcp.rs
  • crates/ipc/src/trust.rs
  • crates/ipc/src/watch.rs
  • crates/ipc/src/wire.rs
  • crates/ipc/tests/bulk.rs
  • crates/ipc/tests/connection_limits.rs
  • crates/ipc/tests/discovery.rs
  • crates/ipc/tests/fetch.rs
  • crates/ipc/tests/fixtures/link-record-filmcraft-effectcraft.json
  • crates/ipc/tests/fixtures/link-record-still.json
  • crates/ipc/tests/hub.rs
  • crates/ipc/tests/open_payload.rs
  • crates/ipc/tests/protocol.rs
  • crates/ipc/tests/provider_stall.rs
  • crates/ipc/tests/schemas.rs
  • crates/ipc/tests/security.rs
  • crates/ipc/tests/watch.rs
  • crates/plugin-abi/ABI.md
  • crates/plugin-abi/Cargo.toml
  • crates/plugin-abi/README.md
  • crates/plugin-abi/schemas/plugin-manifest.schema.json
  • crates/plugin-abi/src/descriptor.rs
  • crates/plugin-abi/src/format.rs
  • crates/plugin-abi/src/gate.rs
  • crates/plugin-abi/src/lib.rs
  • crates/plugin-abi/src/manifest.rs
  • crates/plugin-abi/src/ops.rs
  • crates/plugin-abi/src/permissions.rs
  • crates/plugin-abi/src/text.rs
  • crates/plugin-abi/tests/fixtures/accept.wat
  • crates/plugin-abi/tests/fixtures/accept_minimal.wat
  • crates/plugin-abi/tests/fixtures/accept_no_import.wat
  • crates/plugin-abi/tests/fixtures/legacy_ec.wat
  • crates/plugin-abi/tests/fixtures/legacy_mixed.wat
  • crates/plugin-abi/tests/fixtures/legacy_pc.wat
  • crates/plugin-abi/tests/fixtures/legacy_vc.wat
  • crates/plugin-abi/tests/fixtures/manifest_good.json
  • crates/plugin-abi/tests/fixtures/reject_exceptions.wat
  • crates/plugin-abi/tests/fixtures/reject_extra_import.wat
  • crates/plugin-abi/tests/fixtures/reject_memory64.wat
  • crates/plugin-abi/tests/fixtures/reject_memory_not_exported.wat
  • crates/plugin-abi/tests/fixtures/reject_memory_too_large.wat
  • crates/plugin-abi/tests/fixtures/reject_missing_export.wat
  • crates/plugin-abi/tests/fixtures/reject_multi_memory.wat
  • crates/plugin-abi/tests/fixtures/reject_no_memory_max.wat
  • crates/plugin-abi/tests/fixtures/reject_relaxed_simd.wat
  • crates/plugin-abi/tests/fixtures/reject_simd_unrequested.wat
  • crates/plugin-abi/tests/fixtures/reject_start_function.wat
  • crates/plugin-abi/tests/fixtures/reject_table_too_large.wat
  • crates/plugin-abi/tests/fixtures/reject_tail_call.wat
  • crates/plugin-abi/tests/fixtures/reject_threads.wat
  • crates/plugin-abi/tests/fixtures/reject_too_many_tables.wat
  • crates/plugin-abi/tests/fixtures/reject_wrong_export_signature.wat
  • crates/plugin-abi/tests/fixtures/reject_wrong_import_signature.wat
  • crates/plugin-abi/tests/gate.rs
  • crates/plugin-abi/tests/schema.rs
  • crates/plugin-sdk-macros/Cargo.toml
  • crates/plugin-sdk-macros/README.md
  • crates/plugin-sdk-macros/src/lib.rs
  • crates/plugin-sdk-macros/tests/fixtures/audio.toml
  • crates/plugin-sdk-macros/tests/fixtures/bad_contribution_id.toml
  • crates/plugin-sdk-macros/tests/fixtures/bad_permission.toml
  • crates/plugin-sdk-macros/tests/fixtures/before_without_match.toml
  • crates/plugin-sdk-macros/tests/fixtures/broken.toml
  • crates/plugin-sdk-macros/tests/fixtures/duplicate_id.toml
  • crates/plugin-sdk-macros/tests/fixtures/entry_given.toml
  • crates/plugin-sdk-macros/tests/fixtures/format.toml
  • crates/plugin-sdk-macros/tests/fixtures/good.toml
  • crates/plugin-sdk-macros/tests/fixtures/no_contributions.toml
  • crates/plugin-sdk-macros/tests/fixtures/no_handler.toml
  • crates/plugin-sdk-macros/tests/fixtures/only_unknown.toml
  • crates/plugin-sdk-macros/tests/fixtures/unknown_with_handler.toml
  • crates/plugin-sdk/Cargo.toml
  • crates/plugin-sdk/README.md
  • crates/plugin-sdk/src/backend.rs
  • crates/plugin-sdk/src/entry.rs
  • crates/plugin-sdk/src/error.rs
  • crates/plugin-sdk/src/host.rs
  • crates/plugin-sdk/src/lib.rs
  • crates/plugin-sdk/src/rt.rs
  • crates/plugin-sdk/src/testing.rs
  • crates/plugin-sdk/src/ui.rs
  • crates/plugin-sdk/tests/fixtures/format.toml
  • crates/plugin-sdk/tests/fixtures/plugin.toml
  • crates/plugin-sdk/tests/format_plugin.rs
  • crates/plugin-sdk/tests/host_calls.rs
  • crates/plugin-sdk/tests/macro_plugin.rs
  • crates/plugin-sdk/tests/rt.rs
  • crates/plugin-sdk/tests/ui.rs
  • crates/plugin-sdk/tests/wasm_plugin.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@bigwolfeman
bigwolfeman marked this pull request as ready for review November 2, 2026 01:57
bigwolfeman and others added 7 commits November 1, 2026 20:21
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
- ChangeBus: each subscriber queue holds at most 256 changes. A full queue drops the oldest
  change and counts it. `drain_with_lost` returns the count; `drain` is unchanged.
- ObserverQueue: the coalesce window is kept per id in `last_at`, so `drain` no longer resets it.
  The per-push scan of the call deque is gone.
- json_diff: `Op.value` deserializes a present `"value": null` as `Some(Null)`, so a set to null
  stays a set after a serde round trip. An absent member stays `None`.
- json_diff::patch: ops run on a copy, and the copy replaces the document only when every op
  succeeds. A failed patch leaves the document unchanged.
- Interceptor::before takes `&Call`, not `&mut Call`. A gate can no longer edit params, id,
  origin or depth outside the verdict path, so the panic and budget rules cover every change.
  A compile_fail doctest pins this. Tests and docs follow the new signature.
- Conformance T6: new `CommandHost::take_plugin_gate_ids`. T6 now checks that the plugin's own
  gate sees a probe command before unregister and sees nothing after it. It no longer relies
  on the host's veto-all gate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
- manifest: a format contribution needs fs.picked in permissions.required or
  permissions.optional. Manifest::validate and the JSON Schema both enforce it.
- gate: legacy v1 modules now run the feature allow list and module validation.
- gate: the memory maximum uses checked_shl and saturating_mul. The reader
  already refuses an exponent of 64 or more, so this is hardening only.
- format: SizeMismatch Display computes the byte count in u128. It overflowed
  u64 for u32::MAX by u32::MAX.
- docs: ABI.md and README.md list what legacy modules skip and keep, and the
  format permission rule.
- tests: overflow, page size, legacy feature, and fs.picked cases.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
bigwolfeman and others added 9 commits November 5, 2026 10:32
…ssages

A table with no declared maximum can grow past the 100,000 element cap
through table.grow, so the gate now rejects it (TableMaxMissing), on both
the native and the legacy path. Violation messages repeat import names,
op names and serde text from the package, so Display now applies the same
text rules as manifest text.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8

# Conflicts:
#	Cargo.lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8

# Conflicts:
#	Cargo.lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8

# Conflicts:
#	Cargo.lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
The abi gate now requires fs.picked for a format contribution.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
pull_line dropped the bytes it had read when a read timed out, so next_event
returned Ok(None) and the next read saw only the tail of the line. The client now
keeps the partial line and resumes it on the next read. A test splits an event
line across a timeout.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
bigwolfeman and others added 28 commits November 5, 2026 16:51
… newline

At end of stream LineReader::read returned a buffered line with no length
check, so a peer that sent one byte over the limit and closed its write side got
that line dispatched, while the same bytes with a newline were refused. The end
of stream path now returns TooLarge for a line over the limit. A test covers
both paths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…o_large

write_value passed every reply to write_line, which has no size check. A sink
result larger than MAX_LINE went out as one line that the client refuses, and the
rest of the line stayed unread on the connection. Replies now go through
wire::write_reply_line. A reply over the limit is replaced by a payload_too_large
error that keeps the request id. When that does not fit either, or the value has
no id, nothing is written and the request loop ends. Event lines are unchanged.

wire gains write_reply_line. write_line behaves as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
The test expected the first notice to come from a shallow poll. With a 50 ms deep
interval the poll that first sees the edit can be a deep one, so the assertion
failed on a slow or loaded machine. It now takes the deep flag from the notice
and checks every other field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
FrameLayout::check only tested len == stride * h, so w = 4, stride = 1, h = 3,
len = 3 was a valid rgba8 layout. A reader that splits the payload by stride
cannot decode it. frames.open, a rendered frame and the client reply parser now
require stride >= w * bytes per pixel (4 for rgba8_premul, 8 for rgba16f_premul).
open checks before it mints a ticket, so a refused layout leaves no ticket.

The bulk test source used stride 16 for rgba16f with w = 4, which needs 32. It
now uses a stride that fits the format.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…ed field

valid_rfc3339 checked the day only against 1 to 31, so 2026-02-31T09:30:00Z
passed LinkRecord::validate while the schema's date-time format refused it. The
year, month and day now go through chrono::NaiveDate::from_ymd_opt, as the
workspace rules require for calendar logic. The accepted syntax is unchanged.
The schema agreement table gets rows for impossible days and for leap days.

craft-ipc gains the workspace chrono dependency. chrono is already in Cargo.lock;
the lock file gains one dependency line for craft-ipc.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
The asset.moniker pattern accepted any id without / ? #, so a record the schema
passed could fail LinkRecord::validate (bad or truncated escape, raw space,
escape that decodes to invalid UTF-8, id over 512 bytes). The pattern is now two
patterns under allOf. One checks the shape: unreserved characters and two-digit
escapes only, 1 to 512 of them, which is 1 to 512 decoded bytes. The other checks
that the decoded bytes are valid UTF-8, written as the well-formed UTF-8 byte
sequences. Neither uses lookahead, so any ECMA 262 engine can run them.

The schema agreement test gets rows for each case in the list above and for the
length limits, and a sweep of lead, second and tail bytes that compares the schema
verdict with the Rust verdict. This tightens the published schema: documents it
accepted that Rust refused are now refused.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Server::shutdown and drop join every connection thread, and an Assets call runs
on one with no deadline and no view of the stop flag. The trait docs now say so
and ask implementations to bound their own I/O. Behavior is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Clippy's type_complexity lint failed on the test helper's return type. Behavior
is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…ith a deadline

Server::shutdown and drop join every connection thread, and an Assets or
Activate call ran on one with no deadline and no view of the stop flag, so a
provider that never returned made shutdown hang.

These calls now run on a helper thread (server/provider.rs). The connection
thread waits for the answer in 100 ms slices, for at most
Limits::provider_deadline (default REPLY_DEADLINE_SECS) and not past the stop
flag. On timeout the caller gets a timeout reply and the helper is detached, so
shutdown does not join it. At most 32 helper threads are alive at once, stuck
ones included, and a call past that gets a busy error without starting a thread.
A panic in a provider becomes an io_error reply.

Frames stays on the connection thread because it is one call per frame. Its
trait docs now say that the stop flag is checked before each request and that a
running call is waited for. The module docs describe the shutdown guarantee for
each provider kind.

Public API: new Limits::provider_deadline field. Tests use a provider that
blocks until released, for Assets and for Activate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
When the fetch queue was full, the oldest ask was dropped but its waiting
callers got no answer. They woke, found the key neither cached nor in flight,
and queued it again, which dropped another ask and repeated the cycle.

A dropped ask now records a failure for its key before the waiters are woken, so
each waiter returns Fetched::Failed and does not queue the key again. The
drop-oldest policy and stats.skipped are unchanged. Failure records are
shared with the worker path and kept to a fixed number, oldest first.

No type or variant changes. The Fetched::Failed doc now covers this case. A test
overflows the queue while a caller waits on the oldest key, and checks that the
caller returns at once and that no further asks are dropped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
percent_decode used u8::from_str_radix, which accepts a leading plus, so
file:///tmp/a%+fb decoded to the byte 0x0f and went on to the media probe
instead of getting payload_invalid. The two bytes after a percent sign must now
both be ASCII hex digits. The decode test gets a plus, a minus, a space and a
short escape.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… macOS path test

On Unix systems other than Linux, current_euid read the owner of a file named
.craft-uid-<pid> in the temp directory, made with create and truncate. A file or
symlink planted at that name was opened and truncated. The probe now lives in
probe_euid: random name, create_new (which refuses an existing path), mode 0600,
and removal before it returns. The std library has no safe euid call and this
crate forbids unsafe code, so the probe stays. A test plants a file and a symlink
at the old and a similar name and checks that both are untouched and that the
probe file is gone.

macos_fallback_is_per_user compares Unix paths, so it and the macos_runtime_path
helper it needs are now compiled for tests on Unix only. That removes the unused
function warning in Windows test builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…arker

Server::emit wrote the serialized event unchecked. A line over MAX_LINE made
Client::next_event return TooLarge and left the stream out of sync. enqueue
now checks the serialized size and queues the existing overflow marker instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
# Conflicts:
#	Cargo.lock
check_package gathered facts before the size check, so a package over
the cap with many craft.manifest sections still allocated an entry for
each and could trigger the feature probes. An oversized package now
returns PackageTooLarge alone, before any parsing.
A module that mixed ec_, pc_ and vc_ exports returned before the table
and memory checks, so the author had to resubmit to learn about a table
with no maximum. The caps now run first. The doc comment on
check_package no longer claims an oversized package costs no
allocation; it skips parsing and validation.
# Conflicts:
#	Cargo.toml
#	README.md

@FredApps FredApps left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped review of SDK runtime/host-call helpers and macro wiring at 6729c002141627b6b454747ee498d5f341559cfc; inherited dependencies are outside this review. No local build or wasm runtime tests run; current checks do not establish passing validation.

[P2] Do not expose raw deallocation through a safe public Rust function. rt.rs publicly re-exports wasm::free, and pub fn free(ptr, len, align) calls std::alloc::dealloc after validating only the Layout. Safe wasm32 Rust code can therefore call craft_plugin_sdk::rt::free(0, 1, 1) or free an arbitrary/non-owned address, violating dealloc’s allocation/provenance contract and causing undefined behavior. A comment saying the host must supply a valid block does not impose an unsafe precondition on this public safe Rust API. Make the raw-pointer entry points unsafe where they rely on host ownership/lifetime assumptions (with explicit Safety contracts and correspondingly audited export glue), or validate ownership with an allocation registry so the safe surface is actually safe. Audit invoke/process for the same arbitrary-pointer-to-reference exposure rather than treating linear-memory bounds as proof of Rust allocation validity.

Separately, the requested SDK unsafe exception in RFC #414 question 4 is not yet named in this head’s AGENTS.md, which still requires workspace lint inheritance and prohibits unsafe absent a named exception. That policy decision must be resolved explicitly before this can be marked ready. Remaining SDK adapters/macro paths and current-head CI still need completion before approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants