with one click
add-service
// Implement or enhance an AWS service in winterbaume — creates a new crate from a stub or adds missing operations to an existing crate. Targets moto API coverage parity.
// Implement or enhance an AWS service in winterbaume — creates a new crate from a stub or adds missing operations to an existing crate. Targets moto API coverage parity.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | add-service |
| description | Implement or enhance an AWS service in winterbaume — creates a new crate from a stub or adds missing operations to an existing crate. Targets moto API coverage parity. |
| argument-hint | <service-name> [operations...] |
| user_invocable | true |
Create a new winterbaume service crate from a stub, or add missing operations to an existing crate. Both paths converge on the same implementation workflow.
$0 — Service name (e.g., guardduty, sqs, bedrock, transfer)$1... — (optional) Specific operations to implement. If omitted:
W[ ] M[x] in API_COVERAGE.mdBefore choosing operations or editing code, map the requested name through .agents/docs/services/INDEX.md and read the matching .agents/docs/services/<model-slug>.md. The dossier path is keyed by the model slug, not necessarily by the Winterbaume crate suffix or AWS SDK slug.
Use this document as the service dossier:
AWS model slug, AWS SDK for Rust slug, and protocol metadata help resolve crate/model/SDK naming mismatches.Official AWS Documentation Research, when present, captures AWS-documented semantics worth preserving in handlers and tests.Winterbaume LTM Notes, when present, records service-specific implementation pitfalls, integration boundaries, and parity guidance from previous work.Research Checklist for Parity Work lists behaviours to verify before claiming service parity.If no matching dossier exists, stop this workflow and invoke /service-dossier {service} first. Resume only after that skill has created the dossier and updated .agents/docs/services/INDEX.md. If the match is ambiguous, do not guess between similarly named services; use the index title, AWS model slug, AWS SDK for Rust slug, Smithy service title, and endpoint prefix to disambiguate, and invoke /service-dossier {service} when the index or dossier needs to be created or corrected.
ls crates/winterbaume-{service}/src/handlers.rs 2>/dev/null && echo "EXISTS" || echo "NEW"
EC2 is special.
winterbaume-ec2is split into two crates (winterbaume-ec2for hand-written code andwinterbaume-ec2-generatedformodel.rs/wire.rs), and operations are gated behind Cargo features. Before touching it, read.agents/docs/LTM/ec2-crate-split-and-feature-gating.md— the regeneration command, the feature taxonomy, and the per-operation#[cfg]requirements live there.
ls vendor/api-models-aws/models/ | grep -i {service}
Common mismatches: config -> config-service, cognitoidp -> cognito-identity-provider, elbv2 -> elastic-load-balancing-v2, logs -> cloudwatch-logs, events -> eventbridge.
Prefer the AWS model slug from .agents/docs/services/<model-slug>.md; it is the local canonical mapping from service research to vendored Smithy model directory. Prefer AWS SDK for Rust slug from the same document when naming the aws-sdk-{sdkname} dependency and SDK imports.
The model JSON is at vendor/api-models-aws/models/{model-dir}/service/{version}/{name}.json.
Read the model and find the service shape ("type": "service"). Its traits contains the protocol:
| Trait key | Protocol | Handler pattern |
|---|---|---|
aws.protocols#restJson1 | REST-JSON | HTTP method + path routing, x-amzn-errortype header |
aws.protocols#awsJson1_0 | awsJson1.0 | X-Amz-Target header dispatch, MockResponse::json() |
aws.protocols#awsJson1_1 | awsJson1.1 | X-Amz-Target header dispatch, MockResponse::json() |
aws.protocols#awsQuery | awsQuery | Action form field dispatch, XML responses |
aws.protocols#restXml | REST-XML | HTTP method + path routing, XML bodies |
cargo run -p smithy-codegen -- ops {model-name}
Lists all operations with [x] (implemented) and [ ] (missing) markers.
If the user specified operations, use those. Otherwise:
.agents/docs/API_COVERAGE.md, find the winterbaume-{service} section, and extract all W[ ] M[x] entries (ops moto has that winterbaume doesn't)Cross-check the candidate list against .agents/docs/services/<model-slug>.md before implementation. Prefer root-resource lifecycle operations identified by the service resource table, include documented tag operations when they exist, and respect service-specific Winterbaume LTM Notes or parity-checklist warnings about derived state, external engines, cross-service integrations, idempotency, pagination, or validation semantics.
If the gap is large (>15 ops), prioritise:
For each operation to implement, read its shape in the model JSON:
jsonName overridessmithy.api#http traitUse the service research document as a companion index while reading the model. The generated operation table can quickly surface idempotency tokens, pagination traits, required fields, resource associations, and known error shapes, but the Smithy model remains the source of truth for exact generated Rust fields.
Check moto's source for implementation hints if needed. Always prefer the Smithy model as source of truth.
crates/winterbaume-{service}/src/handlers.rs — dispatch pattern, protocol, existing operations
crates/winterbaume-{service}/src/state.rs — state structure, existing CRUD methods
crates/winterbaume-{service}/src/types.rs — domain types
crates/winterbaume-{service}/src/views.rs — state view types and StatefulService contract ( when the service exposes state views )
crates/winterbaume-{service}/src/wire.rs — available serialiser functions (auto-generated)
crates/winterbaume-{service}/src/model.rs — available model types (auto-generated)
crates/winterbaume-{service}/src/lib.rs — public exports, including any StateView type
crates/winterbaume-{service}/tests/integration_test.rs — existing test patterns, make_client helper
Critical: Match the existing code style exactly. Note the protocol, error response pattern, dispatch structure, state method signatures, and how wire types are constructed.
Also re-check .agents/docs/services/<model-slug>.md after reading the crate. If existing behaviour contradicts documented AWS semantics or the Winterbaume LTM Notes, decide whether the contradiction is intentional mock scope, an implementation bug, or deferred parity work before extending the same pattern.
grep 'pub fn serialize' crates/winterbaume-{service}/src/wire.rs | grep -i '{operation}'
If serialisers are missing, regenerate:
cargo run -p smithy-codegen -- gen-serializers {model-name} \
--output crates/winterbaume-{service}/src/wire.rs \
--model-output crates/winterbaume-{service}/src/model.rs
After regeneration, rebuild to check for errors. New model types may have additional fields — add ..Default::default() to existing wire struct initializers.
Create crates/winterbaume-{name}/ with these files:
Cargo.toml[package]
name = "winterbaume-{name}"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "{ServiceName} service implementation for winterbaume"
[dependencies]
winterbaume-core = { workspace = true }
tokio = { workspace = true }
bytes = { workspace = true }
http = { workspace = true }
uuid = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
# Add chrono = { workspace = true } if you need timestamps
[dev-dependencies]
aws-config = { workspace = true }
aws-credential-types = { workspace = true, features = ["test-util"] }
aws-sdk-{sdkname} = { workspace = true }
aws-smithy-runtime-api = { workspace = true }
aws-smithy-types = { workspace = true }
tokio = { workspace = true }
Only include dependencies you actually use.
src/lib.rspub mod handlers;
pub(crate) mod model;
pub mod state;
pub mod types;
pub mod views;
pub(crate) mod wire;
pub use handlers::{ServiceName}Service;
pub use state::{ServiceName}State;
pub use views::{ServiceName}StateView;
src/types.rsDefine domain types with #[derive(Debug, Clone)]. Keep minimal.
src/state.rsuse crate::types::*;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Default)]
pub struct {ServiceName}State {
pub items: HashMap<String, YourType>,
}
/// Domain-specific error enum. Contains no HTTP status codes or AWS error type strings —
/// those are mapped in the handler's error-shaping function.
#[derive(Debug, Error)]
pub enum {ServiceName}Error {
#[error("{resource_type} {name} does not exist.")]
NotFound { resource_type: &'static str, name: String },
#[error("{resource_type} {name} already exists.")]
AlreadyExists { resource_type: &'static str, name: String },
/// Catch-all for ad-hoc validation errors; use a named variant instead when
/// the same condition occurs 3+ times.
#[error("{message}")]
Validation { message: String },
}
impl {ServiceName}State {
// CRUD methods returning Result<T, {ServiceName}Error>
}
Design rules for the error enum:
"NotFoundException", 404, …) inside the enum definition or any method on it. HTTP mapping belongs exclusively in the handler.NotFound { resource_type, name }) when the message template is identical for many resource types.#[error("...")] attribute. Prefer named fields over positional ones for clarity.Validation { message: String } variant for ad-hoc validation errors that don't warrant their own variant. Use it sparingly.src/views.rsIf the service maintains durable backend state, add a typed serde-compatible view and implement winterbaume_core::StatefulService for the service. This is the contract used by state injection / extraction flows.
The contract is:
snapshot(account_id, region) returns a typed per-account / per-region view of the current staterestore(account_id, region, view) replaces the existing state entirely from that viewmerge(account_id, region, view) additively merges the view into the existing state and does not remove resources that are already presentImplementation rules:
Debug, Clone, Serialize, and Deserialize on view structs.{ServiceName}StateView that represents one account / region, usually keyed by stable resource identifiers.#[serde(default)] on collection fields so partial views are easy to deserialize and merge.impl From<&State> for StateView and impl From<StateView> for State.StateViewError::Invalid(...).Typical structure:
use crate::handlers::{ServiceName}Service;
use crate::state::{ResourceState, {ServiceName}State};
use crate::types::Resource;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {ServiceName}StateView {
#[serde(default)]
pub resources: HashMap<String, ResourceStateView>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceStateView {
pub name: String,
// durable fields only
}
impl From<&{ServiceName}State> for {ServiceName}StateView {
fn from(state: &{ServiceName}State) -> Self {
Self {
resources: state
.resources
.iter()
.map(|(k, v)| (k.clone(), ResourceStateView::from(v)))
.collect(),
}
}
}
impl From<&ResourceState> for ResourceStateView {
fn from(state: &ResourceState) -> Self {
Self {
name: state.resource.name.clone(),
}
}
}
impl From<{ServiceName}StateView> for {ServiceName}State {
fn from(view: {ServiceName}StateView) -> Self {
Self {
resources: view
.resources
.into_iter()
.map(|(k, v)| (k, ResourceState::from(v)))
.collect(),
}
}
}
impl From<ResourceStateView> for ResourceState {
fn from(view: ResourceStateView) -> Self {
Self {
resource: Resource {
name: view.name,
},
// reinitialise transient fields here
}
}
}
impl StatefulService for {ServiceName}Service {
type StateView = {ServiceName}StateView;
async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
let state = self.state.get(account_id, region);
let guard = state.read().unwrap();
{ServiceName}StateView::from(&*guard)
}
async fn restore(
&self,
account_id: &str,
region: &str,
view: Self::StateView,
) -> Result<(), StateViewError> {
let state = self.state.get(account_id, region);
{
let mut guard = state.write().unwrap();
*guard = {ServiceName}State::from(view);
} // write guard dropped here — must precede notify_state_changed (which takes a read lock)
self.notify_state_changed(account_id, region).await;
Ok(())
}
async fn merge(
&self,
account_id: &str,
region: &str,
view: Self::StateView,
) -> Result<(), StateViewError> {
let state = self.state.get(account_id, region);
{
let mut guard = state.write().unwrap();
for (name, resource_view) in view.resources {
guard.resources.insert(name, ResourceState::from(resource_view));
}
} // write guard dropped here — must precede notify_state_changed (which takes a read lock)
self.notify_state_changed(account_id, region).await;
Ok(())
}
fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
&self.notifier
}
}
Reference implementations ( all include the StateChangeNotifier pattern ):
crates/winterbaume-s3/src/views.rs, crates/winterbaume-sqs/src/views.rs, crates/winterbaume-iam/src/views.rs.
src/handlers.rsChoose handler pattern based on protocol. Reference implementations:
crates/winterbaume-athena/src/handlers.rscrates/winterbaume-ses/src/handlers.rscrates/winterbaume-sts/src/handlers.rsThe error-shaping helper must use an exhaustive match (no wildcard _ arm) so that adding a new error variant in state.rs forces a compile error until the handler is updated:
fn service_error_response(err: &{ServiceName}Error) -> MockResponse {
let (status, error_type) = match err {
{ServiceName}Error::NotFound { .. } => (404, "NotFoundException"),
{ServiceName}Error::AlreadyExists { .. } => (400, "AlreadyExistsException"),
{ServiceName}Error::Validation { .. } => (400, "ValidationException"),
// ... all variants — NO wildcard arm
};
// Message comes from thiserror's Display impl — do NOT duplicate it here
rest_json_error(status, error_type, &err.to_string())
}
The (status, error_type) pair is the only thing determined in the match. The message always comes from err.to_string() (which delegates to the #[error("...")] attribute). Never construct message strings inside match arms — that duplicates the text and creates a maintenance hazard.
Common structure:
use crate::state::{ServiceError, ServiceState};
use crate::views::{ServiceName}StateView;
use winterbaume_core::{
BackendState, MockRequest, MockResponse, MockService, StateChangeNotifier, DEFAULT_ACCOUNT_ID,
};
use serde_json::{json, Value};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub struct {ServiceName}Service {
pub(crate) state: Arc<BackendState<{ServiceName}State>>,
pub(crate) notifier: StateChangeNotifier<{ServiceName}StateView>,
}
impl {ServiceName}Service {
pub fn new() -> Self {
Self {
state: Arc::new(BackendState::new()),
notifier: StateChangeNotifier::new(),
}
}
}
impl Default for {ServiceName}Service {
fn default() -> Self { Self::new() }
}
impl MockService for {ServiceName}Service {
fn service_name(&self) -> &str { "{service-name}" }
fn url_patterns(&self) -> Vec<&str> {
vec![r"https?://{service-name}\..*\.amazonaws\.com"]
}
fn handle(&self, request: MockRequest) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
Box::pin(async move { self.dispatch(request) })
}
}
In the dispatch method, capture the response in a let binding and call
notify_state_changed after any successful mutating request before returning:
fn dispatch(&self, request: MockRequest) -> MockResponse {
// ... extract account_id, region, method ...
use winterbaume_core::StatefulService;
let response = match ... { /* routing match */ };
if matches!(method, "PUT" | "POST" | "DELETE" | /* protocol-specific mutating verbs */)
&& response.status / 100 == 2
{
self.notify_state_changed(account_id, region);
}
response
}
For awsJson / awsQuery services where every request is a POST, use a per-action allowlist instead of filtering by HTTP method (e.g., match the action name against a known set of mutating operations).
cargo run -p smithy-codegen -- gen-serializers {model-name} \
--output crates/winterbaume-{name}/src/wire.rs \
--model-output crates/winterbaume-{name}/src/model.rs
For each operation:
In state.rs, add a method returning Result<T, {Service}Error> with input validation, existence checks, and state mutation. Return domain-specific enum variants — never construct error structs with raw error_type/message/status strings. Example:
pub fn get_item(&self, name: &str) -> Result<&MyItem, {ServiceName}Error> {
self.items.get(name).ok_or_else(|| {ServiceName}Error::NotFound {
resource_type: "MyItem",
name: name.to_string(),
})
}
In types.rs, add domain structs for new resource types.
awsJson: match arm in match action.as_str() { ... }:
"OperationName" => self.handle_operation_name(&state, &body),
restJson1: match arm in match (method, segments.as_slice()) { ... }:
("POST", ["resource-path"]) => { ... self.handle_operation_name(...) }
If replacing a 501 stub, remove the old stub arm.
Rule: always use the generated serialiser for success responses. Never build a success response body with
json!()orMockResponse::json()directly. The generatedwire::serialize_*_response()functions encode correct wire field names (PascalCase), handleskip_serializing_if = "Option::is_none"for optional fields, and stay in sync with the model. Hand-rolled JSON silently breaks whenever the model is regenerated.Error responses may still use
error_response()/json!()because they have a fixed two-field shape (__type+message) that is not generated.
fn handle_operation_name(
&self,
state: &Arc<std::sync::RwLock<ServiceState>>,
body: &Value,
) -> MockResponse {
// 1. Extract and validate input fields
let field = match body.get("FieldName").and_then(|v| v.as_str()) {
Some(f) => f,
None => return error_response(400, "ValidationException", "Missing 'FieldName'"),
};
// 2. Call state method
let mut state = state.write().unwrap();
match state.operation_name(field) {
Ok(result) => {
// 3. Build wire output struct and pass to generated serialiser — never use json!() here
wire::serialize_operation_name_response(&wire::OperationNameOutput {
field: result.field.clone(),
..Default::default()
})
}
Err(e) => service_error_response(&e),
}
}
Check the generated model.rs to see whether timestamp fields are f64 or String:
f64 → use timestamp() as f64 (epoch seconds — default for all JSON protocols)String → use to_rfc3339() (the service has @timestampFormat("date-time") override)In tests/integration_test.rs, add basic per-operation tests using the real AWS SDK client. The goal at this stage is to verify each handler you just implemented compiles and round-trips correctly. These tests are necessary but not sufficient — they prove the handler exists and serialises, not that it implements the documented contract. Comprehensive per-operation coverage ( full-input round-trip, per-call uniqueness, validation, idempotency ) and the cross-call invariant inventory come from the mandatory Step 6b ( /write-tests ), which gates publication.
Example notification test:
#[tokio::test]
async fn test_state_change_listener_fires() {
use std::sync::{Arc, Mutex};
use winterbaume_core::StatefulService;
let svc = {ServiceName}Service::new();
let events: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(vec![]));
let events2 = Arc::clone(&events);
svc.notifier().subscribe(move |account_id, region, _view| {
events2.lock().unwrap().push((account_id.to_string(), region.to_string()));
});
// Trigger a mutation via restore or a handler call
svc.restore("123456789012", "us-east-1", Default::default()).await.unwrap();
let got = events.lock().unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0], ("123456789012".to_string(), "us-east-1".to_string()));
}
#[tokio::test]
async fn test_state_change_listener_snapshot_reflects_mutation() {
use std::sync::{Arc, Mutex};
use winterbaume_core::StatefulService;
let svc = {ServiceName}Service::new();
// Pre-seed state
let view = {ServiceName}StateView { /* resource present */ ..Default::default() };
svc.restore("123456789012", "us-east-1", view).await.unwrap(); // ignore first event
// Re-register and capture snapshot
let snapshots: Arc<Mutex<Vec<{ServiceName}StateView>>> = Arc::new(Mutex::new(vec![]));
let snapshots2 = Arc::clone(&snapshots);
svc.notifier().subscribe(move |_account_id, _region, view| {
snapshots2.lock().unwrap().push(view.clone());
});
let view2 = {ServiceName}StateView { /* resource present */ ..Default::default() };
svc.restore("123456789012", "us-east-1", view2).await.unwrap();
let got = snapshots.lock().unwrap();
assert_eq!(got.len(), 1);
// assert that the snapshot reflects the new state
assert!(got[0].resources.contains_key("expected-key"));
}
use aws_sdk_{sdkname}::config::BehaviorVersion;
use winterbaume_{name}::{ServiceName}Service;
use winterbaume_core::MockAws;
async fn make_client() -> aws_sdk_{sdkname}::Client {
let mock = MockAws::builder()
.with_service({ServiceName}Service::new())
.build();
let config = aws_config::defaults(BehaviorVersion::latest())
.http_client(mock.http_client())
.credentials_provider(mock.credentials_provider())
.region(aws_sdk_{sdkname}::config::Region::new("us-east-1"))
.load()
.await;
aws_sdk_{sdkname}::Client::new(&config)
}
&str; optional fields return Option<&str>aws_sdk_xxx::types::EnumName::Variant, not stringsassert_eq! on enums, dereference: &SomeEnum::Variantcargo build -p winterbaume-{service}
cargo clippy -p winterbaume-{service}
cargo test -p winterbaume-{service}
Common issues:
..Default::default() on wire struct initializersOption<T> vs T mismatches (model.rs required vs optional fields)needless_update: if clippy warns that a ..Default::default() has no effect (all fields already specified), remove ittoo_many_arguments: suppress with #[allow(clippy::too_many_arguments)] on state methods that genuinely require many parameterstype_complexity: suppress with #[allow(clippy::type_complexity)] on state methods returning complex nested tuples
--maxfailis pytest, not cargo.--maxfailis a pytest flag, not a Rust libtest flag. For Rust crate verification, use focused filters (cargo test <pattern>),--test-threads=N,--no-fail-fast, and per-crate gates instead. Do not pass--maxfailto cargo test.
Step 5's round-trip stubs are necessary but not sufficient. Before this skill returns and before any subsequent invocation of /quality-gate, run /write-tests {service}. The publication gate ( quality-gate §2 ) requires the cross-call invariant inventory and tests/scenario_test.rs that /write-tests produces — without them the gate fails closed.
/write-tests will:
.agents/docs/services/<model-slug>.md as the local baseline, then refresh or extend it with AWS Documentation MCP research when the service doc is stale, incomplete, or lacks official-doc coverage for the operations you touched. If the dossier is missing, run /service-dossier {service} first and resume after the dossier exists.JOURNAL.md and gates the publication scenario file.tests/integration_test.rs for any documented behaviours Step 5 didn't cover ( validation rules, defaults, error semantics, idempotency, full-input round-trip, per-call uniqueness, FIX(terraform-e2e) markers )tests/scenario_test.rs that chain multiple operations and explicitly cover each non-N/A invariant rowDo not skip this step. A "genuinely stateless" service ( pricing, translate, comprehend, forecastquery, *-data planes ) still runs /write-tests; the invariant inventory simply marks each row N/A — <reason> with a doc URL. The gate accepts that, but it requires the artefact.
If a deliberate deferral is unavoidable ( e.g. you are landing a stub crate as a placeholder ), file a TODO.md row tracking the missing inventory before the gate runs. The gate will fail closed referencing that TODO row until it is resolved — there is no silent grandfathering.
The crates/winterbaume-terraform crate maps Terraform resource attributes to winterbaume service state via the TerraformResourceConverter trait. Each Terraform resource type (e.g. aws_sns_topic) needs one converter. Services that expose StatefulService state views are eligible; if the service has no state view, skip this step.
ls crates/winterbaume-terraform/src/converters/{service}.rs 2>/dev/null && echo "EXISTS" || echo "NEW"
If the file already exists, add any missing resource-type converters to it and skip to step 7e.
Look up the Terraform AWS provider docs (or check existing converters for naming conventions) to find the aws_{service}_{resource} type names that correspond to the resources managed by this service. Only implement converters for resource types whose underlying data is fully captured by the service's state view.
crates/winterbaume-terraform/src/converters/{service}.rsPattern (one struct per Terraform resource type, all sharing the same Arc<{ServiceName}Service>):
//! Terraform converters for {Service} resources.
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use winterbaume_core::StatefulService;
use winterbaume_{service}::{ServiceName}Service;
use winterbaume_{service}::views::{ServiceName}StateView;
use winterbaume_tfstate::ResourceInstance;
use crate::converter::{
ConversionResult, ExtractedResource, ConversionContext, TerraformResourceConverter,
};
use crate::error::ConversionError;
use crate::util::{extract_region, optional_str, require_str};
pub struct Aws{Resource}Converter {
service: Arc<{ServiceName}Service>,
}
impl Aws{Resource}Converter {
pub fn new(service: Arc<{ServiceName}Service>) -> Self {
Self { service }
}
}
impl TerraformResourceConverter for Aws{Resource}Converter {
fn resource_type(&self) -> &str { "aws_{service}_{resource}" }
fn inject<'a>(
&'a self,
instance: &'a ResourceInstance,
ctx: &'a ConversionContext,
) -> Pin<Box<dyn Future<Output = Result<ConversionResult, ConversionError>> + Send + 'a>> {
Box::pin(async move { self.do_inject(instance, ctx).await })
}
fn extract<'a>(
&'a self,
ctx: &'a ConversionContext,
) -> Pin<Box<dyn Future<Output = Result<Vec<ExtractedResource>, ConversionError>> + Send + 'a>> {
Box::pin(async move { self.do_extract(ctx).await })
}
}
impl Aws{Resource}Converter {
async fn do_inject(
&self,
instance: &ResourceInstance,
ctx: &ConversionContext,
) -> Result<ConversionResult, ConversionError> {
let attrs = &instance.attributes;
let region = extract_region(attrs, &ctx.default_region);
// 1. Pull required/optional fields from attrs with require_str / optional_str
// 2. Build the typed view structs from the service's views module
// 3. Call self.service.merge(&ctx.default_account_id, ®ion, view)
// (prefer merge over restore so concurrent resources accumulate)
Ok(ConversionResult { region, warnings: vec![] })
}
async fn do_extract(
&self,
ctx: &ConversionContext,
) -> Result<Vec<ExtractedResource>, ConversionError> {
// 1. Snapshot state: self.service.snapshot(&ctx.default_account_id, ®ion)
// 2. Iterate resources, convert to serde_json::Value attribute maps
// 3. Return Vec<ExtractedResource>
Ok(vec![])
}
}
Key rules:
merge(account_id, region, view) (not restore) in do_inject so that multiple resources in the same plan accumulate rather than overwriting each other.require_str for mandatory Terraform attributes; use optional_str / optional_i64 for optional ones.arn:aws:{service}:{region}:{account_id}:...).converters/sns.rs (simple), converters/dynamodb.rs (complex with nested blocks), converters/iam.rs (policy documents).converters/mod.rsAdd pub mod {service}; in alphabetical order.
In crates/winterbaume-server/src/main.rs, find where TerraformInjector is built (look for .register() and add:
injector.register(winterbaume_terraform::converters::{service}::Aws{Resource}Converter::new(Arc::clone(&{service}_svc)));
cargo build -p winterbaume-terraform
cargo build -p winterbaume-server
Cargo.tomlAdd in three places:
[workspace] members — add "crates/winterbaume-{name}"[workspace.dependencies] — add winterbaume-{name} = { path = "crates/winterbaume-{name}" }aws-sdk-{sdkname} = "1" if not already presentIn crates/winterbaume-server/Cargo.toml, add: winterbaume-{name} = { workspace = true }
In crates/winterbaume-server/src/main.rs, add to register_all_services():
Arc::new(winterbaume_{name}::{ServiceName}Service::new())
In the root Cargo.toml, add entries to both the [dev-dependencies] section and (if not already present) the [workspace.dependencies] section:
# [dev-dependencies]
winterbaume-{name} = { workspace = true }
aws-sdk-{sdkname} = { workspace = true }
Insert in alphabetical order alongside the existing entries.
cargo run -p smithy-codegen -- inject {model-name}
Register every new crate in CRATE_TO_MODEL ( .agents/skills/api-coverage/scripts/generate_coverage.py ). The 2026-04-27 audit found 47 missing mappings ( 164 reported services vs 211 actual ) because this step was implicit. After adding the entry, run python3 .agents/skills/api-coverage/scripts/generate_coverage.py and verify the three-counts canary ( crates / READMEs / docs pages ) before declaring the service shipped.
Check whether an example already exists:
ls examples/{service}.rs 2>/dev/null && echo "EXISTS" || echo "NEW"
If it EXISTS, skip this step.
If NEW, create examples/{service}.rs following this template (pick the most representative read or list operation that was just implemented — prefer a List* or Describe* call that requires no mandatory arguments):
//! Example: {ServiceNameTitle}
//!
//! Demonstrates using aws-sdk-{sdkname} with winterbaume.
//!
//! Run with:
//! cargo run --example {service} --package winterbaume-examples
use aws_sdk_{sdkname}::config::BehaviorVersion;
use winterbaume_{name}::{ServiceName}Service;
use winterbaume_core::MockAws;
#[tokio::main]
async fn main() {
let mock = MockAws::builder()
.with_service({ServiceName}Service::new())
.build();
let config = aws_config::defaults(BehaviorVersion::latest())
.http_client(mock.http_client())
.credentials_provider(mock.credentials_provider())
.region(aws_sdk_{sdkname}::config::Region::new("us-east-1"))
.load()
.await;
let client = aws_sdk_{sdkname}::Client::new(&config);
let resp = client
.{list_or_describe_operation}()
.send()
.await
.expect("{list_or_describe_operation} should succeed");
println!("{ServiceNameTitle}: {:?}", resp.{primary_output_field}());
}
Guidelines:
.expect() each..expect().--package winterbaume-examples label in the run comment matches the convention used by all existing example files.If the service has a Terraform converter (Step 7), write Terraform E2E tests to verify that real terraform apply workflows work against the in-process server. Use the /write-e2e-tests {service} skill, which covers smoke testing, handler fixes, full test suite authoring, and harness registration.
Skip this step if:
tests/e2e/terraform/{service}.rs)cargo run -p smithy-codegen -- ops {model-name} 2>&1 | grep '\[x\]' | wc -l
.agents/docs/services/INDEX.md consulted and the matching .agents/docs/services/<model-slug>.md read before selecting scope/service-dossier {service} was invoked and completed before implementation scope was selectedAWS model slug and AWS SDK for Rust slug from the service document used for model lookup and SDK dependency/import naming.agents/docs/services/<model-slug>.md reflected in implementation and tests, or explicitly skipped with justification./.agents/bin/cargo.sh build -p winterbaume-{service} — no errors./.agents/bin/cargo.sh clippy -p winterbaume-{service} --all-targets --all-features -- -D warnings — passes with no warnings (model.rs/wire.rs are generated; their warnings are suppressed by the generator). If new clippy violations appear, fix them; do not finish with outstanding warnings../.agents/bin/cargo.sh fmt -p winterbaume-{service} -- --check — passes. If it fails, run without --check to apply the formatter../.agents/bin/cargo.sh test -p winterbaume-{service} — all tests passStateChangeNotifier listener fires after a mutation (restore, merge, or handler call) and receives a snapshot reflecting the change..Default::default()wire::serialize_*_response() — no json!() or raw MockResponse::json() for success bodieschrono::Utc::now() or any other chrono:: API, the crate's Cargo.toml declares chrono = { workspace = true }. Recurring failure mode caught manually in the 2026-04-28 sweep ( elasticbeanstalk, panorama, workspaces ) — agents added the call without the dep, cascading to compile-time errors only after they returned.*StateView struct ( and its component view structs ) #[derive(Default)], and every literal construction in tests, converters, or backend wrappers uses ..Default::default(). State-view literal drift is the largest cascade source in /tackle-todos sweeps; per-crate quality gates do not catch the breakage, only winterbaume-terraform integration tests do.{ServiceName}Error is a #[derive(Debug, Error)] enum in state.rs — no error_type/message/status fields, no HTTP status codes anywhere in the enum#[error("...")] attribute; the handler uses err.to_string() for the message, never duplicating message text in match armshandlers.rs has an exhaustive match (no wildcard _ arm)crates/winterbaume-terraform/src/converters/{service}.rs (or skipped with justification if no state view)cargo build -p winterbaume-terraform — no errorsexamples/{service}.rs created (or confirmed pre-existing)winterbaume-{name} and aws-sdk-{sdkname} added to root [dev-dependencies]/write-tests {service} invoked to expand integration coverage and add tests/scenario_test.rs (or skipped with justification — every operation is independent and stateless)tests/e2e/terraform/{service}.rs), or skipped with justification (no converter / no state view)