Skip to content

M2-41 Pairing, confirmation and re-pairing of hosts

No Date Ruling Reason
D-M2-41-1 2026-10-15 A pairing code is stored as HMAC-SHA-256 with the pepper of auth.token_pepper_ref over the full code string, the same derivation as api_tokens.token_hash; identity.Tokens.HashHostPairingCode parses form, checksum and kind hpc first and returns identity.ErrTokenMalformed before any HMAC. One derivation for every token of ADR 0061; the prefix bothy_hpc_ already separates the kinds, so no extra label is hashed.
D-M2-41-2 2026-10-15 One issuer for createHost, issueHostPairingCode and the fallback of confirmHost: app.Service.issueCode over PairingCodes.Issue (bound to identity.Tokens.NewHostPairingCode). Case a of the brief: M2-39 already minted hpc codes through domain/apitoken and the identity port, so its names stay. The format and the hash derivation exist once.
D-M2-41-3 2026-10-15 pairHost runs its write as the rights change host.pair (with the people of the valid assignments), although 05 gives host.pair Permissions no. 03 counts reported_session_users as rights state (D-M2-40-9), so the rights version rises first.
D-M2-41-4 2026-10-15 A redeemed code leaves pairing_code_hash and pairing_expires_at NULL. The index on pairing_expires_at serves only pairing, and a used code must never match again.
D-M2-41-5 2026-10-15 The key and report rules of pairHost with their errors[].code values: invalid_key (Noise key not strict base64url of 32 bytes or a low-order point; JWK kty, crv or x wrong), unknown_member (any JWK member besides kty, crv, x), empty, duplicate, too_long, pattern_mismatch, limit_reached, invalid_format, the textcheck rules for labels; every finding is returned, in body order. The host service is untrusted input, and a host without session users can never give a terminal (HD1).
D-M2-41-6 2026-10-15 Only a well-formed prefix (upper-cased, exactly terminal.fingerprint_confirm_chars characters of the Crockford alphabet) counts as an attempt; I, L, O and U are not mapped. A typo of the form must not burn an attempt; the client may normalise before sending.
D-M2-41-7 2026-10-15 409 fingerprint_mismatch of confirmHost keeps its effect under an Idempotency-Key: the handler calls idempotency.KeepEffectOnFinal, and the stage stores the answer with Store.Complete in the same unit and commits; only this answer is ever marked, a marked 401 or 5xx still rolls back. The new problem member attempts_remaining is carried by errs.FingerprintMismatch through the existing errs.ProblemExtender. Every failed attempt must count also under a key (TM-T19); screen A8 (M4-37) shows the remaining attempts and no other field carries them.
D-M2-41-8 2026-10-15 confirmHost compares the prefix before anything of the body that needs the database (the people of session_users, the credential holder, the assignment rows); it writes one confirm audit entry that also carries the declaration, plus one session_user entry per assignment, all with the same correlation. A replay with the same key gets the stored answer and never burns a second attempt. A wrong host must never reach an assignment or a declaration.
D-M2-41-9 2026-10-15 The fallback after terminal.fingerprint_max_attempts failures: state pairing, the three key columns NULL, key_fingerprint kept, pairing_code_hash the HMAC of a fresh code that is never shown with pairing_expires_at now, fingerprint_failures 0, the next version, host.updated with key_changed. 03 demands a hash exactly in pairing, and no code may work until the owner issues one.
D-M2-41-10 2026-10-15 Dropping keys keeps key_fingerprint; Host and host.updated omit key_fingerprint and noise_public_key while the host has no keys; pairHost with the retained fingerprint is 409 conflict host_key_reused, with the login key of another host host_key_in_use. Refuses a re-sent key pair (T-TM-05). Honest limit: the server keeps no dropped key, so a host service that keeps one of its two keys is not detected; M2-42 generates both anew.
D-M2-41-11 2026-10-15 The visibility of host adds: any principal when the requirement of the action is exactly Public (only host.pair). The pairing code is the proof, checked by the use case before authorize, as for the refresh token.
D-M2-41-12 2026-10-15 key_changed is set exactly when stored keys were dropped (re-pairing from confirming, active or disabled, and the fallback); key_generation rises only at pairHost. Audit details: pairing_code with from_state, to_state, old_fingerprint, key_generation, sessions_ended, reason; confirm with result, fingerprint, key_generation, attempts_remaining, llm_credentials, credential_holder_id, declared_server_host, reason; host.pair with fingerprint, old_fingerprint, key_generation, platform. Fingerprints as compact lower-case codes; the host is the resource, not a detail; from_state and to_state follow M2-39 instead of the brief's state_before and state_after. Devices pin keys by generation (24 5.5); one vocabulary for state changes in the audit.
D-M2-41-13 2026-10-15 New keys terminal.report_label_max_chars (default 100) and terminal.report_max_entries (default 64) of chapter 24; the start check fingerprint_keys requires terminal.fingerprint_confirm_chars between 1 and the length of the compact fingerprint and terminal.fingerprint_max_attempts at least 1. Limits only through named keys; M2-43 reuses the report keys for report.
D-M2-41-14 2026-10-15 The job terminal.sweep (and its clause on expired codes) is not built here; an expired code is rejected at redemption, and its hash stays while the host is pairing. No ticket row names the job; follow-up.
D-M2-41-15 2026-10-15 Names that differ from the brief: PairingCodes.Issue instead of New; the port methods take actor.Actor and commands carry terminal.Precondition (D-M2-39-16); RepairEffects(state, disabledReason); field errors are errs.FieldError; Cache-Control: no-store comes from the chain for every API answer, not from the handler; issueHostPairingCode declares Idempotency-Key in the contract and marks its answer as a one-time secret like createHost instead of x-idempotency-exempt; TestPairingRoutes_AZP1 is part of TestAZP1_HostRoutes; StorePairing writes reported_session_users with the encoder of the report of M2-40 in the pairing statement instead of calling ApplySessionUserReport, which would write a second version and event; the instance id comes from instance.State.Load; the test world makes Alex an owner by a fixture row; 08 section 12 already named the kind hpc. The merged code of M1, M2-39 and M2-40 wins over the brief (Boundaries).
D-M2-41-16 2026-10-15 The spec chapters on this base are German, so the changes to 02, 06 and 24 are written in German like those of M2-40; the planning rulings D-W-65, D-W-101 and D-W-102 of main change no contract and are not ported. Working directory labels are part of Host.reported_session_users, so they appear in the hosts row and in host.updated; TestPairingNeverLeaksSecrets checks the audit, problem bodies and logs for them. Follow the language of the chapter; the brief's leak list would contradict the Host schema of M2-39.
D-M2-41-17 2026-10-19 Port onto main from the integration squash of M2-41 on top of M2-40: the spec additions to 02, 06 and 24, the threat model row and ADR 0061 are written into the English documents of main (this supersedes the language part of D-M2-41-16), the terminal README and the requirement label are English, and the generated files (REST, routes, SDKs, configuration reference, config schema) are regenerated. main is English; the integration branch still carries the German chapters.