Skip to content

feat(fiber): transitionPolicy dial + auto-declared machines deps#194

Open
ottobot-ai wants to merge 1 commit into
feat/fiber-ergonomics-proposalsfrom
feat/transition-policy-and-autodep
Open

feat(fiber): transitionPolicy dial + auto-declared machines deps#194
ottobot-ai wants to merge 1 commit into
feat/fiber-ergonomics-proposalsfrom
feat/transition-policy-and-autodep

Conversation

@ottobot-ai

Copy link
Copy Markdown
Collaborator

Implements F6 + F7 of the fiber-ergonomics program (docs/proposals/fiber-ergonomics/03-cross-fiber-and-authorization.md). Stacked on #192 (the RFCs + the TransitionOwnerGateDivergenceSuite regression test this PR's fix flips green); retargets to main when #192 merges.

F7 — close the transition-authorization enforcement gap (the security fix)

Today the transition owner-gate is coded in validateSignedUpdate but not in the combiner, so the authoritative apply path advances committed state for any signer whose transition passes the guard (pinned by TransitionOwnerGateDivergenceSuite in #192). This PR makes signer-authorization actually bind, and makes the posture explicit:

  • New TransitionPolicy ADT (Open rank 0 / OwnersOrParticipants rank 1 / Owners rank 2) + FiberPolicy.Constrained.transitionPolicy: Option = None. Option/omit-safe, bare-string codec, collapses through isEmpty/constrained like every other dial, and joins the tighten-only lattice as a rankUp dial (a migration may only go Open → OwnersOrParticipants → Owners, never launder back down).
  • Enforced in the combiner (FiberCombiner.processFiberEvent, after the sequence check, before orchestrator.process) as a graceful CombineRejected — the authoritative-gate pattern registry/asset ops already use. Resolves verified signer addresses from update.proofs (mirrors the create path + updateSignedByOwnerOrParticipant).

⚠️ The absent-dial default is Open (deliberate, additive)

An absent transitionPolicy resolves to Open = today's live guard-only behavior, so every existing fiber is unchanged and apps opt up explicitly. This PR ships the mechanism only. Flipping the default to OwnersOrParticipants (restoring the declared gate) is a breaking, security-hardening decision deliberately reserved for maintainers (RFC §3.4 / §6 Q1) — it would break the "counterparty can sign" pattern MultiPartyTransitionSigningSuite encodes, so it must come with an engine-version bump + migration window + reconciling that suite. Not done here.

F6 — auto-declare static machines.$id read dependencies

StaticDependencyScan.staticMachineRefs walks a transition's guard/effect AST and returns every literally-referenced machines.<uuid> id; FiberEvaluator unions those into the runtime dependency set handed to ContextProvider.buildContext. Kills the silent-null trap (read a cross-fiber state without also hand-declaring the dep). A computed target id still needs an explicit dependencies entry / _addDependency — intentionally uncovered.

Invariant compliance (CLAUDE.md)

Tests

  • New TransitionPolicyEnforcementSuite (Open applies a non-owner; Owners rejects; OwnersOrParticipants accepts a participant, rejects a stranger) and AutoDependencyScanSuite.
  • PublishVersionSigningCanonicalSuite +2 cases.
  • Full sharedData/test: 576 passed, 0 failed. The three named existing suites stay green: TransitionOwnerGateDivergenceSuite 1/1 (default-Open still applies — the gap is now opt-in closeable), MultiPartyTransitionSigningSuite 6/6, PublishVersionSigningCanonicalSuite 5/5. scalafmtCheckAll clean.

🤖 Generated with Claude Code

Implements Proposal 03 F6 + F7 (additive, default-preserving).

F7 (combiner enforcement + opt-in dial):
- New `TransitionPolicy` ADT (Open < OwnersOrParticipants < Owners) in
  schema/fiber, modeled on UpgradePolicy's bare-string codec (total,
  fail-closed). Added as `Constrained.transitionPolicy: Option = None`,
  omit-safe, joined to the `tightens` lattice as a rankUp dial.
- Signer-authorization is now enforced in `FiberCombiner.processFiberEvent`
  (the authoritative apply path) as a graceful `CombineRejected`, before
  `orchestrator.process` — reads ONLY the fiber's own hash-pinned policy dial
  + the stable owners/authorizedSigners record fields (rule #2/#3 safe).
- ABSENT dial defaults to `Open` (today's live guard-only behaviour) so every
  existing fiber is UNCHANGED; apps opt UP explicitly. The default-tightening
  flip (§3.4/§6 Q1) is deliberately left to the maintainers.

F6 (auto-declare static machines.<uuid> read deps):
- `StaticDependencyScan.staticMachineRefs` walks a transition's guard/effect
  AST for literal `{"var":"machines.<uuid>..."}` paths and augments the
  RUNTIME dependency set handed to `ContextProvider.buildContext`. Never
  mutates the signed, hash-pinned `Transition.dependencies` (rule #1).

Tests: TransitionPolicyEnforcementSuite (default-Open applies, Owners rejects
non-owner, OwnersOrParticipants accepts participant/rejects stranger),
AutoDependencyScanSuite (pure scanner + end-to-end resolution),
PublishVersionSigningCanonicalSuite (+2 cases: absent dial byte-identical,
set dial round-trips). TransitionOwnerGateDivergenceSuite and
MultiPartyTransitionSigningSuite stay green. Full sharedData: 576/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
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.

1 participant