Skip to main content

plugin-implement

Implement a Resonate transport plugin in Rust from its specification — plugins/<name>/spec/specification.md → plugins/<name>/src.

跳到安装

来源信息

仓库
resonatehq/resonate-plugins
最近来源活动
2026年8月29日 09:25
检测到的 SKILL.md 语言
英语
星标
0
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
7 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
plugin-implement
description
Implement a Resonate transport plugin in Rust from its specification — plugins/<name>/spec/specification.md → plugins/<name>/src.
# plugin-implement Implement `plugins/<name>/src` from `plugins/<name>/spec/specification.md`. The specification is the source of truth; do not re-derive facts from the provider's documentation, and do not deviate from the specification. If the specification is wrong or incomplete, stop and report — do not patch around it. ## Architecture Every plugin is the same Rust library crate shape, copied from [reference/](reference/). The canonical worked example is the in-repo `plugins/airflow/src` — read it before implementing, for the frame's style only: where it disagrees with a specification (param shape, error kinds, polling, helper placement — e.g. a specification's `_check` wrapping each request before its status branches), the specification governs. - `Cargo.toml` — package `resonate-plugin-<name>`, a library. - `src/lib.rs` — exports `Worker`, `Config`, `SCHEME`, and `pub mod plugin` (the integration tests call `plugin::process` directly). The server binary constructs `Worker` with its `ResonateServer` port and registers it on the router under the scheme. - `src/worker.rs` — the standard frame, identical in every plugin. Never edit it. It implements `ResonateWorker::send` (send = accepted for delivery: spawn and return), then per delivery: claim the task (`task.acquire` with the message's fencing version, `pid: "self"`), heartbeat the lease at a third of the TTL in its own task (the lease clock and the downstream clock are independent), call `plugin::process`, and settle: `Ok(Ok(v))` fulfills resolved, `Ok(Err(v))` fulfills rejected, `Err(Ok(reason))` halts the task (`task.halt` — redelivery pauses until an operator continues it), `Err(Err(reason))` drops the task (`task.release`) — the message is redelivered promptly. The server settles timed-out promises itself. - `src/plugin.rs` — everything provider-specific. This is the only file you write. ## Procedure 1. Copy `reference/` into `plugins/<name>/src` — the crate root is `plugins/<name>/src` (so `Cargo.toml` is `plugins/<name>/src/Cargo.toml` and the code is `plugins/<name>/src/src/`). Replace `{name}` with the plugin scheme. 2. Translate specification §2 into `Config`: bare type = required field, `Option<T>` = optional, literal defaults via `#[serde(default = ...)]`, `= poll` cascades via an `unwrap_or(self.poll)` accessor, `Duration` fields via `humantime_serde`. An `= instance` default is resolved by whoever loads the config section — the plugin never sees the address; declare the field required. 3. Implement `process`: decode `param.data` (base64 JSON, as the reference's `process` shows) and match on its `func`, one arm per §4 operation, dispatching to one private function per operation. An unknown func rejects with code `unknown_func`; a missing or mistyped required arg rejects `invalid_request` locally — the Python's `KeyError` is not a model. Both are frame-level rejections (plugin-specify: produced by the frame, not the operation) and legitimately carry codes outside the operation's Rejected enum. 4. Translate each §4.N.5 Python function into its Rust function, preserving structure exactly: the same requests in the same order, the same enumerated permanent status codes → `Ok(Err(...))`, halt conditions → `Err(Ok(...))`, everything unclassified → `Err(Err(...))`, the same loop with the same `promise.timeout_at` bound and failure-streak cap, the same comments. Use async `reqwest` with `.await` (the frame is tokio-async). Build rejected values to the op's Rejected schema; build resolved values by applying the op's Resolved mapping. Translation rules the Python does not state: clamp every poll sleep to the time remaining before `promise.timeout_at`; an absent response field maps to JSON `null`, never a crash (a Rejected `detail` may therefore be `null` — tests assert it as such). The wire bytes are not among them: the specification's Python is what the reviewer executed against the live provider, so the request this code sends must be byte-for-byte the request that Python sent. Where it cannot be — the Python renders a value one way and `reqwest` another — the specification is wrong and rule 4's "stop and report" applies; do not silently correct it here. 5. Injected identity values are `sanitize(&promise.id)` (from the frame, never the raw id) — exactly where the specification writes `sanitize(promise.id)`. 6. Validate: `cargo check` clean, then `python3 skills/plugin-implement/check.py plugins/<name>` clean — it is the mechanical half of this step (every operation has an arm, every rejection code is constructed, no provider fact in the code that the specification does not carry, no template placeholder left behind, and nothing `.gitignore` matches ready to be committed). It compiles nothing; step 7 is the other half. 7. Test per the Testing section: `cargo test` clean — with the §5 provider running when the specification has one. 8. End to end, per the Testing section's last paragraph: a minimal in-process server with only this worker registered; create a promise; see it settle. ## Testing Tests live in `plugins/<name>/src/tests/process.rs` and test `plugin::process` only. The frame, config extraction, and registration are not the plugin's to test. Tests run serially: ship `.cargo/config.toml` with `RUST_TEST_THREADS = "1"` — a §5 provider need not survive parallel load, and its failure under it looks like a plugin bug. When the specification has §5 (Notes `Self-hosted: yes`), tests run against the real provider — never a mock: run 5.1/5.2 first, then build `Config` from the `{SCHEME}_{KEY}` environment variables 5.2 exported, overriding `poll` to `1s` (the test default; an individual test may use a larger value when a condition needs a wider window — e.g. a delete that is only legal on a finished execution). A local test helper constructs the `PromiseRecord` (fresh promise id per test, base64 `{func, args}` param, `timeout_at`). Every documented condition is induced with real inputs: | condition | induction | |---|---| | resolved | the succeeding work item §5 provisions | | `<resource>_failed` | the failing work item §5 provisions | | `not_found` | a nonexistent id | | `invalid_request` | violate a constraint the Param schema documents | | halt | corrupt the credential in `Config` | | recovery / re-entry | call `process` twice with the same promise id | | deadline | `timeout_at` in the past | | pending → terminal | real work takes real time | One test per (operation × documented condition). Each test asserts the verdict variant and the value against the operation's Resolved or Rejected schema. Provisioning the provider with the resources these inductions need (a project, jobs, templates) is test setup, done with raw provider calls from the test files — it is not patching the specification; the two test files may duplicate a small provisioning helper. SaaS-only providers (no §5): the same table against a local mock (wiremock), stubbing responses from the Integration Response schemas with the success status the Integration Request line's `→ <status>` names, and additionally asserting each outgoing request against the Integration Request schema (method, path, injected `sanitize`, body mapping). The mock seam is an environment override: the API base constant honours `{SCHEME}_API`, read once via `OnceLock` — that process-wide override is a second reason the tests run serially. Run one mock server per test binary and `reset()` it per test; stub a pending → terminal sequence as a response series (`up_to_n_times(1)` plus priorities). State the circularity in a test-file comment: the mocks are built from the same specification as the code; only review against the live provider breaks the loop. 429, 5xx, and network conditions are not induced or mocked per plugin — unclassified failure is the frame's release path. One exception: the consecutive-failure streak cap may be covered by a mocked 5xx series; it is unreachable otherwise. Finally, end to end (`tests/e2e.rs`): compile a minimal Resonate server with only this worker — the `resonate` crate (a dev-dependency: `resonate = { git = "https://github.com/resonatehq/resonate", branch = "core-crate" }`, the same source as `resonate-core`) provides an in-process `ResonateServer` implementation; register nothing but this plugin's `Worker`. Create a promise whose `resonate:target` tag is this plugin's address and whose param is a real `{func, args}`, deliver the resulting Execute message to the worker, and await settlement: the promise must reach the terminal state and value the specification's schemas promise — resolved for the succeeding §5 work item, rejected with the right `code` for the failing one. For a SaaS-only plugin the wiremock stands in for §5, and the failing item is the work operation's own terminal failure code. This is the only test that exercises the frame's claim/heartbeat/settle wiring together with `process`; everything upstream of the worker is the server's own test surface, not this crate's.
在 GitHub 查看