Skip to content

service: add native service mode with deterministic runtime paths - #154

Merged
bashrusakh merged 58 commits into
mainfrom
feature/service-mode
Oct 22, 2026
Merged

bashrusakh merged 58 commits into
mainfrom
feature/service-mode

Conversation

@bashrusakh

@bashrusakh bashrusakh commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds an explicit service/background runtime mode for HFDesk, driven by the native supervisor of each platform:

Platform Supervisor Scope
Linux systemd user unit (systemctl --user), machine-wide unit for the explicit server mode --user (default), --system
macOS LaunchAgent / LaunchDaemon (launchctl bootstrap/bootout/kickstart) --user (default), --system
Windows Service Control Manager, through the Go API (golang.org/x/sys/windows/svc) machine-wide only
hfdesk service install      # register with the supervisor
hfdesk service status       # scope, state, registration, paths
hfdesk service start
hfdesk service stop
hfdesk service restart
hfdesk service uninstall    # removes the registration, never the data

The supervisor owns the lifecycle: HFDesk stays a normal foreground process inside it. No self-daemonization, no PID file, no shell wrapper, no nohup, no Task Scheduler, and no HTTP endpoint for service administration.

Why

The supervisor integration was the easy half. The part that made this a real feature is the path model: a service must not resolve its config, jobs, history, logs and cache from the working directory, HOME or the profile the supervisor happens to hand over, and it must never silently look like a fresh, unrelated installation.

  • internal/runtimepaths resolves the paths once at startup and injects them. The interactive resolution is unchanged byte for byte.
  • Each scope owns a separate state root, so a user-scope instance and a machine-wide instance can never read or write each other's state.
  • The service binds loopback by default. Exposing it to the network needs an explicit --listen, and install and status make a non-loopback bind visible.
  • The credentials (HF token, proxy password, basic-auth password) never enter the supervisor registration, the process command line or the unit/plist environment. They live in the service-owned config file, which the instance's own settings API maintains.
  • The service is headless: it never opens a browser and never creates a tray icon.

The machine-wide identity

A machine-wide instance runs as a dedicated, stable, unprivileged account on every platform — hfdesk on Linux, _hfdesk on macOS, the built-in passwordless LocalService on Windows. Root is the platform's LocalSystem analogue, and the issue asks for explicit privilege boundaries and a least-privilege identity.

HFDesk never creates an account and never shells out: it resolves the account, refuses the installation clearly when it is missing, and names the command that creates it. The account is granted only the directories the installation created; a directory that existed before keeps its owner and its permissions and must already be writable by the account. A named cache directory is validated for accessibility and never rewritten — the installation gives ownership guidance instead. On Windows the identity cannot be chosen (--service-user is rejected): a service under a user account would need a credential, and a credential must never reach the command line.

Behaviour worth knowing

  • stop and uninstall of a service that is not installed are clean successes; start and restart fail with a readable "not installed" error. Exit codes: 0 succeeded, 1 the operation failed, 2 the command line could not be understood.
  • A service never daemonizes itself, never enables loginctl enable-linger silently, and never escalates privileges itself.
  • uninstall removes the registration only — models, cache, config, jobs/history and logs stay.
  • The instance runs on the shared logging pipeline: its records reach the supervisor through standard error and land in a log file inside the instance's log directory, which rotates at 5 MiB and keeps three rotated files.

Full reference: docs/SERVICE.md.

Validation

Executed on the development host (Linux, with a live systemd user session):

  • go build ./..., go vet ./... — clean; GOOS=windows and GOOS=darwin build, vet and test-binary compile — clean.
  • go test ./... and go test ./... -race — green.
  • systemd-analyze verify — passed for both the generated user unit and the generated machine-wide unit.
  • A real systemctl --user lifecycle: install → enable → start → running → stop → uninstall → not-installed, with no residue.
  • A live service instance: loopback-only bind, the registered paths honoured under a changed HOME/XDG_CONFIG_HOME/working directory, a credential-free unit file, bounded log rotation, clean SIGTERM shutdown.
  • The interactive CLI is byte-identical: hfdesk --help matches the base commit, and hfdesk --port N --no-open still serves /api/health.

New CI coverage: a macOS job for the launchd manager (including plutil -lint and the launchctl cycle), the service package on the Windows job, and the race detector over the whole suite on Linux.

CI on this pull request is green on all three platforms — Linux, windows-latest and macos-latest. On the macOS runner the launchd lifecycle test runs and passes (install → running → stop → uninstall), and plutil -lint accepts the generated property list for both the LaunchAgent and the LaunchDaemon. On the Windows runner the service-control-manager tests run as an administrator, including a real service registration.

Not executed locally: the Windows SCM runtime and the macOS launchd runtime — the development host is Linux. Both platforms are covered by the CI jobs above. The machine-wide install path on Linux/macOS needs root and a pre-created service account, so only its refusal logic is exercised locally.

Risk

  • The interactive CLI is unchanged, but internal/server now resolves its config, jobs, history and mirror-target paths through the runtime path model. The interactive resolution is asserted against an independent reference implementation of the previous search order.
  • The service-owned directories are created owner-only and are never re-tightened when they already exist; a directory the user named keeps its owner and its permissions.
  • The Windows implementation applies a restrictive descriptor to the paths an installation created and only reads the descriptor of a path that existed before.

Closes #142

@bashrusakh bashrusakh added the enhancement New feature or request label Oct 21, 2026
@bashrusakh
bashrusakh marked this pull request as draft October 21, 2026 16:21
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f2a8c247-14bb-4803-8925-a190f46dedfd
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bashrusakh
bashrusakh marked this pull request as ready for review October 21, 2026 17:35
Codex and others added 26 commits October 22, 2026 15:10
- Add internal/runtimepaths with the startup-resolved path model: the config
  file, the state root and the log directory, plus the derived jobs state,
  download history and mirror target files.
- Resolve the model once and install it process-wide, so a supervised
  instance never depends on the working directory, HOME or the user profile
  of the account the supervisor happens to run it as.
- Keep the interactive resolution per-call and byte-identical to the previous
  behaviour: an existing config next to the launch/executable directory still
  wins over the per-user config directory, and the per-user directory is still
  where new saves go.
- Route the config file, the jobs state, the download history and the mirror
  targets of internal/server through the model, so one instance keeps a single
  coherent state identity.
- Add internal/logsink, a size-bounded rotating log file writer, so a
  supervised process can keep its own diagnostics without unbounded growth.

Co-Authored-By: opencode <noreply@opencode.ai>
- Define the service contract every platform manager implements: the scope,
  the lifecycle vocabulary, the status report, the installation options and
  the install/uninstall/start/stop/restart/status operations.
- Own the command semantics in one place: the scope parsing, the listen
  address parsing and validation, the loopback policy, the installation
  validation and the canonical, non-secret argument vector of the registered
  process.
- Define where an instance keeps its state per scope, so a user scope instance
  and a system scope instance can never read or write each other's state root.
- Add the headless host: it installs the runtime path model, pins the cache
  environment, redirects the log into the bounded log file of the instance and
  runs the existing server with its graceful shutdown path. It never opens a
  browser and never shows a tray icon.
- Parse the argument vector of the registered process back into the run
  options, so the command line and the supervisor registration cannot drift.

Co-Authored-By: opencode <noreply@opencode.ai>
- Linux: register a systemd user unit by default and a machine-wide unit for
  the explicit system scope, managed through systemctl --user and systemctl.
  The unit stays a foreground process (Type=simple) and never forks, writes a
  PID file or daemonizes itself. Linger is never enabled silently: booting
  without a login stays an explicit user or admin decision.
- macOS: register a per-user LaunchAgent by default and a LaunchDaemon for the
  explicit system scope, managed with launchctl bootstrap, bootout and
  kickstart. The plist keeps the program arguments absolute and free of
  credentials, and HFDesk writes its own bounded log file because launchd
  does not rotate the ones it owns.
- Windows: register the service with the service control manager through the
  Go API, never through sc.exe or PowerShell. The service runs as the
  least-privilege LocalService account, its state under ProgramData carries
  restrictive ACLs, and the handler reports StartPending, Running only once
  the HTTP server is usable, StopPending and Stopped around the graceful
  shutdown, with lifecycle and fatal messages in the Windows Event Log.
- Make install, uninstall, start, stop and restart idempotent with
  deterministic outcomes, roll a failed installation back instead of leaving
  a half-configured registration, and report a registration whose executable
  was moved or deleted as broken.
- Keep uninstall free of data deletion: it removes the registration only and
  never touches models, cache, config, jobs/history or logs.

Co-Authored-By: opencode <noreply@opencode.ai>
- Add the install, uninstall, start, stop, restart, status and run subcommands
  with the standard library flag package, one flag set per subcommand and no
  new dependency.
- Resolve the runtime paths, the cache directories and the listen address of
  the selected scope, record them in the registration and report them back, so
  the instance keeps one coherent state identity.
- Bind loopback by default and make a non-loopback address visible in the
  install and status output.
- Warn when the registered executable lives in a temporary or version-stamped
  path, because the next release would break the registration.
- Keep the interactive command line untouched: only the bare "service" token
  selects the new family, and hfdesk [flags] behaves exactly as before.

Co-Authored-By: opencode <noreply@opencode.ai>
- Add docs/SERVICE.md as the reference for the command surface, the scopes,
  the runtime paths, the credential handling, the logs, the lifecycle
  semantics and the per-platform notes.
- Describe the service mode in the README with the quick start, the loopback
  default and a link to the reference.
- Record the feature in the changelog, the repository layout, the runtime
  entry points and the agent rules, and add the service commands to the
  contribution guide.

Co-Authored-By: opencode <noreply@opencode.ai>
- Add a macOS job that builds, vets and tests the repository, so the launchd
  manager, the plutil validation of the generated property list and the
  launchctl lifecycle run on a real macOS runner.
- Run the service package tests on the Windows job, so the service control
  manager code is compiled and its tests can execute there.

Co-Authored-By: opencode <noreply@opencode.ai>
- The cache root and the flat output directory of a service instance now
  resolve from the config file the instance owns, with the values recorded
  in the registration as the fallback. The settings API of the instance
  writes that file, so a change of the cache directory is honoured by the
  next start instead of being saved and then ignored.
- The hub directory follows the cache root when the config file defines it,
  and stays the registered one otherwise.
- Pin the cache environment from the effective values, after the
  configuration was loaded and before the server captures the cache
  identity of the process.
- Create the state and the config directory owner-only: they hold the job
  list, the download history, the mirror targets and the credentials of
  the instance.
- Drop the unused InstallOptions fields and export the loader of a config
  file at an explicit path.

Co-Authored-By: opencode <noreply@opencode.ai>
- Refuse a machine-wide installation that would adopt the state of the
  user who runs the install command, on Linux as well as on macOS, and run
  that check before any directory is created so a refused installation
  leaves nothing behind.
- Require an elevated shell for a machine-wide installation and for a
  machine-wide removal on macOS, like the other platforms, and report that
  requirement clearly instead of a raw permission error.
- Preflight an explicitly reused data, cache or output path on Windows: the
  path must be a directory the service identity can reach, and the
  installation fails with a clear permission error before the registration
  is written. The preflight only reads the descriptor and never changes
  the ACL of a reused tree.
- Restrict the config directory, the state directory, the log directory and
  the config file of a Windows instance, so every directory the instance
  owns carries the descriptor of the service identity.
- Report the effective cache root of the instance in the status of every
  platform, so the status describes the instance that runs.

Co-Authored-By: opencode <noreply@opencode.ai>
- Report the cache root the instance actually uses in the status and in the
  uninstall summary. The scope default was reported before, which named the
  personal cache of the invoking user for a user scope instance and
  disagreed with the instance that runs.
- Report the state the supervisor reports after an installation instead of
  claiming that the instance was not started: a macOS LaunchAgent starts
  with its registration, a systemd unit does not.
- Document the effective cache root of an instance, the identity of a
  machine-wide instance per platform and the migration path of a future
  macOS app bundle to SMAppService.

Co-Authored-By: opencode <noreply@opencode.ai>
- Move the hub directory with the cache root when only --cache-dir is given,
  so a registration never names two different trees. An explicit --hub-dir
  is still honoured as given: it is the deliberate separation of the two
  locations that HF_HOME and HF_HUB_CACHE have.
- Report the hub directory of the instance in the install summary, next to
  the cache root, so both describe the layout the instance will use.
- Name the subcommand that owns --listen in the usage block instead of
  advertising it for the whole family, and correct the description of the
  install command: a macOS LaunchAgent starts the instance when it is
  registered, a systemd unit is only registered and enabled.

Co-Authored-By: opencode <noreply@opencode.ai>
- A wildcard bind has no address a client can connect to, so the readiness
  probe of the Windows service now dials the loopback address of the same
  family. Without it a service that listens on every interface could fail
  its own startup and be reported as stopped while it is running.
- Extract the decision of a broken registration into a testable helper and
  cover it, so the criterion that a moved or deleted executable is reported
  clearly is proven on Windows as well.

Co-Authored-By: opencode <noreply@opencode.ai>
- Extract the decision of a broken registration into a helper and drive the
  real status path with a unit file in a temporary unit directory, so the
  criterion that a moved or deleted executable is reported clearly is
  proven on Linux as well.

Co-Authored-By: opencode <noreply@opencode.ai>
- The directories a server owns are owner-only when a supervised instance
  owns them and keep their historical mode for an interactive run, so the
  per-user layout of an existing installation does not change.
- Let the server create the state directory before the mirror targets are
  written, so the mode of the directory does not depend on which code path
  created it.

Co-Authored-By: opencode <noreply@opencode.ai>
- A machine-wide instance runs as a dedicated, stable, unprivileged account
  instead of root: hfdesk on Linux, _hfdesk on macOS, the built-in LocalService
  on Windows. Root is the platform's LocalSystem analogue, and the issue asks
  for explicit privilege boundaries and a least-privilege identity.
- HFDesk never creates an account and never shells out: it resolves the
  account, refuses the installation clearly when it does not exist and names
  the command that creates it.
- The account is granted only the directories the installation created. A
  directory that existed before keeps its owner and its permissions, and the
  account has to be able to write into it already. Every directory is checked
  before the first one is granted, so a refused installation leaves every
  ownership as it was.
- A named cache, hub or output directory is validated for accessibility and
  never rewritten: the installation gives ownership guidance instead.
- Refuse a top-level directory of the machine as an instance directory on
  both platforms, so the same command line is answered the same way.
- Let the instance configuration come from one source: it installs the
  runtime path model itself instead of relying on an ambient global.
- Create the directories of an instance owner-only, and leave the permissions
  of a directory that already exists alone.

Co-Authored-By: opencode <noreply@opencode.ai>
- Look the registration up with a read-only connection to the service control
  manager before the elevation requirement applies, so uninstalling a service
  that was never installed is a clean success in every shell.
- Report the elevation requirement when a registration exists or when the
  caller may not look, instead of a raw access-denied error, and make the
  start, stop and status answers independent of the rights of the shell.

Co-Authored-By: opencode <noreply@opencode.ai>
- Register the path the binary was invoked with when it is a usable path, so
  a stable name is registered instead of the version-stamped file it points
  at, and add an --executable option to name the path explicitly.
- Follow the upgrade warning from the path that is registered instead of
  from the resolved target.
- Add the --service-user option of a machine-wide installation, and report
  the account of the instance in the install summary.

Co-Authored-By: opencode <noreply@opencode.ai>
- Drive the service host through a complete lifecycle: it serves, it shuts
  down on the cancellation of its context without an error, and it persists
  the job list into the state directory of the instance.

Co-Authored-By: opencode <noreply@opencode.ai>
- Describe the identity of a machine-wide instance per platform, the account
  that has to exist, the directories that are granted to it and the guidance
  for a named cache directory.
- Correct the account creation commands, the command surface, the registered
  executable path, the cache layout and the directory permissions so the
  document matches the implementation.

Co-Authored-By: opencode <noreply@opencode.ai>
- The launchd manager, the plutil validation of the generated property list
  and the launchctl lifecycle are darwin-only code, so they need a macOS
  runner. The full suite still has pre-existing macOS-compatibility gaps, so
  the job stays narrowed to the service package.

Co-Authored-By: opencode <noreply@opencode.ai>
- The log directory and the log file are created owner-only, and a directory
  or a file that already exists keeps its mode. The instance no longer
  requires an ownership the installation never checked, so an instance whose
  log directory belongs to another owner can start instead of failing with a
  permission error before it serves.

Co-Authored-By: opencode <noreply@opencode.ai>
- The jobs state, the download history and the mirror targets resolve their
  state directory directly, so a save no longer re-scans the working
  directory and the executable directory for a config file it does not need.

Co-Authored-By: opencode <noreply@opencode.ai>
- A failed installation leaves the machine as it was: the directories it
  created are removed when they are still empty, and a directory that held
  something is kept. A directory that existed before is never touched.

Co-Authored-By: opencode <noreply@opencode.ai>
- The status of a registered instance is read through a query-only connection
  to the service control manager, so a normal shell can see the state of the
  instance, including a registration whose executable is gone. Only the
  operations that change the registration keep the privileged path.

Co-Authored-By: opencode <noreply@opencode.ai>
- The service mode adds process-global mutable state, so the Linux job runs
  the race detector over the whole suite.

Co-Authored-By: opencode <noreply@opencode.ai>
- A command line that cannot be understood exits with 2 on every subcommand:
  an unknown flag, mutually exclusive scope selectors, a relative path, an
  unusable executable, an unparsable listen address. The failure of an
  operation keeps exit code 1.
- Report honestly that there was no registration to remove when the uninstall
  found nothing.

Co-Authored-By: opencode <noreply@opencode.ai>
- Stopping a service that is not installed is a clean success on every
  platform, because there is nothing running to stop. Starting or restarting
  one still fails with a readable not-installed error.
- The machine-wide start, stop and restart report the elevation requirement
  the same way the installation and the removal do. The not-installed check
  comes first, so it needs no privilege and gives the more useful answer.

Co-Authored-By: opencode <noreply@opencode.ai>
Codex and others added 24 commits October 22, 2026 15:13
- The service control manager has no state of its own for a failure, so it
  reports a crashed instance as stopped; the exit code is the only fact that
  distinguishes it from a clean stop. A non-zero exit code is now reported as
  failed, like the Linux and macOS managers do, so the status of a crashed
  instance is the same on every platform.

Co-Authored-By: opencode <noreply@opencode.ai>
- The refusal for a cache directory the account cannot write names the
  directory, the account and the missing access, and recommends only
  non-recursive alternatives: a recursive ownership change of a model or
  cache tree is out of scope.
- Correct the note about the account lookup: on macOS it goes through the
  directory service, and only the explicit osusergo build tag selects the
  reader of /etc/passwd.

Co-Authored-By: opencode <noreply@opencode.ai>
- The default scope of an installation, the existence of the user scope and
  the possibility to name the account of a machine-wide instance are facts
  of the platform. They now live in the service layer behind its build tags,
  so the command family does not branch on the operating system itself.

Co-Authored-By: opencode <noreply@opencode.ai>
- The service control manager reports a service it never started as stopped
  with the exit code 1077, so the rule that turns a non-zero exit code into
  a failure marked a freshly installed instance as failed. The never-started
  code is excluded, and the instance is reported as stopped like on the
  other platforms.
- Correct the Windows expectations of the data-path preflight and of the
  argument candidates: a path is quoted in the message, and the candidates
  the handler assembles are not identical. The assertions that matter are
  unchanged.

Co-Authored-By: opencode <noreply@opencode.ai>
…vely

- The state of an instance is now parsed from the output of launchctl
  whatever shape it has: the key and the value may be quoted or terminated,
  the field may sit at any nesting depth, and the first occurrence wins.
- A state that cannot be parsed is reported with what the supervisor
  actually said, so an administrator and a failing test can see it. The
  excerpt is bounded and a value that looks like a credential is redacted.
- The launchd lifecycle test skips with that evidence when the environment
  cannot run a LaunchAgent, and still fails when the job is not loaded after
  a successful installation.

Co-Authored-By: opencode <noreply@opencode.ai>
- The server publishes the budget of its orderly shutdown, and every
  supervisor-side wait is derived from it: the launchd job, the systemd unit
  and the service control manager now wait for the same budget. The macOS
  manager waited ten seconds while the orderly path needs up to twenty, so it
  could unload a job that was still saving its state; launchd's own kill
  deadline had the same collision.
- The check whether the service account can use a reused directory asks for
  the effective permission: it needs write and execute on the directory and
  execute on every directory above it, with the supplementary groups of the
  account considered. Owning a directory with the mode 0500 is not writing
  to it. The check lives in one file for the Unix platforms.

Co-Authored-By: opencode <noreply@opencode.ai>
…oken

- The membership of the service identity is read from a token of the
  identity itself instead of a static list of presumed groups, so a
  descriptor that grants access only to Interactive Users is refused: the
  instance runs a service logon, not an interactive one.
- When the platform refuses the logon, the check falls back to the subset
  that is certain and says so in the refusal.
- The waits of the service control manager and of its handler use the shared
  shutdown budget.

Co-Authored-By: opencode <noreply@opencode.ai>
…them

- The fixtures of the access check lived in the per-user temporary directory
  of macOS, which the account under test cannot traverse, so the check
  correctly refused them and the tests measured the temporary directory
  instead of the fixture. They now live in the world-traversable scratch
  directory, and the tests assert that precondition explicitly.

Co-Authored-By: opencode <noreply@opencode.ai>
- State the two phases of an orderly shutdown, the time they may take and the
  budget every supervisor waits for, so an operator can tell what a stop does
  and why it takes as long as it does.

Co-Authored-By: opencode <noreply@opencode.ai>
- A restart replaced the running process with kickstart -k, which does not
  deliver the signal the service host turns into the cancellation of the
  application context, so the server never drained its connections and never
  wrote its job list back. A restart is now the graceful stop followed by the
  start, and the replacement starts only when the stop succeeded.
- A stop that exceeded its budget is reported instead of looking like a
  success.
- A failed reinstall restores the supervisor state and not only the property
  list, so an instance that was running before the attempt runs again.

Co-Authored-By: opencode <noreply@opencode.ai>
- The logon of the service account is made in the documented form: the user
  name, the domain and a null password, so the platform does not refuse it
  and the effective membership of the account is really the one of its token.
  The Interactive Users group is never part of it.
- An entry that only applies to the children of a directory does not grant
  the identity access to the directory itself.
- A stop of an instance that is still starting delivers the control again
  instead of giving up on it.
- A failed start waits for the instance to finish its shutdown before it is
  reported as stopped, like the normal stop path.
- A named data directory that does not exist is refused with the guidance
  instead of being created.

Co-Authored-By: opencode <noreply@opencode.ai>
- The access check resolved the target of a symbolic link but validated the
  ancestors of the link instead of the ancestors of its target, so a
  directory the account could not reach was accepted. The check now judges
  the path the kernel really uses, and still judges the way to the link.
- A named directory inside the state root of the instance is not checked
  against the account, like on the other platform: the installation grants
  the state root to that account.
- The systemd rendering escapes the dollar sign, and a path with a control
  character is refused before anything is written.

Co-Authored-By: opencode <noreply@opencode.ai>
- The executable of an installation was checked for a POSIX execute bit,
  which no file on Windows ever has, so an explicit path was always
  refused. The command layer now checks what holds on every platform and
  leaves the execute bit to the platform managers.
- The hint of the install summary names the scope of the instance that was
  installed, so the printed command starts that instance.
- The command layer is now exercised on the Windows and macOS runners, and
  the macOS job has a timeout.

Co-Authored-By: opencode <noreply@opencode.ai>
- The standard output and the log file were written through a multi writer
  that stops at the first failing destination, so a process without a valid
  standard output - the service control manager gives none - never wrote a
  line into the log file of the instance. The two destinations are now
  written independently.
- A rotation that failed left the sink without a file, so every later write
  was refused and the log of the instance stayed silent. The sink recovers.

Co-Authored-By: opencode <noreply@opencode.ai>
- The logon of the service account is tried in the documented forms, because
  the domain NT AUTHORITY made the authority look for a domain instead of
  the local account. The form that worked is logged.
- When the platform refuses every form, the membership falls back to the
  documented one of a service logon - the account, everybody and the
  authenticated users - and never to the interactive users, which belong to
  an interactive logon. The fallback is announced in the log and in the
  refusal.
- The membership tests pass with either source; only the test of the token
  path itself may skip, with the error of the platform in its message.
- The refusal for a named directory that does not exist names the account as
  well, and the tests of the installation match what it creates.

Co-Authored-By: opencode <noreply@opencode.ai>
- The tests of the command layer assumed the platform facts of a POSIX
  system: a user scope and a nameable service account. On Windows neither
  exists, and the runner that now executes them showed it. They drive the
  platform seam instead, so every platform is asserted from every host.

Co-Authored-By: opencode <noreply@opencode.ai>
- The exemption for a named directory inside the state root of the instance
  was decided on the path as it was written, so a link inside the state root
  that points out of it was exempted and the installation was accepted even
  though the account could not reach the target. The exemption now compares
  the resolved locations of the two paths, and a link that leaves the state
  root is checked like any other path.

Co-Authored-By: opencode <noreply@opencode.ai>
- A deny entry refused the path as soon as it named the identity, whatever
  it denied, so a directory that denies only the right to change its access
  control list was refused although the instance can use it. The denial is
  now judged by its intersection with the rights the instance needs.
- The exemption for a named directory inside the state root is decided on
  the resolved location, like on the other platforms.
- The membership of the service identity is the documented one of a service
  logon. The attempt to obtain a token of the account was never proven on any
  machine, and it is gone: the account itself, everybody and the
  authenticated users, and never the interactive users.

Co-Authored-By: opencode <noreply@opencode.ai>
- The exemption for a named directory inside the state root of the instance
  was granted to a directory that already exists as well. Owning the parent
  grants no right on an existing child, so a cache directory that belongs to
  another account was accepted and the service failed later with a permission
  error. The exemption now covers only a directory that does not exist yet and
  that the service will really create under its own management.

Co-Authored-By: opencode <noreply@opencode.ai>
- The mask of an access control entry was mapped into the rights the
  instance needs by its generic and aggregate forms only, so a denial of a
  single right - of FILE_ADD_FILE, the right to create a file - was ignored
  and the directory was accepted. The individual rights are mapped now, and a
  denial is judged by the right it denies.
- A named directory that does not exist yet is judged through its nearest
  existing ancestor, like on the other platforms, instead of being refused:
  the instance creates it at run time, as the account.

Co-Authored-By: opencode <noreply@opencode.ai>
- The refusal of a named directory names the nearest existing ancestor in its
  resolved location, because that is the path the operator has to grant the
  access on. The test compared it with the path as it was written, which on
  Windows is the short form of the temporary directory and never matches.

Co-Authored-By: opencode <noreply@opencode.ai>
- The service host redirected the standard logger into a file of its own,
  while the server logs through the shared structured pipeline. The records
  of the instance therefore reached the log file as plain text, went to
  standard output instead of standard error, and never reached the journal
  of the supervisor. The instance now installs the shared pipeline: the
  console sink carries the records to the supervisor, the file sink writes
  them into the log directory of the instance.
- The log directory of an instance is created owner-only and a log file that
  cannot be written does not stop the instance.
- The package that implemented a second rotating file sink is gone; the
  shared pipeline owns that job now.

Co-Authored-By: opencode <noreply@opencode.ai>
- The test looked for the flag registrations in the entrypoint. The shared
  application layer owns them now, so the test builds the flag set the
  interactive path builds and asserts the flags and their defaults.

Co-Authored-By: opencode <noreply@opencode.ai>
- The log of an instance is the file of the shared pipeline: the records of
  the server land in it, it rotates at the size of the shared pipeline and
  keeps three rotated files, and the records reach the supervisor through
  standard error.

Co-Authored-By: opencode <noreply@opencode.ai>
@bashrusakh
bashrusakh force-pushed the feature/service-mode branch from 96d4aa6 to 1c686ae Compare October 22, 2026 04:50
- The test released the blocked transfer and only then cancelled the job, so
  a transfer that finished in between left the cancel with nothing to cancel
  and the test failed. The cancel now happens while the transfer is still
  blocked, which is deterministic; the behaviour under test is unchanged.

Co-Authored-By: opencode <noreply@opencode.ai>
@bashrusakh
bashrusakh merged commit d18f8ea into main Oct 22, 2026
11 checks passed
@bashrusakh
bashrusakh deleted the feature/service-mode branch October 22, 2026 07:54
bashrusakh pushed a commit that referenced this pull request Oct 22, 2026
Integrate the upstream native service mode (#154) with the #140 downloader
model, so both survive the merge.

The merge auto-resolved without textual conflicts: the overlapping files
changed in disjoint regions, so both sides are kept in full.

pkg/hfdownloader keeps the #140 model byte for byte. The only upstream
change in that package is targets.go, a comment on DefaultTargetsPath
that we did not touch, so it takes upstream's version; the #140 files
(downloader.go, ranged.go, blob_domain.go, blob_publish.go, hfcache.go,
staging_*.go, verify.go, types.go) are unchanged by this merge.

internal/server/api.go and internal/server/mirror.go keep both sides: the
runtime-path wiring of the service mode and the #140 staging-artifact
exclusion. The exclusion still covers every artifact name the #140 model
introduces - the .incomplete payload, the .meta resume record, the
.meta.tmp atomic record temp and the persistent .lock ownership files
(the staging lock, the record lock and the blob-key domain lock
blobs/tmp-<sha>.lock) - and it stays applied at every blobs/ listing,
selection, mirroring and deletion site. Upstream added no new blobs
listing or selection path, so the filter needed no new application.

AGENTS.md, CHANGELOG.md and docs/API.md keep both sides: the service-mode
rules, the service-mode reference and the service-mode changelog entry
alongside the #140 ownership/publication model, the job contract, the
cleanup ownership rule and the Verify-mode semantics. No section is lost
and the two rule sets do not overlap semantically: the service mode
governs the lifecycle and the runtime paths, the #140 rules govern the
downloader.

Co-Authored-By: opencode <noreply@opencode.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Service mode: native SCM / systemd / launchd management with deterministic runtime paths

1 participant