Skip to content

feat(TaskManager v4): editable budgets + folders + organizer hat#159

Merged
hudsonhrh merged 12 commits into
mainfrom
hudsonhrh/edit-project-budget
May 19, 2026
Merged

feat(TaskManager v4): editable budgets + folders + organizer hat#159
hudsonhrh merged 12 commits into
mainfrom
hudsonhrh/edit-project-budget

Conversation

@hudsonhrh

@hudsonhrh hudsonhrh commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

TaskManager v4 — a single upgrade shipping two features, plus the full rollout (cross-chain deploy script, paymaster retro-fix, OrgDeployer bootstrap update, governance grant scripts, on-chain audit, fork sims for every step).

Editable project budgets — new TaskPerm.BUDGET = 1 << 5 lets a configured hat-holder resize a project's PT cap + per-token bounty caps. Strict gating: no _isPM bypass; project managers need an explicit hat assignment. Implemented by moving setConfig's top-of-function _requireExecutor() into each branch and adding a _requireBudgetEditor(pid) helper for the budget keys. Other admin keys stay executor-only.

Project folders + organizer hatsetFolders(expectedRoot, newRoot) publishes an IPFS root for the org's folder tree, CAS-guarded against concurrent writes. Gated on executor OR any organizerHatIds wearer (configurable via setConfig(ORGANIZER_HAT_ALLOWED, ...)). New bytes32 foldersRoot + uint256[] organizerHatIds appended to Layout (storage-layout safe). Off-chain JSON schema fully spec'd at docs/TASK_MANAGER_FOLDERS.md.

Plus a small follow-up eventsetConfig(ROLE_PERM, ...) now emits RolePermSet(hatId, mask) so the subgraph can answer "which hats have BUDGET globally" from logs alone (closes #161).

What's in this PR

Contract changes

  • src/TaskManager.sol — BUDGET perm, folders, organizer hat, RolePermSet event, per-branch setConfig permissions
  • src/libs/TaskPerm.sol — new BUDGET bit
  • src/OrgDeployer.sol_appendTaskManagerRules includes setFolders so future orgs auto-whitelist
  • test/TaskManager.t.sol — 8 BUDGET tests + folder tests + cross-feature integration test
  • test/UpgradeSafety.t.sol — storage-preservation test
  • test/DeployerTest.t.sol — asserts setFolders is in default paymaster rules

Off-chain specdocs/TASK_MANAGER_FOLDERS.md: JSON schema (flat list with parentId pointers + schemaVersion), CID encoding (same as existing metadataHash convention), pinning policy, CAS retry semantics.

Scripts shipped

  • script/upgrades/UpgradeTaskManagerFolders.s.sol — v4 cross-chain upgrade (Step 1/2/3) + DryRun_GnosisUpgrade
  • script/upgrades/UpgradeOrgDeployerFolders.s.sol — v12 OrgDeployer upgrade + SimulateOrgDeployerFoldersUpgrade
  • script/upgrades/SimTaskManagerV4Integration.s.solSim_HatWearerIntegration (etches a Hats shim to prove hat-wearer paths land on real bytecode)
  • script/fixes/AddSetFoldersSelectorRules.s.sol — retro paymaster fix for KUBI/Test6/Poa + sims
  • script/audit/AuditTaskPermBit5.s.sol — pre-broadcast audit confirming no existing hat got BUDGET silently
  • script/fixes/GrantV4PermsViaGovernance.s.sol — full propose → vote → execute sim + per-org Broadcast variants (KUBI 4320 min, Test6/Poa 30 min)

Process tightening

  • CLAUDE.md — production-profile is now mandatory for sims (default-profile sims deploy different bytecode than broadcast)

Recommended hat grants per org

Each org's existing role tree, with the hat governance should grant TaskPerm.BUDGET + organizer status:

Org Chain Target hat
KUBI Gnosis Executive
Test6 Gnosis Executive
Poa Arbitrum CONTRIBUTOR

Verification status

  • forge build clean (default + production profiles)
  • forge fmt --check clean
  • forge test1332/1332 pass (full suite, no regressions)
  • FOUNDRY_PROFILE=production forge script …:DryRun_GnosisUpgrade --fork-url gnosis → ALL CHECKS PASSED
  • FOUNDRY_PROFILE=production forge script …:Sim_HatWearerIntegration --fork-url gnosis → ALL CHECKS PASSED
  • forge script …:AuditTaskPermBit5{Gnosis,ArbitrumDirect}0 collisions across 9 live orgs
  • FOUNDRY_PROFILE=production forge script …:SimAddSetFolders{Gnosis,Arbitrum} --fork-url … → both PASS
  • FOUNDRY_PROFILE=production forge script …:SimGrant{Kubi,Test6,Poa} --fork-url … → full propose→vote→execute pipeline PASS for all three
  • FOUNDRY_PROFILE=production forge script …:SimulateOrgDeployerFoldersUpgrade --fork-url arbitrum → PASS

On-chain rollout status (broadcast already in progress)

  • TaskManager v4 broadcast — live at 0xD1721E7Bb458C21485cbC7175A557C23Bb4be358 on Gnosis + Arbitrum
  • Paymaster setFolders rules — set for KUBI, Test6, Poa (getRule(...).allowed == true)
  • OrgDeployer v12 broadcast — live at 0xbC8510ca3F017FD889828242c77fb1f1b2FF88df on Gnosis (future orgs auto-whitelist setFolders)
  • Poa governance grant — proposal 0 passed + executed: CONTRIBUTOR has rolePermGlobal[hat] = 32 AND is in organizerHatIds
  • KUBI proposal 16 — created, pending announceWinner call after voting window closes
  • Test6 proposal 14 — created, pending announceWinner call after voting window closes
  • Subgraph PRpoa-box/subgraph-pop#177 ready to merge once this lands
  • Frontend PRpoa-box/Poa-frontend#402 ready to merge once this + subgraph land

Closed by this PR

🤖 Generated with Claude Code

hudsonhrh and others added 4 commits May 13, 2026 13:02
…v3 upgrade)

Lets a configured hat-holder — alongside the Executor (and therefore a passing
vote) — resize a project's PT cap and per-token bounty cap. Permission is
strict: project managers do not get implicit access, they need the hat too.

- TaskPerm.BUDGET = 1 << 5 (new bit; no storage changes).
- setConfig refactored from a single top-of-function _requireExecutor() into
  per-branch checks. PROJECT_CAP and BOUNTY_CAP use a new _requireBudgetEditor
  helper that skips the _isPM bypass; other admin keys (EXECUTOR,
  CREATOR_HAT_ALLOWED, ROLE_PERM, PROJECT_MANAGER) remain executor-only.
- Grant: setConfig(ROLE_PERM, abi.encode(hat, TaskPerm.BUDGET)) globally, or
  setProjectRolePerm(pid, hat, TaskPerm.BUDGET) per-project.
- Cross-chain v3 upgrade script (UpgradeTaskManagerEditableBudgets.s.sol)
  with DryRun_EditableBudgets sim that passes end-to-end on a Gnosis fork:
  pre-upgrade probe -> NotExecutor, post-upgrade probe -> Unauthorized,
  storage preserved, executor flow still works, other admin keys unchanged.
- 8 new unit tests + updated test_SetBountyCapPermissions to the new revert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an IPFS-rooted folder tree for projects, gated by a configurable
organizer hat. Folder structure (names, parents, ordering, project
assignments) lives off-chain in a JSON document; only the root hash
sits on-chain, swapped atomically via `setFolders(expectedRoot, newRoot)`
with CAS-guarded concurrency. Strict permissions: executor or organizer
hat only — creator hats deliberately do NOT inherit, to avoid silent
reparenting of the whole tree by widely-distributed creator roles.

Also: full NatSpec pass across TaskManager errors, events, and externals;
13 new folder tests + storage-preservation upgrade test (199 + 25 pass);
v4 upgrade script with `DryRun_GnosisUpgrade` exercised against a live
Gnosis fork. CLAUDE.md tightened to require Claude to run the sim (not
punt to the user) and to pick `VERSION` by querying `getVersionCount` +
`cast code` on every target chain instead of guessing.

Follow-up issue #158 tracks splitting TaskManager into shared-storage
libraries (mirror HybridVoting pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default-profile sims deploy different bytecode than what broadcast actually
sends (optimizer off, evm=osaka vs cancun). They are not real simulations.
Make the production-profile requirement explicit instead of leaving it
implicit in each script's docstring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s PR

Both PRs upgrade TaskManager; ship them as a single v4 impl. The auto-merge
combined cleanly except for two seams that needed manual review:

1. setConfig: PR #159 moved _requireExecutor() from the top of setConfig into
   each branch. PR #160's new ORGANIZER_HAT_ALLOWED branch was written under
   the old "gate at the top" assumption and so auto-merged WITHOUT an
   _requireExecutor() check — anyone could grant themselves the organizer
   hat. Added the missing check, plus a regression assertion in
   test_SetConfigOtherKeysStillExecutorOnly and a new check (5m) in the
   combined dry-run sim that locks down ORGANIZER_HAT_ALLOWED post-upgrade.

2. CLAUDE.md: kept both clarifications — production-profile-mandate
   rationale (#159) and ImplementationRegistry + CREATE3-slot VERSION
   probing (#160) — and merged the Stack-too-deep guidance so it's
   internally consistent with point 5.

Upgrade scripts consolidated to a single UpgradeTaskManagerFolders.s.sol
(v4). Its DryRun_GnosisUpgrade now also exercises the editable-budgets gate
(pre/post Unauthorized vs NotExecutor on a real project, executor
PROJECT_CAP / BOUNTY_CAP / ROLE_PERM regression checks, organizer-key
executor-only assertion). UpgradeTaskManagerEditableBudgets.s.sol deleted —
its checks now live inside the folders sim.

- forge build: clean (default profile)
- forge fmt: clean
- forge test: 1330/1330 pass
- FOUNDRY_PROFILE=production forge script ...:DryRun_GnosisUpgrade
  --fork-url gnosis: ALL CHECKS PASSED end-to-end on real Gnosis state

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hudsonhrh hudsonhrh changed the title feat(TaskManager): editable project budgets via TaskPerm.BUDGET hat feat(TaskManager): editable budgets + project folders + organizer hat (v4) May 13, 2026
Audit of the PR #159+#160 merge confirmed both features are intact, but neither
the unit tests nor the dry-run sim explicitly proved a single hat-wearer could
exercise the new hat-gated paths for BOTH features end-to-end on real upgraded
bytecode. Closing that gap:

- New unit test (`test_BudgetAndFolders_HatsAreIndependent`) creates a BUDGET
  hat-wearer and an organizer hat-wearer and proves they can each only do their
  own thing — BUDGET hat cannot setFolders (NotOrganizer), organizer hat cannot
  setConfig(PROJECT_CAP) (Unauthorized) — locking down the per-branch refactor
  against future cross-wiring.

- New fork sim (`SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration`)
  applies the v4 upgrade against a Gnosis org's live TaskManager, etches a
  controllable HatsShim over the org's real Hats address, grants a single
  test hat both TaskPerm.BUDGET and organizer status, and verifies on real
  upgraded bytecode that the hat-wearer can:
    - edit PROJECT_CAP
    - edit BOUNTY_CAP
    - setFolders (with CAS guard)
    - chain a follow-up setFolders
  …while still being rejected (NotExecutor) on all five admin keys, and a
  separate non-hat EOA is rejected with the correct Unauthorized / NotOrganizer
  reverts.

Verified:
  - forge build clean (default + production profiles)
  - forge fmt clean
  - forge test --match-contract "TaskManager|UpgradeSafety" -> 233/233 pass
  - FOUNDRY_PROFILE=production forge script
    script/upgrades/SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration
    --fork-url gnosis -> ALL HAT-WEARER INTEGRATION CHECKS PASSED

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hudsonhrh and others added 2 commits May 13, 2026 14:49
…try)

Resolves the open question from PR #159 / issue #162: what is the off-chain
JSON shape, who pins it, how do CIDs round-trip on/off chain, and how do
clients handle the FoldersRootStale revert from concurrent edits.

Decisions baked in:
- Flat list of folder records with parentId pointers (not nested tree). Makes
  drag-drop edits a single-row delta and keeps CAS rebase tractable.
- schemaVersion field with strict forward-incompat rule (newer = refuse to
  render) to keep evolution safe.
- CID encoding identical to existing metadataHash convention (raw sha256
  digest from CIDv0); no new encoding rules.
- foldersRoot == bytes32(0) is reserved as "uninitialized / cleared", treated
  as the empty tree without IPFS resolution.
- Pinning expectation: platform-managed via Poa-frontend/Poa-site backend.
  Contracts make no provision for hosting; clients coordinate off-chain —
  same convention already implicit in every other IPFS-anchored hash in the
  protocol.

The spec is the authoritative reference for frontend, subgraph, and backend
implementations. Unblocks Poa-frontend#399.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…161

setConfig(ROLE_PERM, abi.encode(hatId, mask)) was the only path for granting
TaskPerm.BUDGET globally that emitted no mask-carrying event. HatToggled fires
from _syncPermissionHat, but that only signals "hat is tracked in
permissionHatIds" — not what bits are set. This left the subgraph (and any
indexer) unable to answer "which hats have BUDGET globally" without per-hat
RPC fan-out.

New event mirrors ProjectRolePermSet's shape minus the project id:

  event RolePermSet(uint256 indexed hatId, uint8 mask);

Emitted on every ROLE_PERM update including mask=0 revokes, so indexers see
clears without having to diff state.

Verified:
- forge build clean (default + production profiles)
- forge fmt clean
- forge test --match-contract "TaskManager|UpgradeSafety" -> 234/234 pass
- FOUNDRY_PROFILE=production forge script
  script/upgrades/UpgradeTaskManagerFolders.s.sol:DryRun_GnosisUpgrade
  --fork-url gnosis -> ALL DRY-RUN CHECKS PASSED
- FOUNDRY_PROFILE=production forge script
  script/upgrades/SimTaskManagerV4Integration.s.sol:Sim_HatWearerIntegration
  --fork-url gnosis -> ALL HAT-WEARER INTEGRATION CHECKS PASSED

Unblocks subgraph indexing (poa-box/subgraph-pop#176).

Closes #161

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hudsonhrh and others added 5 commits May 19, 2026 12:04
Three pre-broadcast considerations for v4 that were tracked in conversation
but never landed in-tree:

1. **TaskPerm bit-5 collision audit** (script/audit/AuditTaskPermBit5.s.sol).
   v4 introduces TaskPerm.BUDGET = 1 << 5. Pre-v4, bit 5 was unused, but
   setConfig(ROLE_PERM, ...) accepts uint8 verbatim — any hat historically
   granted a mask with bit 5 set silently gains BUDGET post-upgrade. Script
   reads rolePermGlobal storage via vm.load on ERC-7201 slot (no public
   getter exists). Documented subgraph query covers the per-project surface
   (ProjectRolePermSet has been indexed since deploy).

   Verified against live state: 8 Gnosis orgs (Test, Test6, KUBI, ...) + Poa
   on Arbitrum + per-project subgraph query — all clean. Zero pre-existing
   bit-5 grants. Safe to broadcast.

2. **Retroactive paymaster fix** (script/fixes/AddSetFoldersSelectorRules.s.sol).
   Mirrors AddCreateTasksBatchSelectorRules.s.sol exactly. Adds setFolders
   selector (0x0c1b690e) to KUBI/Test6 (Gnosis) and Poa (Arbitrum) paymaster
   rules so organizer-hat wearers can publish folder updates gaslessly via
   4337/passkey instead of bringing their own ETH.

   Both sims pass under FOUNDRY_PROFILE=production against real forks:
   Arbitrum/Poa: rule false -> true; Gnosis/KUBI + Test6: rule false -> true.
   Ready to broadcast once v4 lands.

3. **OrgDeployer bootstrap wiring** (src/OrgDeployer.sol). _appendTaskManagerRules
   now includes setFolders in the default paymaster whitelist for newly
   deployed orgs. Bumps the count comment from TaskManager(13) to
   TaskManager(14) and total from 40 to 41. Without this, every future org
   would need the same retroactive fix as KUBI/Poa/Test6.

   Verified: forge test full suite 1332/1332 pass (incl. DeployerTest which
   exercises the full org deployment + paymaster bootstrap path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Option B from the post-v4 rollout plan. For each live org, a real proposal
that grants the new TaskManager v4 powers (TaskPerm.BUDGET + organizer
slot) to the recommended role:

  KUBI  (Gnosis)   - Executive hat
  Test6 (Gnosis)   - Executive hat
  Poa   (Arbitrum) - CONTRIBUTOR hat

This is sim-only — the production rollout is a member of each org
submitting the same proposal via the Poa-frontend votes page. What this
file proves is that the proposal payload is correct and the executor
actually lands both setConfig calls when the proposal passes.

Sim covers the FULL governance pipeline:
  1. Apply v4 to the PoaManager beacon.
  2. Etch a PermissiveHatsShim over the org's Hats Protocol address so a
     test voter passes both onlyCreator (createProposal) and class-hat
     (vote) checks without minting real hats.
  3. Build the IExecutor.Call[] batch with both setConfig calls.
  4. createProposal (10-min duration, single option).
  5. Vote 100% for option 0.
  6. vm.warp past endTimestamp.
  7. announceWinner -> executor.execute fires inside.
  8. Verify post-state:
     - rolePermGlobal[hat] has TaskPerm.BUDGET (bit 5) set
     - organizerHatIds contains the target hat

All three sims PASS end-to-end under FOUNDRY_PROFILE=production against
real production state:

  KUBI:  v4 swap OK; proposal id 16; mask 0->32; orgs 0->1; valid=true
  Test6: v4 swap OK; proposal id 14; mask 0->32; orgs 0->1; valid=true
  Poa:   v4 swap OK; proposal id 0;  mask 0->32; orgs 0->1; valid=true

(Proposal ids reflect each org's live history on the fork. KUBI is the
most active; Poa has never voted yet on Arbitrum.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing Sim* contracts proved the full propose -> vote -> execute
pipeline works end-to-end on a fork. These new Broadcast* contracts
actually create the proposal on-chain — useful right now since the
Poa-frontend organizer-admin UI (PR #402) isn't merged yet, so submitting
the proposal from the UI is friction-prone.

Each Broadcast contract:
  1. Reads PRIVATE_KEY / DEPLOYER_PRIVATE_KEY from env.
  2. Sanity-checks the sender wears at least one creator hat on the org's
     HybridVoting (otherwise createProposal would revert NotCreator and
     burn gas).
  3. Builds the same two-call batch the sim validated:
       - setConfig(ROLE_PERM, abi.encode(targetHat, TaskPerm.BUDGET))
       - setConfig(ORGANIZER_HAT_ALLOWED, abi.encode(targetHat, true))
  4. Calls HybridVoting.createProposal with a 3-day (4320 minute) duration
     and a single option (unrestricted poll).
  5. Logs the new proposal ID.

Verified Hudson's wallet (the broadcaster) wears creator hats on all
three orgs:
  KUBI  -> Executive + Member (both are creator hats)
  Test6 -> Executive + Member (both are creator hats)
  Poa   -> CONTRIBUTOR (also the creator hat)

So the sanity-check guard will pass on every chain.

Note: voting + announceWinner remain manual — members vote per their
normal cadence; after the 3-day window expires, anyone calls
announceWinner(id) and the executor lands both setConfig calls atomically.
The Sim* contracts already proved that announceWinner -> executor.execute
pipeline works against real production state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KUBI already broadcast at 4320 min (3 days). Test6 + Poa drop to 30 min
since they're test/governance orgs and don't need a 3-day window.

Constants split out near the bottom of the file so future tuning is a
one-liner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to UpgradeTaskManagerFolders (TaskManager v4 adds setFolders).
Without this, every new org would need the same retroactive
AddSetFoldersSelectorRules fix applied to KUBI/Test6/Poa — instead, this
upgrade rewires the OrgDeployer bootstrap so future orgs auto-whitelist
the setFolders selector on first deploy.

  Live impl: v11 (createTasksBatch bootstrap)
  This PR:   v12 (setFolders bootstrap, in addition to createTasksBatch)

Version selection: v9/v10/v11 taken on Gnosis (registered impls). v12 is
FREE on both Gnosis and Arbitrum CREATE3 slots + ImplementationRegistry.
Verified via the CLAUDE.md probing recipe before writing this file.

Three-step cross-chain pattern (same as UpgradeOrgDeployerCreateTasksBatch):
  Step1_DeployImplOnGnosis        — DD deploy on Gnosis
  Step2_UpgradeFromArbitrum       — DD deploy on Arb + upgradeBeaconCrossChain
  Step3_Verify                    — read Gnosis impl after ~5 min relay

Verification approach changed mid-implementation: an initial bytecode-
pattern scan for the setFolders selector (0x0c1b690e) produced false
negatives because Solidity pre-computes / inlines constants in ways that
break a literal 4-byte search. Replaced with a real behavior assertion in
test/DeployerTest.t.sol:testDeployFullOrgWithPaymasterAutoWhitelist —
deploys a fresh org through the new OrgDeployer and asserts
paymasterHub.getRule(orgId, taskManager, setFolders).allowed.

Verified:
  - forge build clean (production profile)
  - forge test --match-test testDeployFullOrgWithPaymaster -> 3/3 pass
  - FOUNDRY_PROFILE=production forge script ...:SimulateOrgDeployerFoldersUpgrade
    --fork-url arbitrum -> "Arbitrum upgrade simulation: PASS"
    DD deploys v12 at 0xbC8510ca3F017FD889828242c77fb1f1b2FF88df (19093
    bytes) and upgradeBeaconCrossChain dispatches successfully.

Broadcast sequence (after TaskManager v4 lands):
  source .env && FOUNDRY_PROFILE=production forge script \
    script/upgrades/UpgradeOrgDeployerFolders.s.sol:Step1_DeployImplOnGnosis \
    --rpc-url gnosis --broadcast --slow

  source .env && FOUNDRY_PROFILE=production forge script \
    script/upgrades/UpgradeOrgDeployerFolders.s.sol:Step2_UpgradeFromArbitrum \
    --rpc-url arbitrum --broadcast --slow

  # wait ~5 min for Hyperlane relay, then:
  forge script script/upgrades/UpgradeOrgDeployerFolders.s.sol:Step3_Verify \
    --rpc-url gnosis

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hudsonhrh hudsonhrh changed the title feat(TaskManager): editable budgets + project folders + organizer hat (v4) feat(TaskManager v4): editable budgets + folders + organizer hat May 19, 2026
@hudsonhrh hudsonhrh merged commit 1c732c6 into main May 19, 2026
hudsonhrh added a commit that referenced this pull request Jun 1, 2026
… wins

Reconciles main's TaskManager v4/v5 + EligibilityModule lockdown (#159, #167,
#169) with the capability-hat refactor. Per direction, the capability-hat model
is canonical and main's new features were re-expressed in it.

Resolution highlights:
- TaskManager Layout: kept main's foldersRoot/organizerHatIds in their on-chain
  slots (live-org upgrade safety), appended cap-hat gate fields after, plus new
  budgetHat/editMetaHat/editFullHat. Append-only vs deployed v5 layout.
- v4 editable budgets: BOUNTY_CAP/PROJECT_CAP now gated by _requireBudgetEditor
  -> BUDGET capability hat (executor OR cap-hat), not the bitmask.
- v5 post-claim edits: updateTask (EDIT_FULL) + updateTaskMetadata (EDIT_META)
  gate via _hasCap, not _permMask.
- Folders kept verbatim (orthogonal; organizerHatIds stays a HatManager array gate).
- bootstrapGlobalPerms re-expressed as a deployer-time bulk global gate-hat setter
  (expands each mask into per-gate cap-hat assignments); deleted the dead bitmask
  helpers (_permMask, _syncPermissionHat, refcount machinery).
- setConfig(ROLE_PERM)/setProjectRolePerm extended to BUDGET/EDIT_META/EDIT_FULL,
  kept strict single-bit (InvalidCapMask).
- EligibilityModule: kept #167 superAdmin-only lockdown; the sole exception is
  setWearerEligibility via onlySuperAdminOrRevoker (RoleBundleHatter cascade).
- OrgDeployer: DeploymentParams keeps both capabilityHats/roleBundles and
  taskManagerPerms (complementary). Re-added _resolveRoleIndicesToHatIds.
- Added lens variant 12 ([budgetHat, editMetaHat, editFullHat]) for the new gates.

Security fix surfaced by the merge (pre-existing on the branch; DeployerTest was
previously excluded from verification): RoleBundleHatter.mintRole's pre-mint
eligibility reset unconditionally set wearers eligible, bypassing the vouching
gate (defaults.eligible=false) on self-service QuickJoin. Now gated on
hasSpecificWearerRules so it only re-enables previously-revoked wearers (preserves
re-mint-after-revoke) and never overrides a default-driven gate for fresh wearers.

Tests: 1423 passing / 0 failing / 16 skipped (obsolete bitmask tests). Main-era
bitmask tests adapted to capability-hat behavioral assertions. forge build/fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant