This file is the project's committed home for project-intrinsic agent knowledge: build, test, release, architecture, and sharp-edge notes that should travel with the code.
- Add durable project-specific notes here as they are discovered through real work.
- Secure-chat acknowledgement state is maintained in
pkg/edition/java/proxy/chat_queue.goand driven by the handlers inhandle_cmd.go/handle_chat.go; it is a direct port of Velocity (ChatQueue,SessionCommandHandler), which is the reference for correct behavior. TheLastSeenMessages.Offset += delayedAckCountadjustment is intentional and matches Velocity — not dead code — so validate any change against Velocity's source and a proxy-level reproduction. Key invariant: anUnsignedPlayerCommand(1.20.5+, commands with no signable args — the common path in offline mode) carries NO last-seen update, so it must not be fed into the chat queue (that flushes and discards the player's held acknowledgements) and must be forwarded command-only; a consumed command'sChatAcknowledgementgates on the offset, not the acknowledged bitset. Regression coverage:handle_cmd_ack_test.go. - The handshake
ServerAddressGate writes to a backend is also the virtual host the next proxy routes on (Gate behind Connect/Gate Lite is a real topology), so anything appended to it — Forge tokens, forwarding data, TCPShield real IP — must keep the host as the first\0-part.pkg/edition/java/proxy/server.go(handshakeAddr,backendHandshakeBaseHost) owns that invariant;lite.ClearVirtualHostis the consumer that reads the host back out. Upstream Forge scans every\0-part, so a mangled address still works with a single proxy and only breaks one hop down — test the chained form, not just the backend's tolerance (backend_handshake_addresser_test.go,lite/forge_route_test.go). - Lite mode pipes the raw connection, so
config.Validate()returns early and every full-proxy setting (forwarding, servers/try/forcedHosts, onlineMode, compression, status, announceForge) is inert;warnLiteIgnoredSettingsinpkg/edition/java/config/config.gois the list of the ones Gate warns about. Warn only on non-default values and rememberDefaultConfig.Forwarding.Modeislegacy, so warning on it would fire for every Lite config, including the shippedpkg/configs/config-lite.yml. - PROXY protocol on the client listener is a trust boundary, not a parsing detail: a header is only honored from an upstream in
proxyProtocolTrustedProxies(pkg/edition/java/proxy/proxy_protocol.go, defaults inconfig.DefaultProxyProtocolTrustedProxies), otherwise the peer may not send one at all. Never wrap an accepted connection with a bareproxyproto.NewConn(conn)— that defaults toUSEwith no deadline, letting any peer assert an arbitrary client IP (ban evasion plus framing a third party) and stall the sniff forever, since the 10s default only applies viaproxyproto.Listener. Regression coverage:proxy_protocol_test.go.pkg/edition/bedrock/geyser/geyser.gostill wraps bare, guarded only by its loopback-by-defaultgeyserListenAddr. - Startup/boot regressions are covered by
pkg/gate/gate_startup_test.go, which boots Gate from the shipped config templates (pkg/configs) viaLoadConfig+New/Startingo test ./.... Sharp edges it encodes: booting Bedrock is not hermetic (it runs managed Geyser, so only wiring is asserted there); a Connect client with an emptyconnect.namefetches a random endpoint name over the internet; andnewConfigCandidatemust deep-copy mutable defaults before parsing so concurrent loads retain independent last-known-good state. - Gate publishes through the default-branch
.github/workflows/release-publish.ymlreusable workflow; the default-branch-only release-please workflow calls it with the selected tag as input, including after its repository-dispatch rerun, so tag-authored workflow steps never receive write credentials. Its release and container paths have two trust zones:release-build/image-buildrun the checked-out tag withcontents: readand stage allowlisted release assets/OCI archives, while freshpublish-release/publish-imagesjobs check out nothing and alone holdcontents: write/packages: write. Keep workflow-level permissions empty, checkout credentials disabled in code-running jobs, and the artifact handoffs explicit; contracts live inci_write_token_isolation_test.go. The final release guard re-reads GitHub rather than trusting local output (release_asset_verification_test.go) and must remain the publisher's last step. - Gate's verified Velocity sync point — which upstream commit is proven to have landed here, and the log of every upstream review since — lives in
VELOCITY_SYNC.mdat the repo root (shape guarded byvelocity_sync_test.go). Read and append there rather than reconstructing it from history; note the file records a verified sync point as of one commit, not general parity with upstream. - Releases already published with no assets are repaired by
.github/workflows/release-repair.yml(manual dispatch from the default branch, contract pinned byrelease_repair_workflow_test.go). Its two-job boundary is load-bearing: onlybuildchecks out and runs the selected tag withcontents: read, while freshpublishhascontents: write, checks out nothing, and uploads only the allowlisted artifact; the workflow has no ambient permission and no registry scope, so a backwardghcr.io/minekube/gate:latestretag is unrepresentable. It must live on the default branch and check the tag out, becauseworkflow_dispatchcompiles the workflow file at the dispatched ref — a repair input added toci.ymlwould not exist on the old tags that need it. The build job pins Go viago-version-file: go.modof the tag: today's Go is not a neutral substitute for an old tag's, e.g.go vet ./...on v0.48.0 is clean at go1.24.1 and reports two printf diagnostics at go1.26.2, whichmake testwould surface as a false red on source that shipped fine. GoReleaser runs with--skip=publish(assets are uploaded separately withgh release upload --clobber) so a repair never rewrites a historical release's notes or identity — older tags (v0.63.0 and back) predaterelease: mode: keep-existingand would otherwise be deleted and recreated. Tags whose.goreleaser.ymlpredatesversion: 2(v0.41.1 and older) cannot be repaired by a v2 toolchain; that is a finding to report, not a thing to work around. - Config reload correctness must not rely on fsnotify delivery alone:
pkg/internal/reload/watch.goreconciles content fingerprints because macOS can miss delete/recreate events. On Windows, fingerprint handles must allow delete sharing or they can make an editor's atomic replacement fail with access denied. Go'sos.Rename/MoveFileExWstill cannot replace an open destination, so open-handle replacement coverage usesReplaceFileW. Preserve its single-owner loop and silent-watcher regression coverage when changing debounce or watcher behavior. - Bedrock principal v2 (Connect capability
bedrock-verified-principal-v2) is consumed source-level fromgo.minekube.com/connect/bedrockprincipal(pinned as a pseudo-version of the frozen main commit; the spec is moxy commit 396447f,docs/superpowers/plans/2026-07-31-connect-bedrock-go-gate-consumers.md). Gate's generated WatchSessionpredates frozen fields 6-12, sopkg/util/connectutil/principal.goreads them from the unknown-field region by exact number/wire type;pkg/util/connectutil/config/principal.go+setup_client.goverify exactly once and apply only the verifier-produced profile, advertise the capability only whenReadinessState.Ready(), and reject envelope/Bedrock proposals instead of downgrading to proposed profile data. Never log proposals, XUIDs, usernames, or player addresses in these paths (guarded bysetup_client_log_privacy_test.go,geyser_log_privacy_test.go). Linked Java identity is applied only from: (a) a verified principal (signed, Connect path — always authoritative), or (b) opt-in viabackendFloodgate.enabled(default off) from the AES-authenticated Floodgate handshake triplet cross-checked against the connection's own Floodgate bedrock UUID, or (c) the same opt-in, the GeyserMC global link API as fallback when the handshake carries no triplet (pkg/edition/bedrock/geyser/geyser.goonGameProfile; parse infloodgate.ParseLinkedPlayer, cross-check viaBedrockData.FloodgateJavaUuid- note that isnew UUID(0, xuid), NOT the SHA-1JavaUuid; API lookup inProfileManager.GetLinkedAccount, the officialGlobalPlayerLinkingservice). Never promote identity from the GeyserMC API outside that opt-in gate. Note:go.minekube.com/geyserliteresolves through a tags-only module proxy, so it cannot be bumped to untagged commits; its verified-ingress API waits for a real release. - Bedrock gamertags cross into Java profiles in
pkg/edition/bedrock/geyser/geyser.go; the renderedusernameFormatmust be normalized to Java's 16-character ASCII username alphabet there. Modern Paper rejects the traditional dot prefix and Bedrock spaces before login, so preservejavaCompatibleUsernameandusername_test.gowhen changing profile construction. - Java profile properties encode the optional signature with an always-present boolean.
WritePropertiesmust writefalsefor unsigned properties; omitting it shifts the following 26.2 loginSessionIDinto the property decoder and breaks Geyser. Preserve the signed and unsigned wire fixtures inwriter_test.go. - HTTP config writes share the transactional reload boundary in
pkg/gate/gate.go:GetConfigreturns an opaque version,ApplyConfigrequires it and may target edits with JSON Merge Patch, and only route changes in an already-enabled Java Lite config are live-safe. Keeppkg/gate/api_handlers.goonGate.ApplyLiveConfigIfVersion; broad pointer mutation plusreload.FireConfigUpdatecan claim startup-bound fields changed when the running listeners and forwarding mode did not.
Keep this file for knowledge useful to almost every future agent session in this project. Do not repeat what the codebase already shows; point to the authoritative file or command instead. Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve this bar for all agents and keep entries concise.