| D-M2-42-1 |
2026-10-17 |
pair and doctor run with the identity of the service user: directly when the caller owns the state directory; through the admin endpoint of the running service when an administrator calls them; with the service stopped, on Linux and macOS in a child process with the credentials of the service user, on Windows doctor starts the service once with the start argument --doctor-once (one full run, its answer on the admin endpoint, exit 0, no server, no session) and pair requires the running service and otherwise exits 2 naming bothy-host restart. Every command that needs administrator rights and lacks them exits 2 with bothy-host <command>: administrator rights required. |
Keys under DPAPI and files owned by the service user can only be written in its context, and places for all must be resolved by the service user (24 2.2 point 6). |
| D-M2-42-2 |
2026-10-17 |
Commands install [--session-user <name>] [--drive-root <drive>] [--drive-root-confirm yes\|no] [--config <path>], session-user add\|remove <name>, restart, pair --server <url> --code <code> [--ca-file <pem>], doctor, run, pty-helper --session-user <name>; exit codes 0, 1 (a failed check, a runtime failure, a bound name), 2 (usage or environment), 78 (refused start, EX_CONFIG). The drive root prompt reads the terminal; the flag answers it, and without both the answer is no. Local output of doctor: one line per entry in the order HD1 to HD11 (HD<n> ok, WARN <code>, FAIL <code> with session_user=, user=, drive=, item=), then doctor: <failed> failed, <warnings> warnings counting checks, then declaration: <value>. |
24 2.2 "Output on the host" names the content, not the form; 78 is what no service definition restarts. |
| D-M2-42-3 |
2026-10-17 |
One YAML configuration file nested like the key names, decoded strictly with go.yaml.in/yaml/v3 (unknown and duplicate keys and wrong types name the key); durations in the grammar of the server. Paths: Linux /etc/bothy-host/config.yaml, macOS /Library/Application Support/bothy-host/config.yaml, Windows %ProgramFiles%\bothy-host\config.yaml. New key host_service.auth_status_timeout (default 30s) bounds claude auth status and the login shell probe. An invalid file makes run log config_invalid with the key and exit 78; the other commands exit 2. |
24 and 16 named keys but no file; the bound of the probe was missing; a service manager must not loop on a broken file. |
| D-M2-42-4 |
2026-10-17 |
The state directory also holds pairing.json (server URL, host ID, instance ID, fingerprint) and declaration.json (the last llm_credentials of ready), both without secrets, and on Linux and macOS admin.sock; every file is written atomically with mode 0600, on Windows inheriting the protected DACL. |
run needs the server and the declaration before a login; neither is a secret or a subscription login. |
| D-M2-42-5 |
2026-10-17 |
No cgo on any platform. Every platform behaviour sits behind the interfaces of internal/hostos; an ACL entry that cannot be read, parsed or mapped counts as granting every right to every session user. The mode of a symbolic link is ignored; a link is checked by its owner and its folder. macOS ACLs are read from /bin/ls -led, because golang.org/x/sys/unix has no decoder for ATTR_CMN_EXTENDED_SECURITY. |
One right too many only blocks, one too few opens a place; Linux and macOS do not use the mode of a link. |
| D-M2-42-6 |
2026-10-17 |
The codes of doctor are the table "Codes of doctor" in 24 2.2. HD3 has no probe on Windows and passes without code. HD9 lists the homes of root, the service user, every former session user and every person account (Linux UID_MIN to UID_MAX of /etc/login.defs with the documented defaults, macOS UniqueID from 501, Windows every profile under ProfileList); all other system accounts are skipped. |
Elevation on Windows needs administrator credentials and HD2 covers Administrators; system homes such as /usr/sbin or /var/empty are listable by design and hold no files of a person. |
| D-M2-42-7 |
2026-10-17 |
Path resolution walks the path like realpath, records every folder of the result, every component as written (.. kept) and every link with its folder; a missing path, a dangling link, a closed folder or more than 40 links is unresolvable. Identity is dev:ino, on Windows the volume serial number and the 128-bit file ID. |
24 2.2 point 6; a component that resolution skips is still a place a person could change. |
| D-M2-42-8 |
2026-10-17 |
HD10 adds the code helper_unreachable (named with the session user) for a helper whose channel cannot be opened; a setuid or setgid bit on the binary is helper_untrusted; a #! script checks its interpreter, and for /usr/bin/env the first name the search list finds; a missing interpreter makes host_service.claude_bin count as empty. |
An unreachable helper gives no result, and missing results must not pass (fail closed). |
| D-M2-42-9 |
2026-10-17 |
auth_status_unknown (a value outside the six methods, no JSON, a timeout or an exit code other than 0 and 1) counts like a finding; before any ready a finding is the warning declaration_unknown. |
RK-14: an output that cannot be read fails closed; without a declaration no session can start, so a refusal would only hide the names. |
| D-M2-42-10 |
2026-10-17 |
The chain files stay CLAUDE.md, CLAUDE.local.md, AGENTS.md, .mcp.json, .claude and .git: the memory page (retrieved 2026-10-17) names CLAUDE.md, CLAUDE.local.md, .claude/CLAUDE.md, AGENTS.md and .claude/AGENTS.md above the working directory, all inside the list; the CLI reference confirms the JSON and the methods of claude auth status. |
Brief design notes 9 a and 10. |
| D-M2-42-11 |
2026-10-17 |
Peer check: the helper passes one end of a socketpair over the activated connection (SCM_RIGHTS) and speaks only after the first message of the service; the service compares uid and PID of that end with the session user and the main PID of the helper service; on Windows the pipe is created with FILE_FLAG_FIRST_PIPE_INSTANCE and PIPE_REJECT_REMOTE_CLIENTS. The list of platforms with a proven peer check is empty; HD10 fails with peer_check_unavailable until a recorded platform run adds a platform; only the build of make platform-test measures it. |
With socket activation the peer of a connection is the service manager; RK-23 allows no terminal without proof. |
| D-M2-42-12 |
2026-10-17 |
The entry scan runs per session user in its helper after the full run; until its first scan ends the user fails HD11 with entry_scan_running, afterwards the previous finding holds until a new full scan ends; a finished scan sends a new report only after the start was not refused. |
24 2.2 "Check of the entries"; a refused start sends exactly one report. |
| D-M2-42-13 |
2026-10-17 |
Installation: program folders /opt/bothy-host/bin/, /Library/Application Support/bothy-host/bin/, %ProgramFiles%\bothy-host\; Linux useradd --system --user-group for the service user and useradd --create-home --user-group per session user; macOS hidden users through dscl (service user from UniqueID 400, session users from 501, own primary group, not in staff) and createhomedir, the binary signed ad hoc with the hardened runtime unless it already carries the runtime flag; Windows the virtual account NT SERVICE\bothy-host and per session user a local account with a random password handed only to the service control manager, SeServiceLogonRight, CreateProfile and an ACL only for the user and the trusted owners. |
24 2.2 asks the build ticket for the way per platform; the platform suite proves it. |
| D-M2-42-14 |
2026-10-17 |
Exit policy: systemd Restart=on-failure with RestartPreventExitStatus=78, no NoNewPrivileges= and no option implying it; launchd KeepAlive {Crashed: true} with GOTRACEBACK=crash; Windows failure actions on crashes only and a refused start stopped with the service-specific code 78; the DACL of a helper service gives authenticated users only query rights. |
Under no_new_privs sudo -n true fails and hides a sudo rule from HD3; a refused start must not loop. |
| D-M2-42-15 |
2026-10-17 |
run reads the stored declaration, runs the full check, logs in through Control when paired, stores the declaration of ready and evaluates HD6 again; a "start refused" finding sends exactly one report, closes and exits 78; unpaired or without login the stored declaration decides. HostReady gains the required member llm_credentials, and the server sends ready again on every change of the declaration. This ticket wires NoControl, which M2-43 replaces. |
24 2.2 "Message on a refused start"; the host service learns the declaration only from ready. |
| D-M2-42-16 |
2026-10-17 |
pair accepts only https://, trusts the system roots plus --ca-file, sends only public keys and the lists after HD7, stores keys and pairing record only after a 201 whose fingerprint equals its own computation, prints the fingerprint and the host ID, and as an administrator restarts the service afterwards; problem answers print the code, the reason, each field error and for 401 the hint to ask for a new code. |
24 3.2; a stolen or wrong answer must not replace working keys. |
| D-M2-42-17 |
2026-10-17 |
The admin endpoint answers the ops doctor.run (full run, report, the local output and the report entries) and pair.run; on Linux and macOS it is admin.sock in the state directory and answers only uid 0, on Windows the pipe bothy-host-admin with a DACL for SYSTEM and Administrators answers only elevated clients; a client checks that the server process runs as the service user before it sends anything. |
Design notes 1 and 15. |
| D-M2-42-18 |
2026-10-17 |
The helper keeps its end of the passed descriptor open until the client closed the connection. |
On macOS a descriptor in flight whose last reference the sender closes was lost in repeated runs (reproduced in TestPeerCheckPasses). |
| D-M2-42-19 |
2026-10-17 |
Steering ruling 2026-10-15 (no cost): the ticket is built with unit and integration tests only; the platform suite never runs on the build machine, a workstation or shared infrastructure. It is the manual or scheduled workflow host-platform.yml on free GitHub-hosted runners (ubuntu-latest with systemd, macos-latest, windows-latest), which are throwaway machines; Windows 10 has no hosted runner and stays unmeasured. Until measured, the Windows values of 24 2.2 point 6 apply, the default of host_service.entry_scan_timeout stays 10m, the platform suite is a required gate before M2-48, and the terminal stays off on every unproven platform (RK-23). |
The owner's ruling; the brief asked to stop without machines. |
| D-M2-42-20 |
2026-10-17 |
Differences from the brief: the merged M2-38 code had no command table entries and German docs; docs/RISKS.md does not exist on the integration base, so RK-23, RK-84, RK-88 and RK-90 are not updated here; the server integration test fronts the HTTP harness with a TLS reverse proxy, because the harness listens without TLS; createHost already answers with a first pairing code, which issueHostPairingCode replaces. |
Follow the merged code (brief Boundaries). |
| D-M2-42-21 |
2026-10-17 |
The root Makefile runs go vet (typecheck, lint) and golangci-lint (lint) for GO_CROSS_MODULES := services/host with GOOS linux, darwin and windows. |
Code for the other platforms is checked on every build host. |
| D-M2-42-22 |
2026-10-17 |
Windows channels: clients open pipes with SECURITY_SQOS_PRESENT and SECURITY_IDENTIFICATION; the pipe DACLs add OWNER RIGHTS with only FILE_CREATE_PIPE_INSTANCE; a pipe server reads the token of its client by impersonating it at identification level, and a client compares the owner of the pipe (the account that created the server end) and GetNamedPipeServerProcessId with the session user and the main PID, because the token of a helper process gives the service account no access; the process DACL of a helper gives the service account and OWNER RIGHTS only PROCESS_QUERY_LIMITED_INFORMATION; run and pty-helper run under the service control manager from the first step. |
Review of M2-42: without these the second connection, the peer check in both directions and the exit code 78 failed on Windows, and the owner of a helper pipe kept WRITE_DAC. |
| D-M2-42-23 |
2026-10-17 |
HD4 on Windows also accepts SYSTEM or Administrators as owner of the state directory when its DACL holds only the three principals; an existing local account whose helper service exists keeps its password at a repeated install or session-user add. |
Handing the folder to the virtual account needs a restore privilege; a password reset would lose the data DPAPI protects for the user. |
| D-M2-42-24 |
2026-10-17 |
HD6 also reads the folder the login shell exports in CLAUDE_CONFIG_DIR (inside the helper only): a .credentials.json there is a finding, and claude auth status also runs with it. A host_service.claude_bin that fails HD10 counts as empty and never runs. An entry whose owner the entry scan cannot read counts as foreign_entry. The runner starts each program in its own process group, ends the group on the bound and stops waiting for the output shortly after. |
Review of M2-42: a login under an exported folder went unseen, an untrusted program ran in every session user, the scan failed open, and a background job of a login shell stretched the bound. |
| D-M2-42-25 |
2026-10-17 |
The host module carries nosemgrep markers for the rule bothy-go-egress-only at the Unix socket dialers of the local channel and at the HTTP client of pair, and for bothy-go-no-model-credentials in the fake claude of testdata. |
The egress package belongs to the server module and cannot be imported; the dialers reach only Unix sockets on the host; pair calls only the https URL the operator gives, with the system roots plus --ca-file, no proxy and no redirect; the fake reads only its fixture folder. |
| D-M2-42-26 |
2026-10-17 |
Every helper call has a bound: check.run four times host_service.auth_status_timeout plus a grace of the channel, scan.run host_service.entry_scan_timeout plus the grace; a call that runs out counts as helper_unreachable or entry_scan_timeout for that session user. A chain file that is a link counts as its target: the target, its folders and links and, for a folder, everything in it belong to no other session user; a dangling one fails. HD10 never opens a host_service.claude_bin that failed the trust rule, and Head reads only regular files. |
Red team of M2-42: a session user could stop its helper and hold the start of the whole host, a link of root into a file of another person passed HD11 (TM-T29 (j)), and a FIFO at an untrusted claude_bin blocked the run. |
| D-M2-42-27 |
2026-10-19 |
Port onto main from the integration squash of M2-42 on top of M2-41: the chapter additions of 02, 16 and 24, ADR 0068 and AsyncAPI are written into the English documents of main; in 16 section 4.4 main's text stays and the build facts of the ticket follow it as a list "Implementation (M2-42)", because main's section is newer than the base of the ticket; main's usage text of bothy-host (tabwriter) stays; the root Makefile keeps main's lint call without the contract build tags and adds the per-GOOS loop; the generated REST client of M8-10 is not taken (not on main); TestHostPairAgainstServer runs against the HTTP chain of test/terminalworld instead of the full-server harness of M1-71, which is not on main (same steps and assertions); the platform workflow names the fixed runner labels ubuntu-24.04, macos-15 and windows-2025 that main's runner gate requires instead of the -latest labels. |
main is English and newer than the integration base; only the ticket's own changes are ported. |
| D-M2-42-28 |
2026-10-19 |
Fixes after the pull request review: the entry scan reads the env snapshot of its run (a data race with SetDeclaration, found by the race detector in CI); install --session-user applies the checks of session-user add (CheckNewSessionUser: pattern, letter case, binding); booleans of the configuration decode every YAML spelling of !!bool; the platform suite reads systemctl show as key and value; on Windows the PID query opens the service control manager with SC_MANAGER_CONNECT and the service with SERVICE_QUERY_STATUS, a repeated install moves the running image aside before it copies the new one, and the group list of a session user adds the well-known logon SIDs (INTERACTIVE, SERVICE, LOCAL, CONSOLE LOGON, This Organization, Local account). |
Each finding reads true on the code; the Windows changes are checked by go vet and the linters for Windows only, the platform suite proves them on a Windows host. |
| D-M2-42-29 |
2026-10-19 |
Three review findings stay open until the Windows platform run (before M2-48): Close of a pipe listener does not end an Accept that waits in ConnectNamedPipe on a synchronous handle (the pipe DACLs do not let the listener open its own pipe to wake it, so it needs an overlapped handle or CancelSynchronousIo), reads on a synchronous pipe handle do not end when Conn.Call closes the connection at its deadline (overlapped handles would end them), and the impostor of TestPlatform_TMT26 on Windows is created by the administrator of the suite, not by the session user. |
Both need a Windows host to change and to prove; the platform suite is the gate for Windows and runs only there. |
| D-M2-42-30 |
2026-10-20 |
sonar.coverage.exclusions leaves out services/host/**/*_windows.go and services/host/**/*_darwin.go. |
The SonarQube scan runs on Linux and reads the coverage of go test there; files with a Windows or macOS build constraint never compile in that run, so they counted as uncovered new code, and the first scan failed the coverage condition of the Quality Gate. Their tests belong to the host platform suite (D-M2-42-19), as for the native plugin code of D-M4-10-7. Measured in a Linux run, the Linux files of the change stay above that condition. |
| D-M2-42-31 |
2026-10-20 |
The host service has its own internal/platform/testkit with Patience (the named value quality.test.patience), Deadline and Context; the waits of the host tests that the semgrep rule bothy-go-test-wall-clock-wait of PL/8 finds use it, and make arch knows the package as the component testkit. |
The host is its own Go module and cannot import the testkit of the server; the rule exempts exactly services/*/internal/platform/testkit. The waits are for things that must happen, so they take the one patience instead of their own seconds. |