| D-M2-44-1 |
2026-10-19 |
ProfileKnown: the profile must be one the host reports, and either its kind is not claude and no workdir_index is sent, or its kind is claude and 0 <= workdir_index < len(own workdirs), where the own workdirs are the list of the caller's own entry in reported_session_users (the entry whose os_user is the caller's valid assignment; none gives the empty list). A claude profile without an index and an index with another profile both give terminal_profile_unknown. |
07 4.1 names one reason for "an index of no working directory of the own session user"; one rule in one place (domain.ProfileKnown). |
| D-M2-44-2 |
2026-10-19 |
The session token is built and signed inside the unit of work of the opening and of the attach, before the commit; a key store failure rolls the session back (503 unavailable). |
No session exists without a token. The sequence diagram seq-8 shows the order of meaning, not of transactions. |
| D-M2-44-3 |
2026-10-19 |
Token encoding: the claims in the order of the table of 24 5.3, IDs as canonical lower-case UUIDs, iat truncated to seconds, exp = iat + terminal.session_token_ttl, own always present (also false), wdi present exactly for a claude profile with att = open (also with 0), no null. The protected header has exactly the members alg, kid and typ; kernel/jose.SignCompact writes them in its own order (alg, typ, kid). |
The host service compares members, not bytes; the shared JOSE profile (ADR 0067) owns the header serialization. |
| D-M2-44-4 |
2026-10-19 |
One data connection per grant: every grant (open or attach) replaces the relay slot of its session and sends one session.open; the host service opens exactly one data connection per session.open. An attach to an opening session issues att = open with the wdi of the opening, which the relay slot of this node keeps (03 does not store it); without that slot the attach answers 409 invalid_state_transition. 24 6.2 changed accordingly. |
A new handshake always runs on a fresh pair of connections, so the host service never has to tell a new message 1 from transport data; 05 allows an attach to every session that is not closed and 24 5.3 allows wdi only with att = open. |
| D-M2-44-5 |
2026-10-19 |
Relay: the first message of each direction since a grant is the handshake and is not counted; every later message adds its length to bytes_in or bytes_out, a device message sets the time of the last device frame; when a pairing ends for another reason than the end of its session, the counts are written in one update without event or version; the end path takes the live counts (RelayControl.Counters takes and clears them, so they are written once). The deadline of a slot is met by session.started before the PTY runs and by the pairing afterwards; on a host that reports require_local_confirm the pairing meets the deadline of an opening, and a pairing that ends before session.started gives the slot a new deadline of terminal.open_timeout. The relay logs one record per closed side with session ID, side, frames, bytes, duration and close code. |
The server cannot see the handshake; handshake messages are no transport bytes (03); the consent window host_service.local_confirm_timeout may be longer than terminal.open_timeout; SEC-TERM-1 allows only length, time and session ID. |
| D-M2-44-6 |
2026-10-19 |
The host service refuses a handshake by closing its data connection with 4101 (token_expired, token_replayed) or 4104 (every other detail of 24 5.3) and the close_detail as close reason; the relay hands the same code and reason to the device; an opening, and a started session refused with 4104, end open_failed with the detail; a started session refused with 4101 is detached. A data connection for a session that is unknown, of another host or closed gets 4100; without an expectation on the node or while a paired host side exists 1000; a newcomer replaces an unpaired older host side (1000 for the older). |
24 5.3 said the host reports the reason but not how; a refusal through session.ended reaches the device through StreamCodeForEnd with the same code, so the device gets exactly one code either way (R-TM-16). |
| D-M2-44-7 |
2026-10-19 |
The effects of an end after its commit come from the event log tail: internal/wire hands every event after Authorizer.Observe and the hub to Relay.ObserveLogEvent, which closes the sides of a session whose terminal_session.changed says closed (device StreamCodeForEnd, host 4100), drops the slot without writing counters and sends session.close. |
txn has no after-commit hook, and ends commit in transactions of terminal, authz and identity, in api and worker processes and on other nodes; the tail reads every committed row in seq order. |
| D-M2-44-8 |
2026-10-19 |
After the commit of an opening or an attach the use case calls authorize again with the same action and fresh facts under authorize.Recheck(ctx) (implies filter mode, passes step 8 only); HostConnected keeps the value of the first decision. When it no longer allows, the session ends with access_revoked, i6_violation (i6_holder_only) or the reason of the host state (host_disabled, host_retired, host_rekeyed), no session.open is sent and the answer is the recheck's error. |
03 6.3 locks permission_version only for rights changes, so a revocation can commit between the decision and the commit of the opening; the recheck can only end what the decision allowed. |
| D-M2-44-9 |
2026-10-19 |
Start of the roles api and all: before the public listener opens, every opening session ends open_failed open_timeout and every active session becomes detached, one unit of work per session in ascending id. The key cluster.mode does not exist on this base (planned with M2-47), so the step always runs; M2-47 adds the key and skips the step with multi. Stop: the stop step terminal_relay_connections in PhaseConnections closes every relay connection with 4013. |
Slots and connections lived only in the memory of the previous process; a single node is the only mode this build runs. |
| D-M2-44-10 |
2026-10-19 |
listTerminalSessions orders by id descending with a keyset cursor on id. With host_id the collection check is terminal.read_own on that host; without it the catalogue has no instance resource for the action, so the check runs in filter mode on a descriptor of an own session that no host holds: the gate and the liveness of the principal answer, not_found only means that the rows decide, each through its own check. |
03 has no index for all sessions of a person and the core contract is not edited; M2-40 gives terminal.read_own the resources terminal_session and host only. |
| D-M2-44-11 |
2026-10-19 |
closeTerminalSession: the reason passes textcheck.DisplayText and has at most audit.reason_max_chars characters, else 400 validation_failed at /reason; it is the mandatory reason of the action and stands only in the audit details (reason). An own session ends client_closed, another person's (owner) closed_by_owner, with closed_by; an already closed session answers 200 unchanged with no_change: true. |
03 allows free text in audit_events.details only as the mandatory reason of an administrative action; 06 2 "No effect". |
| D-M2-44-12 |
2026-10-19 |
Close codes of the device stream before the relay: invalid JSON, an unknown member, a binary frame before identify and protocol_version other than 1 give 4002; no identify within terminal.open_timeout, an invalid token, proof, nonce, htu or device_id, identify without access_token and any forbidden other than not_self give 4004; unauthenticated with principal_inactive, session_revoked or device_inactive 4012; not_found and an own closed session (reason = its close_reason) 4100; not_self 4103. A device without an expectation on this node gets 4102. Authentication failures are recorded as a denial of terminal.stream for the anonymous principal on the session (aggregated, AZ-P13). |
Every code is one of terminalStream; the denial of a stream that never authenticated has no principal to name. |
| D-M2-44-13 |
2026-10-19 |
Base without a contract commit: int/m1-wave45 holds the claim table of 24 5.3 with own, osu and wdi, the schemas TerminalSession*, TerminalStream* and HostIdentify.session_id, the purpose terminal and the five terminal.* actions; the rulings D-W-62, D-W-63, D-W-69 and D-W-105 the brief names are planning rulings whose content is already in 24, 07 and signals.md there. 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-44-14 |
2026-10-19 |
docs/api/openapi.yaml stays at most repo.max_file_kb: the descriptions of openTerminalSession, workdir_index and TerminalStreamIdentify are shortened while the brief's additions (the client_ephemeral_key_hash pattern, the attach to an opening session, the rule of workdir_index) are in. Superseded in part by D-M2-44-18: on main the descriptions of openTerminalSession and TerminalStreamIdentify keep the text of main. |
The file was 18 bytes below the gate; the rules stand in 24. |
| D-M2-44-15 |
2026-10-19 |
Integration tests run on the full-server harness (test/harness) instead of the M2-39 host world: real access tokens, DPoP proofs and device-key step-ups, H1 paired and confirmed through the API. The harness gains Options.TerminalTimers (passed as wire.Deps.TerminalTimers, also the deadline of identify) and Client.Proof; api/specschema.ValidateChannelFrame gains the channel terminalStream. Windows of the step-up and the quick reattach are moved in the database (step_up_at, detached_at) instead of a clock. |
The device stream must authenticate a real access token with a real proof; the fake authenticator of the host world cannot. No test sleeps for a window. |
| D-M2-44-16 |
2026-10-19 |
terminal.Register lists every op of terminalStream and hostData (hello, identify, error, the Noise frames), in both directions. |
The brief asks for all of them for AZ-P2; M2-43 listed only the ops a host sends on hostControl. |
| D-M2-44-17 |
2026-10-19 |
The test seam Deps.BeforeOpenCommit lets a unit test commit a revocation between the decision and the commit; the race is proven with fakes in app (TestRecheck_EndsSessionWithMappedReason), not over HTTP. |
The full-server harness cannot reach into a use case; the recheck itself is the same code either way. |
| D-M2-44-18 |
2026-10-21 |
The ticket was built on an integration branch and lands on main as a port: one branch from main with only the ticket's own commits and the integration fix for it (the canary scenario step for the terminal operations), adapted to main, one pull request, squash merge. On main the REST contract falls under api.contract_max_file_kb, so the descriptions of openTerminalSession and TerminalStreamIdentify keep the text of main (D-M2-44-14 no longer needs it shortened). |
Integration branches are retired; every ticket lands on main on its own. |
| D-M2-44-19 |
2026-10-21 |
The canary scenario gets the step terminalSessions (test/canary/scenario_terminal.go): the owner creates, pairs and confirms a host with canary name and reasons, a Fake-Host of test/terminal/fakehost connects it, and the owner opens, lists, reads, attaches and closes a shell session with a canary close reason; the session tokens of both grants are registered as secrets. Test code only. |
TestSEC_T10_ScenarioCoversEveryEnabledOperation requires the scenario to call every enabled operation, and this ticket enables five terminal operations (the integration fix INT-W49/3 of the ticket). |