Local service daemon¶
Kwt runs at most one writable local service daemon for each canonical kwt home. The daemon is a host for kwt domain services; it is not the multi-machine sync hub. It owns current worktree inventory, repository-config approval, worktree and project removal, and reviewed SSH route and lease lifecycle. It also carries bounded operation events. Git status and changed-file collection remain in the foreground client, and tmux workspace establishment is not a daemon route.
The process binds an automatically selected IPv4 loopback port and publishes
an owner-only runtime record under <kwt-home>/runtime. Clients verify the
recorded PID, process creation identity, service identity, and a challenge
proof before they send the record's bearer credential. A matching live but
unresponsive owner is preserved; kwt never starts a competing writer.
kwt daemon start|stop|restart|status manages the background process. kwt
serve runs the same host in the foreground, disables idle exit, and refuses to
replace an existing owner. Background logs are written to
<kwt-home>/daemon.log, rotate at 10 MiB, and retain three owner-only backups.
Compatible clients share the newest running daemon. Build order uses an exact
full source revision first, then differing semantic versions, then the source
commit time. Source times are canonical RFC3339 UTC values authenticated in
both the private runtime record and status response. Hashes are never compared
lexically. Matching semantic module versions count as the same build only when
both installations explicitly lack VCS revision and time identity, as with
go install ...@version. Different revisions with equal source times, missing
contemporary metadata, or invalid values have unknown order. A build from a
dirty worktree marks its revision dirty and omits revision time, so it cannot
claim the clean checkout's identity or source order.
With daemon.auto_restart = "newer", a provably newer client asks an older
daemon to drain before replacement. Automatic start reuses a ready daemon when
order is unknown; it never guesses. Explicit restart accepts the same or a
newer invoking build, returns daemon_downgrade_refused for an older build,
and returns daemon_build_order_unknown when order cannot be proved. A
draining daemon applies the same refusal before another binary waits and
launches. kwt daemon stop followed by kwt daemon start is the deliberate
operator override for unknown order and may install either build. Ghosthub may
use that override only for an explicit helper-update action, never routine
polling.
Draining rejects new operations with a retryable typed response that carries
the deadline. The command requesting shutdown prints the returned drain state
immediately, then continues reporting observed drain state while it waits.
Active work and leases may finish until daemon.replacement_grace; the default
is five minutes.
The API schema is 1.12.0. It exposes authenticated status, graceful shutdown,
worktree inventory, repository-config approval, guarded worktree and project
removal, operation events, and SSH route and lease lifecycle under /api/v1.
It exposes proof-capable liveness at /api/ping and credential-free OpenAPI at
/openapi.json. Inventory clients require the
worktree.inventory.v2 capability. Foreground change inspection additionally
requires worktree.inventory.config.v1, which guarantees that repository
inventory carries the effective global configuration used to derive protected
environment-variable names. A client fails closed rather than running Git when
that capability or configuration is absent. Guarded unregistration requires
project.removal.v1, SSH route resolution requires ssh.resolve.v1, and
daemon-owned connection leases require ssh.lifecycle.v1. Worktree removal
uses worktree.removal.v2 for session, branch, and HEAD guards. Clients that bind
short commands to a connection-owned hold additionally require
ssh.lease.hold.v1; stale development daemons fail capability negotiation
before acquiring a lease rather than falling back to periodic touches.
Daemons advertise operation.stream.v1 when they can
carry ordered domain-operation events and bound prompt responses. Advertising
that transport capability does not start a domain operation or move any
existing command behind the daemon. An operation never has simultaneous direct
and HTTP execution paths.
Service failures cross the in-process, HTTP, and machine-readable CLI
boundaries as one descriptor with code, human-facing message, retryable,
and optional typed details. Adapters preserve the descriptor; they do not
infer a failure from prose or HTTP status. HTTP uses the same message as its
RFC problem detail. Unknown HTTP codes become daemon_transport_failed
instead of being guessed. Detail keys are allowlisted per code: draining may
carry an RFC3339 drain_deadline, and repository trust interaction carries
its typed digest-bound prompt fields. Within API major 1, draining responses
also mirror the deadline in the legacy top-level drain_deadline field, and
clients recognize a legacy busy response only when it carries a valid drain
deadline.
The daemon and inventory paths currently emit these stable codes:
| Code | Meaning |
|---|---|
invalid_request |
The request is structurally invalid. |
daemon_start_failed |
The daemon could not launch or become ready. |
daemon_unresponsive |
A verified owner exists but cannot safely be reused or replaced. |
daemon_incompatible |
The owner lacks the required API major or capability. |
daemon_downgrade_refused |
An older client attempted replacement. |
daemon_build_order_unknown |
Replacement order cannot be proved. |
daemon_draining |
The owner is draining; retry according to its deadline. |
daemon_transport_failed |
The verified daemon exchange failed or was not understood. |
inventory_timeout |
A current inventory refresh exceeded its bound. |
inventory_failed |
Inventory discovery failed for another known source cause. |
removal_failed |
A known worktree removal failure retained safe actionable detail. |
project_not_found |
No exact persisted project path matched the request. |
registration_changed |
Project identity or registry state changed; retry from inventory. |
unregistration_failed |
Project metadata could not be removed safely. |
protected_session_live |
A live protected tmux endpoint still belongs to the project. |
protected_endpoint_inventory_incomplete |
Durable endpoint authority could not be verified. |
interaction_required |
Repository configuration needs digest-bound approval. |
operation_id_conflict |
An operation identifier was reused for a different request. |
operation_capacity_exhausted |
Bounded operation capacity was exhausted. |
operation_outcome_unknown |
The operation's terminal outcome can no longer be proved. |
ssh_invalid_target |
Structured SSH target validation failed. |
ssh_resolution_failed |
Effective OpenSSH configuration could not be observed. |
ssh_route_unreviewable |
A proxy route cannot be bound to independently reviewed hops. |
ssh_configuration_changed |
A later lifecycle request observed a different route identity. |
ssh_unsupported_version |
The installed OpenSSH cannot support the required prompt policy. |
ssh_interaction_required |
Connection preparation needs a prompt-capable client. |
ssh_prompt_rejected |
The client rejected an OpenSSH prompt. |
ssh_prompt_timed_out |
A bound OpenSSH prompt exceeded its response deadline. |
ssh_connection_failed |
OpenSSH could not establish the reviewed route. |
ssh_connection_changed |
A generation-bound lease is no longer usable. |
ssh_control_path_occupied |
A verified private control path is occupied unexpectedly. |
ssh_cleanup_failed |
Verified SSH connection cleanup did not complete. |
internal |
An unexpected failure was withheld from the public response. |
operation_journal_unavailable remains reserved until kwt has a durable
operation journal. The current operation stream is deliberately in-memory and
same-daemon only.
Operation events carry an opaque operation ID and a strictly increasing sequence number. A reconnect sends its last accepted sequence and receives retained later events before live events. One operation may carry multiple prompt rounds; each response is accepted only for the exact current prompt ID, including an intentionally empty response. Stale, duplicate, and cross-operation responses fail closed. A client acknowledges a prompt sequence only after its bound response succeeds, so reconnect replays an unanswered prompt. Prompt events carry the daemon's response deadline. When that deadline expires, the client stops waiting for input, consumes the prompt sequence without a response, and continues reading until the daemon publishes the authoritative terminal failure.
Each operation retains at most 256 events and 1 MiB of event payload. Public
failure sanitization happens before admission, and each admitted event is
retained and replayed from one immutable encoded representation so later
caller mutation cannot change the stream or its byte accounting. The daemon
admits at most 128 active operations and retains at most 128 completed
operations for five minutes. An operation admits at most eight subscribers,
with at most 128 live subscribers across the daemon. Retained terminal replays
and streams detached after queue overflow count against both limits until the
HTTP subscriber closes. Publishing a terminal event does not release an active
operation slot until its worker cleanup returns. Each response write has a
five-second deadline. Clients wait at most two seconds for event-stream headers
and for a prompt-response exchange; after event headers arrive, the stream body
lifetime follows the caller's operation context. Replay queues reserve a slot
that progress and warning events cannot consume; prompts and terminal
completion remain replayable when noncritical delivery saturates. The daemon
rejects excess work or subscriptions instead of dropping a prompt or terminal
result. If retained-event capacity terminates an admitted worker, the terminal
result is operation_outcome_unknown, never a safely retryable capacity
rejection. A new operation has five seconds to gain its first subscriber.
Afterward, losing the final subscriber starts the same five-second reconnect
grace before the worker is canceled. A client retries one interrupted stream
against the same proof-verified daemon. Daemon loss, retention loss, or
replacement of the runtime owner returns
operation_outcome_unknown; the client never repeats the domain mutation to
guess its result. Every unknown-outcome descriptor is non-retryable because the
original mutation may already have completed. If a client cannot render or
otherwise handle a preterminal event, it requests cancellation on a best-effort
basis and reports operation_outcome_unknown. A terminal result remains
authoritative even when the client cannot render its terminal event.
Operations reserve daemon work until their workers return, including cleanup after a terminal outcome is published. Draining refuses new operations and lets admitted work run until the published replacement deadline. At the deadline it publishes an unknown terminal outcome, cancels remaining workers and HTTP handlers, then waits up to five seconds for their reservations to release.
POST /api/v1/ssh/resolve evaluates system OpenSSH configuration through one
daemon-owned service and returns an immutable route snapshot. On POSIX, the
service runs nonce-framed ssh -G inside the account's configured login shell
so shell startup banners cannot become configuration. Windows invokes system
OpenSSH directly. Each request reloads the global fleet-token environment name
and uses the invoking CLI's fresh environment and working directory rather than
daemon startup state. The client strips configured credential variables before
transport, and the daemon repeats that stripping after reloading its
authoritative configuration. OpenSSH's executable path is bound from this
invocation context before a login shell can change PATH. Requests without
invocation-context authority fail closed.
Direct ProxyJump hops are resolved in connection order; opaque ProxyCommand and
nested proxy routes fail closed. The complete normalized option stream
contributes to route identity but never crosses the HTTP boundary. Each target
projection is target-local; a downstream projection requires master-backed
proxy transport through its preceding prepared target and is never a
standalone direct-connect command. This resolution route caps stdout and
stderr at 1 MiB each and cancels the complete resolver process tree when either
bound is exceeded. The daemon also caps the encoded public route snapshot at
8 MiB; the client reserves 64 KiB above that bound for response framing, and an
oversized snapshot fails as ssh_resolution_failed. Resolution does not create
a connection, control socket, trust decision, credential prompt, or lease.
kwt ssh lease may resolve and acquire in one CLI process when the caller has
no prior snapshot. Clients performing a conditional launch continue supplying
the reviewed route identity and projection policy. kwt ssh exec and
kwt ssh copy use the combined form for one bounded command or transfer: the
daemon owns route resolution, prompt handling, ProxyJump masters, and lease
lifecycle, while the foreground kwt process owns the system SSH or SFTP child
and streams its output directly. Copy uses SFTP rather than a remote shell,
resolves the local source to an absolute literal path, and escapes SFTP batch
metacharacters; the child receives only the daemon's
generation-bound, fail-closed arguments. Consumers do not reconstruct those
arguments or load SSH configuration again. Cancellation terminates the entire
client process tree. An authenticated HTTP stream binds the lease to the
foreground owner without depending on that process to run heartbeat code while
job-control suspended; disconnecting the stream restores ordinary bounded
expiry. The client releases the daemon lease under a separate cleanup deadline.
kwt projects and kwt list auto-start or reuse the daemon and require a
current inventory result. They fail instead of falling back to cached or direct
filesystem data. The TUI may paint immediately from the derived last-known-good
cache at <kwt-home>/cache/inventory-v2.json, then requests one current
snapshot. Failure to initialize or publish the disposable cache is diagnostic;
current inventory remains available without it. Only dashboard snapshots with
protected tmux sockets resolved may replace the shared cache. The cache is never
mutation authority. Git status and fetch remain in the foreground client so
their credential environment is unchanged.
A current snapshot carries the effective global configuration used for its discovery. Before enabling actions, the TUI installs that configuration, collects status with its worktree base directory, and rebuilds config-derived tmux and credential handling. Cached first paint never changes client configuration.
Each inventory request carries the invoking client's working directory, home directory, and sanitized environment map for path expansion. The daemon does not use its startup environment or working directory to interpret global path configuration, so a reused daemon preserves foreground CLI semantics.
Repository-local configuration is resolved per request. Unknown content produces a digest-bound interaction requirement. Approval reopens and hashes the file before persisting trust; rejection and noninteractive ignore are request-local. Noninteractive commands preserve the historical global-only fallback and warning.
Project unregistration takes an exact persisted path, the credential-free
repository identity, and the opaque registration fingerprint from current
project inventory. The fingerprint covers the complete decoded raw registry
entry, including unknown fields and last_touched; it is a concurrency token,
not authorization. A home-scoped transition lock hands the exact raw
registration from registry writers to its
identity-keyed project fence: the registration and identity are revalidated
after that fence is acquired, before the transition lock is released.
Registration changes acquire both old and new identity fences in deterministic
order. Worktree creation may remain unregistered, but pull-request import and
protected attach require a current registration. Under the identity fence,
removal reads durable pull-request provenance, follows repository-transfer
alias history across clone-path drift, derives each protected socket, performs
a fail-closed three-state tmux probe, and finally compare-and-swaps the raw
registration. Live sessions block removal; indeterminate probes, disconnected
same-path provenance, or incomplete authority reject it. The transaction
mutates only project metadata and never sends a tmux kill command.
The daemon compares the required fingerprint against its own freshly loaded
exact registration before identity resolution or endpoint inspection. The
raw-entry revalidation and final compare-and-swap remain mutation authority.
A mismatch returns retryable registration_changed; clients must refresh and
ask the user to authorize the newly observed entry rather than retrying
automatically. If a transport response is lost, current inventory distinguishes
a completed removal, an unchanged registration, and a same-path replacement.
For remote use, including Ghosthub, the remote shell invokes the remote kwt
CLI and that CLI talks only to its same-machine loopback daemon. The daemon is
never exposed as a remote service. A monitoring read may start or replace a
daemon after a kwt upgrade. Active SSH leases participate in the documented
replacement grace period and are never transferred to the successor.