| D-M2-48-1 |
2026-10-21 |
Four layers and one catalogue: the e2e packages under services/server/test/terminal/e2e (conformance, hostpolicy, limits, twonode, restart, hostplatform) run the scenario files in make check, TestChaosTMT16 runs tm-t16.yaml under load in make perf, the platform suite runs host-local on disposable machines, and the earlier tests of M2-38 to M2-47 stay where they are as catalogue runs of kind unit. M2-46's phrase that this ticket "turns them into its data-driven suite" means that the suite repeats them end to end against the real host service; no test is deleted. |
One stack per package keeps each package inside the default go test timeout and lets the packages run in parallel; deleting module tests would lose their fast red runs. Place: services/server/test/terminal, catalogue.yaml. |
| D-M2-48-2 |
2026-10-21 |
Stack: the installation file infra/compose/compose.yaml unchanged with M1-73's smoke overlay, for twonode M2-47's overlay, then the test overlays compose.terminal.yaml and compose.terminal-twonode.yaml; the server image is built from the worktree with services/server/Dockerfile for the platform of the Docker host and tagged termsuite-server:<12 hex of the image ID>; a test-only TLS front (nginx-unprivileged at BOTHY_NGINX_VERSION) is the only published port (127.0.0.1 with a random host port) and switches routing by replacing routing.conf and nginx -s reload; the host containers sit on the internal network termhosts, which reaches only the front; TERMSUITE_TRUSTED_PROXIES is the subnet of <project>_default read after compose up --no-start. |
A server inside the test process would have to bind outside loopback for the containers (P-10), and only the image carries the key store directory that M1-32 prepares for the named volume; the front stands where the operator's edge stands in the profile direct, so DPoP htu, Origin and server.public_url stay those of the instance. Place: services/server/test/terminal/stack, infra/compose/test. |
| D-M2-48-3 |
2026-10-21 |
Host containers: infra/compose/test/host/Dockerfile (test only) on Debian trixie-slim pinned by digest with APT pinned to one snapshot.debian.org timestamp, systemd as process 1, privileged only on the service termhost of the test overlay, the marker file /etc/termsuite-disposable, and the unprivileged default user nobody in the image with user: "0:0" on termhost (Trivy's DS-0002 names no line, so the inline ignore the brief names cannot take it; make trivy stays green without an exception). Processes that must never start (hooks, apiKeyHelper, another claude, a foreign node) are marker programs that append their name to /var/tmp/termsuite-markers; a case passes only with that folder empty. The image also carries strace, gdb, python3-minimal and netcat-openbsd, because without them the platform tests of TM-T25, TM-T26 and the admin endpoint counted "command not found" as a refusal. |
systemd as process 1 needs a writable cgroup tree, and bothy-host install creates real users and units (24 2.2); the snapshot makes the package set reproducible; a marker leaves a trace for any execution through any path and as any user. Place: infra/compose/test/host/Dockerfile, stack/host.go. |
| D-M2-48-4 |
2026-10-21 |
Device: HTTP, DPoP and WebSocket live in services/server/test/terminal/device; Noise and frames run in the sidecar services/host/test/noisedevice (JSON lines on stdin and stdout, one process per device session), which imports the host service's internal/noise and internal/frame unchanged. The harness gains harness.External for a server the suite did not start, additively. |
Go forbids importing services/host/internal from the server module, and a requirement on the host module would pull it into the server's build; TM-T01 and TM-T02 keep proving the same implementation against independent vectors and the Dart package. Place: device, test/noisedevice, test/harness/external.go. |
| D-M2-48-5 |
2026-10-21 |
Catalogue and scenarios: catalogue.yaml (format: 1, one entry per ID with runs, implemented_by, deferred, deferred_parts, threats) and one strictly decoded scenario file per ID; each case gets its own world (fresh people with unique handles, its own Owner, its own host containers) and runs with t.Parallel(), bounded by a test constant on host containers starting at once. Expectations are literal data from 24 14 and the chapters it names. |
One world per case keeps cases from disturbing each other while they run in parallel; literal expectations keep the suite independent of the code under test. Place: suite, scenarios/. |
| D-M2-48-6 |
2026-10-21 |
Notes per scenario: relay faults are produced at the device end (TM-T03 sends the recorded message 1 of the first opening on the stream of a quick reattach; the relay forwards it unchanged); TM-T04 forges a token of typ agent-invocation+jwt with the stack's own active terminal key, copied with docker compose cp into a file of mode 0600; TM-T11's manipulated host is M2-44's Fake-Host paired through pairHost and connected through the TLS front; three TM-T10 kinds stay unit runs (the report without the name, the report naming the server host, the end of a login session by expires_at), and session-user remove expects host_restarted (R-TM-27); TM-T28 (c) writes host_session_users and sequence_counters.permission_version with sql steps marked bypass: true, the only writes outside the API. |
Each is the smallest way the real stack can produce the fault the row names; the three TM-T10 kinds cannot be produced through the TLS front or without shortening auth.session_max_age for the bootstrap owner. Place: scenarios/tm-t03.yaml to tm-t28.yaml. |
| D-M2-48-7 |
2026-10-21 |
TM-T15 runs through the TLS front in the modes hash (M2-47's example edge) and split (device stream to api, host data to api-2); TM-T16 runs once per make check (e2e/restart) and with parallel sessions in make perf (TestChaosTMT16, the stack's PostgreSQL with the image defaults). |
The front is the one place that can route the two sides of a session to different nodes without changing M2-47's edge; the PLAN row names TM-T16 in both gates. Place: e2e/twonode, e2e/restart, test/chaos/terminal_test.go. |
| D-M2-48-8 |
2026-10-21 |
Platform runs: make -C services/host platform-bundle OUT=<dir> builds the bundle without running it; platform-test builds the bundle of the build host and runs it; the suite's TestMain refuses without the marker file (exit 2); in make check e2e/hostplatform runs the Linux bundle as root in a fresh host container. Without runs on disposable macOS, Windows 10 and Windows 11 machines the ticket's verdict is BLOCKED platform runs missing and a later run only adds the runs. |
The PTY and doctor tests need administrator rights and change users and services, which is safe only on a disposable machine; one bundle keeps the local, container and machine runs on the same files. Place: services/host/Makefile, test/platform/guard.go, e2e/hostplatform. |
| D-M2-48-9 |
2026-10-21 |
Docs gate: tools/docs/terminal_catalogue.py (run by make docs) holds the catalogue equal to the table of 24 14, the PLAN row and exit criterion 8 (every ID with a run that make check executes, TM-T16 also perf, the doctor matrix with platform runs for macOS, Windows 10 and Windows 11), threat model 6.7 (every threat names a test that exists) and every ref. |
Without a gate the catalogue, the chapter and the threat model drift apart, and a threat could name a test nobody runs. Place: tools/docs. |
| D-M2-48-10 |
2026-10-21 |
CI path filter: GATE_COMPONENTS["server"] gains infra/compose/ and infra/otel/; docs/quality/gates.md states that the Docker runtime of the GitHub-hosted runners must start privileged containers on cgroup version 2 with kernel.yama.ptrace_scope at least 1. |
The suite reads both folders, which selected no component before, so a change there would skip the server gates that run it. Place: tools/ci/components.py. |
| D-M2-48-11 |
2026-10-21 |
Non-conformances found by the suite are fixed in the owning modules with a regression test that was red first (Design note 13): the audit of a denied listTerminalSessions (TestList_DenialIsAudited), session-user remove stopping the host service before the helper (TestSessionUserRemove), the relay closing the device with the end's code instead of 4102 when the host side ends with the session (RelayHostLost, TestRelayDecide), the ready after confirmHost sent after the outermost commit (TestReadyResentAfterConfirmCommits) and read and queued in one order per link (TestReadyQueuedInReadOrder), and a declaration arriving during a doctor run kept by the runner (TestSetDeclarationDuringRunKeepsDeclaration). |
Each was reproduced end to end (TM-T17, TM-T10, TM-T29 hosts that never offered claude); none needs a change of the core contract. Place: internal/terminal, services/host/internal. |
| D-M2-48-12 |
2026-10-21 |
Names and places of this branch win over the brief (D-W-100): the base carries docs/spec/02-architektur.md, docs/adr/0068-terminal-ueber-host-dienst-mit-noise-strom-und-invariante-i6.md and German section titles, so the added text is English inside these files, as M2-45 to M2-47 did; the stack's identifiers avoid the product name outside the places of 02 6 (Binaries.HostService, Host.Service). |
The task bases the ticket on the integration branch, not on origin/main; the product-name gate rejects other identifiers. Place: the docs of commit "the conformance suite in the spec". |
| D-M2-48-13 |
2026-10-24 |
The server image gets its empty data folders /var/lib/bothy/signing-keys and /var/lib/bothy/files (owner 65532, mode 0700) from the build stage, where owner and mode are set, by copying their parent folder; this replaces COPY --chown --chmod of each folder (M1-23/5). |
The terminal suite builds the image with the default builder of the runner. BuildKit 0.20 (Docker 28) applies COPY --chmod of a folder only to its contents, so both folders stayed at 0755 and the server refused to start. Copying the parent keeps the owner and mode of both folders on every BuildKit; TestSigningKeyDir checks them. |
| D-M2-48-14 |
2026-10-24 |
An opening that the job terminal.sweep ends with open_failed and open_timeout closes its lone host data connection with 4102, like an end at open_timeout committed on another node (the rule of sessionEnded in the relay); TM-T11 open_timeout in test/terminal/deadlines_test.go expects 4102 on the data connection instead of 4100 open_failed. |
24 9 "The lone side and the phase rule": in opening a lone host data connection closes with 4102. The relay cannot tell an end of the sweep from an end of the node of the other side, and TM-T15 needs the lone side of the other node to close with 4102 and count as unpaired. The session itself still ends open_failed with open_timeout, with one audit entry, one event and session.close at the host. |