Hmbown/CodeWhale · error

effective permissions should round-trip

Error message

effective permissions should round-trip

What it means

This is the panic from `back.effective_permissions.expect("effective permissions should round-trip")` in the `fleet_resolved_route_round_trips` test at crates/protocol/src/fleet.rs:1744. `FleetReceipt.effective_permissions` is an `Option<FleetEffectivePermissions>` describing the worker's actual runtime permissions (write, network, shell scope, tool list, spawn depth, profile). The test serializes a receipt that has `effective_permissions: Some(...)`, deserializes it back, and unwraps the option, asserting the permissions survived the JSON round trip. It panics when the deserialized receipt's `effective_permissions` is `None`, meaning serde dropped or failed to restore the field — typically a missing/incorrect `#[serde(default)]`/rename on `effective_permissions`, a field name mismatch in `FleetEffectivePermissions`, or the fixture accidentally producing a receipt without permissions.

Solutions

  1. Check the deserialized receipt directly (print or assert before unwrapping) to confirm whether the JSON even contains the `effective_permissions` key — if absent, the serialization side (skip attribute, renamed key) is at fault; if present, the struct deserialization side is.
  2. Verify `FleetReceipt.effective_permissions` and `FleetEffectivePermissions` still derive `Serialize`/`Deserialize` with matching field names, and that every field of `FleetEffectivePermissions` is JSON-compatible with its declared type (`shell`/`tool_scope`/`source` as `String`, `tools` as `Vec<String>`, `profile_id`/`profile_origin` as `Option<String>`).
  3. If the fixture regressed, restore `effective_permissions: Some(FleetEffectivePermissions { ... })` in `sample_receipt_with_route` as shown at fleet.rs:1702-1713.
  4. If a newly added permission field breaks parsing, annotate it `#[serde(default)]` (with `skip_serializing_if` for Options) so old receipts — like the pre-#3154 legacy ledger JSON covered by `fleet_receipt_without_resolved_route_still_deserializes` — still deserialize.
  5. Re-run: `cargo test -p codewhale-protocol fleet_resolved_route_round_trips`.

Example fix

// before
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_permissions: Option<FleetEffectivePermissions>,

pub struct FleetEffectivePermissions {
    pub tool_scope: ToolScope, // renamed type/field: JSON key no longer matches, parse fails => None
}
// after
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_permissions: Option<FleetEffectivePermissions>,

pub struct FleetEffectivePermissions {
    pub tool_scope: String, // or #[serde(alias = "old_name")] on the renamed field
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm a receipt round-trips its permissions before unwrapping
fn permissions_preserved(before: &FleetReceipt, after: &FleetReceipt) -> bool {
    after.effective_permissions == before.effective_permissions
}
// use: assert!(permissions_preserved(&receipt, &back));

Type guard

fn effective_permissions(r: &FleetReceipt) -> Option<&FleetEffectivePermissions> {
    r.effective_permissions.as_ref()
}
// use: if let Some(perm) = effective_permissions(&back) { ... } else { /* handle missing */ }

Try / catch

let Some(permissions) = back.effective_permissions.as_ref() else {
    panic!("effective_permissions lost in round trip: json={} ", serde_json::to_string(&back).unwrap_or_default());
};

Prevention

When it happens

Trigger: The panic occurs when `FleetReceipt::effective_permissions` deserializes to `None` despite being `Some` before serialization: (1) the field or struct lost/misspelled its serde attributes so the key is written under a different name and silently ignored on read; (2) `FleetEffectivePermissions` gained a required (non-default) field that the serializer omits, failing that branch — combined with `#[serde(default)]` on the outer Option this yields `None` instead of an error; (3) a test fixture builds the sample receipt with `effective_permissions: None` after an edit; (4) new fields in `FleetEffectivePermissions` use types that don't match their JSON representation so deserialization of the struct fails and a lenient `default` masks it as `None`.

Common situations: A developer adds a permission knob (e.g. a new tool-scope field) to `FleetEffectivePermissions` or renames one of its fields, runs `cargo test -p codewhale-protocol fleet_resolved_route_round_trips`, and the expect at line 1744 panics because the round-tripped option came back `None`. Also hit when serde attribute refactors accidentally apply `skip_serializing_if` to `effective_permissions` without a matching default path, or when the fixture helper `sample_receipt_with_route` is edited to drop the permissions.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/8c738d381872a5d1. Report an issue: GitHub.

Appendix: source

Thrown at crates/protocol/src/fleet.rs:1744

        assert_eq!(route.wire_model_id, "deepseek-v4-pro");
        assert_eq!(route.protocol, "chat_completions");
        assert_eq!(route.role.as_deref(), Some("builder"));
        assert_eq!(route.loadout.as_deref(), Some("auto"));
        assert_eq!(route.model_class.as_deref(), Some("balanced"));
        assert_eq!(route.model_route.as_deref(), Some("auto"));
        assert_eq!(route.reasoning_effort.as_deref(), Some("high"));
        assert_eq!(route.role_source.as_deref(), Some("task.role"));
        assert_eq!(route.loadout_source.as_deref(), Some("task.loadout"));
        assert_eq!(
            route.model_class_source.as_deref(),
            Some("task.model_class")
        );
        assert_eq!(route.model_source.as_deref(), Some("task.model"));
        assert_eq!(route.source, "resolver");

        let permissions = back
            .effective_permissions
            .expect("effective permissions should round-trip");
        assert!(permissions.write);
        assert!(permissions.network);
        assert_eq!(permissions.shell, "full");
        assert_eq!(permissions.tool_scope, "explicit");
        assert_eq!(
            permissions.tools,
            vec!["read_file".to_string(), "apply_patch".to_string()]
        );
        assert!(permissions.background);
        assert_eq!(permissions.max_spawn_depth, 2);
        assert_eq!(permissions.profile_id.as_deref(), Some("builder"));
        assert_eq!(permissions.profile_origin.as_deref(), Some("built_in"));
        assert_eq!(permissions.source, "worker_runtime_profile");
    }

    #[test]
    fn fleet_receipt_without_resolved_route_still_deserializes() {
        // An old ledger receipt JSON written before #3154 has no

View on GitHub (pinned to 433685b202)