M2-42: bothy-host: installation, state, doctor and pair¶
Status: ready | Depends on: M2-38, M2-41 | Milestone: M2
Goal¶
After this ticket the host service bothy-host can be installed on macOS, Linux and Windows without leaving administrator rights behind, checks itself before every start and on command, and pairs itself with a server. bothy-host install creates the service user bothy-host with the state directory host_service.state_dir, one session user per person with a home only for that user, one PTY helper per session user as a system service of the operating system (launchd LaunchDaemon, systemd system service with socket activation, Windows service under the session user's account) and the host service itself, and on Windows, after the operator's confirmation, removes only the entry Authenticated Users:(AD) from the root of each drive with a location. bothy-host session-user add and session-user remove, restart, pair, doctor, run and pty-helper work as 24 "Host service bothy-host" and 16 "Terminal: forwarding and host service" describe. The private keys lie in the state directory as files with mode 0600 (on Windows encrypted with DPAPI in the context of the service user) and never leave it. bothy-host doctor runs HD1 to HD11 with the per-platform terms of 24 2.2 point 6 (trusted owners, "writable" including creating entries, appending and attributes, full path resolution, places of a session user resolved by its own helper, directory identity, the Windows drive root), reports codes and, by the rule for names, the names of session users, never paths, contents or values, and refuses the start of the host service with its own exit code after which no service manager restarts it. The local channel between the service user and each helper checks its peer by user and by the process ID the service manager reports. A fake claude in testdata proves HD6 without a real model or account. Why: a person with a terminal must never reach the keys of the host, another person's account, a subscription login of someone else or a place another person can write (Rulings T2, T12, T13; threats T-TM-04, T-TM-05, T-TM-08, T-TM-12, T-TM-13, T-TM-14), and a host that cannot prove this for its platform gets no terminal (RK-23). This is the host side of TM-T13, TM-T22, TM-T24, TM-T26, TM-T27 and of the parts of TM-T21 and TM-T29 that need no session; the control connection (M2-43) and the sessions with PTY (M2-45) build on it.
Read first¶
docs/PLAN.md: section 1 "Principles", 3 "Way of working per ticket", 4 "Closing a milestone", 5 "Test strategy" (rows "Test doubles": fakeclaudefordoctor; "Security"; "End to end"); section 8 "M2 Agent platform, extensions and terminal": "Goal", "Scope" (terminal part), "Out of scope", "Exit criteria" item 8, "Test strategy" (terminal part: PTY tests on macOS and Windows on own machines, fakeclaude), "Red team" (terminal paragraph), "Dependencies", "Risks" (output ofclaude auth status, list of chain files, RK-88); block "terminal" with the rows M2-38 and M2-41 (what exists), M2-42 (this ticket, the contract), M2-43 (depends on this ticket; it builds the control connection and the wire ofreport), M2-44, M2-45 (sessions, PTY, profiles, the periodic and pre-PTY runs of HD6 and HD11), M2-46, M2-47 and M2-48 (conformance and platform runs); M7-23 (release chain of the host service, signatures and installation packages); section 18.2 row "I6" and 18.3 row "Claude Code session in the terminal"; section 19: P-01, P-05, P-09, P-10, P-11, P-12, P-20.- Dependencies: the header names M2-38 and M2-41. Through M2-41 this ticket also builds on M2-40, M2-39, through M2-38 on M1-01, and on the M1 tickets M1-48, M1-49, M1-44, M1-35, M1-34, M1-33, M1-32, M1-31, M1-29, M1-28, M1-20, M1-17, M1-13, M1-12 and their closure (all implied, so not in the header). D-W-102 struck no dependency from this ticket's plan column (it named M2-38 and M2-41 before and after); it struck M1-44 from M2-41 (pairing code format
hpc), M1-35 from M2-40 (step-up), M1-29 and M1-32 from M2-39 and M2-39 from M2-43. Those stay in this read list:docs/briefs/M1-44-identity-api-tokens.md(token format, checksum),M1-35-identity-step-up.md,M1-29-authz-invariants.md,M1-32-signing-keys.md. docs/spec/24-terminal.md(English, normative): 1 "Purpose and limits" (point 5); 2.1 "Overview"; 2.2 "Host servicebothy-host" completely: the users, the local channel, points 1 to 6 of "One session user per person and host, never shared" with the four terms of point 6 ("Trusted owner", "Writable", "Path resolution", "Drive root under Windows"), "Limit, honestly" and "Consequence, honestly", the PTY helper points 1 to 4, the table HD1 to HD11, "Check of the entries (HD11)", "Reason for HD9", "Reason for HD10", the rule for names, "Output on the host", "Message on a refused start", the paragraph on the own exit code; 2.3 "Server host"; 3.1 "States"; 3.2 "Pairing"; 3.3 "Re-pairing"; 4.1 "Two key pairs per host"; 4.2 "Storage on the host"; 4.3 "Fingerprint"; 5.3 "Terminal session token" (claimsosuandwdi, which M2-45 checks against what this ticket reports); 6.2 "Detach and reattach" (restart ends sessions); 6.5 "End of a session on the host" (long runners); 7.2 "Declaration"; 7.3 "Invariant I6"; 7.4 "Enforcement on the host" completely; 10.1 "Audit"; 10.3 "Reasons for ending (terminal_sessions.close_reason)"; 11.1 "Definition"; 13 "Errors"; 14 "Tests" (TM-T13, TM-T21, TM-T22, TM-T24, TM-T26, TM-T27, TM-T28, TM-T29); 16 (R-TM-01, R-TM-12, R-TM-14, R-TM-17, R-TM-18, R-TM-20 to R-TM-30); 17 "Configuration values of this chapter".docs/spec/16-operations.md4.4 "Terminal: forwarding and host service", part "Installation of the host service on a host", points 1 to 6 (you edit it, Design note 17).docs/spec/02-architecture.md6 "Code layout" (blockservices/host/, "Places that carry the product name").docs/spec/18-release.md2 "Components" (rowhost) and 9.1 "Host service and server".docs/spec/19-security.md: SEC-TERM-4, SEC-TERM-9, SEC-TERM-10, SEC-TERM-12, SEC-TERM-13, SEC-T18.docs/spec/23-quality.md: 1 "Principles", 3 "Component contract" with "Documentation in code", 11 (quality.coverage.go_min_percent).docs/spec/03-data-model.md(read only):hosts(reported_profiles,reported_session_users,reported_doctor,reported_is_server_host,llm_credentials),host_session_users(os_userrule).- For the terminal security group:
docs/spec/07-permissions.md2.12 "Hosts and theterminalcapability", SR10, SR11, SR12;docs/spec/08-identity.md6.3 "Fresh step-up for the terminal" and ID-T28;docs/spec/10-backend-policy.md3 "Invariants" (I6). docs/api/openapi.yaml: operationpairHost(x-rate-limit-class,x-idempotency-exempt, examples); schemasHostPairRequest,HostPairResult,HostProfile,HostReportedSessionUser,HostDoctorCheck,HostReport,HostReady,HostIdentify(live_sessions),HostPlatform,LlmCredentials,Problem(x-problem-codes); for the integration testcreateHost,issueHostPairingCode,getHost.docs/api/asyncapi.yaml: channelhostControlwith the messageshost.readyandhost.report.docs/operations/configuration.md: the rowshost_service.*under "24 Terminal" andhost.os_user_max_chars.- ADRs:
docs/adr/0068-terminal-through-a-host-service-with-noise-stream-and-invariant-i6.md(decision and "Confirmation"),0061-token-format-with-a-fixed-prefix-and-checksum.md,0005-go-as-the-server-language.md,0046-multi-user-dev-environment-only-with-isolation.md,0053-no-json-null-in-api-and-events.md. docs/DECISIONS.md: the rulings T1 to T13 (D-T1 to D-T11 in section 7.1, T12 as D-W-57 and T13 as D-W-62), D-T10, D-T12, D-T14, D-T15, D-T38, D-OP-03, D-W-17, D-W-18, D-W-45, D-W-63, D-W-64, D-W-67 to D-W-70, D-W-80 to D-W-91 and D-W-109 (each names this ticket or its checks), D-W-100, D-W-101, D-W-102; section 10 for the format of new rows.docs/security/threat-model.md5.16 "Terminal" and 6.7 "Terminal" (T-TM-01 to T-TM-14).docs/RISKS.md: RK-14, RK-22, RK-23, RK-84, RK-88, RK-89, RK-90.docs/OPEN-QUESTIONS.md: OQ-03 (M2-45), OQ-04 (M2-46), OQ-24 (terminal only over a private network, default no).docs/operations/tailscale.md3 "Tailnet policy (required)".docs/quality/gates.md,docs/quality/licenses.md,docs/quality/red-team-gate.md,docs/quality/push-discipline.md.- Briefs: the brief of M2-38 (
docs/briefs/M2-38-noise-core.md: module path,cmd/bothy-hostdispatcher,internal/noisewithGenerateKeyPair,internal/fingerprintwithCompute,internal/frame, the hand-maintainedservices/host/.go-arch-lint.yml,services/host/Makefilewithrelease-build); the brief of M2-41 (docs/briefs/M2-41-host-pairing.md:pairHostwith401 unauthenticatedfor every redemption failure, the report rules with theirerrors[].codevalues,issueHostPairingCode,confirmHost, the server's fingerprint check and its integration test pattern); the brief of M2-39 (docs/briefs/M2-39-terminal-hosts.md:createHost,getHost,hosts),M1-12-postgres-migrations.md(pgtest),M1-34-identity-sessions.mdandM1-35-identity-step-up.md(identitytest),M1-07-config-schema-loader.md(duration grammar, YAML withgo.yaml.in/yaml/v3, unknown keys abort). - Code on the base branch:
services/host(M2-38),services/server/internal/terminal(M2-39 to M2-41),services/server/internal/wire, the rootMakefile(GO_MODS,typecheck,lint,test,coverage),services/host/README.md. - External, read only (Boundaries): the Claude Code documentation pages that 24 cites (
memory,settings,mcp,cli-reference,authentication,setupunderhttps://code.claude.com/docs/en/), the git documentation (https://git-scm.com/docs/git), and the Microsoft pages that 24 2.2 cites.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. - Limits, windows and thresholds only through named configuration keys listed in
docs/operations/configuration.md; code comments and docs name the key, never the number. - Specification first: a change to behaviour, API, event, right or configuration updates
docs/spec/ordocs/api/in the same change. The core contract (docs/spec/00-glossary.md,03-data-model.md,04-sync-protocol.md,05-actions.md) is not edited by this ticket.
Product requirements implemented here:
- Users without administrator rights (24 2.2, 16 4.4 point 1, Ruling T12): the service user
bothy-hostowns the state directory (mode0700, on Windows a protected DACL); each person gets an own session user fromhost_service.session_userswith a home only for that user (mode0700, on Windows an ACL only for the user and the trusted owners); no session user can read the state directory; on macOS no session user is instaff. - Each PTY helper is a system service under its session user, never setuid; binary, service definition and the folder of the channel endpoint belong to a trusted owner and are writable by no session user; on Linux and macOS the service manager creates the endpoint (socket activation); the session user cannot stop or reconfigure the service (24 2.2 PTY helper points 1 and 2, HD10).
- The local channel carries only control messages, check results and (later, M2-45) decrypted frames, never keys;
bothy-hostchecks the peer by user and by the process ID the service manager reports, sends no frame on a mismatch and reports HD10 failed with the name of that session user (24 2.2 PTY helper point 3, D-W-109). - Private keys: an X25519 key pair (Noise) and an Ed25519 key pair (login) generated locally by
pair, stored only in the state directory as a file with mode0600(Windows: DPAPI in the context of the service user); a key chain only where the service user has an unlocked session, which no installation of this ticket has (24 4.2, Ruling T10). pair --server <url> --code <code>redeems the code throughpairHostwith both public keys, platform, host version, the profiles after HD7 and the session users with the display names of their working directories after HD7, shows the fingerprint of 24 4.3 in groups of four characters, and stores the keys only after the server accepted them (24 3.2, D-W-63, D-W-69).doctorruns HD1 to HD11 before every start of the host service and on command (full run with the entry scan of HD11); it outputs per check result and code, with the names of the rule for names, with each listable home under HD9 and with the drive underdrive_root_writable, never contents or values, and exits non-zero as soon as one check fails; warnings alone do not change the exit code (24 2.2, D-W-91).- The report to the server carries codes and, by the rule for names, one entry per account a person with a terminal can cause the finding in (HD6, HD10 with a wrong peer, HD11 at a place of a session user, HD9 for each former session user with a listable home); names of other users never leave the host; never paths, contents or values (24 2.2, D-W-70, D-W-109,
HostDoctorCheck). - A refused start starts no PTY and accepts no session; a paired host reports first (login, one
report, close) and then ends with its own exit code, after which the service definition of each platform does not restart it; onlybothy-host restartstarts it again (24 2.2, D-W-91, 16 4.4 points 2 and 6). - HD6 asks the coding agent only through the resolved
host_service.claude_binwith the fixed search list (the configured folder ofhost_service.claude_bin, then/usr/binand/bin, on Windows%SystemRoot%\System32) and the environment of 7.4, in every session user, plus the known login locations; atnoneororganizationa detected subscription login (authMethodclaude.aioroauth_token, or a login file) refuses the start, one entry per affected session user; an empty value or one that counts as empty gives the warningauth_status_unchecked; an output that cannot be read fails closed (24 7.4, RK-14, D-W-45, D-W-85). - HD7, HD10 and HD11 use the per-platform terms of 24 2.2 point 6, resolve every path fully before the check (places of a session user by its helper, places for all by the service user), check each folder of the resolved chain, each configured component and each link with its folder, treat a path that cannot be resolved as absent, and compare working directories by file system identity (device and inode; on Windows volume serial number and the 128-bit file ID of
FILE_ID_INFO) (D-W-80, D-W-83, D-W-84, D-W-86). - HD11 checks one rule for every configured path and every place of a session user, including the chain files
CLAUDE.md,CLAUDE.local.md,AGENTS.md,.mcp.json,.claudeand.gitin every folder on the way; the entry scan runs per session user in its helper, only that session user waits for it (entry_scan_running),host_service.entry_scan_timeoutbounds it (entry_scan_timeout), and only a later full run lifts its finding (D-W-68, D-W-89). host_service.session_usersandhost_service.claude_workdirsare read only at start; a folder listed under two session users drops out of both lists; theCLAUDE_CONFIG_DIRof the profileclaudeatorganizationis the fixed folder.config/bothy-host/claudein the session user's home (D-W-69, 24 7.4).session-user addcreates the user with a home only for that user and its helper service, refuses a name the state directory already binds to a person, and restarts the host service;session-user removeremoves the name fromhost_service.session_usersand its entry fromhost_service.claude_workdirs, removes the helper service, closes the home and restarts the host service, keeping the binding;restartrestarts the host service so that a changed configuration takes effect (D-W-67, D-W-88, D-W-90).- On Windows
install(also with--drive-root <drive>) removes, after the operator's confirmation, only the entryAuthenticated Users:(AD)at the root of the system drive and of each drive with a location, by rewriting exactly that folder's DACL withSetFileSecurityW, keeping every other entry in order and with its control bits; without confirmation nothing changes and HD11 reportsdrive_root_writable(D-W-87). - The host service opens no network listener; its only network connection in this ticket is the outgoing HTTPS request of
pair(R-TM-01).
Terminal security requirements (D-W-101):
- Rights per machine (07 "Hosts and the
terminalcapability": capabilityterminal, SR10, SR11 with I6, SR12): the rights themselves are server decisions of M2-39 (host.read, AZ-M60) and M2-40 (terminal.*, SR10, SR11 with I6, SR12; AZ-M54 to AZ-M59, AZ-M61, TM-T08, TM-T09, TM-T28 (a)). What this ticket enforces on the host: SR12 rests on names the host reports, so HD1 refuses an empty list, a duplicate name (compared without case, like the server) and a session user equal to the service user (TestHD1),pairreports exactly the configured names with their lists after HD7 (TestPairRequestBody,TestHostPairAgainstServer), andsession-user addrefuses a name bound to a person, so a session user is never given to a second person (T12;TestSessionUserAddRefusesBoundName,TestPlatform_TMT21_SessionUserAddRemove). For SR10 on the server host, HD5 reports the server host (TestHD5); the server refuses non-owners (M2-40) and the host service refusesown = false(M2-45). - Fresh step-up (device key in the apps, only passkey in the console; 08 6.3, ID-T28): not touched here. M2-40 builds step-up
freshand its consumption (ID-T28 without the console part), M2-44 TM-T07 without the console part, M4-36 and M5-12 the step-up key in the apps, M6-18 the passkey-only console. The commands of this ticket are local administrator commands on the host and grant no server right; the owner confirms the paired host with step-up throughconfirmHost(M2-41). - Network: there is no public inbound path to the terminal; the host service opens only outbound connections (R-TM-01); an installation with the profile
tailscalereaches the server only over the tailnet with a policy only for port 443 and without Funnel (D-OP-03,docs/operations/tailscale.md); the product does not require Tailscale; whether the terminal must be reachable only over a private network is owner question OQ-24 (default no). This ticket enforces: the admin endpoint and every local channel are Unix sockets or named pipes withPIPE_REJECT_REMOTE_CLIENTS, never a TCP or UDP listener (TestNoNetworkListener,TestPlatform_NoNetworkListener);pairaccepts onlyhttps://URLs (TestPairRequiresHTTPS). M2-43 builds the outbound control connection and M2-44 the data connections; on an answer "yes" to OQ-24 those change, not this one. - Audit without content: the server's audit of
host.pairis M2-41's, ofhost.connectM2-43's, of sessions M2-46's (TM-T06). This ticket enforces the host's share: the report and the output ofdoctorcarry codes and names only, never paths, contents or values (TestReportHasNoPathsOrValues,TestDoctorOutputHasNoPathsOrValues); logs of the host service carry codes and names only; the pairing code and private keys appear in no output, log, report or test failure message (TestPairNeverPrintsCodeOrKeys). - Idle timeout and maximum duration (24 "Limits"): not touched here, because this ticket starts no PTY. M2-45 measures idle on the host, M2-46 adds the server side (
terminal.idle_timeoutplusterminal.idle_grace,terminal.max_session_duration, TM-T11). - Service user and session users without administrator rights (T12, HD rules): the core of this ticket.
installandsession-user addcreate users outside every forbidden group and, on macOS, outsidestaff;doctorrefuses the start on HD1 to HD3, HD4, HD8, HD9, HD10 and on HD11 at a place for all; the helper keeps the keys out of reach of the session user (HD8) and cannot be replaced (HD10). Proven by TM-T13, TM-T22, TM-T24 and TM-T26 on macOS, Linux and Windows (TestPlatform_TMT13,TestPlatform_TMT22,TestPlatform_TMT24,TestPlatform_TMT26) and by the unit tests of Design notes 6 to 10. - Threats of threat model 6.7 that concern this ticket, each with its test: T-TM-04 (escalation on the host) by HD1 to HD3 and HD9, TM-T13 and TM-T24; T-TM-05 (theft of the host keys) by the state directory,
0600or DPAPI and HD8, TM-T22 (TM-T23 is M2-43's, TM-T18 M4-35's); T-TM-06 (pairing of a foreign host) by the fingerprint shown atpair,TestHostPairAgainstServer(TM-T19 and TM-T20 are M2-41's); T-TM-08 (a foreign LLM login) by HD6 throughhost_service.claude_binonly, TM-T13 (subscription case) and TM-T27; T-TM-11 (processes after revocation) bysession-user removeclosing the home and keeping the binding,TestPlatform_TMT21_SessionUserAddRemove(the session part TM-T10 is M2-46's); T-TM-12 (replacing the PTY helper) by socket activation, the peer check and memory protection, TM-T26 and TM-T22; T-TM-13 (shared places) by HD7 and HD11, the TM-T29 parts (a), (b), (c) withpair, (d), (e), (f), (h), (i), (j); T-TM-14 (standstill by one person) by the names in the report and the local output, TM-T24, TM-T26, TM-T29 (i) and the parts (e) to (g) of TM-T21 without sessions. Not touched here: T-TM-01 and T-TM-02 (M2-38, M2-45, M2-46), T-TM-03 and T-TM-09 (M2-40, M2-44), T-TM-07 (M6-18), T-TM-10 (M4-35).
Scope¶
Paths under services/host/ unless noted. M2-38 names below are the expected ones; where its merged code names them differently, follow the code (Boundaries). This ticket creates no migration, no server operation and no new top-level make target.
Create:
cmd/bothy-host/:install.go,sessionuser.go(session-user add <name>,session-user remove <name>),restart.go,pair.go,doctor.go,run.go,ptyhelper.go, registered in the M2-38 command table; flags with the standard libraryflagonly (Design note 2).internal/buildinfo:Version(default0.0.0-dev, set byrelease-buildwith-ldflags -X),Platform()returning aHostPlatformvalue.internal/clockandinternal/clock/clocktest:Clock{Now() time.Time; NewTimer(d time.Duration) Timer}with a real and a fake implementation, so that no test sleeps for a window.internal/config: the configuration file of Design note 3 (Load,DefaultPath,AddSessionUser,RemoveSessionUserediting the YAML node tree so that comments and other keys stay).internal/state: state directory layout, key file, pairing record, bindings, last declaration (Design note 4).internal/hostoswithlinux,darwinandwindowsfiles andinternal/hostos/hostostest: the port to the operating system (users, groups, file security, path resolution with identity, service manager, command runner, probes) and its fake (Design note 5).internal/doctor: the checks HD1 to HD11, modesFullandLight, the result model, the report entries by the rule for names, the local output (Design notes 6 to 10).internal/localchan: endpoints, dial and listen per platform, peer verification, the message codec,FuzzDecodeMessage(Design note 11).internal/helper: thepty-helperprocess: serves its channel, sets its memory protection, runs the helper-side checks (Design note 12). M2-45 adds the PTY to it.internal/install: plans and executors ofinstall,install --drive-root,session-user add,session-user remove,restart; templates of the service definitions ininternal/install/units/(Design notes 13 and 14).internal/startup: the start sequence ofrun, the admin endpoint, theControlport and its implementationNoControl, per session user readiness (Design notes 15 and 16).internal/pairing:pair(Design note 16).testdata/fakeclaude/:main.go(a Go program built by the tests,.exeon Windows),fixtures/with outputs ofclaude auth status,README.md(English: sources, CLI version, date) (Design note 9).test/platform/: the platform suite behind the build tagplatform(Design note 19).services/server/test/terminal/hostpair_test.go(server module):TestHostPairAgainstServer(Tests).
Change:
services/host/.go-arch-lint.yml(hand-maintained, M2-38 Design note 2): one component per new package, vendorsgolang.org/x/sysandgo.yaml.in/yaml/v3;cmdmay use every internal package;doctor,install,helper,startup,pairingusehostosonly through its interfaces.services/host/go.mod,go.sum:golang.org/x/sys(already indirect through M2-38) andgo.yaml.in/yaml/v3at the version the server module pins; no other dependency.services/host/Makefile:release-buildsetsinternal/buildinfo.Version; new targetplatform-test(Design note 19).- Root
Makefile:GO_CROSS_MODULES := services/hostand, intypecheckandlint,go vetandgolangci-lint runfor that module withGOOSlinux,darwinandwindows, so code for the other platforms is checked on every build host. Ruling for docs/DECISIONS.md. docs/spec/24-terminal.md(English): Design notes 3, 4, 6 to 11, 14, 15 and 18 name each change; section 17 gainshost_service.auth_status_timeoutand, after the measurement, the new default ofhost_service.entry_scan_timeout.docs/spec/16-operations.md4.4 (English, Design note 17);docs/spec/02-architecture.md6 "Code layout" (the new packages,test/platform, the places of the product name of Design note 13).docs/api/openapi.yaml:HostReadygains the required memberllm_credentials(LlmCredentials);docs/api/asyncapi.yamlchannelhostControl: the description names the repeatedreadyof Design note 15. Thenmake api-lint,make api-checks,make api-tablesand the server code generation (make codegen-checkclean).docs/operations/configuration.md: regenerate withmake config-reference.docs/adr/0068-terminal-through-a-host-service-with-noise-stream-and-invariant-i6.md"Confirmation": the tests of this ticket for TM-T13, TM-T22, TM-T24, TM-T26, TM-T27 and the TM-T29 parts; thenmake adr-index.docs/RISKS.md(facts only): RK-90 (measured durations, new default), RK-88 (date and result of the check of the chain files), RK-23 and RK-84 (which platforms passed TM-T26).services/host/README.md(English),services/host/CHANGELOG.md,docs/DECISIONS.mdsection 10 rowsD-M2-42-<n>for every Ruling below.
Out of scope¶
- The control connection
/hosts/connect(hello,identifywithlive_sessions,ready, heartbeat, the wire ofreport, close codes4110to4114, the server's handling ofreportandhosts.reported_*): M2-43 implements theControlport of Design note 15 and sendsllm_credentialsinready. Until thenrunbehaves like a host that cannot reach the server (24 2.2). - Sessions, session token, data connections, relay: M2-44; PTY, profiles at runtime,
host_service.require_local_confirm, the binding ofosutosubat the first session, HD6 and HD11 before every PTY and everyhost_service.credential_check_intervalwith their consequences in operation (ending sessions withhost_policy_violation), the environment of the profileclaude, the API key oforganizationand its Windows import, TM-T03 to TM-T05, TM-T21 (a) to (d), TM-T25, TM-T28 (c) and (d), TM-T29 (g), (k), (l) and the session parts of (a), (e) and (f): M2-45. This ticket providesdoctor.RunLightand the readiness that M2-45 schedules and consults. - The server side of pairing,
confirmHost, the fingerprint check of the server, TM-T19, TM-T20, TM-T28 (b) and (e): M2-41. Rights, SR10 to SR12, I6, step-up: M2-40. Revocation, limits, audit of sessions, metrics: M2-46. Several nodes: M2-47. The conformance and chaos suite, the host service in a Linux container inmake check, TM-T16: M2-48. Client warnings and pins: M4-35, M4-36, M5-12, M6-18. - Developer ID signatures, notarization and Authenticode (
signtool), SBOM, checksums, installation packages and their service definitions ininfra/host/: M7-23; it may render its packages from the templates of Design note 14. - Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M2-42-host-install-doctor; one writer per worktree. - Conventional Commits with the component as scope (for example
feat(host): ...,test(host): ...,docs(spec): ...,build(gates): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests of
make checkrun without administrator rights, bind only127.0.0.1and port 0, create no operating system user, service or ACL outsidet.TempDir(), and PostgreSQL runs only as a throwaway container on a random port throughpgtest(P-09, P-10). Without a Docker-compatible runtime the server integration test fails; it never skips. - The platform suite (Design note 19) creates and deletes operating system users, services and ACLs with administrator rights. Run it only on disposable machines the launcher's host rules name (a Linux machine with systemd, a macOS machine, Windows 10 and Windows 11 with default rights), never on a workstation or a machine with real users. Without a machine for a platform, stop with
Verdict: BLOCKED platform machine missing: <platform>, because RK-23 allows no terminal on a platform without proof. - Stop only processes you started, by PID. Temporary directories only via
mktemp -d(Got.TempDir()), removed at the end. - Never read or print secrets; test keys are generated in the test, pairing codes come only from the server under test, and fixed text values contain
EXAMPLE. Never log in to a real Claude account and never call a model. - No network calls except package downloads for pinned tools and Go modules, one download of a pinned Claude Code CLI for the fixture recording of Design note 9, and read-only fetches of the public documentation pages named under "Read first" for the checks of Design notes 9 and 10.
- Never lower a threshold, disable a gate or delete a test.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100). Where the merged code of an M1 ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>. - The same holds for M2-38 to M2-41: their merged names win over the expected names here; list each difference. If
pairHostis not enabled on the base branch, stop withVerdict: BLOCKED pairHost missing. - Commands this ticket runs on the host (
id,sudo,useradd,systemctl,dscl,launchctl,lsand the like) are started by absolute path from the fixed list/usr/bin,/bin,/usr/sbin,/sbin(Windows:%SystemRoot%\System32), with an argument vector, never through a shell orPATH.
Design notes¶
-
Who runs what. Three identities: the administrator (root, or an elevated member of
Administrators) runsinstall,session-user,restart, andpairanddoctorwhen the service user cannot run them itself; the service user runsrunand the service-side checks; each session user runs itspty-helperand the helper-side checks. Every command that needs administrator rights and lacks them exits2withbothy-host <command>: administrator rights required. Ruling for docs/DECISIONS.md:pairanddoctorrun with the identity of the service user: directly when the caller is the owner of the state directory (the service user itself, and the tests); through the admin endpoint of the running host service when an administrator calls them; with the service stopped, on Linux and macOS in a child process with the service user's credentials (SysProcAttr.Credential), and on Windowsdoctorstarts the service once with the start argumentdoctor-once(it runs the full run, hands the result to the waiting command over the admin endpoint and stops with exit code0, connecting to no server and accepting no session), whilepairon Windows requires the running service and otherwise exits2namingbothy-host restart. Reason: 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). -
Commands and exit codes.
install [--session-user <name>] [--drive-root <drive>] [--drive-root-confirm yes|no] [--config <path>];session-user add <name>;session-user remove <name>;restart;pair --server <url> --code <code> [--ca-file <pem>];doctor;run;pty-helper --session-user <name>. Exit codes:0success;1a check failed (doctor) or a runtime failure;2usage or environment error (unknown flag, missing rights, unreadable configuration in a command);78refused start ofrun(constantExitStartRefused, the valueEX_CONFIGof sysexits). Ruling for docs/DECISIONS.md.--drive-rootalone makesinstalldo only the drive root step (24 2.2 point 6). The confirmation is a prompt on the terminal;--drive-root-confirmanswers it for unattended installs; without a terminal and without the flag the answer is no. Ruling for docs/DECISIONS.md, the local output ofdoctor(24 2.2 "Output on the host"): one line per check in the order HD1 to HD11,HD<n> ok,HD<n> WARN <code>orHD<n> FAIL <code>, followed bysession_user=<name>for a named entry,user=<name>for each user whose home HD9 could list anddrive=<letter>:fordrive_root_writable, one line per entry; then the summarydoctor: <failed> failed, <warnings> warnings; the declaration in use on its own line (declaration: none|personal|organization|unknown); never a path, a content or a value. -
Configuration file. 24 and 16 name the keys
host_service.*but no file. Ruling for docs/DECISIONS.md (update 24 17 and 16 4.4): one YAML file nested like the names (host_service: {session_users: [...], ...}), decoded withgo.yaml.in/yaml/v3like the server (M1-07): unknown keys, duplicate keys and wrong types are errors naming the key; durations in the duration grammar of M1-07. Paths: Linux/etc/bothy-host/config.yaml, macOS/Library/Application Support/bothy-host/config.yaml, Windows%ProgramFiles%\bothy-host\config.yaml;--configoverrides it for tests. The file and every folder on the way belong to a trusted owner and are writable by no session user; HD10 checks them (Design note 8), because the file chooses every program of every session user. a.host_service.profiles: a list whose items areshell,claudeor a mapping{name, display_name, kind: command, program, args}with an absoluteprogram; names match the pattern ofHostProfile.name;shellandclaudeget the display namesShellandClaude Code. b.host_service.claude_workdirs: a mapping from a session user name to a list of{display_name, path}; display names are not empty and unique within one list, else HD7 drops that entry with a warning. c. New keyhost_service.auth_status_timeout(Host service, default30s): the bound of one call ofclaude auth statusand of the login-shell probe of HD6. Add the row to 24 17. Ruling for docs/DECISIONS.md. d. An invalid file makesrunlogconfig_invalidwith the key and exit78, so no service manager loops on it;doctorand the other commands exit2. -
State directory (
internal/state). Files, each written atomically (temp file in the directory,fsync, rename,fsyncof the directory), mode0600on Linux and macOS, on Windows inheriting the protected DACL of the directory (service account,SYSTEM,Administrators):keys(both private keys; on Windows the content is a DPAPI blob fromCryptProtectDatawithoutCRYPTPROTECT_LOCAL_MACHINE),pairing.json(server URL,host_id,instance_id, the fingerprint; no private member),bindings.json(session user name to person ID, written by M2-45 at the first session, read here),declaration.json(the lastllm_credentialslearned fromready, a code),admin.sockon Linux and macOS (Design note 15). The nameclaude_api_keyis reserved for the API key oforganization(16 4.4 point 5), which M2-45 reads. Ruling for docs/DECISIONS.md: 24 4.2 and 7.4 say the state directory holds only the keys, the bindings and the API key; the pairing record and the last declaration are added there and in 24 17 (rowhost_service.state_dir), both without secrets and without a subscription login. -
Port to the operating system (
internal/hostos). All platform behaviour sits behind small interfaces so that every rule is unit-tested on any build host withhostostest:Users(look up by name and ID, home, enumeration of local users with their IDs,UID_MINandUID_MAXof/etc/login.defson Linux and the profiles ofProfileListon Windows for HD9, membership byid -G <name>on Linux and macOS, because the pure Go user database misses directory services, and local plus global groups on Windows),Security(owner, mode, POSIX ACL fromsystem.posix_acl_accesson Linux, the macOS ACL read withgetattrlistandATTR_CMN_EXTENDED_SECURITYwheregolang.org/x/sys/unixoffers it, else parsed from/bin/ls -led, the Windows security descriptor),Resolver(Design note 7),Services(install, remove, start, stop, restart, main PID, exit policy per platform),Runner(absolute path, argument vector, environment, timeout),Probe(sudo -n trueas a user, the Docker socket,kernel.yama.ptrace_scope). Ruling for docs/DECISIONS.md: no cgo on any platform; an ACL entry that cannot be read, parsed or mapped to a user or group counts as granting every right to every session user (fail closed), because one right too many only blocks, one too few opens a place (24 2.2 point 6). -
Checks HD1 to HD5 and HD8, HD9. The check names of the report are the
checkvalues ofHostDoctorCheck(hd1_usertohd11_shared_locations). Codes, Ruling for docs/DECISIONS.md and a new table "Codes ofdoctor" in 24 2.2 (the core contract only says "code"):- HD1:
session_users_empty,session_user_duplicate(names compared without case on every platform, likehost_session_users),session_user_is_service_user,privileged_user(root,Administrator,SYSTEM, or uid 0),user_missing,session_user_name_invalid(not the pattern ofHostReportedSessionUser.os_user; the length is the server'shost.os_user_max_charsand fails atpairHost). - HD2:
forbidden_group(the fixed list of 24 HD2, compared without case on Windows, plushost_service.extra_forbidden_groups; the operator cannot shorten the fixed list),docker_socket_writable(Linux:/var/run/docker.sockwritable by the user, one of its groups or others, by mode or ACL). - HD3:
passwordless_sudo(sudo -n truesucceeds for the service user or, in its helper, for a session user). On Windows HD3 has no probe and passes without code, because elevation needs the credentials of an administrator and HD2 coversAdministrators. Ruling for docs/DECISIONS.md. - HD4:
state_dir_owner,state_dir_mode,key_file_unprotected(mode not0600, or on Windows not a DPAPI blob or a DACL with more than the three principals). - HD5: warning
server_host_detected: the host part of the paired server URL resolves to an address of an own interface or to loopback; reported asis_server_host. - HD8:
state_access_for_session_user(owner, mode, ACL or group give a session user or one of its groups an entry),helper_reads_state(a helper could open the state directory or the key file). - HD9:
foreign_home_listable. Each helper lists the home of each of these accounts that is not a session user, and every listing must fail:root, the service user, every former session user (bound inbindings.json, not inhost_service.session_users), and every person account of the platform. Person accounts are: on Linux an account with a UID fromUID_MINtoUID_MAXof/etc/login.defs(the file's documented defaults when a key is missing); on macOS an account with aUniqueIDof at least 501, the first ID macOS gives a regular user; on Windows every account with a profile underProfileList(theProfileImagePathof its SID subkey; the valuesDefaultandPublicname no account). HD9 skips all other system accounts (Linux belowUID_MIN,nobody; macOS below 501,nobody), because their homes are system folders that every user may list by design (macOSnobody/var/empty,_www/Library/WebServer,_networkd/var/networkdwith mode0755and owned by itself; Debiandaemon/usr/sbin,systemd-network/), and they hold no files of a person. Ruling for docs/DECISIONS.md; update 24 2.2 (the HD9 row, point 4, "Reason for HD9") and R-TM-17 with this rule and reason. Report: one entry with the name for each former session user (bound inbindings.json, not inhost_service.session_users) whose home can be listed, one entry without a name for all others; the local output names every such user.
- HD1:
-
Path resolution and identity.
Resolver.Resolve(path) (Resolved, error)returns the final path (realpathsemantics on Linux and macOS,GetFinalPathNameByHandleon Windows), each folder of the resolved chain, each component of the configured path, and each link followed (symbolic link, on Windows also a junction) with the folder it lies in;Resolver.Identity(path)returns device and inode, or on Windows the volume serial number and the 128-bit file ID ofGetFileInformationByHandleExwithFileIdInfo. A missing path, a dangling link or a folder on the way the resolving user cannot open givesErrUnresolvable: a working directory drops out of its session user's list, acommandprofile drops out,host_service.claude_bincounts as empty, and an unresolvable home fails HD11 for its session user (home_unresolvable). A link counts as an entry of its folder and as its target. -
HD10 helper integrity. The service user checks, after resolution, that each of these belongs to a trusted owner and is writable by no session user (the per-platform lists of 24 2.2 point 6 and D-W-86, unit-tested right by right): the helper binary, every service definition, every folder of a channel endpoint, the configuration file (Design note 3), and, when set,
host_service.claude_binwith every folder on the way, every folder of the search list of HD6 and, for a file starting with#!, the interpreter the search list finds (for#!/usr/bin/env node:envand thenodein the search list). Codes:helper_untrusted(binary, definition, endpoint folder, configuration file, or a setuid or setgid bit on the binary),claude_bin_untrusted(also a Windows value that is not an.exe),peer_mismatch(named with the session user of the channel),peer_check_unavailable(the platform has no proven peer check, Design note 11),ptrace_scope_low(Linux,kernel.yama.ptrace_scopebelow1),helper_unprotected(macOS: the binary lacks the hardened runtime flag or carriesget-task-allow). A missing interpreter in the search list leaveshost_service.claude_binchecked but counting as empty for HD6 and the profile (D-W-85). -
HD6 and the fake
claude. In each helper, atFullandLight: run the resolvedhost_service.claude_binwith the argumentsauth status, the environmentHOME(WindowsUSERPROFILE),CLAUDE_CONFIG_DIRof the check andPATHequal to the search list, no startup files, bounded byhost_service.auth_status_timeout; once with the default folder in the home and once with.config/bothy-host/claude(the helper creates that folder with mode0700, on Windows an ACL only for its user, if it is missing). Read only the JSON memberauthMethod; a value outsidenone,claude.ai,oauth_token,api_key,api_key_helper,third_party, an unreadable output, a timeout or an exit code other than0and1givesauth_status_unknown(fail closed, RK-14). Login locations:.claude/.credentials.jsonin the home,.credentials.jsonin each checkedCLAUDE_CONFIG_DIR, and a probe of the session user's login shell (Windows: the user's environment in its registry hive) that prints only whetherCLAUDE_CODE_OAUTH_TOKENis set and the value ofCLAUDE_CONFIG_DIR, never a token. A finding isauthMethodclaude.aioroauth_token, a login file, or the variable set. Evaluation with the declaration:noneororganizationand a finding failssubscription_login_detectedper session user;personalpasses; an unknown declaration (never received) gives the warningdeclaration_unknownand the local output names the accounts, because no session can start beforereadydelivers it (Design note 15); an empty or empty-countinghost_service.claude_bingives the warningauth_status_uncheckedand only the locations are asked. Ruling for docs/DECISIONS.md forauth_status_unknownanddeclaration_unknown. a. Before the build, check the cited Claude Code pages (cli-reference,authentication,setup) for the arguments and the JSON ofclaude auth statusand the login locations; a difference from 24 7.4 updates 24 7.4 first (report). b.testdata/fakeclaude/main.gobehaves likeclaude auth status: it prints the fixture named by the filefake-authin the checkedCLAUDE_CONFIG_DIRor.claudefolder (none,claude.ai,oauth_token,api_key,unknown_method,garbage,hang), and appends one line with its name to a start log whose path the test sets at build time with-ldflags -X. A second build namednodeserves as interpreter and as the wrongnodeof TM-T27. c. Fixtures: one recorded from a pinned Claude Code CLI without login, in a throwaway directory with an emptyHOMEandCLAUDE_CONFIG_DIRandDISABLE_TELEMETRY,DISABLE_ERROR_REPORTINGandCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICset; ifauth statusneeds the network, record nothing and say so in the report. The others are derived from the documented format;README.mdnames the CLI version, the URLs and the date. -
HD7 and HD11. HD7 is warnings only:
workdir_dropped(a key that is no session user name with its list, a folder its helper cannot resolve, a folder under two session users from both lists, a display name that is empty or repeated),claude_profile_unavailable(organizationor unknown declaration andhost_service.claude_binempty or counting as empty),profile_dropped(acommandprogram missing or not absolute); one entry, the code by this precedence, the local output lists every item by profile name or display name. Each helper reports the identity of each working directory of its list; the service user compares them and builds the lists thatpairandreportsend. HD11, place of a session user (home,CLAUDE_CONFIG_DIR, working directories; checked by its helper): owner, mode, ACL and groups of the place, of every folder on the way and of the chain files in those folders give no other session user write access and belong to no other; the place itself gives no other session user read or execute access; codes, one entry per session user by precedencehome_unresolvable,shared_location,foreign_entry,entry_scan_timeout,entry_scan_running. HD11, place for all (helper binary,host_service.claude_bin,commandprograms; checked by the service user): trusted owner, writable by no session user; one entry without a name,drive_root_writablebeforeprogram_location_untrusted. Entry scan (Fullonly, in each helper, concurrently per session user, bounded byhost_service.entry_scan_timeout): no entry in a place of the session user belongs to another session user (foreign_entry). Its finding, a timeout included, stays until a laterFullrun finishes and finds nothing;Lightnever runs it and never lifts it. Ruling for docs/DECISIONS.md: before the build, check the current Claude Code pages (memory,settings,mcp) and the git page for files loaded from folders above the working directory; any further name extends the list in 24 2.2 point 6, 16 4.4, SEC-TERM-13, T-TM-13, RK-88 and D-W-68 in the same change (report). -
Local channel and peer check (
internal/localchan). Endpoints: Linux/run/bothy-host/helper-<name>.sock(systemd socket unit,SocketUser=root,SocketGroup=bothy-host,SocketMode=0660,DirectoryMode=0755,Accept=no), macOS/Library/Application Support/bothy-host/run/helper-<name>.sock(LaunchDaemonSocketswithinetdCompatibilityWait = true, so the listening socket arrives on standard input without cgo; owner and mode as on Linux), Windows\\.\pipe\bothy-host-helper-<name>created by the helper withFILE_FLAG_FIRST_PIPE_INSTANCE,PIPE_REJECT_REMOTE_CLIENTSand a DACL for the service account only. Ruling for docs/DECISIONS.md (update 24 2.2 PTY helper point 3): with socket activation the operating system reports the service manager as the peer of a connecting client, so on Linux and macOS the helper creates asocketpair, passes one end withSCM_RIGHTSover the accepted connection, and all further messages run over that pair;bothy-hostreads the peer of its end (SO_PEERCREDon Linux,LOCAL_PEERCREDandLOCAL_PEERPIDon macOS) and compares user ID and process ID with the session user and the main PID the service manager reports (systemctl show --property=MainPID --value,launchctl print system/<label>); on Windows it comparesGetNamedPipeServerProcessIdand the process token's user SID with the session user and the process ID ofQueryServiceStatusEx. The helper accepts only the service user as client (SO_PEERCRED,LOCAL_PEERCRED,GetNamedPipeClientProcessId). Before both checks pass, no message flows. If the platform run of TM-T26 cannot prove the mechanism for a platform, HD10 fails there withpeer_check_unavailable(RK-23: no terminal without proof) and the report says so. Codec: a 4-byte big-endian length, one kind byte and the body; kind0x00is a JSON control message{"op", "id", "d"}, kind0x01is reserved for the session frames of M2-45, any other kind closes the channel; the limit isframe.MaxLenof M2-38 plus the header (protocol constant, rule 9). Ops of this ticket:hello(helper, protocol version1),check.run(mode, the other session users, resolvedhost_service.claude_binand search list, declaration, working directories),check.result. -
The helper (
internal/helper). At start: Linuxprctl(PR_SET_DUMPABLE, 0); Windows a process DACL without rights for its own user, with anOWNER RIGHTSentry that removes the owner's implicit rights; macOS relies on the hardened runtime thatinstallsigns in (Design note 13) and HD10 checks. It serves one channel tobothy-host, runs HD3, HD6, the helper part of HD8, HD9 and HD11 and reports the working directory identities for HD7, all as its session user; results carry codes, names and identities, never paths or contents. It closes the channel on any malformed message. -
Installation (
internal/install).installcomputes a plan of steps per platform from the configuration and the running system and executes it throughhostos; every step is idempotent, nothing existing is removed or overwritten except bysession-user remove. Places of the product name (update 02 "Places that carry the product name"): program folder Linux/opt/bothy-host/bin/, macOS/Library/Application Support/bothy-host/bin/, Windows%ProgramFiles%\bothy-host\, into whichinstallcopies the running binary owned by a trusted owner, mode0755, no setuid; systemd unitsbothy-host.service,bothy-host-helper-<name>.socketand.service; launchd labelsbothy-hostandbothy-host.helper.<name>; Windows servicesbothy-hostandbothy-host-helper-<name>; pipesbothy-host-adminandbothy-host-helper-<name>. macOS: after copying,installsigns the program binary ad hoc with the hardened runtime:/usr/bin/codesign --force --sign - --options runtime <program folder>/bothy-host, with no entitlements and so noget-task-allow. It skips this step when the binary already carries a signature with theruntimeflag, so the Developer ID signature of M7-23 stays intact. HD10 reads the flags with/usr/bin/codesign --display --verbose=4 <binary>and the entitlements with/usr/bin/codesign --display --entitlements - --xml <binary>. Ruling for docs/DECISIONS.md. Users: Linuxuseradd --systemfor the service user with the state directory as home and no login shell,useradd --create-home --user-groupper session user, home mode0700; macOS hidden users created withdscl, the service user with the state directory asNFSHomeDirectory(HD9 lists it), each session user with an own primary group and not instaff, home created withcreatehomedirand mode0700; Windows the virtual accountNT SERVICE\bothy-hostfor the host service, and per session user a local account with a random password handed only to the service control manager,SeServiceLogonRight, the profile created withCreateProfileand its folder ACL only for the user and the trusted owners. The way per platform is proven by the platform suite and described in the README (24 2.2 "the build ticket proves the way to do this"). a.session-user add <name>: refuses a name inbindings.json(bothy-host session-user add: <name> is bound to a person on this host; create a new name), refuses a name already configured, creates user, home and helper service, adds the name tohost_service.session_users, restarts the host service. b.session-user remove <name>: removes the name fromhost_service.session_usersand its entry fromhost_service.claude_workdirs, removes the helper service, closes the home (mode0700, Windows ACL only for the user and the trusted owners), restarts the host service, keeps the binding; it ends no process of the user and prints the operator steps of 16 4.4 point 1. c. Windows drive root: a pure function on the parsed DACL removes exactly the access-allowed entry forAuthenticated Users(S-1-5-11) with the rights of(AD)and no inheritance flags, keeps every other entry and the control bits, and the executor writes it withSetFileSecurityW(throughgolang.org/x/sys/windowslazy loading), never withSetNamedSecurityInfoorSetSecurityInfo. The roots are the system drive and the drive of every configured place. d. Before the build, measure owner and ACL ofC:\,C:\Users,C:\Program FilesandC:\Windows\System32withicaclson Windows 10 and Windows 11 with default rights (report); a result that differs from 24 2.2 point 6 changes 24 first. -
Service definitions and the exit policy. Templates in
internal/install/units/rendered per platform and checked by golden tests. Linux: the host serviceUser=bothy-host,Restart=on-failure,RestartPreventExitStatus=78,UMask=0077; each helper socket unit as in Design note 11 and its serviceUser=<name>,Restart=on-failure. The host unit and each helper service unit set neitherNoNewPrivileges=nor any option that systemd.exec lists as implying it for a unit withoutCAP_SYS_ADMIN(for exampleSystemCallFilter=,RestrictSUIDSGID=,DynamicUser=). Under the no_new_privs flag everysudo -n trueof the service user (or, in its helper, of a session user) fails, which would hide a passwordless sudo rule from HD3 (24 HD3).TestServiceDefinitionsGoldenasserts their absence. Ruling for docs/DECISIONS.md. macOS: LaunchDaemons withUserName,KeepAlive{Crashed: true}andEnvironmentVariablesGOTRACEBACK=crash, so a Go crash raisesSIGABRTand is restarted while every exit with a code,78included, is not. Windows: services with failure actions restart on a crash,FailureActionsOnNonCrashFailuresfalse, and a refused start stops withSERVICE_STOPPED,ERROR_SERVICE_SPECIFIC_ERRORand the service-specific code78; the service DACL of a helper gives its session user no right to stop or reconfigure it. Ruling for docs/DECISIONS.md (24 2.2: "the build ticket proves the method per platform"); the platform suite proves both directions per platform. -
Start sequence and the declaration (
internal/startup).run: load the configuration (Design note 3 d), open the state, open the admin endpoint (Linux and macOS<state_dir>/admin.sock, clients only with user ID0; Windows\\.\pipe\bothy-host-adminwithPIPE_REJECT_REMOTE_CLIENTS, a DACL forSYSTEMandAdministrators, clients only elevated members ofAdministrators), runFull. At start HD6 uses the declaration stored indeclaration.json; it is unknown only if noreadywas ever stored. If the host is paired,runcallsControl.Loginbefore deciding. The declaration of thereadyreplaces the stored one indeclaration.json, and HD6 is evaluated again with it. A remaining finding with the consequence "Start refused" (HD1 to HD4, HD8, HD9, HD10, HD11 at a place for all, HD6 at a knownnoneororganization) refuses the start: exactly oneControl.Reportwith all checks,Control.Close, exit78. Without such a finding,Control.Reportfollows and the host waits for M2-43 to M2-45. If the host is not paired orLoginfails, the stored declaration decides: log the codes and, on such a finding, exit78. A later change of a result (an entry scan ends, areadywith another declaration) sends a new report. Thedoctorcommand against a running service makes it runFull, return the result (opdoctor.runof the admin endpoint, answering with the local result and the report entries asHostDoctorCheckitems, which the platform suite reads directly), report it, and on a "Start refused" finding other than HD6, HD10 and HD11 end like a refused start.Readiness(sessionUser)tells M2-45 whether a session user may get sessions (HD11 per session user,entry_scan_running) and whether the host may (every other finding).ControlisLogin(ctx) (Ready, error),Report(ctx, Report) error,Close() error; this ticket wiresNoControl, which answers everyLoginwithErrUnreachable, exactly the case 24 2.2 names, and M2-43 replaces the wiring. Ruling for docs/DECISIONS.md (update 24 2.2 and 7.4, openapiHostReady, asyncapihostControl): the host service learnshosts.llm_credentialsonly fromready, the server sendsreadyagain on every change of the declaration (M2-43 builds it), and the host treats eachreadyas the current declaration; until areadywas ever stored the declaration is unknown, and no session is possible then. -
pair(internal/pairing). Refuse any URL that is nothttps://(pair: the server URL must use https); trust the system roots plus--ca-file. Generate the X25519 key pair with M2-38'snoise.GenerateKeyPair(crypto/rand.Reader)and an Ed25519 key pair withcrypto/ed25519; compute the fingerprint withfingerprint.Compute; POST<server>/api/v1/hosts/pairwithHostPairRequest(noise_public_keybase64url without padding,auth_public_jwkwithkty,crv,xonly,platform,os_version,host_version,profilesafter HD7,session_usersafter HD7). The lists come from the lastFullrun of the running service; in the direct path without a running service the helpers are asked over their endpoints, and an unreachable helper gives its session user an empty list with the warningpair: working directories follow with the first report. On201: ifkey_fingerprintdiffers from the own computation, store nothing and exit1(pair: fingerprint mismatch); else writekeysandpairing.jsonatomically (a new pair replaces the old keys only now), printfingerprint: XXXX-XXXX-...andhost: <host_id>, and, when an administrator called it, restart the host service. On a problem answer printpair: server refused: <code>withreasonwhen present and, for400 validation_failed, eacherrors[].codewith its pointer, then exit1; on401 unauthenticated(M2-41 answers every redemption failure so, also a retry after a lost answer, because the code is used) add the hintask an owner for a new pairing code. After a400the same code stays valid, so the operator fixes the configuration and runspairagain. The code, the private keys and the request body appear in no output or log. Ruling for docs/DECISIONS.md:pairneeds the identity of the service user (Design note 1) and restarts the service so that M2-43 logs in with the new keys. -
16 4.4 "Installation of the host service on a host" (English): the configuration file and its path (Design note 3), the program folder, the commands and flags of Design note 2,
pairwith administrator rights and the restart, the exit code78andbothy-host restartafter a refused start, the drive root prompt and--drive-root-confirm, the way per platform (Design notes 13 and 14), the operator step for a platform without a proven peer check. -
24 changes (English, each with its Ruling row): 2.2 the table "Codes of
doctor" (Design notes 6, 8, 9, 10), the accounts HD9 checks in its row, point 4 and "Reason for HD9" (6), the socketpair of the peer check (11), the configuration file as a place checked by HD10 (3, 8), the state directory contents (4), the declaration fromready(15), the definitions of the exit policy (14); 4.2 the state files; 7.4auth_status_unknown,declaration_unknown,host_service.auth_status_timeout; 16 R-TM-17 (the accounts of HD9, 6); 17 the new key and the measured default ofhost_service.entry_scan_timeout. -
Platform suite and measurements.
test/platform(build tagplatform) runs the built binary throughinstall,session-user add,remove,restart,doctor,runandpairon a disposable machine with administrator rights and cleans up after itself;make -C services/host platform-testbuilds the binary and the fakeclaudeand runs it withgo test -tags platform -count=1 ./test/platform/...(on Windows the samego testline). Before each platform test that needs a declaration (organizationinTestPlatform_TMT13,TestPlatform_TMT21_SessionUserAddRemove,TestPlatform_TMT27andTestPlatform_TMT29_Parts(f)) or a binding (TestPlatform_TMT21_SessionUserAddRemove,TestPlatform_TMT24), the platform suite writesdeclaration.jsonandbindings.jsonthroughinternal/statewith the service user as owner, becauseNoControlnever delivers areadyand no session of this ticket writes a binding. Each platform run writes a result file (platform, OS version, Go version, commit, test names, pass or fail, durations) that the report lists. Entry scan measurement:go run ./test/platform/bighomecreates a synthetic home of one million entries (files and folders mixed, depth up to twelve) for a session user; the measured duration of the entry scan per platform goes into the report, and the default ofhost_service.entry_scan_timeoutbecomes three times the slowest measured duration rounded up to a whole minute (24 17, configuration reference, RK-90). Ruling for docs/DECISIONS.md. -
Rulings for docs/DECISIONS.md, one row each with reason: notes 1, 2, 3 and 3c, 3d, 4, 5, 6 (codes, HD3 on Windows, the accounts of HD9), 9 (
auth_status_unknown,declaration_unknown), 10 (chain file check result), 11, 13 (paths, users, the macOS ad hoc signature), 14 (also noNoNewPrivileges=), 15, 16, 19, the rootMakefilecross-check. Facts already in the spec (the per-platform terms, the rule for names, the SetFileSecurityW choice) are not repeated there.
Tests (write them first)¶
Unit tests in services/host (make check on any build host, no administrator rights, hostostest and clocktest; world: service user bothy-host, session users bothy-s-alex (A), bothy-s-sam (B) and bothy-s-robin, server https://chat.example.com):
internal/config:TestLoadDefaultsPerPlatform,TestLoadRejectsUnknownAndDuplicateKeys,TestProfilesForms(shell,claude, acommandmapping, a relativeprogram),TestClaudeWorkdirsForm,TestDurationKeys(host_service.auth_status_timeoutincluded),TestAddRemoveSessionUserKeepsComments(remove drops the name and itshost_service.claude_workdirsentry, nothing else changes).internal/state:TestLayoutModes(directory0700, every file0600),TestAtomicWrite(crash between temp and rename leaves the old file),TestPairingRecordHasNoPrivateMember,TestBindings,TestDeclaration.internal/doctor:TestHD1(empty listsession_users_empty;root,Administrator,SYSTEMprivileged_user;Bothy-S-Alexnext tobothy-s-alexsession_user_duplicate; the service user as session user; unknown user; an invalid name),TestHD2(each fixed group, an extra group, Docker socket writable by mode, by ACL and by group, the fixed list cannot be shortened),TestHD3(sudo probe succeeds for the service user and for one session user; Windows passes without code),TestHD4,TestHD5(loopback, own interface, foreign address),TestHD6(declarationnone,organization,personal, unknown, each againstauthMethodnone,claude.ai,oauth_token,api_key,api_key_helper,third_party, an unknown method, garbage, a timeout athost_service.auth_status_timeout, a login file in the home, in.config/bothy-host/claude, the variable set;host_service.claude_binempty, unresolvable, script without interpreter in the search list: warningauth_status_unchecked; one entry per affected session user),TestHD7(each drop rule and the precedence of the code; the lists after HD7),TestHD8,TestHD9(former session user named, other users one entry without a name, local output names all;daemon(UID 1, home/usr/sbin, listable) skipped; a macOS system account (UID below 501) that owns a0755home skipped; the service user with a system UID checked;rootchecked; a person account (Linux UID 1000, macOS UID 501) with a listable home fails and is named locally;UID_MINandUID_MAXfrom alogin.defsfixture and their defaults when a key is missing; a Windows profile underProfileListchecked, the valuesDefaultandPublicnot),TestHD10(trusted owner per platform; each write right of 24 2.2 point 6 and D-W-86 as its own row for Linux, macOS and Windows, an inherit-only entry ignored for the folder itself,GENERIC_WRITEandGENERIC_ALLexpanded, an unmappable entry fails; configuration file;host_service.claude_binchain, interpreter, each search list folder;.cmdon Windows; setuid bit;ptrace_scope_low; anadhoc,linker-signedmacOS binary withoutruntimegiveshelper_unprotected; a macOS binary withget-task-allowgiveshelper_unprotected;peer_mismatchnamed;peer_check_unavailable),TestHD11(place of a session user: writable or owned by another at the place, on the way and at each chain file, readable place; place for all;drive_root_writable; unresolvable home; the precedence of the codes).internal/doctor:TestPathResolution(chain of links, a dangling link, a closed folder on the way, a link of a trusted owner pointing into a folder of A fails like (a), a configured component checked),TestIdentityCompare(the same folder through a link and in other case is one folder; dropped from both lists).internal/doctor:TestEntryScanSemantics(finding stays afterLight; aFullwithout finding lifts it; timeout givesentry_scan_timeoutwith the name; while running,entry_scan_runningonly for that session user and the others are ready),TestReportNamesRule(D-W-109 for HD6, HD9, HD10, HD11;checkandsession_userunique),TestReportHasNoPathsOrValues(a canary path, a canary token value and a canary file content appear nowhere in the report),TestDoctorOutputFormatAndExitCode(format of Design note 2, exit0with warnings only,1with one failure, HD9 user and drive named locally),TestDoctorOutputHasNoPathsOrValues.internal/localchan:TestPeerCheckWrongUser,TestPeerCheckWrongPID,TestNoMessageBeforePeerCheck,TestHelperRejectsOtherClient,TestCodecLimits(oversized, unknown kind, kind0x01refused in this ticket),FuzzDecodeMessage(any input gives a message or an error, never a panic).internal/install:TestInstallPlan_Linux,TestInstallPlan_Darwin,TestInstallPlan_Windows(golden step lists with argument vectors;TestInstallPlan_Darwincontains thecodesignstep of Design note 13 with its argument vector, and no such step for a binary already signed withruntime),TestServiceDefinitionsGolden(the exit policy of Design note 14 on each platform; noNoNewPrivileges=and none of the options implying it in the Linux host unit and helper service units),TestSessionUserAddRefusesBoundName,TestSessionUserAddCreatesAndRestarts,TestSessionUserRemove(config both keys, helper service removed, home closed, restart, binding kept),TestDriveRootDACL(only the(AD)entry ofAuthenticated Usersremoved; order and control bits kept; the inheritable(OI)(CI)(IO)(M)entry stays; a DACL without the entry is unchanged),TestDriveRootDeclined(no write; HD11drive_root_writable),TestRestartPlan(per platform: restart of a running and of a stopped host service after a refused start; no other service touched).internal/startup:TestRefusedStartReportsThenExits(fakeControl: oneLogin, exactly oneReportwith all checks,Close, exit78, no session accepted; TM-T21 (g) host part),TestRefusedStartUnpairedLocalOnly,TestDeclarationFromReady(HD6 finding with unknown declaration starts;readywithorganizationrefuses after one report;personalpasses; stored indeclaration.json; a secondreadyre-evaluates; storedorganization, HD6 finding,readywithpersonal: no refusal, one report with HD6 ok,declaration.jsonnowpersonal; storedorganization, HD6 finding,Loginfails: the codes logged and exit78),TestDoctorCommandInRunningService(full run, report, HD9 finding ends the service with78, HD11 finding only blocks its session user),TestSessionUserAddReportedAfterRestart(after the restart the report contains the new name with its list; TM-T21 (f) host part),TestNoControlIsUnreachable,TestConfigReadOnlyAtStart(a change ofhost_service.session_usersorhost_service.claude_workdirsin the file whilerunruns changes neither the checks nor the report until a restart),TestNoNetworkListener(every listener of the package is a Unix socket or a pipe).internal/pairing:TestPairRequestBody(members ofHostPairRequestonly, nodin the JWK, no path, lists after HD7 with a duplicated folder dropped; TM-T29 (c) withpair),TestPairRequiresHTTPS,TestPairStoresKeysOnlyAfterAcceptance,TestPairFingerprintMismatch,TestPairServerRefusal,TestPairNeverPrintsCodeOrKeys(canary code in stdout, stderr, logs and errors).cmd/bothy-host:TestCommandsRegistered,TestAdminRightsRequired(exit2with the message for each command),TestExitCodes.
Integration in the server module (make check; PostgreSQL 18 via pgtest, real modules through internal/wire, HTTP through the real chain behind an httptest TLS server on 127.0.0.1 whose certificate goes to --ca-file; people from identitytest; Robin is the owner):
TestHostPairAgainstServer: buildbothy-hostfromservices/hostwithgo build -o <tmp> ./cmd/bothy-host(working directory resolved from the test file,GOTOOLCHAIN=local); Robin creates a host with step-up (createHost) and issues a code (issueHostPairingCode);bothy-host pair --server <url> --code <code> --ca-file <pem> --config <tmp>runs in the direct path (the test user owns the temporary state directory); exit0; stdout shows the fingerprint in groups of four;getHostshowsconfirming, the samekey_fingerprint, the configured names inreported_session_users; the state directory holdskeyswith mode0600and both private keys, and the captured request holds neither private key nor the state path; the pairing code is in no output; a secondpairwith the same code exits1with401 unauthenticatedand the hint of Design note 16, and the stored keys of the first pairing stay unchanged.
Platform suite (make -C services/host platform-test, build tag platform, administrator rights, on Linux with systemd, macOS, Windows 10 and Windows 11; each test name in the run file):
TestPlatform_TMT13:doctorwith the service user as root, insudo(adminon macOS,Administratorson Windows), indocker, with a writable Docker socket (Linux), with passwordless sudo (Linux and macOS) once for the service user and once for a session user, each started through the service manager withbothy-host restartand refused withpasswordless_sudo, with a subscription login (fakeclaudeclaude.ai) atorganization, with the same service and session user, with an emptyhost_service.session_users, with two equal session users: each start refused with exit78and the code;doctorexits1and prints the code.TestPlatform_TMT22: a process as session user cannot listhost_service.state_diror read the key file; a helper whose user gets read access to the folder makes HD8 fail and the start refused.TestPlatform_TMT24: as 24 14 without sessions: a session user that can list the home of a person account (Design note 6) that is not a session user (macOS: instaffwith default home rights) gives HD9 refused with the user named locally and an unnamed report entry; without the right it passes; A opens her home: HD11 fails only for A with her name and B stays ready; after a restart HD9 passes and HD11 names A; A's long runner (crontab entry, on Windows a scheduled task) reopens her home aftersession-user remove:doctorexits1, the report entries the admin endpoint returns have one HD9 entry with A's name, and the service ends with78; after deleting A with her long runners andrestart, HD9 passes.TestPlatform_TMT26: a process of B kills B's helper and listens on the endpoint or creates the named pipe first: it gets no message, HD10 fails withpeer_mismatch, and the report entries the admin endpoint returns name B; a second process attaches to the running helper (ptrace,task_for_pid,OpenProcesswith write access) and fails; on macOS the installed binary carries theruntimeflag. A platform where this cannot be proven fails HD10 withpeer_check_unavailableand the report says so.TestPlatform_TMT27:organization, a subscription login in B's account; aclaudein B'sPATHand startup files reportingapi_key: HD6 runs onlyhost_service.claude_bin, detects the login, start refused;host_service.claude_binwritable by B: HD10 fails;#!/usr/bin/env nodewith a wrongnodein B'sPATH: the start log shows only the checkednode; anodein the search list writable by B, and on Linux/binwritable by B with the checkednodein the folder ofhost_service.claude_bin: HD10 fails.TestPlatform_TMT29_Parts: (a) a working directory of B writable by A with.claude/settings.jsonfrom A: HD11 fails with B's name, the fake hook andapiKeyHelpernever start (start log); (b) a folder above B's working directory writable by A; (d) B'sCLAUDE_CONFIG_DIRreadable by A; (h) aCLAUDE.mdof A above B's working directory, one of root writable by A, a.claudefolder of A: each HD11 failed for B only, A stays ready; (e) an entry of A in B's working directory: HD11foreign_entryfor B; removed, aLightrun keeps it,doctorlifts it; a home of B whose scan exceedshost_service.entry_scan_timeoutgivesentry_scan_timeoutwith B's name while A is ready first; (f) acommandprogram writable by a session user, owned by another user that is neither session user nor trusted owner, inside a folder of such a user, the same three forhost_service.claude_binatorganization, a.cmdon Windows: start refused (HD10, HD11), the report names no session user; (j) the link cases of 24 14 (j); (c) a folder under A and B dropped from both lists in the request ofpair; (i, Windows 10 and 11) the full case of 24 14 (i) with a test folder inC:\whose DACL has an inherited entry before an own one: beforeinstalland after a declined prompt HD11drive_root_writable, start refused, ACL ofC:\unchanged; after confirmation only(AD)is gone, order and control bits kept, every direct subfolder's security descriptor equal before and after; HD10 and HD11 pass;mkdir C:\.gitas A fails; an oldC:\.gitof A fails HD11 for every other session user with its name and passes after removal.TestPlatform_TMT21_SessionUserAddRemove:session-user addwith a name bound inbindings.jsonfails; with a new name it creates the user with a home only for it and the helper service and restarts the service (start counted);session-user removeremoves the name from both keys, the helper service, closes the home, restarts; the binding stays; a refused start (HD6 atorganization) is not restarted by the service manager (exactly one start within a wait ofhost_service.credential_check_interval),doctornames the account and exits1; after removing the login andrestart, HD6 passes.TestPlatform_ExitPolicy: a crash (SIGSEGV, on WindowsTerminateProcesswithoutSERVICE_STOPPED) is restarted; exit78is not.TestPlatform_DPAPIAndModes:keysis0600and owned by the service user, on Windows a DPAPI blob only the service account decrypts.TestPlatform_NoNetworkListener: no process of the service or the helpers holds a TCP or UDP listening socket (ss,lsof,netstat).TestPlatform_AdminEndpointPeer: a session user cannot connect to the admin endpoint; on Windows a pipe created first by a session user under the admin pipe name makesdoctorandpairrefuse to send anything.
Commands that must pass, from the repository root:
cd services/host && go test -race -count=1 ./...cd services/server && go test -race -count=1 ./test/terminal/...make arch,make codegen-check,make api-lint,make api-checks,make api-tablesmake config-referencethenmake config-doc-checkmake licenses,make osv,make docsmake -C services/host platform-teston each platform machinemake check
Run FuzzDecodeMessage once locally with go test -run=^$ -fuzz=FuzzDecodeMessage -fuzztime=<duration of your choice> ./internal/localchan and report duration and result; commit any crashing input as a seed after the fix.
Red team focus of the milestone (PLAN 8 "Red team", terminal: privilege escalation from the shell and bypassing doctor, replacing or tapping the PTY helper, hooks, apiKeyHelper or CLAUDE.md of another person, a readable CLAUDE_CONFIG_DIR, a program owned by another account, a drive root under Windows, standstill by HD6 or a chain file) is covered by TestPlatform_TMT13, TestPlatform_TMT26, TestPlatform_TMT27, TestPlatform_TMT29_Parts and TestPlatform_TMT24; name them in the red team request.
Acceptance criteria¶
-
install,session-user add,session-user remove,restart,pair,doctor,runandpty-helperexist with the flags, identities and exit codes of Design notes 1 and 2. - TM-T13:
doctorrefuses the start with a code in each case, on macOS, Linux and Windows. - TM-T22: no session user reads the state directory or the key file; a helper with read access fails HD8 and the start is refused, on all three platforms.
- TM-T24 without sessions: HD9 only against the homes of
root, the service user, former session users and person accounts that are not session users (other system accounts skipped, Design note 6), with the former session user named in the report and every user named locally; an open home of a session user blocks only that session user under HD11 with its name, on all three platforms. - TM-T26: a replaced helper or a pipe created first gets no message, HD10 names the session user of that channel; attaching to the helper fails (on macOS with the hardened runtime that
installsigns in); a platform without proof fails HD10 withpeer_check_unavailable. - TM-T27: HD6 runs only
host_service.claude_binwith the fixed search list; a writable file, interpreter or search list folder fails HD10; the wrongnodenever runs, on all three platforms. - TM-T29 (a) up to the result of
doctor, (b), (d), (e), (f), (h), (i) and (j), and (c) with the request ofpair, as in Tests; Windows 10 and Windows 11 with default rights pass HD11 afterinstallwith confirmation, and only(AD)is removed from the drive root. - TM-T21 (e), (f) and (g) host parts: a bound name is refused; a new name is reported after the restart without another step; a refused start reports once and is not restarted by the service manager;
doctornames the account and exits non-zero. -
doctorreports codes and names only, never paths, contents or values; HD6 and HD11 have one entry per affected session user, HD10 names the session user of a wrong peer, HD9 names each former session user. -
pairagainst the real server shows the fingerprint, and both private keys exist only in the state directory with mode0600or as a DPAPI blob. - The measurements of the Windows default ACLs and of the entry scan per platform are in the report; 24 and the default of
host_service.entry_scan_timeoutfollow them; the list of chain files is checked against the current documentation. -
HostReady.llm_credentials,host_service.auth_status_timeout, the doctor codes and the rulings of Design note 20 are in the specification and indocs/DECISIONS.md. -
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why.
services/host/README.md(English): commands, identities, files and paths per platform, the configuration file, the state layout, the doctor codes, the exit policy, the way per platform for users, helpers and the peer check, what M2-43 and M2-45 add.- Rulings of Design note 20 recorded in
docs/DECISIONS.mdsection 10 (one row each with reason); the changes to 24, 16, 02,openapi.yaml,asyncapi.yaml, ADR 0068 "Confirmation" and RISKS of the Scope made in the same change; configuration reference regenerated. services/host/CHANGELOG.mdentry under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). - The platform runs of all four machines recorded with their result files;
make checkgreen. - An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check. - Review and red team verdicts and the findings fixed.
- What is not proven.
- Follow-ups that could not be done in this scope, each with its reason, and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.