| D-M2-43-1 |
2026-10-18 |
On hostControl an unknown op, a server op sent by the host, a second identify, a binary frame, invalid JSON and a frame over sync.ws_max_frame_bytes (reason frame_too_large) close with 4002. |
hostControl names no 4001; every one of these is a frame the server cannot take in this order. |
| D-M2-43-2 |
2026-10-18 |
The proof of identify is a compact JWS with alg exactly EdDSA, typ exactly host-auth+jwt and kid the RFC 7638 thumbprint of the login key (hosts.auth_jkt), over exactly the claims challenge, host_id and iat; iat lies in [hello.server_time minus auth.clock_leeway, now plus auth.clock_leeway]. An unknown host, a host in pairing or another kid closes with 4110; every other failure with 4113. |
The kid lets the server tell a replaced key ("run pair again") from a forged signature; a closed claim set and a short iat window keep a proof from being reused outside its exchange. |
| D-M2-43-3 |
2026-10-18 |
Audit entries of host.connect have empty actor columns and name the host as the resource; a denial for a host without a row has no resource. |
Audit actor IDs point to principals with a row; a host is no principal row (07 section 3). |
| D-M2-43-4 |
2026-10-18 |
A report whose session users changed is the rights change host.connect on the host with the people of its valid assignments; its session users go through M2-40's ApplySessionUserReport (next permission_version, sessions of vanished names end with access_revoked, one host.updated), the other reported_* columns in the same unit. The server-host flag raises no rights version; when it turns on, the sessions of people who are not active owners end with access_revoked in the same unit. |
Permissions no of host.connect (05 4.14) does not exempt a report from 03 4.3, which counts the session users as rights state; 03 does not list the flag, which SR10 reads per decision through authz.HostSource. |
| D-M2-43-5 |
2026-10-18 |
Host.connected and host.presence_changed mean "usable for sessions": true from the first applied report after ready until the control connection ends. |
A client that retries after 409 host_unavailable on connected: true must not race the first report, before which the server sends no session.open. |
| D-M2-43-6 |
2026-10-18 |
A live duplicate of a host in confirming puts it back into pairing with its keys dropped (the fallback of M2-41 with a fresh code that is never shown, host.updated with key_changed); both connections close with 4112. The life cycle draws confirming --> pairing for it (24 3.1, 3.4). |
03 allows disabled only after the confirmation; for an unconfirmed host pairing without keys is the only state from which only a new pairing helps. |
| D-M2-43-7 |
2026-10-18 |
The deny reason host_duplicate_connection (decision forbidden) joins 07 4.1; a live duplicate writes one deny entry host.connect with it and code: duplicate_connection, a dead older connection that is replaced one allow entry with code: stale_connection_replaced. Until M2-47 the probe runs within the node of the older connection. |
The closed list of reasons had no reason for a denial with valid credentials; both outcomes of 24 3.4 must be traceable. |
| D-M2-43-8 |
2026-10-18 |
Every session listed in live_sessions that is closed for this host, unknown or of another host gets session.close right after ready: the stored close_reason, else access_revoked. |
A PTY the server no longer allows ends at the next login (24 8.3 "instructs the host service"), fail closed for IDs the server does not know. |
| D-M2-43-9 |
2026-10-18 |
Session user names enter the audit details of host.connect only as lists of the value kind auditing.OSUserNames under the members hd6_users, hd9_users, hd10_users, hd11_users, session_users_added and session_users_removed, each name in the form of host_session_users.os_user; those members take nothing else (19 SEC-AUD-5). |
M2-39's OSUserName is accepted only under os_user; a closed set of members with their own form keeps free text out of the details while naming the accounts T-TM-14 needs. |
| D-M2-43-10 |
2026-10-18 |
The host service may report only client_closed, process_exited (with exit_code), idle_timeout, detach_expired, open_failed (with any close_detail) and host_policy_violation (with subscription_login_detected, helper_untrusted or shared_location); close_detail exactly with the last two, exit_code only with process_exited. |
The other reasons are decisions of the server (revocation, limits, host state); a host that reports them is wrong or forged and gets validation_failed without a change. |
| D-M2-43-11 |
2026-10-18 |
The server closes with 4009 when no heartbeat arrived for terminal.host_heartbeat_interval times (terminal.host_heartbeat_miss_limit + 1); heartbeat_ack carries jwks whenever the set of purpose terminal changed since the last set sent on the connection. |
The extra interval absorbs the first delay, as M1-64 rules for the gateway; a scheduled rotation publishes the next key agent.jwks_max_age before use, so a long-lived connection learns it in time (limit after an emergency rotation in 24 5.3). |
| D-M2-43-12 |
2026-10-18 |
When a control connection ends without a state change of the host (not 4110, 4111, 4112) and no newer connection of the host is registered on the node, its active sessions become detached; at shutdown a stop step of PhaseConnections detaches and closes every connection with 4013. |
24 6.2: the PTYs keep running until terminal.detach_grace; a close that comes with a state change already ended the sessions. |
| D-M2-43-13 |
2026-10-18 |
The host service reacts per close code: 1000, 1001, 4000, 4002, 4009, 4014, 4113 and any unknown code reconnect with exponential backoff and full jitter between host_service.reconnect_backoff_base and host_service.reconnect_backoff_cap, reset after ready; 4013 after a random delay below the base; 4008 after retry_after, else the cap; 4111 ends all PTYs and waits the cap; 4110, 4112 and 4114 end all PTYs and stop with exit code 78. |
The codes of 24 13 name the client action; 78 is the one exit code no service definition restarts (D-M2-42-14). |
| D-M2-43-14 |
2026-10-18 |
On a refused start the host service logs in through the same Control with empty live_sessions, retries for at most host_service.reconnect_backoff_cap in total (then startup.ErrUnreachable, and the stored declaration decides), sends exactly one report and closes with 1000 over the same connection. |
D-W-91: the console must name the accounts of a refused start; a separate report connection would be a second login with the same keys. |
| D-M2-43-15 |
2026-10-18 |
Recordings in both directions hold the hand-written frames of the host service to the contract: services/host/testdata/control/host_frames.json (written by the host test, validated and its proof verified by the server) and server_frames.json (written by the server test, decoded strictly by the host); UPDATE_RECORDINGS=1 (make -C services/host recordings) rewrites the host recording and go test -update the server recording, because only cmd/bothy reads the environment in the server module. The gitleaks configuration allows the rule jwt for host_frames.json only. |
The host module has no generator; M2-38 does the same for Noise. The recorded proof is signed with a test key whose seed is public. |
| D-M2-43-16 |
2026-10-18 |
GET /api/v1/hosts/connect is the second route marked AuthorizedInBand (action host.connect, class rl.host): the proof and authorize run on the connection. The route is mounted only for the roles api and all, after the gateway, whose hub is the EphemeralPublisher of the presence. |
Like the gateway (M1-64) the upgrade request carries no credential; counting rl.host and failed proofs is M2-46. |
| D-M2-43-17 |
2026-10-18 |
The control client takes the proxy from the environment (http.ProxyFromEnvironment), unlike pair, which uses none (D-M2-42-25). |
The brief (Design note 12 a) names it; a long-lived outgoing connection of a host behind a company proxy needs it, and the TLS verification of the server stays end to end. |
| D-M2-43-18 |
2026-10-18 |
startup.Events is the optional part of a Control that keeps its connection: run passes every later ready to Service.OnReady and exits with 78 when the connection stops for good. |
M2-42 offers OnReady for a second ready (TestDeclarationFromReady) but no path from the port to it. |
| D-M2-43-19 |
2026-10-18 |
The two sections of docs/api/openapi.yaml that M2-43 changes stay minimal (no description on heartbeat_miss_limit and HostHeartbeatAck.jwks), and the description of HostDoctorCheck.session_user refers to its schema instead of repeating it; the rules stand in 24 3.5. |
The file was 2 bytes below repo.max_file_kb; the content is unchanged. |
| D-M2-43-20 |
2026-10-18 |
Base without a contract commit: the base int/m1-wave42 already holds HostReady.llm_credentials, HostIdentify.live_sessions and the planning rulings that M2-43 relies on (D-W-90, D-W-91, D-W-109 as content of 24 and openapi); the brief, its spec chapters and the core contract are read from origin/main. |
The task asks to bring a missing contract change in as its own commit only when the base lacks it. |
| D-M2-43-21 |
2026-10-18 |
pair --ca-file stores the CA in the state directory (server_ca.pem, public data; a pairing without --ca-file removes it), and run trusts it for the control connection in addition to the system roots. |
Red team of M2-43: a server with a certificate of a private CA could be paired but never reached by the control connection, so the host never reported and never became usable. |
| D-M2-43-22 |
2026-10-19 |
Integration fix INT-W45/3: In the test build (tag contract_validate) the G8 response check passes requests that a route with AuthorizedInBand serves: Chain.Wrap records the pattern of such a route and installContractCheck hands a request matching one of them to the handler unchecked. Test TestG8_SkipsInBandRoutes. |
With M2-43 the plain GET /api/v1/hosts/connect of TestServeRolesStartReadyAndStop answers 426, and the checker matched it to getHost (GET /api/v1/hosts/{host_id}) because the in-band route is no contract operation; the violation ended the internal/wire test binary under make check (reproduced on the M2-43 branch alone). The production build has no G8 and the server's mux already prefers the exact path, so only the check changes. |
| D-M2-43-23 |
2026-10-21 |
Port to main: the ticket's own squash plus D-M2-43-22, without the other rulings of its integration wave. The entry scan of the host doctor keeps the form already on main (RunFull takes the environment snapshot before the scans start and passes it to scan), so the doctor part of the squash is dropped. The doc changes of the ticket are applied to the English chapters and the ADR on main. |
main already solved the race the squash addressed in the doctor; the rest of the wave belongs to other tickets. |
| D-M2-43-24 |
2026-10-21 |
The single-method interfaces wsrelay.ReadySource and app.TerminalKeys keep their names and carry NOSONAR for Sonar rule S8196. |
Each names what the use case reads from (the source of ready, the signing keys of purpose terminal), like instance.Keys; an -er name would describe neither. |
| D-M2-43-25 |
2026-10-21 |
Later ready frames reach the ready handler of the host service through one worker: a ready that arrives while the handler runs replaces an older waiting one, so the handler never runs twice at once and the newest declaration is applied last. Test TestLaterReadiesApplyInOrder. |
One goroutine per frame let an older declaration (for example personal after organization) be stored and evaluated by HD6 last (review finding on the pull request, test red before the fix); calling the handler in the reader loop would delay heartbeats while HD6 runs. |
| D-M2-43-26 |
2026-10-21 |
After a later ready the host service takes the connection as its live one before it sends the first report, and every report of a connection sends the current report under one lock of that connection. Test TestReportAfterReadyOnlyOnTheLiveConnection. |
A Report between the first report and the delivery of ready found no live connection and only stored the change, so the server kept the older report until the next reconnect (CI failure of TestReportIsFirstFrameAfterReady on the pull request, reproduced by holding the client lock); the lock keeps the newest report last when both send at once. |