docs: finalize v1 design and Windows service model

This commit is contained in:
2026-08-28 11:00:58 +00:00
parent a89253be96
commit 85f4d5d2a0
11 changed files with 833 additions and 241 deletions
+195 -29
View File
@@ -4,6 +4,10 @@ Daemon configuration uses strict TOML as specified in
[`configuration.md`](configuration.md); the annotated examples contain every
v1 knob and default.
The implementation order is Windows client first, Linux client second. The
sections below describe both final contracts; their document order does not
override that release gate.
## Unix-like clients
The client starts `sh` or `bash` in a new session/process group. Unix signals
@@ -65,52 +69,210 @@ supervisory cgroup imposes no resource limit.
## Windows clients
The minimum supported v1 Windows versions are Windows 10 and Windows Server
2016. For every command, create a non-inheritable Job Object, set
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and do not enable breakaway. The daemon
starts an RVBox per-command launcher suspended with `CREATE_NEW_CONSOLE`,
`CREATE_UNICODE_ENVIRONMENT`, and `EXTENDED_STARTUPINFO_PRESENT`; it assigns the
launcher atomically through `PROC_THREAD_ATTRIBUTE_JOB_LIST`. Only an explicit
standard-I/O and launcher-control handle list is inherited, and the Job handle
is never inherited.
2016. Desktop Experience is required only for the tray and active-session
execution contexts; the service and Session 0 contexts may run headless on
Server Core. Installation is an explicit UAC-elevated operation that registers
one machine-wide Automatic SCM service. The service runs as `LocalSystem` before
login and alone owns configuration, WSS, durable state, logs, admission,
history, process handles, and Job Objects. Normal service startup never shows
UAC and is not blocked by tray or interactive-user availability.
The launcher invokes exactly the selected shell against the generated wrapper:
The same signed `rvbox.exe` has explicit `service`, `tray`, `install-service`,
`uninstall-service`, `configure-service`, per-command launcher, and signal-
helper modes. Internal modes require SCM state or a service-created launch
proof. Task Scheduler is not used. The installer registers an unelevated per-
user tray launch through the machine-wide `Run` key. One tray may run in each
logged-in session; its exit, failure, disablement, or Explorer restart does not
stop the service or commands.
The executable uses the Windows GUI subsystem so service, tray, and internal
helpers do not flash consoles. Human-invoked console modes such as
`--check-config`, install, uninstall, and service configuration first call
`AttachConsole(ATTACH_PARENT_PROCESS)`, rebuild the standard handles, and emit
normal UTF-8 diagnostics when a parent console exists; otherwise they use a
native dialog or documented exit code. Internal launcher arguments are never
printed. This preserves usable terminal help/errors without adding a second
persistent executable or process.
The tray is a thin frontend over a local-only named pipe. It never opens the
SQLite store/spool or owns a server connection. The pipe rejects remote clients,
has explicit ACLs, and the service impersonates each caller for authorization.
Read-only health/status is available to an interactive local user. Service
start/stop/restart, Automatic/Manual startup changes, config editing, incident
resolution, and other machine-wide mutations require a locally elevated
administrator helper. `Exit` closes only that tray. Server Core and logged-out
machines simply have no tray.
When no config path was installed, resolve `FOLDERID_ProgramData` with the
Windows Known Folder API and use `RVBox\client.toml`, `RVBox\state`,
`RVBox\work`, and `RVBox\logs\rvbox.log` beneath it. Do not expand
`%ProgramData%` text from TOML. State and the work root are writable only by
SYSTEM and Administrators. When `cwd` is omitted, the service creates or opens
an identity-scoped child beneath the work root: a user-SID directory for either
active-user context, a LocalService directory, or a SYSTEM-only directory for
each SYSTEM context. Its non-inherited ACL grants only SYSTEM and the effective
non-SYSTEM SID the required access. The service validates the existing owner,
ACL, and reparse-point state before reuse, so one user cannot pre-create or
modify another identity's work area. Config/log read access needed by the tray
is separate from edit access. First installation writes the annotated template
and lets the service remain live but not ready until routing validates.
Configuration is restart-only. File opening uses exact resolved regular paths,
never a constructed shell command.
### Windows execution-context hierarchy
`ExecutionSpec.elevated` is the only caller-facing privilege choice. Windows
combines it with the presence of one usable active interactive session to select
an effective context. The ordered rules are:
| Login state | `elevated` | Ordered pre-launch contexts |
| --- | --- | --- |
| usable active user | false | `active_user` only |
| usable active user | true | `active_user_elevated` -> `active_system` -> `local_system` |
| no usable active user | false | `local_service` only |
| no usable active user | true | `local_system` only |
Here `active_user` is the selected user's deliberately non-elevated token;
`active_user_elevated` is that user's traditional full administrator token;
`active_system` is SYSTEM placed in the selected session; and the two `local_*`
contexts run in Session 0. “Usable” means the deterministic WTS selection and
token validation below succeeded, not merely that some disconnected session
record exists.
Fallback is allowed only during token/session selection before
`launch_prepared`. Failure to create a launcher/shell, an uncertain launch, CWD
or executable rejection, or failure after authorization never tries another
identity. The persisted attempt list and selection detail state why an elevated
request reached SYSTEM. If a formerly active user logs out during selection,
the service re-enumerates once and applies the no-user row; it never retargets a
different user silently.
The LocalSystem service obtains `local_service` with passwordless
`LogonUserW("LocalService", "NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, ...)`.
`local_system` duplicates the service token. To find an active user, enumerate
WTS sessions and obtain the selected token with `WTSQueryUserToken`. Prefer a
valid active physical-console session; if none exists, accept exactly one
`WTSActive` interactive session. Multiple remaining candidates are ambiguous,
so there is no usable active user rather than a nondeterministic choice.
Immediately before release, require the same session ID, logon SID, and user SID.
For `active_user_elevated`, inspect `TokenElevationType`. A traditional limited
administrator token must expose a linked full token; an already-full
administrator token is usable as-is. A standard user returns
a `CODE_ELEVATION_UNAVAILABLE` attempt reason. Windows Administrator Protection, or
another policy requiring interactive approval rather than exposing a reusable
full token, records a bounded policy-specific attempt reason. V1 never waits
for a UAC/Hello prompt; an elevated request proceeds to `active_system`.
That context duplicates the service token and sets its `TokenSessionId`
explicitly. If session-scoped SYSTEM construction also fails before preparation,
the final context is Session 0 `local_system`. This fallback intentionally
bypasses user-scoped approval because the installed service already holds
SYSTEM; it must be conspicuous in status/audit history. The final
`local_system` fallback supplies elevation but not access to the interactive
desktop: a display, audio, or other session-scoped command may therefore fail
normally in Session 0. Selection fallback is not a promise that the requested
operation is meaningful in the resulting context.
For normal `active_user`, use the filtered token when Windows supplies one. If
an administrator is logged in with only a full token (for example traditional
UAC is disabled), create and verify a LUA-style restricted medium-integrity
token with administrator SIDs deny-only and unnecessary privileges removed.
Never let `elevated=false` inherit a full administrator token merely because of
machine policy; if a verified non-elevated token cannot be built, reject rather
than use SYSTEM or LocalService while that active session remains selected.
Build the base environment from the effective token and then apply persisted
overrides. Session 0 contexts do not inherit a person's profile, mapped drives,
user certificates, or per-user proxy settings. LocalService may use ordinary
DNS/TCP/HTTP(S), localhost sockets, and ACL-permitted local files, but presents
anonymous credentials to remote Windows resources. Active-user contexts load
that user's profile/environment as needed and unload only after the whole Job
exits. `active_system` remains SYSTEM: putting it in the user's session does not
give it that user's HKCU, profile, mapped drives, or network credentials.
### Windows process launch and supervision
For every command, create a non-inheritable Job Object, set
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and do not enable breakaway. Use a short-
lived stateless per-command launcher mode of the same executable for every
context. This uniform path is required because Windows does not permit normal
inherited handles across Terminal Services sessions. The launcher owns no
durable state or policy and remains inside the command Job only to bridge
standard I/O and watch its shell.
The service creates local named pipes for launcher control and stdin/stdout/
stderr using unpredictable per-command names, `PIPE_REJECT_REMOTE_CLIENTS`, and
ACLs limited to SYSTEM plus the selected token SID. It creates the launcher
suspended with `CreateProcessAsUser`, no inherited handles, the selected token/
session, and an opaque channel identifier. Before resuming it, the service
assigns the launcher to the empty Job. On connection, verify the named-pipe
client PID, its `GetProcessTimes` creation `FILETIME`, expected token SID,
session ID, and launch generation; a same-user process racing for a pipe cannot
satisfy that complete identity.
The launcher opens only those pipes, creates the selected shell suspended in
its own dedicated console with `CREATE_NEW_CONSOLE`, `CREATE_SUSPENDED`,
`CREATE_UNICODE_ENVIRONMENT`, and `EXTENDED_STARTUPINFO_PRESENT`, and uses
`STARTF_USESHOWWINDOW`/`SW_HIDE`. It inherits only the explicit standard-I/O
handles through `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`. Non-breakaway Job
membership propagates from the launcher. It reports the suspended shell PID and
creation `FILETIME` and waits for the service's release/watchdog channel.
Invoke exactly the selected shell against the generated wrapper:
`cmd.exe /D /S /C` for a `.cmd` wrapper, or `powershell.exe` with `-NoLogo`,
`-NoProfile`, `-NonInteractive`, and `-File` for a `.ps1` wrapper. Application
paths and argument quoting are constructed by the Windows launcher, never by
concatenating an untrusted command line. There is no fallback between shells.
paths and argument quoting are constructed by one reviewed Windows routine,
never by concatenating an untrusted command line. There is no fallback.
Persist and flush `launch_prepared` with the launcher PID,
`GetProcessTimes` creation `FILETIME`, and launch generation. Persist and flush
`launch_authorized` before calling `ResumeThread`. The launcher then starts the
requested `cmd` or `powershell` suspended in its console with
`CREATE_NEW_PROCESS_GROUP`, reports the shell PID/group through the private
control channel, connects the allowlisted pipes, and resumes it. This two-step
shape is required because `CREATE_NEW_PROCESS_GROUP` is ignored when combined
with `CREATE_NEW_CONSOLE`, and console control events reach only groups sharing
the caller's console. Failure after authorization terminates the Job and is
reported as interrupted; it never redispatches the UUID.
The launcher remains the in-console signal proxy and calls
`GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, shell_group_id)` on request. The
daemon retains the sole Job handle, so an unclean daemon exit closes the last
handle and terminates the launcher, shell, and descendants. Recovery never
kills by persisted PID alone; the PID/creation-time tuple is diagnostic evidence
for PID reuse or cleanup anomalies. Child processes normally join the Job.
Persist and flush `launch_prepared` with requested elevation, attempted/effective
context, selection detail, effective token SID, selected session/user SID where
applicable, launcher and shell PID/creation times, and launch generation while
the shell remains suspended.
Persist and flush `launch_authorized` before sending the release token that
makes the launcher call `ResumeThread`. Control-channel EOF before authorization
exits without running user code; EOF afterward terminates the Job. A failure
after authorization is interrupted and never redispatched. The service retains
the sole Job handle, so an unclean service exit terminates launcher, suspended
or running shell, and descendants. Recovery never kills by persisted PID alone.
Job Object limits enforce requested profiles and `KILL_ON_JOB_CLOSE` protects
against lost supervision.
Do not request `CREATE_NEW_PROCESS_GROUP` with `CREATE_NEW_CONSOLE`; Windows
ignores that combination. TERM instead launches a short-lived private mode of
the same `rvbox.exe` under the command's effective token and session. It uses a
separate PID-verified local named-pipe handshake, not inherited cross-session
handles, to receive the target PID/creation-time/generation. It attaches to the
command's dedicated console, installs a handler that consumes its own
CTRL_BREAK, and calls
`GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0)`. Prefer the live root PID; after
root exit, select and birth-verify a live PID from the Job list. Because each
command owns its console, this does not address an unrelated command. The
helper detaches and reports delivery; it receives neither the Job handle nor
command stdio.
Root-process exit begins the same drain grace period. Completion waits for the
Job Object to reach zero active processes; after the grace period RVBox
terminates the Job, drains its capture handles, records any incomplete-output
marker, and emits the terminal lifecycle event last.
Only `TERM`/`SIGTERM` and `KILL`/`SIGKILL` are accepted. `TERM` attempts `CTRL_BREAK_EVENT`
and waits 10 seconds, then calls Job Object termination if the job persists;
Only `TERM`/`SIGTERM` and `KILL`/`SIGKILL` are accepted. `TERM` attempts the
console-helper `CTRL_BREAK_EVENT` and waits 10 seconds, then calls Job Object
termination if the job persists;
`SIGKILL` calls Job Object termination immediately. A console signal is
best-effort, so callers receive an explicit escalation result. Windows status
uses process and Job Object accounting APIs; it does not claim Linux-only
diagnostics such as an I/O wait channel.
Display topology APIs that require the console desktop, DDC/CI monitor
enumeration, and active-user audio policy normally require an active context;
SYSTEM privilege in Session 0 does not substitute for session visibility.
Localhost TCP is machine-wide and normally works in every context, subject to
the listener's own authentication. Fixed local-drive access follows NTFS ACLs;
the client never broadens a requested directory automatically. Every command
record and `stat` view includes requested elevation, attempted/effective
contexts, selection detail, effective token SID, and target session ID/user SID
when present.
## Storage and recovery
SQLite runs in WAL mode. Every append-only segment has a SQLite-owned
@@ -186,6 +348,10 @@ crossing one rejects new unreserved allocations even if the logical quota has
headroom. Already-reserved terminal/loss closeout remains writable while bytes
physically remain.
The Windows service installs with Automatic startup. Its bounded file logging
defaults to a 10 MiB current file and five retained sealed files under the
resolved ProgramData log directory; tray lifecycle does not affect log output.
These bounds protect RVBox's own loops; they cannot make arbitrary child
commands harmless when no resource profile is requested. Operators should
enable resource profiles for untrusted or expensive workloads and keep nginx,