diff --git a/docs/architecture.md b/docs/architecture.md index 3a3d4dc..59ea0ea 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,9 +8,19 @@ state and exposes a local control plane to `rvc` and, optionally, JSON-RPC callers. This design is the v1 contract for Go implementations and a later Rust client implementation. -RVBox deliberately executes arbitrary commands with the identity, permissions, -and base environment of the client daemon. It is therefore an administrative -tool, not a multi-tenant remote-execution service. +The first supported Go client is the Windows service client. The Linux client +is implemented afterward against the already proven shared runtime/protocol +and remains a primary v1 target. The server remains Linux-first. + +RVBox deliberately executes arbitrary commands under locally selected +execution identities. It is therefore an administrative tool, not a multi- +tenant remote-execution service. On Windows the main service runs as +`LocalSystem` and maps the request's elevation intent plus current login state +to an effective user/session context. A peer that can successfully impersonate +the client's routing identity can request elevation, whose fallback may reach +SYSTEM. This makes the accepted self-reported-identity/network trust boundary +especially consequential; the explicit elevation bit and tray visibility are +intent/audit signals, not authorization controls. Both daemons use strict TOML 1.0 configuration. The normative schema and fully annotated examples are in the [configuration contract](configuration.md). @@ -47,11 +57,11 @@ external access-control layer. ## Components ```text -rvc -- gRPC/Unix socket -- rvbox-server -- WSS/nginx -- rvbox client -- shell - | | - SQLite/WAL +-- optional HTTP JSON-RPC (debug/batch) - | - compressed output segment files + audit log +rvc -- gRPC/Unix socket -- rvbox-server -- WSS/nginx -- rvbox service -- shell + | | | + SQLite/WAL | local named pipe + | | | + compressed data +-- optional JSON-RPC +-- per-session tray ``` `rvc` is a thin control-plane client. The server owns durable command history, @@ -60,10 +70,18 @@ owns active process supervision, unacknowledged output spooling, and safe reconnection. Neither daemon lets a slow peer, command, or output stream block its dispatch loops. +On Windows, SCM starts one machine-wide `LocalSystem` service before login. It +alone owns networking, durable state, logs, command admission, process handles, +and Job Objects. A separate unelevated tray may run in each logged-in session +and communicates only through an ACL-protected local named pipe; it is an +optional frontend and its exit or absence does not stop the service. Task +Scheduler is not used. + ## Identity, sessions, and lifecycle 1. The client connects over WSS and sends `ClientHello` with its client ID, - protocol capability, OS/architecture, daemon version, current daemon CWD, + protocol capability, OS/architecture, daemon version, configured daemon CWD/ + Windows work root, supported shells, and a durable random client-instance UUID. 2. The server accepts the current compatible protocol version, fences the previous connection for that ID and same client instance, and returns a @@ -83,7 +101,8 @@ its dispatch loops. Each user request has a UUID (`issue_uuid`) and a durable request record: target client, request/issue timestamps, shell type, command text or script descriptor, -CWD, environment overrides, resource-profile flags, and lifecycle state. `rvc` +CWD, environment overrides, Windows elevation intent and effective execution +identity, resource-profile flags, and lifecycle state. `rvc` normally supplies this UUID as its optional `request_id`; the server generates one when it is omitted. Command and mutation identifiers are UUIDv7 values. Transport is at-least-once, but the client durably @@ -160,9 +179,21 @@ client and 10,000 queued commands globally, subject to the stricter byte quotas. PowerShell. This avoids transport quoting and Windows command-line limits; it never performs shell detection or fallback. Wrapper cleanup follows the same terminal rule as uploaded scripts. -- A command receives the daemon account's permissions and startup environment, - overlaid with the persisted `env_overrides` map. The effective CWD is the - requested existing directory or the registered daemon CWD when omitted. +- Unix commands receive the daemon account's permissions and startup + environment. Windows interprets the request's `elevated` boolean through a + login-aware hierarchy. With a usable active session, normal work uses a + deliberately non-elevated `active_user` token; elevated work tries + `active_user_elevated`, then `active_system`, then `local_system`. With no + usable active session, normal work uses `local_service` and elevated work uses + `local_system`. Fallback is allowed only during token selection before + `launch_prepared`, never after a process may have started. Requested elevation, + attempted contexts, effective token SID, session ID/session-owner SID, and + selection detail are persisted in command history. +- A command receives the base environment for its effective identity, overlaid + with the persisted `env_overrides` map. The effective CWD is the requested + existing accessible directory when supplied. When omitted it is the registered + daemon CWD on Unix or an ACL-isolated identity child beneath that registered + root on Windows. - Each command is isolated into a process tree: a Unix session/process group or a Windows Job Object. A daemon that cannot supervise its children terminates them and reports interruption rather than claiming recovery it cannot make. diff --git a/docs/configuration.md b/docs/configuration.md index eabc8cf..faf749e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -4,11 +4,19 @@ RVBox v1 uses TOML 1.0 for daemon configuration. The normative annotated examples are [`examples/server.toml`](examples/server.toml) and [`examples/client.toml`](examples/client.toml). They list every supported v1 knob, with the routing, persistence, and safety limits first in each section. +The runnable Windows-first deployment example is +[`examples/client.windows.toml`](examples/client.windows.toml); omitted entries +use the defaults documented by the all-knob client reference. ## Loading and precedence - `rvbox-server --config PATH` and `rvbox --config PATH` load one UTF-8 TOML - file. There is no implicit merge of multiple files and no hot reload in v1. + file. There is no implicit merge, runtime rewrite, or hot reload in v1. + On Windows, the SCM service command line holds the canonical explicit config + path; omitting it during installation selects the resolved + `%ProgramData%\RVBox\client.toml`; first run creates its annotated template + and the service reports not-ready until routing is valid rather than + connecting to a placeholder server. - Precedence is compiled default, then TOML, then an explicitly supplied CLI flag. Flags exist for operationally important scalar keys; they use the same validation as TOML. RVBox does not implicitly import configuration from @@ -21,10 +29,13 @@ knob, with the routing, persistence, and safety limits first in each section. bytes; comments show the equivalent binary unit. URLs and paths are strings. - Relative paths are rejected for state, socket, CA, shell-executable, and allowed-CWD-root fields. `client.daemon_cwd` is resolved once at startup and - then stored and advertised as an absolute path. The annotated client example - uses Unix paths; a Windows deployment replaces `state_dir`, `daemon_cwd`, and - relevant shell paths with absolute Windows paths. Shell fields for the other - platform are syntax-checked but not resolved or advertised. + then stored and advertised as an absolute path. On Unix it is the omitted-CWD + default; on Windows it is the protected parent under which the service creates + an ACL-isolated default directory for the selected execution identity. The + annotated client example uses Unix paths; a Windows deployment replaces + `state_dir`, `daemon_cwd`, and relevant shell paths with absolute Windows + paths. Shell fields for the other platform are syntax-checked but not resolved + or advertised. - The daemon prints its effective configuration after validation, with no command data or TLS material. Since v1 stores command/environment payloads in plaintext, configuration output is hygiene rather than a secrecy guarantee. @@ -44,6 +55,13 @@ Any enabled non-loopback bind produces a conspicuous warning but is permitted by the accepted v1 debugging contract. The control Unix socket always uses mode `0600`; it is not a configurable relaxation. +Windows automatic start is SCM state, not TOML state. Installation registers +the machine-wide service as Automatic; an administrator may change it to Manual +through the tray or normal service-management tools. RVBox has no +`windows.start_on_boot` key and never attempts to reconcile two sources of +truth. The optional per-user tray uses the installer-created logon registration +and is never required for service readiness or command execution. + ## Shell executable resolution Each `ShellType` maps to one startup-validated absolute executable path from diff --git a/docs/control-plane.md b/docs/control-plane.md index e3882d8..298638b 100644 --- a/docs/control-plane.md +++ b/docs/control-plane.md @@ -90,6 +90,21 @@ actual lifecycle with a late-after-expiry warning. It renders terminal `Rejected` with the client's structured validation/platform reason; `Failed` means the requested code actually launched. +`rvc run --elevated` maps directly to `ExecutionSpec.elevated`; omission is +false. Non-Windows clients reject true as unsupported in v1. Windows chooses +the effective context from that bit and launch-time login state: normal commands +use `active-user` when possible and otherwise `local-service`; elevated commands +with an active user try `active-user-elevated`, `active-system`, then +`local-system`, while logged-out machines use `local-system` directly. These +fallbacks finish before `launch_prepared` and never retry a process. + +Detailed `rvc stat CLIENT ISSUE_UUID` output shows requested elevation, every +attempted Windows context, selection/fallback detail, effective context and +process-token SID, and target session ID/owner SID when applicable. +`active-system` is rendered conspicuously as SYSTEM in another user's session, +never as that user. A pre-launch rejection shows the structured final context- +selection error rather than implying requested code ran. + `rvc append` turns a string into `StdinWrite` with `append_newline=true` unless the caller selects raw mode; `--file` supplies raw bytes; `--attach` streams local standard input. `CloseStdin` is available separately. All stdin actions diff --git a/docs/examples/client.toml b/docs/examples/client.toml index 0606525..69585e2 100644 --- a/docs/examples/client.toml +++ b/docs/examples/client.toml @@ -119,6 +119,12 @@ metrics_path = "/metrics" log_level = "info" # Structured log encoding: json or text. log_format = "json" +# Optional log path; empty uses stderr on Unix and the conventional file on Windows. +log_file = "" +# Rotate a nonempty log_file after this many bytes (10 MiB). +log_max_bytes = 10485760 +# Number of sealed rotated log files to retain. +log_max_files = 5 # Resource profiles are administrator policy. These illustrative values are not # protocol guarantees. LIGHT is exclusive; otherwise combine at most one tier diff --git a/docs/examples/client.windows.toml b/docs/examples/client.windows.toml new file mode 100644 index 0000000..dc7e3a0 --- /dev/null +++ b/docs/examples/client.windows.toml @@ -0,0 +1,53 @@ +# RVBox v1 Windows-first client example. Omitted keys use client.toml defaults. + +[client] +# Reverse WebSocket endpoint exposed by nginx; replace before first connection. +server_url = "wss://rvbox.example.test/v1/agent" +# Private durable state; first-run code resolves ProgramData rather than expanding env text. +state_dir = "C:\\ProgramData\\RVBox\\state" +# Empty selects the local hostname; otherwise use an opaque 1-128 ASCII ID. +client_id = "" +# Protected root for per-identity default CWDs when a request omits cwd. +daemon_cwd = "C:\\ProgramData\\RVBox\\work" +# Maximum simultaneously running supervised Job Objects. +max_running_commands = 16 +# Maximum durably accepted commands waiting to start. +max_queued_commands = 100 +# Grace for reserved terminal cleanup during orderly exit. +shutdown_grace = "30s" + +[tls] +# Optional PEM CA bundle; empty uses the Windows trust store. +ca_file = "" +# Optional certificate-name override; empty derives it from server_url. +server_name = "" + +[shells] +# Required Windows default shell enum. +default_windows = "powershell" +# Unix shell is inactive on Windows but remains syntax-checked. +default_unix = "sh" +# Inactive Unix path. +sh = "" +# Inactive Unix path. +bash = "" +# Exact absolute executable used for SHELL_CMD. +cmd = "C:\\Windows\\System32\\cmd.exe" +# Exact absolute executable used for SHELL_POWERSHELL. +powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" +# Absolute CWD roots permitted by local policy; empty allows any accessible path. +allowed_cwd_roots = [] + +[observability] +# Loopback HTTP listener for local liveness, storage, and supervisor health. +listen = "127.0.0.1:6902" +# Structured logging threshold: debug, info, warn, or error. +log_level = "info" +# Structured log encoding used in the rotating file. +log_format = "json" +# Current service log opened read-only by the tray menu. +log_file = "C:\\ProgramData\\RVBox\\logs\\rvbox.log" +# Rotate the current log after this many bytes (10 MiB). +log_max_bytes = 10485760 +# Number of sealed rotated log files to retain. +log_max_files = 5 diff --git a/docs/implementation-plan.v1.md b/docs/implementation-plan.v1.md index bbd492f..b495fa4 100644 --- a/docs/implementation-plan.v1.md +++ b/docs/implementation-plan.v1.md @@ -11,12 +11,39 @@ binaries: spool, and reconnect/reconciliation owner. - `rvc`: local CLI over the server's Unix-domain gRPC socket. -The primary release target is a Linux server and Linux client. The Go client is -structured behind OS interfaces from the outset; Windows implementations of -process management, diagnostics, and resource controls are completed before a -Windows client is declared supported. Windows v1 requires Windows 10 or Windows -Server 2016 or newer. Do not claim Windows feature parity while those -implementations are absent. +The first supported client target is Windows connecting to a Linux server; +Linux client support follows and remains a primary v1 goal. Build shared client +runtime code behind OS interfaces, but complete and exercise Windows process +management, diagnostics, resource controls, desktop hosting, and native CI +before beginning the Linux supervisor. Windows v1 requires Windows 10 or newer, +or Windows Server 2016 or newer. Desktop Experience is required only for the +tray and active-session command contexts; the service and Session 0 execution +contexts support headless Server Core. + +The released `rvbox.exe` is one signed binary with explicit SCM-service, tray, +installer/configurator, per-command launcher, and signal-helper modes. +Installation is UAC-elevated and registers one machine-wide Automatic service +running as LocalSystem. That single main service owns WSS, all accepted +requests, durable state/history, logs, token policy, Job Objects, process +handles, and recovery before any user logs in. There is no separate persistent +worker or elevation broker. + +The installer also registers an unelevated per-user tray through the machine- +wide `Run` key. A tray is only a named-pipe frontend to the service: it may show +health, open config/log, and request locally authorized service actions, but it +never opens the store or owns remote work. Closing it affects neither service +nor commands. Task Scheduler is not used anywhere in v1. + +Use conventional machine-wide paths: `%ProgramData%\RVBox\client.toml`, +`%ProgramData%\RVBox\state`, and `%ProgramData%\RVBox\logs\rvbox.log`, with +state ACLs limited to Administrators and SYSTEM and separate read-only tray +access for config/log. The menu opens the exact config or current log via a safe +explicit file-opening path. SCM Automatic/Manual state is +the only service-startup source of truth and is not mirrored into TOML. Create +`%ProgramData%\RVBox\work` as a SYSTEM-owned root. For omitted CWDs, create an +ACL-isolated child for the selected user SID, LocalService, or SYSTEM context; +do not make one shared work directory writable by every identity. These +requirements are part of Phase 4, not post-v1 polish. This plan implements the already agreed v1 contract. In particular, it does not add authentication, client enrollment, mutual TLS, or a client allowlist. @@ -80,8 +107,10 @@ internal/ runtime/ # reconnect loop, transport, dispatcher spool/ # active command/event/output durable spool supervisor/ # OS-neutral interface - supervisor/unix/ # process groups, /proc, cgroup v2 - supervisor/windows/# Job Objects and Windows diagnostics + supervisor/windows/# first target: tokens/sessions, launcher, Jobs, diagnostics + supervisor/unix/ # later target: process groups, /proc, cgroup v2 + windowsservice/ # SCM install/control and service host + windowstray/ # per-session named-pipe frontend and notification icon observability/ # logging, metrics, health/readiness testkit/ # clocks, fake transport, fault helpers gen/go/rvbox/v1/ # generated protobuf/grpc code @@ -101,6 +130,11 @@ versions in `go.mod`; record reasons for non-standard dependencies in toolchain for normal Linux/Windows client builds. Use a maintained, context-aware WebSocket implementation and a Zstandard implementation that supports bounded decompression. Do not rely on an archived WebSocket package. +For Windows, prefer `golang.org/x/sys/windows` plus narrow reviewed wrappers for +SCM, tokens/WTS, Job, console, named-pipe, shell, ACL, and notification-area APIs. Do not +adopt an unmaintained tray abstraction merely to reduce Win32 code. Pin the +resource compiler used for icon/version manifests and keep signing credentials +out of build images and repository state. ### 3.2 Containerized developer tooling @@ -134,9 +168,23 @@ Add CI (or a repository script ready for CI) that runs, in order: 2. Generation freshness check. 3. `go fmt`, `go vet`, static analysis, and unit tests. 4. Race tests for server/client concurrency packages. -5. Linux integration tests in Compose. -6. Cross-compilation/build verification for Windows client packages; Windows - runtime tests run on a Windows runner once available. +5. Linux server/storage/session integration tests in Compose; no Linux client + supervisor is required at this stage. +6. Cross-compilation checks for Windows packages in the container plus native + Windows build/runtime smoke tests on a Windows runner. A Windows runner is a + Phase 0 prerequisite, not a later optional enhancement. + +Provide two native Windows lanes before Phase 4: a clean noninteractive runner +for unit/Job/process/storage tests, and a resettable interactive VM with Explorer, +a split-token administrator, UAC enabled, and Desktop Experience for tray/WTS/ +token-session tests. Add repeatable test users/policy fixtures for a standard +user, no logged-in user, multiple active sessions where supported, UAC disabled, +and Administrator Protection enabled when the current Windows image exposes it. +Tests that require the interactive desktop may be a +release-candidate gate rather than per-commit, but cannot be replaced by mocks +or a service-account runner. Add a Server Core service/Session-0 smoke lane. +Exercise at least the oldest supported Windows baseline and one current desktop +release before publishing. **Exit criteria:** all three empty `main` packages build in the toolchain container; generated code is checked in; `make verify` works from a fresh clone. @@ -163,6 +211,11 @@ At the application boundary, validate what protobuf cannot express: <= 16 MiB, JSON-RPC HTTP bodies are <= 24 MiB, and decoded field limits remain identical across the two control transports. - shell type is explicit or assigned only to the documented platform default. +- `elevated=false` is normal privilege; non-Windows clients reject true in v1. + Windows applies the documented login-aware context hierarchy and records + every attempted/effective context. Fallback ends before `launch_prepared`. +- active Windows contexts bind one revalidated session ID, logon SID, and user + SID; process creation is never retried under a different context. - CWD exists, is a directory, and is allowed by local daemon policy. - an execution request has exactly one source: command text or script descriptor. - script descriptors and payloads agree on SHA-256 and <= 10 MiB size. @@ -182,6 +235,9 @@ It contains: races; - typed domain errors mapped to gRPC status/structured details, JSON-RPC error objects, or agent-protocol `ControlError` as appropriate; +- typed Windows pre-launch errors for a failed normal execution context or an + exhausted elevated-context chain; ambiguity and per-attempt token/policy + failures remain bounded selection detail rather than separate terminal codes; - event-sequence validation, duplicate equivalence checks, and declared gap validation through `OutputTruncation` metadata; - default values and hard-limit validation; @@ -214,7 +270,9 @@ Use strict TOML 1.0 and implement the normative contract in [`configuration.md`](configuration.md). Keep the annotated [`examples/server.toml`](examples/server.toml) and [`examples/client.toml`](examples/client.toml) synchronized with the Go config -structs and compiled defaults. +structs and compiled defaults. Keep +[`examples/client.windows.toml`](examples/client.windows.toml) as the runnable +Windows-first subset using those same defaults. Implement configuration in this order: @@ -265,6 +323,9 @@ takeover TTL, and observability. Client TOML covers WSS/TLS routing, durable identity/state, exact shell paths, allowed CWD roots, queue/concurrency, reconnect/liveness, spool/flow limits, execution diagnostics/grace periods, resource profiles, and observability. +Windows client configuration additionally covers rotating service-log output. +SCM startup type, service/launcher/tray identities, execution-context selection, +conventional paths, and secure ACLs are mandatory policy rather than TOML knobs. Do not add or claim application-level at-rest encryption in v1. Command-owned payloads are stored in plaintext under the private state directories; document @@ -274,8 +335,8 @@ Hard defaults are: 10-second heartbeat-idle interval, 30-second liveness timeout, 1–60-second full-jitter reconnect, reset after 60 stable seconds, 16 running/100 queued client commands, 1,000 server-queued commands per client and 10,000 server-wide, 5-minute one-shot live-conflict takeover grant, 15-minute -queue TTL, 64 KiB raw stream -chunk, 1 MiB decoded agent envelope, 768 KiB serialized execution spec, 16 MiB +queue TTL, 64 KiB raw stream chunk, 1 MiB decoded agent envelope, 768 KiB +serialized execution spec, 16 MiB decoded control request, 24 MiB JSON-RPC HTTP body, 10 MiB output window/raw script cap, 32 MiB total per command, 256 MiB per client/client daemon, 4 GiB server-wide command storage, 30-day terminal retention, 100 MiB audit storage @@ -286,18 +347,19 @@ plus unacknowledged send windows of 1 MiB per command and 8 MiB per session. Reserve 64 KiB within every accepted command's quota for terminal/loss closeout metadata and bound protocol detail/reason plus incident-note text to 4 KiB. Use emergency filesystem free-space floors of 256 MiB server-side and 64 MiB -client-side by default. +client-side by default. The Windows service installs as Automatic; service logs +rotate at 10 MiB and retain five sealed files. -Test both annotated example files through the production decoder. Add table tests -for every unknown key, invalid duration/size, high/low inversion, tier inversion, -zero semantic, noncanonical path/device, unsupported default shell, conflicting -profile combination, and flag-precedence case. Add a test proving a request-level -`PATH` override cannot change the chosen shell executable. Golden-test the -redacted effective configuration and keep its key order stable enough for -operators to compare deployments. +Test all annotated example files through the production decoder on their target +platforms. Add table tests for every unknown key, invalid duration/size, +high/low inversion, tier inversion, zero semantic, noncanonical path/device, +unsupported default shell, conflicting profile combination, and flag-precedence +case. Add a test proving a request-level `PATH` override cannot change the +chosen shell executable. Golden-test the redacted effective configuration and +keep its key order stable enough for operators to compare deployments. **Exit criteria:** state-machine and configuration tests cover all legal/illegal -transitions and boundary values; both examples parse to the documented defaults; +transitions and boundary values; all examples parse to the documented defaults; generated protos compile and are the only DTOs crossing process boundaries. ## 5. Server persistence and retention (Phase 2) @@ -349,7 +411,7 @@ invariants must not. | --- | --- | | `clients` | client ID, most-recent platform/capabilities/CWD/version, durable instance ID, current generation, connection/last-seen timestamps, latest rejected live-conflict instance/time, unified charged command-storage total | | `sessions` | opaque session ID, client ID, durable client-instance UUID, generation, opened/fenced/closed times, close reason | -| `commands` | UUID, client ID, indexed issue/queue-expiry/terminal times, lifecycle, revision, exit result, last event sequence, retention status, and a Zstandard-compressed immutable execution-spec payload including plaintext environment values | +| `commands` | UUID, client ID, indexed issue/queue-expiry/terminal times, lifecycle, revision, exit result, last event sequence, retention status, a Zstandard-compressed immutable execution-spec payload including plaintext environment values, and optional Windows selection attempts/effective identity | | `command_payloads` | command UUID, payload kind (script or other command-owned blob), Zstandard compression, raw/stored sizes, digest, inline bytes or validated segment reference | | `command_events` | command UUID + event sequence unique key, observed and server receipt times, event type, payload metadata, immutable duplicate checksum | | `output_segments` | command UUID, segment ordinal/path, `committed_end_offset`, min/max event sequence, stream mix, compressed/raw byte totals, checksum, created time | @@ -740,13 +802,15 @@ slow client that cannot block another client or local control RPC. receive bounded dispatches, and recover connection faults without duplicate execution or server deadlock. -## 7. Client runtime, spool, and Unix supervisor (Phase 4) +## 7. Windows-first client runtime, spool, and supervisor (Phase 4) ### 7.1 Client state and reconnect runtime -Keep client state private (mode `0700`): durable accepted-command records, -active process metadata, command event journal, stdout/stderr spool segments, -stdin write acknowledgements, and outstanding script upload state. It contains +Keep client state private: mode `0700` on Unix; on Windows disable inherited +ACLs and grant only SYSTEM and Administrators the required access. Store durable +accepted-command records, active process metadata, command event journal, +stdout/stderr spool segments, stdin write acknowledgements, and outstanding +script upload state. It contains no terminal command history after server acknowledgement and local cleanup. It retains a separate compact FIFO of the most recent 1,000,000 command tombstones (binary UUIDv7, immutable request hash, and acknowledgement time) so @@ -775,6 +839,15 @@ first registration. Never regenerate it merely because the network/server rejects a session. If the identity file is corrupt, enter dirty local health and require the repair path rather than silently presenting a new installation. +Define durable-file primitives per platform and use them everywhere. Unix uses +file `fsync`, atomic same-filesystem rename, then parent-directory `fsync`. +Windows writes a same-directory temporary file with restrictive ACLs, calls +`FlushFileBuffers`, atomically installs it with `ReplaceFileW` or +`MoveFileExW(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)` as applicable, +and flushes a directory handle where supported. Treat an unsupported/failed +required flush as a storage error; do not acknowledge durable state based only +on buffered writes. Test power-loss boundaries on NTFS, not only process exit. + Give the client store the same committed-offset segment discipline as the server. Persist, at minimum, command phase/revision/hash, platform launch identity, local event ordinal, optional assigned wire sequence, payload digest, @@ -899,6 +972,10 @@ directory, and only then make the command launch-eligible. A permanent upload failure durably ends the command as `REJECTED` before launch authorization and releases every associated reservation. +On Windows create wrappers with an explicit non-inherited ACL, exclusive random +basename, and delete-sharing suitable for terminal cleanup; use the common +Windows durable replacement primitive rather than POSIX rename assumptions. + Materialize ordinary `command_text` through the same private generated-file machinery, while retaining its separate command-text metadata. Execute the file with exactly `sh`/`bash`, `cmd.exe /D /S /C`, or `powershell.exe` with @@ -916,7 +993,7 @@ reject if it is no longer the validated regular executable. Add tests with an attacker-controlled `PATH` entry and same-named fake shell to prove it cannot be selected. -### 7.4 Unix process supervisor +### 7.4 Supervisor contract and Windows process implementation Define a narrow interface used by the runtime: @@ -929,107 +1006,229 @@ type Supervisor interface { } ``` -The Unix implementation starts an RVBox launcher as leader of a new -session/process group; after authorization that launcher creates the selected -`sh`/`bash` child inside the same group and remains as its watchdog. It rejects -unsupported shell values. It applies daemon environment plus persisted -overrides, validates the CWD, connects stdin/stdout/stderr pipes, and records -launcher/root/group identities. Signal the full process group. -On orderly shutdown and unclean-start recovery, terminate surviving managed -groups and emit `interrupted` rather than pretending pipe monitoring survived. +`StartSpec` includes the validated `elevated` intent; a successful `Process` +exposes an immutable effective `WindowsExecutionIdentity` after preparation, +and a context-selection error carries the same record without an effective +context. The runtime persists the intent, attempted contexts, selection detail, +and optional effective identity. It emits the record with a pre-launch +rejection or with `running` before reporting requested code as started. -Treat root exit as the start of a configurable 5-second tree/output drain grace -period. Wait for the cgroup/process group and capture readers; terminate residual -descendants after the grace period, drain to EOF, and emit incomplete-output -metadata as a sequenced `OutputIncomplete` event if handles still cannot be -drained. The terminal lifecycle event must be sequenced and spooled only after -all retained output/truncation events and is -always the final client event. +Implement Windows code in platform-specific files so non-Windows builds never +import Windows APIs. Keep launch phases identical across platforms: +`accepted -> launch_prepared -> launch_authorized -> running`, with no shortcut. -Implement launch through an internal blocked-launcher mode with a private -release/watchdog channel. The launcher remains alive as a non-user-code -supervisor for the process-tree lifetime. After the launcher reports ready, -persist and flush -its PID/process-group/platform birth identity as `launch_prepared`; persist and -flush `launch_authorized` before releasing requested command code. EOF before -release aborts without executing it. On Linux create a per-command cgroup v2 -for supervision regardless of resource-profile selection, place the blocked -launcher into it before release (`clone3` with `CLONE_INTO_CGROUP` and -`CLONE_PIDFD` where available), and persist the cgroup path plus -`/proc//stat` start time. Recovery prefers `cgroup.kill`; a process-group -fallback is allowed only after positive birth-identity verification. Other Unix -platforms use the watchdog/process-group fallback and document that descendants -which deliberately create a new session may escape it. +Implement one exhaustive token selector; do not scatter token fallback across +launch code: -Encode launch as `accepted -> launch_prepared -> launch_authorized -> running` -and permit no shortcut: +1. Reject `elevated=true` on non-Windows in v1. On Windows, enumerate WTS + sessions immediately before preparation. Prefer the physical console only + when it is `WTSActive` with a valid user token; otherwise accept exactly one + `WTSActive` interactive session. Zero candidates means logged out. Multiple + candidates without a preferred console are recorded as ambiguous and treated + as no *usable* active user, never selected nondeterministically. +2. With a usable active user and `elevated=false`, attempt only `ACTIVE_USER`. + Use an existing filtered/standard token. If Windows supplies only a full + administrator token, call `CreateRestrictedToken` with LUA/max-privilege + restriction, make administrator/privilege-bearing groups deny-only, set and + verify medium integrity, and retain the user's SID/session. If a verified + non-elevated token cannot be built, reject; do not use LocalService or SYSTEM + while that selected active session remains valid. +3. With a usable active user and `elevated=true`, attempt contexts in this exact + order: `ACTIVE_USER_ELEVATED`, `ACTIVE_SYSTEM`, `LOCAL_SYSTEM`. + `ACTIVE_USER_ELEVATED` queries `TokenElevationType`, uses a linked full token + for a traditional limited administrator, and accepts an already-full admin + token. A standard user/missing linked token records + `CODE_ELEVATION_UNAVAILABLE`; + Administrator Protection or another just-in-time approval policy records a + bounded policy-specific attempt reason. V1 shows no UAC/Hello prompt and + continues to `ACTIVE_SYSTEM`. +4. `ACTIVE_SYSTEM` duplicates the service SYSTEM token, sets `TokenSessionId` to + the selected session while `SeTcbPrivilege` is enabled only around that call, + verifies SYSTEM SID/session, and hides all windows. A pre-preparation failure + records its error and continues to Session 0 `LOCAL_SYSTEM`. This fallback + intentionally bypasses user-scoped approval because the installed service + already holds SYSTEM and must be conspicuous in status/audit history. The + final `LOCAL_SYSTEM` fallback supplies elevation but no interactive-desktop + access; session-scoped commands may launch successfully and then fail in the + ordinary way in Session 0. +5. With no usable active user, `elevated=false` attempts only `LOCAL_SERVICE`: + passwordless `LogonUserW` for `NT AUTHORITY\LocalService` with + `LOGON32_LOGON_SERVICE`, required SID `S-1-5-19`, and session 0. Do not use a + restricted SYSTEM token as a substitute. `elevated=true` attempts only + `LOCAL_SYSTEM` by duplicating the service token and requiring SID `S-1-5-18` + plus session 0. +6. Fallback decisions cover token/session capability only and all complete + before `launch_prepared`. Never fall back after launcher creation, shell + creation, CWD/executable/profile validation failure, authorization, or an + uncertain outcome. If the selected user logs out during selection, close all + provisional handles, enumerate once more, and apply the resulting row; never + switch silently to another user. +7. Verify the final user SID, session ID, token type, elevation state, and + integrity level against the selected context. Close every token/profile + handle on all paths. Persist the request's elevation bit, ordered attempted + contexts with a total bounded selection detail, optional effective context/ + user SID, and target session/user/logon SIDs; never persist token handles or + credentials. If every context fails, emit that decision record with no + effective context on the terminal pre-launch rejection. -1. Create the private cgroup/process group, pipes, wrapper, and close-on-exec - release/watchdog channel without executing user code. -2. Start the blocked launcher and obtain a positive ready message containing - the platform process identity; place and verify it in its command cgroup. -3. Commit and fsync `launch_prepared` with that identity. Recheck the latest - revision and pending cancellation while the launcher remains blocked. -4. If still executable, commit and fsync `launch_authorized`, then send the - one-byte release and keep the watchdog channel open until tree cleanup. - Record `running` only after the launcher reports successful requested-shell - creation/`exec`. -5. A crash before durable preparation cleans an untrusted orphan and may retry. - A prepared-but-unauthorized launcher must exit on channel EOF and may retry - only after positive death verification. After release, channel EOF makes the - launcher terminate its process group; an authorized but uncertain launch is - killed as a tree and ends `interrupted`, never launched again. +Build the base Unicode environment with `CreateEnvironmentBlock` for the +effective token, then apply validated request overrides deterministically. +Load/unload an active user's profile only when required and keep it loaded until +the complete Job exits. Session 0 contexts use their service-account profile and +cannot see interactive mapped drives. Validate an explicit CWD and wrapper ACL +access while impersonating the effective token. For an omitted CWD, treat +`%ProgramData%\RVBox\work` (or configured `daemon_cwd`) as a SYSTEM-owned root +and create/open an ACL-isolated child keyed by the selected user SID, +LocalService, or SYSTEM context. Reject insecure owners, inherited write grants, +and reparse points on reuse. Generated wrappers grant only SYSTEM and the +effective token SID the required access. -Make launch-journal and launcher-control records checksum-framed and bounded. -Fault-inject process death before and after every fsync, ready/release, and exec -acknowledgement; assert that user-visible side effects occur at most once and -that recovery cannot confuse a reused PID. +1. Create a non-inheritable per-command Job Object, enable + `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and do not enable breakaway. +2. Resolve the hierarchy's effective token/session as specified above. Create + unpredictable local named pipes for control/stdin/stdout/stderr with + `PIPE_REJECT_REMOTE_CLIENTS` and an ACL restricted to SYSTEM and the effective + token SID. Do not rely on inherited service handles: Windows prohibits normal + handle inheritance across Terminal Services sessions. +3. Create a stateless per-command launcher mode of the canonical `rvbox.exe` + suspended through `CreateProcessAsUser`, with no inherited handles and an + opaque pipe-channel identifier. Assign this still-suspended launcher to the + empty Job, then resume it. Verify the connecting pipe client's PID, creation + `FILETIME`, token SID, session ID, and generation before sending any launch + material. +4. The launcher opens only the verified pipes and creates the requested shell + suspended with `CREATE_NEW_CONSOLE`, `CREATE_SUSPENDED`, + `CREATE_UNICODE_ENVIRONMENT`, and `EXTENDED_STARTUPINFO_PRESENT`. Use + `STARTF_USESHOWWINDOW`/`SW_HIDE`; pass only standard-I/O handles with + `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`. Job membership propagates because + breakaway is disabled. Report shell PID/creation time and wait for release. +5. Persist and flush `launch_prepared` with requested elevation, attempted/ + effective contexts and reasons, token SID, session/user/logon SIDs when + active, launcher/shell birth identities, + Job generation, and console identity. Recheck revision/cancellation, WTS + session identity, executable identity, CWD access, and requested Job limits + while requested code remains suspended. +6. Persist and flush `launch_authorized`, then send the authenticated release + message. The launcher resumes the shell and acknowledges the attempt; record + `running` with effective identity only after that acknowledgement. Failure or + crash after authorization terminates the Job and becomes interrupted, never + redispatched. +7. The service retains the sole Job handle. Control EOF before authorization + makes the launcher exit without executing; EOF afterward terminates the Job. + Service exit therefore kills launcher, shell, and descendants in every crash + window. Recovery never signals or kills by PID alone. -Implement the portable Unix set `HUP`, `INT`, `TERM`, `KILL`, `USR1`, and -`USR2`, accepting optional `SIG` prefixes in the CLI and mapping only through -the `SignalKind` enum. Reject arbitrary native numbers and unsupported names. -Never allow signal zero or arbitrary PID targeting. The process's group ID -comes only from durable supervisor metadata, never a request field. +Do not combine `CREATE_NEW_PROCESS_GROUP` with `CREATE_NEW_CONSOLE`: Windows +ignores the former, and it is unnecessary because every command owns a distinct +hidden console. For TERM, start a short-lived private signal-helper mode of the +same canonical `rvbox.exe` using the command's effective token/session. Pass the +target identity through a separate unpredictable, PID/token/session-verified +local named pipe rather than inherited cross-session handles or the command +line. Prefer the root shell while it is live; +otherwise select a live PID from the Job's process list and verify its creation +`FILETIME`/generation before use. The helper calls `AttachConsole(target_pid)`, +installs a control handler that consumes its own CTRL_BREAK, then calls +`GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0)` so only processes attached to +that command's console receive it. It reports a checksum-framed result and +detaches. The helper receives neither the Job handle nor command stdio. -### 7.5 Linux diagnostics and resource profiles +Apply `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` to every same-session child creation. +Every database, log, listener, Job, unrelated pipe, and SCM handle is non- +inheritable. Length/type/generation/checksum-frame launcher/signal-helper +control messages and reject mismatched generations. -Poll active processes at a configurable interval outside pipe/network loops. -Read readable `/proc` values for CPU time, RSS, I/O, state, CWD, and wait reason; -aggregate only clearly associated group/child data. Missing/unreadable values -remain absent. Set `suspected_hung` only after default 10 minutes without -observable progress and label it diagnostic, not lifecycle. +Open the configured shell by canonical absolute path and pass it as explicit +`lpApplicationName`. Use one reviewed Windows argument-quoting routine and an +explicit Unicode environment block. Never invoke `%COMSPEC%`, search +`PATH`/file associations, or let command environment overrides choose the +executable. -Make requested resource-profile handling explicit. On Linux the supervisor -uses a delegated writable cgroup v2 for tree supervision whenever available, -even without a profile. Profiles add administrator-defined CPU/memory/disk/ -process controls to that cgroup. Without delegation, no-profile execution uses -the watchdog/process-group fallback; a profile whose essential control cannot -be applied is rejected as unsupported. No profile means no resource restriction, -even when a supervisory cgroup exists. +Associate the Job with an I/O completion port. Use +`JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO` plus capture-pipe EOF to establish complete +tree/output drain. Persist shell creation `FILETIME` before trusting a PID. +Recovery may reopen a process only to compare creation time/generation; +inability to prove ownership creates a dirty incident rather than risking +termination of a reused PID. -Compile TOML resource profiles into immutable validated launch policies during -startup. Enforce `LIGHT` as exclusive; otherwise allow at most one CPU, one -memory, and one disk profile. On Linux, translate configured policy into the -per-command cgroup's `cpu.max`/`cpu.weight`, `memory.max`/`memory.high`, -`pids.max`, and per-device `io.max` controls as applicable. Write and read back -all essential controls while the launcher is blocked. If any essential control -is unsupported or cannot be applied, destroy the empty command cgroup and -permanently reject the dispatch; never run partially constrained. +Accept only `TERM`/`SIGTERM` and `KILL`/`SIGKILL`. TERM runs the verified +console-attach helper, waits the configured 10 seconds, then terminates the Job +if necessary. KILL terminates it immediately. Emit the actual attach, delivery, +wait, and escalation outcome. Root exit starts the common 5-second drain grace; +the terminal lifecycle event remains last. -Define diagnostic progress as a change in process-tree CPU ticks, cumulative -I/O counters, retained output/input activity, or lifecycle state. Persist only -the latest sample and no-progress start time, clear `suspected_hung` on the next -observed progress, and tolerate counter reset/process exit. Diagnostics must not -keep a command alive, change terminal status, or block the supervisor. +Compile requested TOML profiles before launch and apply CPU, memory, active +process, and supported I/O rate limits to the empty Job. Read back every required +limit before authorization. Permanently reject `UNSUPPORTED` rather than run +partially constrained. Use process/Job accounting for CPU/RSS/I/O and omit +Linux-only wait reasons. -### 7.6 Client tests +### 7.5 Windows desktop host and lifecycle -Use fake transport/clock plus real subprocess tests for shell defaults, CWD/env -overlays, at-most-once duplicate dispatch, concurrent output with no newlines, -stdin ordering/close, process-group termination, reconnect/replay, offline -rolling truncation before sequence assignment, assigned-window pinning, script -progress/checksum failure/cleanup, atomic tombstone replacement, queue limits, -shutdown interruption, and `/proc` absence. Run race tests with multiple +Build the executable with the Windows GUI subsystem so service/helper/tray modes +never flash an unwanted console. Human-invoked `--help`, `--check-config`, +install, uninstall, and configuration modes call +`AttachConsole(ATTACH_PARENT_PROCESS)`, reopen the inherited standard handles, +and use UTF-8 terminal diagnostics when attached; define native-dialog plus exit- +code behavior when no console exists and test both paths. `install-service` and +`uninstall-service` use `ShellExecuteEx(..., "runas", ...)` only when their caller +is not already elevated, validate the canonical executable/config paths, and +make idempotent SCM changes. The installed service command line selects only the internal +`service` mode and exact config path. At service entry, verify SCM startup rather +than accepting that mode from an ordinary process invocation. Hold one machine- +wide service/state lock and report SCM start/stop checkpoints within their +deadlines; lengthy spool recovery remains asynchronous and does not block +service start. + +Register the tray with the machine-wide `Run` key, not Task Scheduler and not +service-created cross-session injection. Use one ACL-protected mutex per WTS +session so each logged-in user gets at most one tray while multiple RDP/console +sessions remain independent. Run its Win32 message pump on one locked OS thread, +recreate the icon after `TaskbarCreated`, and use bounded nonblocking IPC/UI +channels. The tooltip shows connection/readiness/dirty state without payloads. +`Exit` closes only that tray. The service continues through tray exit, user +logoff, Explorer restart, and periods with no interactive user. + +Expose a versioned, length-bounded local named-pipe tray protocol with +`PIPE_REJECT_REMOTE_CLIENTS`, explicit SYSTEM/Administrators/interactive-user +ACLs, peer PID/session/token inspection, deadlines, and no command payloads. +The tray never reads the spool/database. Read-only status may be served to a +local interactive user. For a mutating service/config action, the tray launches +the narrow canonical `configure-service` mode with UAC; that helper sends one +authenticated request and exits. The service rechecks the helper token rather +than trusting a tray-supplied administrator claim. + +Resolve defaults under `%ProgramData%\RVBox`; create the config, state, and log +directories with explicit Administrators/SYSTEM ACLs and reject insecure +existing objects. Create `%ProgramData%\RVBox\work` as a SYSTEM-owned root and +create identity-scoped child directories on demand with non-inherited ACLs for +SYSTEM plus the effective non-SYSTEM SID; no command identity receives state- +directory access. Give tray users only the documented read access to config/ +current logs. On first installation, copy the annotated Windows +TOML template and leave the service live/not-ready until routing is valid. `Open +config` and `Open log` use exact resolved regular paths—never an unquoted shell +command—and show actionable errors. Config edits are administrator-only and +take effect after explicit service restart; v1 has no hot reload or TOML rewrite. + +Install the service with `SERVICE_AUTO_START`. `configure-service` maps the tray +Automatic/Manual control directly to SCM start type; there is no +`windows.start_on_boot` TOML key or second desired-state store. Stop/restart is a +separate administrator action and reports that stopping the service terminates +all supervised Jobs. Uninstall first stops admission, performs bounded orderly +shutdown, removes the Run entry and service registration, and preserves config/ +state/log data unless a separately confirmed purge operation is designed later. + +Use bounded rotating service file logs because SCM has no useful interactive +stderr. Flush warnings/errors promptly, redact as elsewhere, and make `Open log` +target the current file after rotation. Add native icon/version/service metadata; +release signing and checksum publication are Phase 8 gates. + +### 7.6 Windows-first client tests + +Use fake transport/clock tests for shared runtime logic and real Windows +subprocess tests for both shells, CWD/env overlays, at-most-once duplicates, +concurrent output without newlines, stdin ordering/close, Job tree termination, +reconnect/replay, offline truncation, script verification/cleanup, tombstone +rotation, queue limits, and shutdown interruption. Run race tests with multiple commands and forced network churn. Add a table-driven crash suite for every acceptance, script-upload, launch, @@ -1038,15 +1237,45 @@ the same duplicate dispatch after each restart and assert exactly one of: durable rejection before authorization, one supervised process, or an `interrupted` uncertain launch—never a second execution. -**Exit criteria:** a Linux client can stay alive through server loss/restart, -execute up to its capacity at most once, preserve/replay bounded history, and -cleanly manage full command process trees. +Fault-inject daemon death around Job creation, suspended shell creation, +launcher pipe authentication, prepared/authorized fsync, release/resume, and +terminal drain; fault the signal helper before/after attach, control-event +delivery, and result. Test paths with spaces/non-ASCII, malicious +`PATH`/`COMSPEC`, nested descendants, hidden-console behavior, CTRL_BREAK +refusal/escalation, Job-limit rejection, cross-session output/stdin, and +inherited-handle leaks. + +Add a native table for every hierarchy row: active standard user, traditional +split-token administrator, already-full administrator/UAC-off token, enabled +Administrator Protection where available, logged-out machine, ambiguous RDP +sessions, and logout between selection/revalidation. Assert normal active-user +execution is medium/non-admin; elevated attempts are ordered +`ACTIVE_USER_ELEVATED -> ACTIVE_SYSTEM -> LOCAL_SYSTEM`; no-user normal/elevated +use `LOCAL_SERVICE`/`LOCAL_SYSTEM`; and attempted/effective identities are +persisted and displayed. Inject failure at each token attempt and prove fallback +occurs only before `launch_prepared`, never as a process retry. + +Test idempotent UAC installation/uninstallation, SCM Automatic/Manual/start/ +stop/restart, preservation of state on uninstall, service boot before login, +service continuity through logon/logoff/tray exit, one tray per session, all tray +actions and authorization checks, malicious named-pipe clients, Explorer +restart, Run-key registration/removal, first-install bootstrap, ProgramData/work +ACLs, Server Core headless behavior, and log rotation on each supported Windows +version. Assert no Task Scheduler object is created or required. + +**Exit criteria:** the native Windows client can stay alive through server +loss/restart, execute up to capacity at most once, preserve/replay bounded +history, manage complete Job trees, and satisfy tray/elevation/autostart behavior +on Windows CI, including all execution-hierarchy rows. Do not begin the Linux +supervisor phase until this gate passes. ## 8. End-to-end agent protocol (Phase 5) -Wire the server session layer and client runtime together before adding the CLI. -Use real protobuf bytes through an in-memory WebSocket test server first, then -through Docker Compose with nginx proxying a WSS endpoint. +Wire the server session layer and Windows client runtime together before adding +the CLI. Use real protobuf bytes through an in-memory test transport first. +Then run Linux server/nginx/storage containers and connect a native Windows CI +runner to their short-lived WSS endpoint; do not substitute Wine or a +cross-compiled binary that was never executed on Windows. Implement flows in this order: @@ -1087,9 +1316,10 @@ the visible CLI result. Include these cross-cutting cases: - heartbeat control traffic remains serviceable while both data lanes are full over a slow, intermittently writable WebSocket. -**Exit criteria:** Compose tests demonstrate a complete background command, -foreground follow, stdin interaction, signal, reconnect, and restart recovery -through the nginx WebSocket path. +**Exit criteria:** a native Windows client against the Compose-hosted Linux +server demonstrates a complete background command, history/follow behavior, +stdin interaction, signal, reconnect, and restart recovery through nginx WSS. +This is the first supported-client milestone and gates Linux supervisor work. ## 9. Server control plane and `rvc` (Phase 6) @@ -1221,70 +1451,72 @@ structured RVBox detail as gRPC. stack; the Unix socket has mode `0600`; JSON-RPC behavior matches gRPC unary semantics and is off unless explicitly enabled. -## 10. Windows client implementation (Phase 7) +## 10. Linux client implementation (Phase 7) -Keep Windows code in platform-specific files/build tags so Linux builds never -import Windows APIs. Implement the same supervisor interface and event/spool -runtime; only OS execution/diagnostics/resource enforcement differ. +Begin this phase only after the Windows Phase 4/5 release gates pass. Keep Unix +code in platform-specific files/build tags and reuse the proven runtime/store/ +protocol contracts without changing their wire semantics to suit Linux. -1. Create a non-inheritable per-command Job Object, enable - `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and do not enable breakaway. -2. Start an RVBox launcher suspended with `CREATE_NEW_CONSOLE`, - `CREATE_UNICODE_ENVIRONMENT`, and `EXTENDED_STARTUPINFO_PRESENT`. Assign it - to the Job at creation through `PROC_THREAD_ATTRIBUTE_JOB_LIST`; inherit only - an explicit standard-I/O/control handle list and never the Job handle. -3. Persist and flush `launch_prepared` with launcher PID, `GetProcessTimes` - creation `FILETIME`, and launch generation; persist and flush - `launch_authorized` before `ResumeThread`. The launcher then creates the - requested shell suspended in its console with `CREATE_NEW_PROCESS_GROUP`, - reports its PID/group, connects the allowlisted pipes, and resumes it. A - failure after authorization terminates the Job and becomes interrupted. -4. Keep the launcher as an in-console signal proxy. This separation is - mandatory: Windows ignores `CREATE_NEW_PROCESS_GROUP` when combined with - `CREATE_NEW_CONSOLE`, and `GenerateConsoleCtrlEvent` reaches only groups - sharing the caller's console. On daemon crash, closing the sole Job handle - terminates the launcher and complete tree; restart never signals by PID alone. -5. Expose Job Object accounting snapshots. Children normally remain in the Job. -6. Accept only `SIGTERM` and `SIGKILL`. For TERM, have the launcher call - `GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, shell_group_id)`, wait 10 - seconds, then terminate the Job. For KILL, terminate the Job immediately. - Report graceful-attempt/escalation outcome in the event. -7. Apply requested profile limits through Job Object limits. If the requested - control cannot be applied, reject the dispatch with structured `UNSUPPORTED`. -8. Use available process/Job Object telemetry for CPU/RSS/I/O; do not emit a - Linux-style wait reason. +The Unix supervisor starts an RVBox launcher as leader of a new session/process +group; after authorization the launcher creates the selected `sh`/`bash` child +inside the same group and remains its watchdog. It applies daemon environment +plus persisted overrides, validates the CWD, connects stdin/stdout/stderr, and +records launcher/root/group identities. On shutdown or unclean-start recovery, +terminate managed groups and emit `interrupted` rather than claiming pipe +monitoring survived. -Implement the Windows launcher as a small mode of the same RVBox binary, not a -searchable external helper. Pass it an inherited, random per-launch control -pipe and fixed-size launch-generation token. Apply an explicit -`PROC_THREAD_ATTRIBUTE_HANDLE_LIST` so only stdin/stdout/stderr and that control -pipe cross creation; make every database, log, listener, Job, and unrelated -pipe handle non-inheritable. Frame ready/release/exec/error messages with length, -type, generation, and checksum, and reject any mismatched generation. +Implement launch with a private release/watchdog channel. Persist and fsync +launcher PID/process-group/platform birth identity as `launch_prepared`; recheck +revision/cancellation, persist and fsync `launch_authorized`, then release user +code. Keep the channel open through tree cleanup. EOF before authorization exits +without execution; EOF after release terminates the group. An authorized but +uncertain launch is killed and interrupted, never retried. -Open the configured shell executable by canonical absolute path and pass it as -the explicit `lpApplicationName`. Produce the command line with one reviewed -Windows argument-quoting routine and construct an explicit Unicode environment -block. Do not invoke `%COMSPEC%`, search `PATH`/file associations, or let command -environment overrides influence executable selection. +On Linux create a per-command cgroup v2 for supervision whenever a delegated +writable cgroup exists, even without a resource profile. Put the blocked +launcher into it before release using `clone3(CLONE_INTO_CGROUP | +CLONE_PIDFD)` where available; otherwise migrate only the still-blocked launcher +through `cgroup.procs`. Persist cgroup path, pidfd-derived identity where +available, PID/process group, `/proc//stat` start time, and launch +generation. Recovery prefers `cgroup.kill`; a PID/process-group fallback is +allowed only after positive birth-identity verification. -Associate the Job with an I/O completion port and use -`JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO` plus pipe EOF for tree/drain completion. -Persist the launcher and shell creation `FILETIME` identities before trusting -any PID. On recovery, reopen a process only to compare creation time and -generation evidence; if ownership cannot be proven, record a dirty incident -rather than terminating an unrelated reused PID. +Other Unix platforms use the same barrier plus watchdog/process-group fallback. +Document that descendants deliberately creating a new session may escape that +fallback. The at-most-once authorization rule still applies. -Fault-inject daemon/launcher death around Job creation, attribute-list process -creation, prepared/authorized fsync, resume, requested-shell creation, and -terminal drain. Test both supported shells, paths with spaces/non-ASCII, -malicious `PATH`/`COMSPEC`, nested descendants, CTRL_BREAK refusal/escalation, -Job limit rejection, and inherited-handle leaks on supported Windows versions. +Treat root exit as the configured 5-second tree/output drain grace. Wait for +cgroup/process-group emptiness and capture EOF; terminate residual descendants, +drain, and sequence `OutputIncomplete` if EOF remains unprovable. Emit terminal +lifecycle last. -Run build checks on every platform and dedicated Windows integration tests for -shell selection, process-tree kill, forced termination, profile rejection, -reconnect, and output/spool behavior. No Windows release is supported until -these tests run on Windows CI. +Implement portable `HUP`, `INT`, `TERM`, `KILL`, `USR1`, and `USR2`, accepting +optional `SIG` prefixes and mapping only through `SignalKind`. Reject native +numbers, signal zero, unsupported names, and arbitrary PID targets. Derive group +identity only from durable supervisor metadata. + +Poll readable `/proc` data outside pipe/network loops for CPU time, RSS, I/O, +state, CWD, and wait reason, aggregating only clearly associated descendants. +Missing data remains absent. Define diagnostic progress as changes in tree CPU +ticks, cumulative I/O, retained input/output activity, or lifecycle. Clear +`suspected_hung` on progress and never let diagnostics alter lifecycle. + +Compile profile policy before launch. A no-profile supervisory cgroup imposes +no limit. Profiles apply required `cpu.max`/`cpu.weight`, +`memory.max`/`memory.high`, `pids.max`, and per-device `io.max` values while the +launcher is blocked, then read them back. Without required delegation/control, +permanently reject as `UNSUPPORTED`; never run partly constrained. + +Run native Linux integration/race/crash tests mirroring the shared Windows +suite: exact shell resolution, CWD/env, duplicate dispatch, output/stdin, +process-group/cgroup tree cleanup, launcher death at every durable boundary, +reconnect/replay, truncation, scripts, tombstones, queue limits, `/proc` +absence, profile failure, and shutdown interruption. Add explicit tests for +pidfd/`clone3` availability fallbacks and deliberate `setsid` escape behavior. + +**Exit criteria:** the Linux client passes the same protocol/durability suite as +Windows, plus cgroup/process-group tests, without weakening the already shipped +Windows behavior or changing the v1 wire contract. ## 11. Reliability, observability, and operational delivery (Phase 8) @@ -1313,6 +1545,9 @@ Provide: policy, working directory, file descriptor limits, and least privilege; - example server/client configuration files with every default and an explicit JSON-RPC exposure warning; +- Windows `rvbox.exe` GUI/icon/version resources, SCM service installation, + per-session tray registration, first-run TOML template, ACL setup, rotating- + file-log support, code-signing hook, and SHA-256 manifest; - a backup/restore procedure for SQLite plus output/audit segment directories; - an upgrade procedure that stops dispatch safely, snapshots data, migrates, and verifies recovery; @@ -1324,8 +1559,9 @@ operationally important identity/listener/state/TLS/quota keys first in each section. Add `--check-config` to both daemons: it must run the production strict decoder, defaulting, cross-field/profile/path/shell validation, print a redacted normalized summary, and exit without opening stores/listeners or changing -state. CI parses both examples with this path on Linux; Windows CI additionally -validates the documented Windows path/shell variant. +state. Linux CI parses the server and all-knob client reference without starting +a client. Native Windows CI parses and normalizes `client.windows.toml` with the +production Windows path/shell/ACL checks. Use `Delegate=yes` in the Linux client systemd unit when cgroup supervision is enabled and create a private writable cgroup subtree for the service. Refuse a @@ -1340,7 +1576,10 @@ Before v1 release, review path traversal, script temp-file permissions, command logging, environment override redaction, malformed compression, decompression bombs, oversized frames, Unicode/ASCII ID validation, Unix socket ownership, JSON-RPC external bind warnings, SQL injection (all parameterized), segment -record corruption, and process-group PID reuse. Add fuzz tests for envelope +record corruption, process-group PID reuse, UAC relaunch argument handling, +SCM service/Run-key executable and config quoting/ownership, ProgramData ACLs, +tray file-opening paths, global single-instance behavior, and Windows inherited +handles. Add fuzz tests for envelope decode, compressed output validation, segment-tail recovery, pagination tokens, and JSON-RPC parsing. @@ -1356,10 +1595,12 @@ The recommended merge order is deliberately vertical: 2. Phase 1: domain/config validation and state machine. 3. Phase 2: SQLite/segments/audit/retention with recovery tests. 4. Phase 3: server registration, fencing, heartbeat, and persisted dispatch. -5. Phase 4: Linux client spool/supervisor and at-most-once execution. -6. Phase 5: full WSS protocol, fault injection, and nginx end-to-end tests. +5. Phase 4: shared client spool plus Windows supervisor/tray and at-most-once + execution on native Windows CI. +6. Phase 5: full WSS protocol and fault injection from native Windows to the + Compose-hosted Linux server/nginx stack. 7. Phase 6: gRPC Unix socket, `rvc`, and optional JSON-RPC. -8. Phase 7: Windows supervisor and Windows CI. +8. Phase 7: Linux supervisor/cgroup implementation and Linux client parity. 9. Phase 8: operational assets, stress/fuzz/recovery testing, release review. Do not merge a later vertical slice by stubbing a durability/safety invariant. @@ -1368,8 +1609,10 @@ not bypass persistence; client output may be truncated under the documented caps, but must never block a child pipe; and a reconnection may replay work, but may never re-execute an already accepted UUID. -The v1 release is ready only after all release-gate tests pass on clean Docker -environments, Linux server/client end-to-end behavior matches the design docs, -Windows support is either fully tested or explicitly not shipped, and every -accepted limitation (self-reported identity and unauthenticated optional -JSON-RPC) is conspicuous in deployment documentation. +The Windows-client milestone is releasable only after Phases 0–6 plus its +applicable Phase 8 packaging/security gates pass on native Windows and a clean +Linux server environment. Windows support cannot be marked optional or replaced +by cross-compilation-only checks. Full v1 is ready after the later Linux client +also passes the common protocol/durability suite and Linux-specific cgroup/ +process tests. Every release must conspicuously document self-reported identity, +the elevated Windows execution authority, and unauthenticated optional JSON-RPC. diff --git a/docs/platform-and-operations.md b/docs/platform-and-operations.md index 42529af..1be525f 100644 --- a/docs/platform-and-operations.md +++ b/docs/platform-and-operations.md @@ -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, diff --git a/docs/protocol.md b/docs/protocol.md index d3169f0..af6f76f 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -59,6 +59,14 @@ on `issue_uuid`. A client whose queue is full sends a transient capacity rejection, which the server requeues with backoff. A permanent validation or unsupported-platform rejection makes the server command terminal `rejected`; it is not retried or mislabeled as a launched-process failure. +`ExecutionSpec.elevated=false` requests normal privilege and true requests the +platform's elevated policy; non-Windows clients reject true in v1. Windows +resolves the effective token/session immediately before launch. With a usable +active user, false selects a verified non-elevated `ACTIVE_USER`; true tries +`ACTIVE_USER_ELEVATED`, `ACTIVE_SYSTEM`, then `LOCAL_SYSTEM`. With no usable +active user, false selects `LOCAL_SERVICE` and true selects `LOCAL_SYSTEM`. +Fallback is confined to token selection before `launch_prepared`; it is never a +process retry. Token/session attempts and effective identity are durable history. An exact UUID/request-hash hit in the compact tombstone ledger returns `CODE_ALREADY_EXECUTED`; the server suppresses dispatch and reconciles its stale state instead of representing the prior execution as a new rejection. @@ -137,6 +145,20 @@ authorization. User cancellation in that interval emits `cancelled`; an uncertain crash window emits `interrupted`. None is mislabeled as process `failed`. +On Windows, a context-selection rejection or the `running` lifecycle event +carries `WindowsExecutionIdentity`; later lifecycle events repeat it unchanged. +The server persists it into `CommandRecord`. If selection exhausted every +allowed context, `effective_context` and the effective/session identity fields +are absent. Session 0 effective contexts omit session fields. +Active contexts include both the target session/user SID and actual process- +token SID so `ACTIVE_SYSTEM` cannot be mistaken for execution as the desktop +owner. The ordered `attempted_contexts` and bounded `selection_detail` explain +fallback caused by a standard user, absent linked token, Administrator +Protection, or failed active-SYSTEM token construction. `selection_detail` and +every individual reason embedded in it are subject to the common 4 KiB detail +limit. Token selection and this event are downstream of durable acceptance but +upstream of requested code execution. + ## Flow control and failure containment No receive loop runs an executor, database write, decompressor, or slow socket diff --git a/protos/rvbox/v1/agent.proto b/protos/rvbox/v1/agent.proto index ad764db..bd3d38b 100644 --- a/protos/rvbox/v1/agent.proto +++ b/protos/rvbox/v1/agent.proto @@ -40,6 +40,7 @@ message ClientHello { string daemon_version = 3; Platform platform = 4; string architecture = 5; + // Omitted-CWD default on Unix; protected per-identity work root on Windows. string daemon_cwd = 6; repeated ShellType supported_shells = 7; // Generated once and persisted in the client state directory. diff --git a/protos/rvbox/v1/common.proto b/protos/rvbox/v1/common.proto index 7b1d548..124b1eb 100644 --- a/protos/rvbox/v1/common.proto +++ b/protos/rvbox/v1/common.proto @@ -35,6 +35,16 @@ enum ShellType { SHELL_POWERSHELL = 4; } +// Effective Windows process token/session context selected by the client. +enum WindowsExecutionContext { + WINDOWS_EXECUTION_CONTEXT_UNSPECIFIED = 0; + WINDOWS_EXECUTION_CONTEXT_LOCAL_SERVICE = 1; + WINDOWS_EXECUTION_CONTEXT_LOCAL_SYSTEM = 2; + WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER = 3; + WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED = 4; + WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM = 5; +} + enum Compression { COMPRESSION_UNSPECIFIED = 0; COMPRESSION_NONE = 1; @@ -101,6 +111,24 @@ message ExecutionSpec { string command_text = 5; ScriptDescriptor script = 6; } + // Requests the platform's elevated execution policy. False is the normal + // least-privilege policy. Non-Windows clients reject true in v1. + bool elevated = 7; +} + +// Effective Windows identity captured at launch. session_id and +// session_user_sid are absent for Session 0 contexts. effective_user_sid is +// the actual process-token user, not the owner of the target desktop session. +message WindowsExecutionIdentity { + // Absent when every allowed context failed before launch preparation. + optional WindowsExecutionContext effective_context = 1; + optional uint32 session_id = 2; + string session_user_sid = 3; + string effective_user_sid = 4; + // Ordered contexts considered during pre-launch selection, including the + // effective final context. This is never a record of process retries. + repeated WindowsExecutionContext attempted_contexts = 5; + string selection_detail = 6; } message CommandRecord { @@ -121,6 +149,9 @@ message CommandRecord { ControlError rejection = 14; uint64 command_revision = 15; bool late_after_expiry = 16; + // Present after Windows context selection was attempted, including a + // pre-launch rejection for which effective_context is absent. + WindowsExecutionIdentity windows_execution_identity = 17; } message LifecycleChange { @@ -128,6 +159,9 @@ message LifecycleChange { optional int32 exit_code = 2; string detail = 3; uint64 command_revision = 4; + // Set on a Windows context-selection rejection or RUNNING, then repeated + // unchanged on later lifecycle events. + WindowsExecutionIdentity windows_execution_identity = 5; } // data is compressed according to compression. uncompressed_size is mandatory @@ -235,6 +269,8 @@ message ControlError { TRANSIENT = 8; INTERNAL = 9; CODE_ALREADY_EXECUTED = 10; + CODE_EXECUTION_CONTEXT_UNAVAILABLE = 11; + CODE_ELEVATION_UNAVAILABLE = 12; } Code code = 1; string message = 2; diff --git a/protos/rvbox/v1/control.proto b/protos/rvbox/v1/control.proto index 225cfb7..cf7c274 100644 --- a/protos/rvbox/v1/control.proto +++ b/protos/rvbox/v1/control.proto @@ -35,6 +35,7 @@ message ClientSummary { Platform platform = 7; string architecture = 8; string daemon_version = 9; + // Omitted-CWD default on Unix; protected per-identity work root on Windows. string daemon_cwd = 10; repeated ShellType supported_shells = 11; string client_instance_id = 12;