Repository navigation
security: the owner's four decisions — server-owned needs, chat needs a character, share codes billed in the room, gives ask first - #101
Merged
Conversation
The owner reversed the ruling the server half's header recorded ("the client
owns the values during play ... the guards are ownership and bounds rather
than simulation"). Under it a client decayed its own hunger and thirst and
pushed them every two minutes, so a client that pushed 100 was never hungry
and nothing could tell.
Now the server decays every need on its own clock (DECAY_MS, each need's
DECAY_PER_MINUTE) while the character is loaded -- loaded on the server's own
`character:loaded`, not on a client's ask -- saves on autosave, unload and
departure, and sends `values` (citizenId, values, reason) every time they
move. A need goes up only through the server contract (`GetNeeds`, `SetNeeds`,
`AddNeeds`): an item consumed (the inventory side is in the inventory commit),
the new ACL-gated `/opx.needs.set <player> <need> <value>`, or the creator
exports `GetNeeds` / `AddNeeds` / `SetNeeds` (WRITERS for the writes). Every
move is published on the server bus as `opx:on:needs:changed` with the reason.
The push event, its acknowledgement and the client's decay are gone; the
client contract is read-only (`GetNeeds`) and its `changed` event now carries
the server's reason, so the HUD and subscribers keep working unchanged.
`PUSH_MS` and `PUSH_DELTA` leave the config. There is no starvation in this
runtime, and none is added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every line goes to every client, and it went out for any slot the host had admitted -- one on the character screen, or one whose entry failed and left it behind the gate. The owner's ruling: global chat requires a loaded character. Such a slot is refused `chat.notLoaded` (en + fr), cooled so a flood earns one toast. Staff still may (`STAFF_PERMISSION` in config/chat.lua, `command.opx.admin` by default): they are who a player stuck on that screen needs to reach. The console speaks through the `chat` contract and is never asked. With no character contract the chat keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s prices A redeemed code put somebody else's saved look on the player through a free look grant, from anywhere: the share code was a way round every counter in the city. The owner's ruling: a code is redeemed only inside a shop's fitting room, and its pieces are billed at that shop's prices. The room is now opened under the shop's own name (`shops:<key>`), and `appearance` answers which priced room a player holds (`PricedRoom`). A redeem must name the shop; the server checks the player stands at its counter (`shopAt`: reach, bucket, job gate, not downed) with that shop's room open, and only then puts the look on -- with no grant of its own. The save that leaves the room is admitted and billed by the room's charge, slot by slot, at the shop's `PRICES`, through the server-side billing every rack piece goes through (#75/#96). Cancel costs nothing and keeps nothing. Refusals `shops.redeemAtShop` and `shops.redeemInRoom` are en + fr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A GIVE IS AN OFFER. It moved the stack on the giver's press alone, so anybody in reach could fill a stranger's bag. Now the press raises an offer: the receiver is sent the item, the count and where the giver stands -- side and distance from the receiver's facing, the #91 wording, never a name -- and answers Accept or Refuse on a menu card (closing it is a no). The server holds the token for fifteen seconds; a sweep withdraws an unanswered one from the receiver's screen and tells the giver. On a yes it checks again that both can act, that they are in reach, that the same stack is still in the giver's slot and that the receiver has room, and only then moves it. One offer out per giver, one in per receiver, three seconds between two offers from one giver; the answer spends the request window. Only the receiver it was made to may answer a token, and a replay finds nothing. en + fr for every new line. AN ITEM'S STATUS IS ADDED ON THE SERVER, after the unit is consumed, through the needs contract; the client no longer adds it to a copy it pushes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`CatalogueSliced` yields every CATALOGUE_PART keys and so does the key list after it, but the copy's last stretch and the list's first shared one resume: up to two parts' worth, depending on how many keys the catalogues hold. The fifty strings these decisions add moved that remainder past the boot budget check (4,684 against 4,500). One `Wait(0)` between the two keeps every resume to one part, whatever the catalogue's size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- needs: the server loads, decays, raises and sends; there is no push door, a client claiming to be full is not believed, staff and creator writes land, departure saves the server's values, and the client is read-only and publishes the server's reason. Replaces "the last needs push of a disconnect", which tested a push that no longer exists. - chat: no character, no line, and told why; staff without a character still speak, signed with the server id; the trimmed-config test now speaks as a loaded character. - give: an offer moves nothing, carries no name, is rate-limited, answered only by its receiver, refused out of reach or with no room, withdrawn after fifteen seconds, and a replayed yes moves nothing more. The two existing give tests accept the offer first. - share codes: refused away from a counter, at a counter with no room, and in another shop's room; inside the shop's own room the look goes on with no free grant. The guessing-window test stands in a shop's room. README gains "Four rules the server keeps" and the needs exports and event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This implements the owner's four decisions on the questions from #99, one commit per decision.
lua tests/run.luapasses with 5143 checks.luac -pand the sandbox-globals check are clean, the manifest is unchanged, and every new message has an en and fr version.1. Needs are server-authoritative
opx:net:needs:push). The server stored whatever came in after bounding it, so a client that always pushed 100 was never hungry.DECAY_MS, each need'sDECAY_PER_MINUTE) while the character is loaded. Values load when the character module announces the character on the server, without waiting for the client to ask.needscontract. Three paths use it:USE.STATUSis consumed (the inventory adds it after taking the unit);/opx.needs.set <player> <need> <value>command (ACLcommand.opx.needs.set);GetNeeds, plusAddNeeds/SetNeeds(both gated by WRITERS).valuesevent it receives; its contract is now read-only (GetNeeds).opx:on:needs:changedwith its reason (loaded,decay,use,staff,ext:<caller>). This happens on the server bus too, so the HUD and subscribers keep working unchanged.PUSH_MSandPUSH_DELTAare removed from the config.2. Global chat requires a loaded character
chat.notLoaded(en+fr), with a cooldown so a flood earns one toast.STAFF_PERMISSIONinconfig/chat.lua,command.opx.adminby default.chatcontract and never goes through this check.3. Share codes are redeemed in a shop's fitting room and billed at its prices
shops:<key>), and the appearance contract can report it (PricedRoom).shopAt: reach, bucket, job gate, not downed) with that shop's room open.PRICES, through the same server-side billing as rack pieces (audit: body, entry and survival — relog/restart deadlocks, free revive on rejoin, server-side shop billing #75/fix: player-flow papercuts 2 — the lead's decisions on heal, uniforms, prices, contacts, garages, spawn hold, labels #96). Cancelling costs nothing.shops.redeemAtShopandshops.redeemInRoom.4. Giving an item asks the receiver to accept
Also
core/client/ui.luanow yields between copying the locale catalogue and building its key list. Without this, the ~50 new strings pushed one resume of the client boot over its budget test (4,684 instructions against a 4,500 limit). With the yield, each resume handles at most one part, whatever the catalogue's size.The #99 fuzz battery still passes: 85 net events (the needs push is gone, the give answer is new). It also covers the new
inventory:giveAnswerevent automatically.README: new section "Four rules the server keeps", plus the needs exports and the server
opx:on:needs:changedevent. Config comments inconfig/needs.lua,config/chat.luaandconfig/shops.luaare updated.🤖 Generated with Claude Code