The presents transport layer got explicit streamers in 1.21/1.22, but crowd's
value classes (OccupantInfo, chat messages, BodyObject/PlaceObject fields)
still streamed reflectively — nondeterministic field order on Android/ART.
Generated with bang's genStreamableLibs task (ancestor-aware flatten mode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Follow-up to 0f1626ece, found when 85 persisted .board files stopped
deserializing: the ancestor walk reflected on classes compiled from the
STRIPPED sources, but javac binds the emitted super.readObject to the nearest
declaration in the FINAL sources. CargoTank extends Counter extends Prop —
Counter regenerates a method covering `count`, so CargoTank's tail computed
against Prop streamed `count` twice and desynced the stream.
findStreamMethodAncestor now asks willDeclare(): an ancestor counts iff it
declares the method in current source (hand-written / substituted frame) OR
this run will generate one for it (concrete, in-fileset, nonempty own tail —
recursive, memoized; the filesets are pre-scanned so out-of-fileset ancestors
are never assumed generatable). Consequence: a subclass whose tail below the
NEAREST post-regen ancestor is empty generates nothing (CargoTank inherits
Counter's method).
The hand-written-custom skip is also per-SIDE now, not per-class: Prop has a
custom readObject but no writeObject, and its write side must regenerate both
for legacy parity and because subclasses emit super.writeObject against it
(the old whole-class skip left those uncompilable).
Verified: all 165 persisted .board files load with the regenerated streamers;
regen is idempotent (second run converts 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Three structural fixes to flatten mode, found bringing up games on-device
(Epic A1 Phase 5):
1. Nested classes: processFlatten now recurses through getDeclaredClasses()
(not gated on the outer class's eligibility), so nested Streamables like
BangConfig.Round get deterministic methods too. All detection/substitution/
insertion is scoped to each class's own body region (brace-matched,
comment/string-aware, member-class bodies masked during detection) so an
instrumented outer class can't hide — or poison — a nested one.
2. Ancestor-aware generation: a class whose ancestor declares read/writeObject
now generates `super.readObject(ins)` + only the fields declared below that
ancestor, instead of a self-contained full-set method. A self-contained
subclass method silently dropped the ancestor's side effects for every
subclass that previously inherited it: Prop.init() (server NPE cloning
board props at startRound), Item's _nondb identity extras (itemId=0 on
every wire-streamed item), InvocationMarshaller's _invdir rebind. Byte
layout is unchanged: the ancestor's method streams exactly the flattened
prefix. When the subclass adds no fields, nothing is generated — plain
inheritance is already complete (the field-free marshaller/unit case).
3. Abstract classes: substitution-only. Their default*Object() pass-throughs
are replaced in place with the class's own flattened prefix (making them
safe super-call targets — the reflective pass-through reads the full
dynamic-class set and would double-read the subclass tail), but whole
methods are never generated for them. Requires every Streamable ancestor
to be covered by the task's filesets; see findStreamMethodAncestor doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Ports the two Claude GitHub workflows from the bang repo (which narya lacked):
claude-code-review.yml auto-reviews PRs; claude.yml responds to @claude
mentions on issues/PRs. Review prompt tuned for narya (wire/streaming compat,
DObject/marshaller correctness, concurrency). Requires the
CLAUDE_CODE_OAUTH_TOKEN repo secret to authenticate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
The flatten-mode codegen gave each *Marshaller subclass a self-contained
read/writeObject, which overrode InvocationMarshaller.readObject and thus
skipped its side-effect of binding the transient _invdir from the stream's
client -> null _invdir -> NPE on the first invocation request (hit by ART
AND HotSpot clients). Fix: give InvocationMarshaller explicit read/writeObject
(GenStreamUtil, deterministic order, byte-identical to the old reflective
default) that keeps the _invdir binding; field-free subclasses inherit it.
narya 1.22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Pre-crash safety checkpoint (box rebooted mid-implementation; NOT verified
complete). GenStreamableTask flatten-mode + GenStreamUtil runtime helper +
ObjectInputStream changes; explicit read/writeObject instrumentation across
presents net + dobj event classes so ART matches HotSpot's ClassUtil.getFields
order without the global sort flag (no data migration). narya 1.21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Merges the TLS transport work (PR #1). Conflict resolution note: master carried
an abandoned June-14 experiment pointing deps at local-stack SNAPSHOTs
(ooo-util 1.6-SNAPSHOT, depot 1.9-SNAPSHOT, jmustache 1.17-SNAPSHOT) plus the
upstream post-release 1.20-SNAPSHOT bump; the merge resolves all three poms to
the bang-tls state — released deps (ooo-util 1.5, depot 1.8, jmustache 1.16)
and version 1.20, which is exactly what was built, verified against Bang, and
published to GitHub Packages.
A clean minor-version bump (retiring the Epic 2 -bangN patched-fork suffix —
that scheme is superseded; we now own these forks outright and version them
normally). GitHub Packages won't let us re-publish 1.19, and 1.20 makes the
TLS-capable build obvious in dependency listings.
When the cluster's client port is TLS-enabled, peer connections (which connect to
that same port) must also speak TLS. PeerManager gains setPeerClientContext/
getPeerClientContext, and PeerNode installs that context on its client before
logon. Null (the default) leaves peering plaintext — a no-op, so non-TLS clusters
are unchanged.
Wraps the entire client<->server TCP object stream in TLS, strictly opt-in via
an injected SSLContext (null = byte-for-byte the original plaintext behavior).
Connection: a ByteChannel _ioChannel indirection (defaults to the raw socket;
a TLS-wrapping channel when enabled). Selector registration, connection-pending
checks and close still use the raw SocketChannel.
Server (PresentsConnectionManager/PresentsConnection/ConnectionManager):
setSSLContext/isTlsEnabled; accepted sockets are wrapped in a non-blocking
ServerTlsChannel after init+registration; reads/writes route through the io
channel. tls-channel NeedsWrite/NeedsRead on write are treated as partial writes
(stashed in the existing per-connection overflow queue, retried each timed-select
tick — the server never needed OP_WRITE); on read, NeedsRead stays OP_READ and
NeedsWrite adds OP_WRITE so the re-dispatch pumps the handshake flush, cleared
after a clean read. processAuthedConnections carries the established io channel
from the authing connection to the running connection (init() would otherwise
revert it to the raw socket mid-session).
Client (Client/BlockingCommunicator): Client.setSSLContext; the connected
blocking socket is wrapped in a ClientTlsChannel (handshake driven transparently
in blocking mode); the Reader/Writer threads share it (one-unwrap + one-wrap, the
concurrency tls-channel allows).
DatagramChannelReader: fails closed — refuses to bind datagram ports while TLS is
enabled (we have no DTLS, so plaintext datagrams alongside TLS would be a footgun;
Bang binds zero datagram ports, so this never triggers today).
Adds the marianobarrios:tls-channel:1.0.0 dependency (thin ByteChannel-over-
SSLEngine wrapper, MIT, zero transitive deps, Java 8 bytecode) and a
TlsContextFactory that builds server (keystore) and client (pinned truststore)
SSLContexts, TLS 1.3 only. No transport wiring yet — this just compiles in the
building blocks for full-session encryption of the Presents NIO stream.
I can't help but over-optimize. We're parsing the result of class.getName(),
it will be a bare class name if not in the array.
Also: Stop going out of our way to not throw an exception if the input
is malformed, as that's the desired result for bad input.
Add three defenses to the Streamable deserialization system to prevent
exploitation by hacked clients:
- Class whitelist: ObjectInputStream.setAllowedClassPrefixes() validates
class names before Class.forName(), blocking arbitrary class loading
from crafted wire data. Callers configure at startup; unconfigured
streams retain existing behavior for backward compatibility.
- Container size caps: BasicStreamers.validateSize() rejects arrays,
collections, and maps with negative or >65536 element counts. Prevents
OOM from a small frame declaring a massive allocation. Applied to all
12 array/collection read paths and Streamer.ArrayStreamer.
- Recursion depth limit: ObjectInputStream.readObject() tracks nesting
depth and throws at 32 levels, preventing stack overflow from
maliciously nested structures (e.g., list-of-list-of-list).
Some networks will kill connections sooner than 60s, especially if they are not using keepalive. This fix should reduce the random disconnects some players experience
I don't think anything is directly streaming these over the wire, but it would
have worked before, so we'll try to keep it working now to avoid unpleasant
surprises.
- It would update if an AttributeChangedEvent was received, but not an
ElementUpdatedEvent!
- If you have 50 fields and 50 editors, then every one was updating
whenever an event arrived? Filter by the field that actually changed?
Does this break something?