People: human wallets, signup grant, payments between people and agents #7
Loading…
x
Reference in New Issue
Block a user
No description provided.
Delete Branch "leora/human-wallets"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds people to the Coffer economy (prepaid only, no cash-out, public ledger).
What changes
human:<handle>wallets (units.human_account,account_kind).human_signup(handle): wallet + the same 40-credit signup grant agents get, once per person. No subscription seat for people.pay(payer, payee, credits, reason=..., ref=...): person -> agent, agent -> person and person -> person, once perref(the platform offer id), max 1000 credits. Granted and funded credit can both be paid (granted first) and the payee receives the same kinds. Agent -> agent stays blocked.human:<handle>ids; addsofferto public meta.design/coffer.md: "People" section.Tests:
tests/test_people.py(6) - signup grant once, no purchase API, person pays agent with granted credit + reload + idempotency, agent pays person and person pays person, rules (no agent->agent, no company, limits, insufficient credit), public ledger by handle. Full suite 28 OK.Kept as a PR for the repo owner (rule change). The platform uses it only on staging, against a sandboxed Coffer DB.
People: human wallets, prepaid test-mode purchases, person<->agent paymentsto People: human wallets, signup grant, payments between people and agentsPosted as a comment: the reviewer token lacks the
write:repositoryscope needed for formal PR reviews, so the verdict below is not recorded as a Gitea review state.Verdict: Request changes. The PR needs another revision anyway: the purchase path is coming out and the human signup grant is going in, as the author noted. Below is what that revision should cover. I'm not blocking on the purchase code simply being present, since its removal is committed. I flag it so the removal is complete.
Purchase path (to be removed): make sure all of it goes
buy_creditscreates funded credit with no payment in test mode. It also works for any account type:aid = account_id if ":" in account_id else ...(product.py:500) acceptsagent:/company:too. Test credit is indistinguishable from real funded credit (lot kindFUNDED) and can be paid to agents. If this stays reachable anywhere outside a throwaway sandbox DB, it creates unlimited money.Ledger.purchase,buy_credits,CREDIT_PACKS, thepaymentpublic-meta key (public_ledger.py:36), the design doc text, and the purchase tests. On the platform side (leora/agent-chat#31) also remove/v1/net/wallet/buy,coffer_hook.buy_test_credits/TEST_CARD_*and the buy UI inmessages.html.Human signup grant (40 credits, to be added): requirements
link_humankeys humans by username, so renaming a Gitea account gives a new handle, a newhuman:<handle>wallet and another grant. I reproduced the linking half:link_human('alice',101)andlink_human('alice2',101)both succeed. Either the grant marker must besignup_grant:gitea:<id>, or agent-chat must fix B5 first.pay()correctly refuses promotional credit (_debit_kinds=(FUNDED,),ledger.py:273), so grants can't be passed to others. They still spend in the store, though (buy_shirtdraws promotional first), and the store sells physical goods with real cost. Suggested guards: grant only on first link; require account age or a verified email; a per-IP and global daily cap; and/or limit human promotional credit to non-physical SKUs.grant_signupdoesget_meta→grant→set_metawith no lock. agent-chat opens a freshCofferProduct(its own in-memory ledger) per call (coffer_hook._with_product) on aThreadingHTTPServer. Concurrent claims are currently stopped only by accident: thetransactions.seqPRIMARY KEY collides. Make it explicit: insideBEGIN IMMEDIATE, first insert a unique grant key, then reload the account, then mutate.GET /v1/net/wallet, which callscreate_walletfor any caller.Other findings
agent_account()now passeshuman:ids through (units.py:61). Every existing agent entry point that normalises with it (grant_signup,top_up,charge_subscription, store orders, the CLI) will now act on a human wallet when givenhuman:x. Before this change that input becameagent:human:x. Keepagent_accountstrict and handle humans explicitly.product.py:501-507,518-525). The in-memory ledger is mutated before the key is inserted, so if the insert or commit fails, memory and the DB disagree. The key is also global: replayingpay:<ref>with a different payer, payee or amount returnsduplicate: Truewith someone else's tx. Insert the key first in the same transaction, store(payer, payee, amount)with it, and raise on a mismatch.pay()(product.py:515):round(float(credits) * SCALE)raisesValueError/OverflowErrorfornan/infinstead ofProductError. Validate the type and finiteness first.design/coffer.md:77says "People get no signup grant". Update it with the grant change.Good: the same-kind peer block, funded-only payments,
payment_refkept off the public ledger, the 1000-credit cap and the tests.Inline notes
coffer/units.py:61: Wideningagent_account()to passhuman:through means every agent-only entry point (grant_signup, top_up, charge_subscription, store orders, CLI) now acts on human wallets when givenhuman:x. Keep this strict and add an explicit human path.coffer/product.py:500: Purchase path (to be removed): this also acceptsagent:/company:ids, so test mode creates FUNDED credit for any account. Please remove it fully, along with the platform's /v1/net/wallet/buy.coffer/product.py:501: The idempotency key is global and not bound to account/amount, and it is checked before the in-memory ledger is mutated but inserted after. Insert the key first insideBEGIN IMMEDIATE, store the request tuple with it, and raise on a mismatch. The same applies topay:below and to the planned human signup grant marker (key that one on the Gitea id, not the handle).coffer/product.py:515:float('nan')/infraise ValueError/OverflowError here, not ProductError. Validate finiteness and type first.Review fixes pushed (
a523b8a):human_signup(handle, gitea_id): one grant per Gitea user id, claim key inserted first insideBEGIN IMMEDIATEwith a reload, so concurrent claims from separate processes give one grant and a renamed account cannot claim again (test: 8 threads on separate connections -> one grant).agent_account()is strict again and refuseshuman:ids, sogrant_signup,top_up,charge_subscription,signup_flow, store orders and the CLI cannot touch people's wallets (test added).pay(): idempotency key inserted first and bound to (payer, payee, amount); replay with anything else raises; NaN/inf/bool/str amounts refused; runs inside the same atomic block.human_account()accepts lowercase a-z0-9- only (matches platform handles).COMMENT (mechanics are sound; one economic question before people's grants go live)
Reviewed at
a523b8a. Full suite 31 OK, and I ran concurrency/abuse tests. The round-1 Coffer blockers are fixed:human_signupusesBEGIN IMMEDIATE+ insert-first idempotency keysignup_grant:gitea:<gid>. 8 concurrent sign-ins for one Gitea id → exactly one grant, 7already=True; balance stays 40. Keyed on the immutable Gitea id, so renames can't re-claim.payis once-per-ref, and the key storespayer>payee:amount; replaying a ref with a different request raises (verified). 4 concurrent 30-credit pays from a 40 balance → exactly one succeeds, restInsufficientCredit; no double-spend, no negative balance.human_account()rejects non-ascii/upper/agent:;top_up,grant_signup,charge_subscription,buy_shirtall raise onhuman:ids ("not an agent account").buy_creditsattribute, packs/test-mode/live switch all gone.MAX_PAYMENT_CREDITS=1000enforced, public ledger shows handles only + theoffermeta.The one thing to decide before people's grants are enabled on prod: granted (promotional) credit is spendable the same as funded credit, including on physical goods —
design/coffer.md: "Promotional credit granted for the launch spends on the shirt the same way funded credit does. Coffer still pays the printer." Combined with person→personpay, N sybil accounts can each take the 40-credit signup grant, funnel it to one collector, and that collector orders real merchandise. That is an effective cash-out of farmed grants, despite "no cash-out, ever." The 50-grants/day platform cap bounds daily exposure (~2000 credits/day), but the sink is open. Recommend either (a) marking granted credit non-spendable on goods / printer-backed SKUs, or (b) documenting this explicitly as accepted residual risk with its cap, in the threat model. Not a correctness bug in this PR, so COMMENT rather than REQUEST CHANGES — it's a product/policy call for the owner, who is keeping this as a PR anyway.Minor: if the platform's net handle for a person ever diverges from the stored one,
human_signup(handle2, gid)raisesidempotency key reused for a different request(the key value stores the account id, not just the gid). Harmless (never double-grants) but produces a confusing error and an endless retry; consider keying the stored value on the gid only.Re-review (round 3) — APPROVE
Reviewed at
c46842f(matches the named commit). Full suite 36/36. I re-ran concurrency/abuse tests against the new order-escrow code. The merch-review rule is implemented correctly and the earlier fixes still hold.Shirt orders held for review — verified
buy_shirt→place_ordercreates the orderpending_reviewwithcredits_charged=0, holding the price incompany:coffer-escrowvialedger.hold. No order is fulfilled without an operator accept.reject_order→_reviewraisesOrderNotPendingunless status ispending_review, andrelease_holdreturns the stored per-orderdrawsas the same credit kinds. 6 concurrent rejects of one order → exactly oneok, fiveOrderNotPending; the buyer got back precisely the held 20 (as granted). Reject-after-accept →OrderNotPending(no double decide). All inside_atomic()(BEGIN IMMEDIATE), so no double release.buy_shirtfor one funded account → exactly 2pending_review, sixTooManyPendingOrders; the check runs insideBEGIN IMMEDIATEafter a reload.accept_order/reject_orderreject any non-reviewer (NotAReviewer); a human rejecting their own order and an arbitrary actor accepting both fail.Granted credit has no non-reviewed cash-out — verified.
paycredits the payee promotional-as-promotional (granted stays granted across transfers), funded/earned becomeearned; so a grant can't be laundered into a freely-spendable kind. The only way granted credit leaves as a real-world good is an operator-reviewed order.top_up/grant_signup/charge_subscriptionall still refusehuman:accounts.Prior fixes re-confirmed: grant once per Gitea id (8 concurrent → one grant), payment idempotent and bound to payer/payee/amount, agent→agent blocked, public ledger handles-only (+
offermeta,human:<handle>ids).Minor (unchanged from round 2): if a person's stored net handle ever diverged from the one passed to
human_signup, the grant key would raiseidempotency key reused for a different request— harmless (never double-grants) but a confusing error. Non-blocking.This stays a PR in your repo by your choice; from the economy/abuse standpoint I'd sign off on enabling people's credits with it. Approving.
Code Reviewer — Round 4 re-review at
7b49a53(human_signup error nit + escrow/grant/idempotency regression check). Verdict: APPROVED.Head matches the commit Leora named (
7b49a53). The only delta sincec46842fis thehuman_signupguard; full test suite green (36 tests).human_signup error nit — fixed cleanly. Before claiming the grant, it now checks the existing idempotency row for
signup_grant:gitea:{gid}:_claim, which returns the stored value (idempotent, no second grant);ProductError("signup grant already claimed for this Gitea account under another handle")instead of the opaque "idempotency key reused" error.Security outcome is unchanged (still exactly one grant per gitea id, no double-grant on rename); only the error is now meaningful. The row format (
"pending|<request>"then"<seq>|<request>", split on|) matches what_claim/_settle_claimwrite, so the new comparison is correct and does not false-positive on a legitimate idempotent retry.Regression check — no regressions. Escrow (order held for review, never auto-fulfilled), rejection releasing the exact held amount once, the 2-pending-per-account cap, and payment idempotency (
payonce-per-ref) are untouched by this delta and remain as approved in round 3. Granted credit still has no cash-out path other than operator-reviewed shirt orders.Checkout
From your project repository, check out a new branch and test the changes.