CreateTreasury — Treasury entity_id Trust Semantics
Status: draft — design / audit (trust-semantics definition, not implementation)
Truth class: descriptive
Canonical: no — current implementation truth lives in docs/STATE.md and docs/PHASE_PROGRESS.md
Last Reviewed: 2026-07-01
Source basis: read against main @ d37d7d43 (#2267). Code anchors/line numbers were verified at that commit — re-verify before relying on exact numbers.
Related: coop-id-entity-resolver.md (resolver seam) · ADR-0084 (controlled apply contract) · ADR-0035 · RFC-0018 · issues #2082 (this lane), #2081 (blocked, untouched), #2080 (separate, untouched)
This document defines trust semantics for a path that must not be casually mutated. It answers what the
CreateTreasurymessage path is, what authority it carries today, why it must not populate treasuryentity_idby projection, and what single future implementation slice would be safe. It changes no code behavior. Acoop_id ↔ EntityIdmapping binds an identity target only — it grants zero membership, role, capability, mandate, standing, or permission.
1. Current behavior (verified at d37d7d43)
CreateTreasury exists twice, as near-identical duplicates on two parallel actor paths:
| Path | Message | Handler |
|---|---|---|
icn/crates/icn-coop/src/actor.rs |
CoopMessage::CreateTreasury (~L226, dispatch ~L409) |
handle_create_treasury (~L732) |
icn/apps/membership/src/coop_core/actor.rs |
CoopMessage::CreateTreasury (~L122, dispatch ~L288) |
handle_create_treasury (~L484) |
Both handlers do exactly this:
- Verify the cooperative exists in the store (
get_cooperative). - Reject if
coop.treasury_didis already set ("already has a treasury"). - Derive the treasury DID deterministically from the flat
coop_id(derive_treasury_did/derive_treasury_anchor). - Assign the treasury to the cooperative and, if a
TreasuryManageris wired, register the treasury in the ledger via the plainregister_treasury(...)— i.e.entity_id: None, with a default settlement unit and the first listed member ascreated_by. - Save and announce.
What the handlers do not do:
- No
CoopEntityMapinteraction of any kind — no read, no bind, no provenance. - No lifecycle-state gate — only "exists" and "no treasury yet"; a merely proposed (non-activated) cooperative qualifies.
- No authority gate at the actor layer — the caller is whatever holds the actor
handle. There is no production caller today: no gateway route, RPC method, or
binary invokes
create_treasury(verified by repo-wide search atd37d7d43); it is reachable only through the actor handle API and is exercised by tests. - No overlap with activation: since #2266, cooperative activation creates the
treasury itself (register
entity_id: None→ commit activation → record theActivation-provenance map binding last → populate the treasuryentity_idfrom that recorded binding; when no map is wired, activation instead uses the pure reject-not-normalize projection —icn-coop/src/actor.rs~L587-591 — because there is no map to disagree with). A coop that went through activation therefore already has a treasury, andCreateTreasuryis rejected for it (tests pin this on both paths).CreateTreasuryconsequently fires only for cooperatives that have not been activated — exactly the population for which no trusted binding exists.
1.1 Which institutional act is this?
None, currently. Activation is the institutional act that owns treasury creation and
the only act that may record Activation provenance ("only the node that
authoritatively activated may write Activation" — the trust assertion documented at
icn-coop/src/actor.rs ~L79). CreateTreasury as wired today is library/test
scaffolding: a handle-level surface with no authority basis, no governance receipt,
and no accountable origin for any identity claim it might make. That is precisely why
it must stay entity_id: None until this document's semantics are implemented.
1.2 The sibling gap (out of scope here, recorded for #2082)
apps/membership coop_core has no CoopEntityMap integration at all — its
activation path does not record an Activation binding either (verified: zero
matches for CoopEntityMap / bind_coop_entity_map / register_treasury_with_entity
under icn/apps/membership/ at d37d7d43). That is #2082 gap 12b, a separate
activation-parity rung; this document only notes that any CreateTreasury slice must
either land on both actor copies or explicitly sequence them.
2. Problem
A treasury row's entity_id is authorization-relevant under the (off-by-default)
enforce-trusted-resolver mode: it is the membership target the entity-auth gate
verifies against. Populating it from an unaccountable source would let an unwired,
authority-free scaffolding path mint the identity target that a future enforcement
mode trusts. Two failure shapes matter:
- Projection without provenance.
project_coop_idis pure and reject-not- normalize, but a projection performed byCreateTreasurycarries no accountable origin. #2266's review (P2#1) established the rule for the map-wired activation path: treasuryentity_idmay be populated only from the successfully recorded binding, never from a bare projection racing ahead of the bind. (The no-map activation path does populate from pure projection —icn-coop/src/actor.rs~L587-591 — which is tolerable there only because activation is the authoritative institutional act and there is no map for the projection to disagree with.)CreateTreasuryis not that act and owns no binding, so this document deliberately holds it to the stricter rule: no projection fallback at all — see §5. - Silent trust creation. If
CreateTreasurywrote a map binding itself, that binding's provenance would be a lie — the path is not activation, not an operator backfill, not a surrogate allocation, and holds no governance receipt. Every trustedCoopEntityBindingProvenancevariant records a concrete accountable origin this path does not have.
3. Non-goals
- No change to
CreateTreasurybehavior in this document or its PR. - No treasury mutation, no map mutation, no provenance write.
- No new authority gate design for treasury creation itself (who may create a treasury is an institutional-act question for a future governance slice; this document only pins the identity-target semantics).
- No gateway enforcement, route-default, or token-issuance change (#2081 and #2080 remain untouched and blocked/separate respectively).
- No #2082 closure.
4. Trust sources available / not available to this path
| Source | Available? | Why / why not |
|---|---|---|
Activation provenance |
No | CreateTreasury is not activation and must never impersonate it. |
OperatorBackfill provenance |
No | It is not an operator-run backfill command. |
Surrogate provenance |
No | It performs no surrogate allocation. |
GovernanceReceipt provenance |
No (today) | No governance decision flows into this path. |
| Reading an existing trusted binding | Yes | binding_for_coop + is_trusted_for_resolution() + reverse-index agreement + cooperative well-formedness — the same fail-closed discipline the ADR-0084 planner (WouldPopulate) and #2266 activation-populate already use. |
The only legitimate trust relationship this path can ever have with the map is read-only consumption of a binding somebody accountable already recorded.
5. Options considered
- Reject projectable
coop_ids without a trusted binding. Rejected: couples treasury existence to mapping state, which is authority-adjacent creep — the mapping would start gating what an institution can do, violating zero-authority. Also changes behavior on a surface with no production caller, for no safety gain (entity_idstaysNoneeither way). - Require an existing trusted map binding as a creation precondition. Rejected for the same coupling reason: a legacy cooperative without a binding could no longer receive a treasury at all.
- Create a pending/untrusted
entity_idtarget. Rejected: violates the fail-closed doctrine. ADR-0084 §3 forbids trustingUnknownLegacy; a "provisional" treasuryentity_idis indistinguishable from a trusted one to every consumer that reads the row. There is no such thing as a safely-untrusted populated target. - Remain
entity_id: None; let the evidence-bearing backfill path handle it. Current state, and the correct default. The ADR-0084 planner/report/apply chain already covers rows created this way, fail-closed.
Recommended future slice (the only mutation later allowed): option 4 plus
read-only trusted-binding consultation — at CreateTreasury time, if (and only
if) a trusted, structurally consistent binding already exists for the byte-exact
coop_id (trusted provenance via is_trusted_for_resolution(), reverse index
agrees byte-for-byte, target is a well-formed cooperative EntityId), populate the
treasury's entity_id from that binding; otherwise entity_id: None exactly as
today. The map is never written, no provenance is recorded, and a
missing/untrusted/ambiguous/malformed binding degrades silently to today's
behavior. This mirrors #2266's populate-from-recorded-binding and ADR-0084's
re-verification discipline, and leaves rows without bindings to the existing
backfill chain.
Registration path constraint (coop_id preservation). The implementation MUST
NOT use TreasuryManager::register_treasury_with_entity for this: that API derives
the treasury row's coop_id from entity_id.identifier()
(icn-ledger/src/treasury.rs ~L400-408), so for a trusted surrogate (or any
binding where entity_id.identifier() != coop_id) it would file the treasury under
the surrogate slug instead of the original legacy coop_id — breaking byte-for-byte
preservation and get_treasury_by_coop/reverse-audit consistency. The slice must
instead use the two-step path activation already uses: plain
register_treasury(...) with the byte-exact original coop_id, then populate
entity_id from the trusted binding through the existing fail-closed populate seam
(the populate_entity_id_at_activation-class primitive: byte-for-byte coop_id
re-check + entity-uniqueness guard), or an equivalent new coop_id-preserving
registration helper with the same checks.
Note the deliberate divergence from activation: the slice has no no-map projection
fallback. Activation may project when no map is wired because it is the
authoritative institutional act; CreateTreasury is not, so with no map — or no
trusted binding — the result is entity_id: None, full stop (required test 5).
6. Required tests before any implementation PR may mutate entity_id
An implementation PR is acceptable only with all of the following, on both actor copies (or with the membership copy explicitly sequenced as a follow-up in the same lane):
- Trusted binding (each of
Activation,OperatorBackfill,Surrogate,GovernanceReceipt) → treasury registered with that binding'sEntityId;coop_idpreserved byte-for-byte. Must include a surrogate case whereentity_id.identifier() != coop_id: the treasury row keeps the original legacycoop_id(andget_treasury_by_coop(legacy_id)still resolves), withentity_idset to the surrogate — proving the registration path iscoop_id-preserving (see §5's registration-path constraint). UnknownLegacy/ missing provenance →entity_id: None(fail-closed, no error).- Reverse-index mismatch or one-sided binding →
entity_id: None. - Non-cooperative or malformed target →
entity_id: None. - No map configured →
entity_id: None(no store created). - No map write in any branch (a write-through test double must prove no
bind_*call occurs). - Post-activation
CreateTreasuryrejection unchanged. - Entity-id uniqueness conflict (binding's
EntityIdalready owned by another treasury) → fail-closed error, no partial write (the #2265EntityIdConflictdiscipline).
7. Explicit non-claims
This document and its PR claim none of the following: any code behavior change;
any treasury, map, or provenance mutation; any enforcement or default route-outcome
change; any positive entity_id/entity_type token issuance; #2081 progress or
readiness; #2080 progress; #2082 closure; mapping-as-authority (a binding never
grants membership, role, capability, mandate, standing, or permission); trust of
UnknownLegacy (it remains untrusted unless a future evidence-bearing workflow
repairs it); production, pilot, member, organizer, or live-federation readiness;
Phase 2 completion.
8. Validation
- Frontmatter +
docs/registry.tomlentry +docs/INDEX.mdlink added per docs-control-map — Adding a new doc. python3 docs/scripts/doc_control_check.pyrun locally before commit.- Vocabulary: settlement/allocation language only; no prohibited terms.