Raised by the Codex gate on #63 (review), dismissed for that PR after deliberation, filed here as the real follow-up.
The limitation
TrustTBClient.createUserWallet() accepts ordinary wallet ids containing a single colon (acct:123), but every budget entry point routes through costCenterUserId(), whose PARENT_USER_ID_PATTERN is /^[a-zA-Z0-9._@-]{1,128}$/ and excludes :. A tenant that keys wallets with colon-delimited ids therefore cannot call allocateBudget, reclaimBudget or getBudgetStatus against a wallet that is otherwise valid and funded.
Why it was not simply widened
The exclusion is load-bearing for the current encoding, not an oversight. The cost-center id is built by concatenation as parent + "::" + costCenter, and createUserWallet(id, { derived: true }) validates it by splitting on :: and rejecting any part that itself contains :. Widening PARENT_USER_ID_PATTERN alone would produce ids that the wallet validator rejects on the other side, and a parent ending in : would derive acct:::x, which split("::") reads as ["acct", ":x"] — exactly the ambiguity the reservation exists to prevent.
Codex agreed on deliberation: "simply widening PARENT_USER_ID_PATTERN would produce IDs that the wallet validator rejects... That is a product limitation, not a safe regex fix for this PR."
The actual fix
Derive the cost-center account id from a hash of the (parentUserId, costCenter) tuple rather than from a concatenated string — e.g. domain-separated and length-prefixed, so no parent/cost-center pair can produce another pair's id regardless of charset. That removes the need for a reserved separator entirely and lifts the : restriction as a side effect.
Worth doing sooner rather than later: the budget feature is new and unreleased, so no derived account ids exist in the wild yet. Once cost centers have been allocated against the string derivation, changing it becomes a migration.
Raised by the Codex gate on #63 (review), dismissed for that PR after deliberation, filed here as the real follow-up.
The limitation
TrustTBClient.createUserWallet()accepts ordinary wallet ids containing a single colon (acct:123), but every budget entry point routes throughcostCenterUserId(), whosePARENT_USER_ID_PATTERNis/^[a-zA-Z0-9._@-]{1,128}$/and excludes:. A tenant that keys wallets with colon-delimited ids therefore cannot callallocateBudget,reclaimBudgetorgetBudgetStatusagainst a wallet that is otherwise valid and funded.Why it was not simply widened
The exclusion is load-bearing for the current encoding, not an oversight. The cost-center id is built by concatenation as
parent + "::" + costCenter, andcreateUserWallet(id, { derived: true })validates it by splitting on::and rejecting any part that itself contains:. WideningPARENT_USER_ID_PATTERNalone would produce ids that the wallet validator rejects on the other side, and a parent ending in:would deriveacct:::x, whichsplit("::")reads as["acct", ":x"]— exactly the ambiguity the reservation exists to prevent.Codex agreed on deliberation: "simply widening
PARENT_USER_ID_PATTERNwould produce IDs that the wallet validator rejects... That is a product limitation, not a safe regex fix for this PR."The actual fix
Derive the cost-center account id from a hash of the
(parentUserId, costCenter)tuple rather than from a concatenated string — e.g. domain-separated and length-prefixed, so no parent/cost-center pair can produce another pair's id regardless of charset. That removes the need for a reserved separator entirely and lifts the:restriction as a side effect.Worth doing sooner rather than later: the budget feature is new and unreleased, so no derived account ids exist in the wild yet. Once cost centers have been allocated against the string derivation, changing it becomes a migration.