M1-64: Gateway connection and session¶
Status: ready | Depends on: M1-19, M1-21, M1-63 | Milestone: M1
Goal¶
The WebSocket endpoint /api/v1/gateway accepts connections, checks the Origin, sends hello with a server DPoP nonce, authenticates in-band with identify (access token plus DPoP proof bound to that nonce, or an API token), answers ready with the snapshot, keeps the session alive with heartbeats, renews access tokens with reauth, resumes sessions within sync.resume_window and closes with the codes of 04. It is the connection layer on which delivery and revocation (M1-65), focus and typing (M1-66) and frame conformance (M1-67) build.
Read first¶
docs/spec/04-sync-protocol.md: 'Frames on the WebSocket'; 'WebSocket gateway' with 'Endpoint and version', 'Ops', 'Lifecycle', 'Close codes', 'Reconnecting'; 'resync_required'; 'Sync epoch'; 'Revocation of a session, device or principal'; sequence diagram 'Reconnect with catch-up'; decisions R-SY-01, R-SY-14; configuration table.docs/spec/05-actions.md:gateway.connect('Personal data, notifications, sync'), 'WebSocket ops and action', 'Rate-limit classes' (rl.gateway_connect), 'Decision' (pending devices,4016), 'Scopes for apps'.docs/spec/02-architecture.md: 'Gateway', 'Layers within a module' rule 4 (onlygateway/adaptersimportsgithub.com/coder/websocket), 'Allowed dependencies' (gateway), 'Cross-cutting ports' (actor.Authenticator,Authorizer), 'Chain in the api node', 'Proof of the check', 'Deadlines' (gateway.write_timeout), 'Panics', R-AR-11.docs/spec/06-api.md: 'Real-time gateway', 'WebSocket ops and events', 'DPoP', 'Rate limits' point 3, 'Errors' point 6.docs/spec/08-identity.md: 'Access token', 'DPoP', 'API tokens for agents, apps and tools', 'Approval' (pending devices); configurationauth.dpop_nonce_ttl,auth.clock_leeway,auth.dpop_proof_max_age.docs/spec/07-permissions.md: 'Principal context', 'Decisions and reasons'.docs/spec/19-security.md: 'CORS and WebSocket origin' (SEC-CORS-3), input rules SEC-IN-1 to SEC-IN-3, tests SEC-T04 and SEC-T10.docs/spec/16-operations.md:server.public_url.docs/api/asyncapi.yaml: channelgateway,x-close-codes, messagesop.*.docs/api/openapi.yaml: schemasGatewayHello,GatewayIdentify,GatewayClientInfo,GatewayReady,GatewayResume,GatewayResumed,GatewayHeartbeat,GatewayHeartbeatAck,GatewayReauth,GatewayReauthRequired,GatewayResyncRequired,GatewayError,Snapshot.docs/observability/signals.md(gateway rows, spangateway.connect);docs/security/threat-model.md'Gateway (WebSocket)'.- ADRs 0013, 0017, 0020 and, if present, 0052, 0053, 0061.
docs/PLAN.md: 'Way of working per ticket', M1 rows M1-33, M1-34, M1-44, M1-63, M1-65 to M1-67, M2-07, M2-20.- Code:
internal/kernel/actor,internal/kernel/authorize,internal/identity(DPoP verifier and nonce source of M1-33, authenticator of M1-34, API tokens of M1-44, test helpers),internal/platform/ratelimit(M1-19),internal/platform/httpserver(M1-18, M1-21),internal/syncportsSnapshotandCatchUp(M1-63),api/rest(generatedGateway*types),api/routes,architecture/modules.yaml.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. - Limits, windows and thresholds only through named configuration keys listed in
docs/operations/configuration.md; code comments and docs name the key, never the number. - Specification first: a change to behaviour, API, event, permission or configuration updates
docs/spec/ordocs/api/in the same change. The core contract (docs/spec/00-glossary.md,03-data-model.md,04-sync-protocol.md,05-actions.md) is not edited by this ticket.
Product requirements implemented here:
- One endpoint
wss://<host>/api/v1/gateway; JSON text frames{op, d}of at mostsync.ws_max_frame_bytes(04 'Endpoint and version', 'Frames on the WebSocket'). - The upgrade checks only the origin; a foreign
Origingives403before the upgrade (19 SEC-CORS-3, 04 'Lifecycle' step 1). helloimmediately withheartbeat_interval_ms,resume_window_ms,protocol_versions,dpop_nonce,server_time,sync_epoch(04, schemaGatewayHello).- Authentication only in
identify,resumeandreauth, with the DPoP proof bound to the nonce fromhello(R-SY-01, 08 'DPoP'); thenauthorize(principal, gateway.connect, instance); a pending device gets4016. readywith session id, principal, device, permission version, head and snapshot;resumewithinsync.resume_window, otherwise4006; heartbeats with4009aftersync.heartbeat_miss_limitmissed;reauth_requiredsync.reauth_lead_timebefore token expiry and4011without a validreauth(04 'Lifecycle').- All close codes of 04 'Close codes'; this ticket owns
4001to4009,4011and4016to4018, and uses4000,4012and4014where named below. The causes at connection setup map to codes as the table in 04 'Close codes' says. - Rate-limit class
rl.gateway_connect; when exhausted the gateway closes with4008(06 'Rate limits' point 3). - Gateway sessions live only in process memory; a resume on another node gives
4006(R-AR-11). - Every op goes through
authorizewith its action from 05 'WebSocket ops and action' (B7). - One connection per device: a new
identifyorresumeof the same device closes the older open connection with4017(04 'Endpoint and version'). - Agents and apps get no
reauth_required; at theexpires_atof their API token the server closes with4011(04 'Lifecycle' step 6). - The connection registers with delivery before
head_seqis read (04 'Lifecycle' step 4).
Scope¶
All paths under services/server/ unless noted.
Create:
internal/gateway/domain/closecodes.go: one constant per code of 04 'Close codes' with the name fromx-close-codesand a doc comment with the client reaction;internal/gateway/domain/session.go: session statesawaiting_identify,ready,detached(disconnected, resumable) andclosedwith the allowed transitions.internal/gateway/app/:sessions.go(in-memory store per process with resume window and sweeper),dispatcher.go(op registryRegister(op string, action authorize.Action, handler OpHandler), unknown op handling, proof that each handler calledauthorizewith its declared action),identify.go,resume.go,heartbeat.go,reauth.go,delivery.go(interfaceDelivery { Register(*Session); Attach(ctx, *Session, fromSeq int64) error; Detach(*Session) };Registercomes before the head is read, note 11), and the outgoing ports the use cases need (authenticator, authorizer, nonce source, rate limiter,sync.Snapshot,sync.CatchUp, instance epoch, clock).internal/gateway/adapters/ws/: the HTTP handler forGET /api/v1/gatewaywithgithub.com/coder/websocket, the reader and writer goroutines, the frame codec on the generatedGateway*types ofapi/restwith the strict JSON decoder of M1-18.internal/gateway/adapters/wstest/: a Go test client for this and later tickets:Dial(ctx, baseURL string, opts ...Option) (*Client, error),Hello() GatewayHello,Identify(ctx, IdentifyParams) (GatewayReady, error),Resume(...),Send(op string, d any) error,Next(ctx) (op string, d json.RawMessage, err error),ExpectClose(ctx) (code int, reason string, err error), and an automatic heartbeat loop whose first heartbeat waits a uniformly random share ofheartbeat_interval_ms(04 'Lifecycle' step 5).internal/gateway/README.md(status, ops, close codes owned, sessions, configuration, dependencies).
Change:
architecture/modules.yaml: allowgithub.com/coder/websocketforgateway/adaptersonly; regenerate.go-arch-lint.yml;go.modandgo.sumwith the module pinned to a full release version (MIT-compatible ISC license, allowed inquality/licenses/allowed.txt).internal/wire: the gateway route on the public listener for rolesapiandall, the registration of the opsidentify,resume,heartbeat,reauth, the sweeper in the lifecycle, and aDeliverybinding that does nothing until M1-65 replaces it (doc comment names M1-65).services/server/api/operations.yaml: new sectiongateway_opswithenabled(identify,resume,heartbeat,reauth) andlater(focus,typing.start,typing.stop: M1-66;ack,run.update,command.autocomplete_result: M2;stats.subscribe,stats.unsubscribe: M6, ticket M6-15), P-04. The dispatcher registers exactly the enabled ops; a gate check (the one M1-17 built for operations) also verifies that every op of the section is a client-to-server op of the channelgatewayinasyncapi.yamland that none is missing. The terminal channels (terminalStream,hostControl,hostData) are not gateway ops and stay out of this list; their handlers come with M2 (M2-43, M2-44).- Route registry (
internal/platform/httpserverand the registration test of M1-25): registerGET /api/v1/gatewaywith actiongateway.connectand classrl.gateway_connect, marked as authorized in-band, so AZ-P1 counts it and the proof check of 02 'Proof of the check' does not require a route-levelauthorizefor the101response. Add that marker if the registry has none, with a test that only this route uses it. internal/kernel/actorandinternal/identity, only if needed: ifactor.Authenticatoraccepts only an*http.Request, add a transport-neutral methodAuthenticateCredentials(ctx, actor.Credentials{Scheme, Token, DPoPProof, Method, URL string; ExpectNonce bool}) (actor.Actor, error)and implement the HTTP path on top of it, with tests in identity. Expose the current server DPoP nonce through the identity port (M1-33 source) if it is not exposed.docs/api/openapi.yaml(W7, SEC-IN-1; 06 "Basic rules" row Unknown):additionalProperties: falseon every schema of a client-to-server frame (GatewayIdentifywithGatewayClientInfo,GatewayResume,GatewayHeartbeat,GatewayReauth,GatewayFocus,GatewayTyping,GatewayAck,GatewayRunUpdate,GatewayAutocompleteResult), closed the same way M1-18 closes request schemas; extend the M1-18 generator check "request schemas closed" to these schemas;make api-lintgreen.services/server/CHANGELOG.md;docs/DECISIONS.md.
Out of scope¶
- Hub, delivery of events, streaming catch-up with
cursorframes andcatchup.complete, revocation of open connections onsession.revokedandaccess.changed, end-to-end slow consumer test, shutdown with4013(M1-65). focus,typing.start,typing.stopandtyping.started(M1-66); frame conformance against the schemas and the op registry test (M1-67).- Persistent agent consumer cursors, fencing and
4015(M2-07);ack,run.update,command.autocomplete_result(M2); agent presence fromheartbeat.agent(M2-20). - Gateway metrics (M1-68); the canary test (M1-69); connection load and memory targets (M1-72).
Boundaries¶
- A new migration needs its line in
migrations.sha256: runmake migrations-lockafter creating the file and beforemake check; never edit a merged migration (M1-01, ADR 0007). - Work in a dedicated git worktree on the branch
feat/M1-64-gateway-connection; one writer per worktree. - Conventional Commits with the module as scope (for example
feat(gateway): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push, tag, open pull requests or create remote repositories.
- Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests bind only
127.0.0.1and port 0; PostgreSQL runs only as a throwaway container on a random port throughpgtest(P-09, P-10). Without a Docker-compatible runtime the integration tests fail; they never skip. - Stop only processes you started, by PID. Temporary directories only via
mktemp -d, removed at the end. - Never read or print secrets. No network calls except package downloads for pinned tools and Go modules.
- Never lower a threshold, disable a gate or delete a test.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). Do not invent close codes.
Design notes¶
Upgrade and frames
GET /api/v1/gatewayexists only on the public listener. It passes the chain steps panic, correlation, span and header limits (02 'Chain in the api node' steps 1 to 3). If anOriginheader is present and its scheme, host and port differ from the origin ofserver.public_url, answer403with problem codeforbiddenbefore the upgrade (SEC-CORS-3). Nothing else is checked at the upgrade. Accept with compression disabled (memory per idle connection,perf.ws_idle_conn_mem_max) and withOriginPatternsset to the host ofserver.public_url; never use the option that turns origin verification off.- Send
helloright after the upgrade:heartbeat_interval_msfromsync.heartbeat_interval,resume_window_msfromsync.resume_window,protocol_versions: [1],dpop_nonce= the current server nonce from identity (validauth.dpop_nonce_ttl; the current and the previous nonce are accepted, 08 'DPoP'),server_time,sync_epoch. - Frames: text only. Read each message through a streaming reader limited to
sync.ws_max_frame_bytesplus one byte; a larger frame closes with4002and reasonframe_too_large. Binary frames, invalid JSON, duplicate keys, invalid UTF-8, a missingop, adthat does not match its schema and unknown members ind(Ruling W7) close with4002. - Before a successful
identifyorresume, any other known op closes with4003; an unknown op closes with4001at any time. Withoutidentifyorresumewithinsync.identify_timeoutthe server closes with4003(04 'Close codes'). - Ops in
gateway_ops.later(focus,typing.start,typing.stopuntil M1-66;ack,run.update,command.autocomplete_resultuntil M2;stats.subscribe,stats.unsubscribeuntil M6) are treated as unknown (4001). The dispatcher takes each op's action from the generated mapping inapi/routes; after a handler returns, it checks thatauthorizewas called with that action, otherwise it closes with4000and raises the alarm counter used by the HTTP proof check.
identify
- Exactly one of
access_tokenandapi_token, otherwise4002.protocol_versionnot in[1]gives4007. - Authenticate with
actor.Authenticator: token, DPoP proof, methodGET, HTUhttps://<host and port of server.public_url>/api/v1/gateway(the HTTPS form even behind TLS termination, 04 'Lifecycle' step 3), nonce required. People must senddevice_idequal to the token's device claim. API tokens need a proof when the token hasdpop_jkt. - Rate limit
rl.gateway_connectthroughplatform/ratelimit, keyed by the verified device id (people) or token id (agents and apps), checked after the credentials are verified so that nobody can drain another device's bucket. Exhausted: close4008with reasonretry_after=<seconds>. - Then
authorize(actor, gateway.connect, instance). - Outcome to close code (04 'Close codes', table of causes at connection setup):
| Outcome | Close |
|---|---|
malformed or unknown token, bad signature, wrong iss or aud, invalid DPoP proof (htm, htu, ath, iat, replayed jti, wrong or missing nonce), device_id mismatch |
4004 |
access token expired beyond auth.clock_leeway |
4011 |
authorize unauthenticated with principal_inactive, session_revoked, device_inactive, token_revoked or app_not_installed |
4012 |
authorize forbidden with device_pending |
4016 |
valid credentials, but any other forbidden (for example scope_missing of an app) |
4018 |
| rate limit exhausted | 4008 |
database unavailable while building ready |
4000 |
- Success: create the session (UUIDv7
gateway_session_id, principal id and kind, device id, auth session id, token id, token expiry,consumerfor agents and apps with defaultdefault, client info); if another open connection of the same device exists on this process, close it with4017(note 15); callDelivery.Register(session); then read the head and build the snapshot withsync.Snapshot.Build, sendready {gateway_session_id, principal (from the snapshot), device_id, permission_version, head_seq, snapshot, sync_epoch}. - After
readyorresumed: if the client sent acursor, callsync.CatchUp.Check(cursor, sync_epoch); on a reason sendresync_required {reason, head_seq, sync_epoch}and callDelivery.Attach(session, head_seq): the connection stays open and is live fromhead_seq, the client reloads the snapshot without reconnecting (04 7.3). Otherwise callDelivery.Attach(session, cursor), orDelivery.Attach(session, head_seq)without a cursor. - A second
identifyor aresumeon an identified connection closes with4005.
heartbeat, reauth, resume
heartbeat {last_seq}(at least 0): answerheartbeat_ack {server_time}and checkauthorize(actor, gateway.connect, instance)from the cache; a revoked session, device, token or principal closes with4012. The server closes with4009when no heartbeat arrived forsync.heartbeat_intervaltimes (sync.heartbeat_miss_limit+ 1) sinceready,resumedor the last heartbeat; the extra interval absorbs the client's random delay of the first heartbeat. The memberagentis accepted and ignored until M2-20.- One connection per device (04 'Endpoint and version'): the session store keeps an index device id to open connection. A new
identifyorresumeof a device closes the older connection with4017before the new one getsreadyorresumed. Connections of agents and apps have no device and are not affected (their consumer rule is M2-07). Across api nodes the rule needscluster.mode = multi(M7-17). - People: send
reauth_required {expires_at, dpop_nonce}sync.reauth_lead_timebefore the access token expires.reauth {access_token, dpop}is authenticated likeidentify; the new token must belong to the same principal, device and auth session, otherwise close4004. An invalid token or proof answerserror {op: reauth, code}with the problem code and the connection stays. When the current token expires without a successfulreauth, close4011. Agents and apps get noreauth_required; at the API token'sexpires_atthe server closes with4011. resume {gateway_session_id, access_token or api_token, dpop, cursor, sync_epoch}on a new connection afterhello: an unknown session, a session detached longer thansync.resume_windowand a session of another principal or device all close with4006, identically. Authenticate, rate limit and authorize as in notes 7 to 10. If the session's old connection is still open, close it with4017first (same device, note 15;4006would make the old client reconnect and push out the new one). Sendresumed {gateway_session_id, head_seq, sync_epoch}and continue as in note 12.- Sessions: a per-process map; a detached session keeps its id and data for
sync.resume_window; a sweeper started and stopped by the lifecycle removes expired ones.
Connection mechanics
- One reader goroutine handles ops in order. One writer goroutine drains a queue bounded by
sync.outbound_queue_maxframes; each write has the deadlinegateway.write_timeout. Queue overflow or a write timeout closes with4014(the end-to-end slow consumer test is M1-65). A panic in a connection closes it with4000(02 'Panics'). - Non-fatal op errors answer
error {op, code, detail, ref}with problem codes of 06; fatal ones close. Close reasons are at most 123 bytes (RFC 6455) and contain only machine data, never tokens, proofs, nonces or content. - Telemetry: span
gateway.connectwithbothy.outcome,bothy.close.codeandbothy.principal.kind(docs/observability/signals.md); instruments come with M1-68. Logs never contain tokens, proofs, nonces or the snapshot. - Agents and apps identify with API tokens in M1 and get
readylike people;consumermust be non-empty and is kept on the session. Persistent cursors and4015come with M2-07. - Configuration keys used:
server.public_url,sync.identify_timeout,sync.heartbeat_interval,sync.heartbeat_miss_limit,sync.resume_window,sync.reauth_lead_time,sync.ws_max_frame_bytes,sync.outbound_queue_max,gateway.write_timeout,auth.dpop_nonce_ttl,auth.clock_leeway,ratelimit.rl.gateway_connect.per_minute,ratelimit.rl.gateway_connect.burst. Tests may override durations with short values through the configuration loader. - Rulings for
docs/DECISIONS.md: heartbeat deadline of note 14; reauth rules of note 16 for people; resume rules of note 17. The close codes at setup (note 10), the rate limit atidentifyandresume(note 8), the agents'4011at token expiry and4017are in 04 and need no ruling.
Tests (write them first)¶
Unit:
internal/gateway/domain:TestCloseCodesMatchAsyncAPIreadsdocs/api/asyncapi.yaml(x-close-codes) and compares every code and name with the constants.internal/gateway/appwith fakes and a fake clock:TestIdentifyOutcomeMapping(one case per row of note 10),TestHeartbeatDeadline,TestReauthSchedule,TestResumeWindow,TestDispatcherRequiresAuthorize,TestUnregisteredKnownOpIsUnknown.
Integration (real server on 127.0.0.1:0, PostgreSQL 18 via pgtest, people, devices, tokens and DPoP proofs from the identity test helpers, the wstest client):
TestSECCORS3ForeignOriginRejected: foreignOrigingives403before the upgrade; the instance origin and a missingOriginare accepted.TestHelloFields: values from the configuration, a nonce accepted by identify.TestIdentifyPersonReady:readydecodes intoGatewayReadywith unknown members rejected;snapshot.head_seqequalsready.head_seq.TestIdentifyAgentToken: a DPoP-bound agent token with proof getsready; without proof4004.TestClose4001UnknownOp,TestClose4002DecodeError(bad JSON, duplicate key, unknown member, binary frame, frame abovesync.ws_max_frame_bytes),TestClose4003OpBeforeIdentify,TestClose4003IdentifyTimeout,TestClose4004BadProof(wronghtu, wrong nonce, replayedjti, missing proof),TestClose4005SecondIdentify,TestClose4006ResumeUnknown,TestClose4006ResumeAfterWindow,TestClose4007ProtocolVersion,TestClose4008RateLimited,TestClose4009HeartbeatTimeout,TestClose4011ExpiredWithoutReauth,TestClose4011AgentTokenExpired,TestClose4012RevokedAtIdentify,TestClose4016PendingDevice,TestClose4017SecondConnectionOfDevice(identify and resume both replace the older connection),TestClose4018AppWithoutEventsScope(an app token withoutevents:subscribeis closed with4018, not4004).TestRegisterBeforeHead: with a fakeDelivery,Registeris called before the head is read.TestGatewayOpsEnableList: the dispatcher registers exactlygateway_ops.enabled; an op oflatercloses with4001.TestResumeWithinWindow:resumedwith the same session id.TestReauthKeepsConnection: a refreshed token of the same session keeps the connection and moves the expiry; a token of another session closes4004; an invalid proof answerserrorand keeps the connection.TestFirstHeartbeatJitterTolerated: a first heartbeat shortly before twiceheartbeat_interval_mskeeps the connection open;TestTestClientJittersFirstHeartbeat: thewstestclient's first delay lies in the interval from zero toheartbeat_interval_ms.TestResyncRequiredAfterReady: anidentifywith a wrongsync_epochgetsreadyfollowed byresync_requiredwithepoch_changed; the connection stays open and delivery is attached athead_seq.TestOutboundQueueOverflowCloses4014: with a writer that blocks, filling the queue closes with4014.TestNoSecretsInCloseReasonsAndLogs: tokens, proofs and nonces of the suite appear in no close reason and no captured log line.
Commands that must pass, from the repository root:
cd services/server && go test -race -count=1 ./internal/gateway/... ./internal/identity/... ./internal/kernel/... ./internal/platform/httpserver/... ./internal/wire/...make codegen-check,make arch,make register-check,make licenses,make osvmake check
Acceptance criteria¶
- A foreign
Origingets403before the upgrade (SEC-CORS-3, part of SEC-T04). -
helloarrives first and carries a nonce thatidentifyaccepts; a proof with another nonce is rejected with4004. - People and agents reach
readywith a valid snapshot; a pending device gets4016. - Every close code owned by this ticket (
4001to4009,4011,4016to4018) has a passing test;4012at identify and4014on overflow are tested. -
gateway_opsinapi/operations.yamllists every client op ofasyncapi.yamlexactly once. -
resumewithinsync.resume_windowgivesresumed, later4006. -
reauth_requiredcomessync.reauth_lead_timebefore expiry; withoutreauththe connection closes with4011. - Frames above
sync.ws_max_frame_bytesare rejected; unknown members are rejected. - Only
gateway/adaptersimportsgithub.com/coder/websocket(make arch). - The gateway route is registered with
gateway.connect; AZ-P1 passes. -
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why.
internal/gateway/README.mdwritten; READMEs ofidentityandplatform/httpserverupdated if they changed.- Rulings of note 24 recorded in
docs/DECISIONS.md(one row each with reason). If the implementation adds a threat thatdocs/security/threat-model.mdsection 'Gateway (WebSocket)' does not list (for example connections held unauthenticated untilsync.identify_timeout), add the row with its mitigation. services/server/CHANGELOG.mdentry under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Merge locally into the base branch only if review, red team and
make checkare green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check. - Review and red team verdicts and the findings fixed.
- What is not proven (for example timing margins of the heartbeat tests).
- Follow-ups that could not be done in this scope, each with its reason, and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.