Skip to content

fix(state): typed Permission on SetPermission#440

Merged
intendednull merged 1 commit into
claude/adoring-euler-DvNnkfrom
auto-fix/issue-240-typed-set-permission
Apr 27, 2026
Merged

fix(state): typed Permission on SetPermission#440
intendednull merged 1 commit into
claude/adoring-euler-DvNnkfrom
auto-fix/issue-240-typed-set-permission

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Why

SetPermission.permission was String. Anything stored. "FrobnicateWidgets"
walks past apply, lands in role.permissions. Role.permissions also
String — pollution sticks. Compare GrantPermission, already typed.

Fix

  • SetPermission.permission: Permission (typed enum)
  • Role.permissions: BTreeSet
  • Custom Deserialize maps unknown string-form names (JSON / MCP) to
    hidden Permission::__UnknownLegacy sentinel. apply_event drops
    sentinel + warns — DAG sig + chain stays intact, bad perm dropped
  • Agent MCP tool keeps wire param as string, parses via
    Permission::from_name at boundary, errors on unknown
  • All emit sites converted to typed enum

Migration choice

Custom Deserialize back-compat (a) over hard cutover (b). Bincode wire
path is pre-1.0 — no production SQLite storage round-trips
String-form events to disk (storage holds bincode bytes for typed
enum, JSON path retained for MCP / future snapshots). Sentinel keeps
DAG resilient against rogue / future clients.

Tests

  • set_permission_with_typed_permission_round_trips — bincode round-trip + apply
  • set_permission_legacy_string_form_still_loads — JSON String → enum
  • set_permission_legacy_unknown_string_drops_silently — unknown name → sentinel → no-op apply

cargo test --workspace green, clippy clean, WASM check clean.

Refs #240


Generated by Claude Code

SetPermission.permission was String. Took anything. "FrobnicateWidgets"
sail past apply, land in role.permissions set. Same role.permissions
also String-typed — pollution sticks.

Now: SetPermission.permission: Permission. Role.permissions:
BTreeSet<Permission>. Type system bars unknown names from ever
entering DAG.

Back-compat: custom Deserialize maps unknown legacy string-form names
(JSON / MCP boundary) to hidden __UnknownLegacy sentinel. apply_event
drops sentinel as no-op + tracing::warn — DAG signature stays intact,
bad name dropped. Bincode wire path emits typed enum directly;
pre-1.0, no production storage round-trips legacy String form, so
binary cutover safe (storage SQLite holds bincode bytes, not JSON).

Agent MCP tool keeps string param on wire, parses via
Permission::from_name at boundary, errors on unknown.

Runner-up: hard cutover (Deserialize accepts only typed enum). Rejected
— a rogue / future client emitting unknown name would currently break
the chain on insert; sentinel keeps DAG resilient with no security
loss.

Refs #240
@intendednull intendednull merged commit 354f6c8 into claude/adoring-euler-DvNnk Apr 27, 2026
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