Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1cf5014
server: resolve runtime paths from an injected path model
Oct 21, 2026
f97ec29
service: add the platform-neutral service layer
Oct 21, 2026
1898b46
service: add the systemd, launchd and SCM managers
Oct 21, 2026
3d5f699
cmd: add the hfdesk service command family
Oct 21, 2026
9748467
docs: document the service mode
Oct 21, 2026
f496947
workflow: validate the service mode on macOS and Windows in CI
Oct 21, 2026
6c5c849
fix(service): let the config file of an instance own its destinations
Oct 21, 2026
7c5d725
fix(service): align the supervisor managers with the instance contract
Oct 21, 2026
a093ce4
fix(cmd): report the effective cache root and the state of the instance
Oct 21, 2026
4d8b80a
fix(cmd): keep the registered cache layout together
Oct 21, 2026
d01038b
fix(service): probe a wildcard bind over the loopback address
Oct 21, 2026
ea3d5c5
tests: cover the broken registration of the systemd manager
Oct 21, 2026
dfc442e
fix(server): keep the interactive directory mode unchanged
Oct 21, 2026
99836f2
service: run a machine-wide instance as a dedicated account
Oct 21, 2026
fbf43c4
fix(service): make the Windows uninstall idempotent without elevation
Oct 21, 2026
253b030
fix(cmd): register a stable executable path and the service account
Oct 21, 2026
756250d
tests: cover the graceful shutdown of a service instance
Oct 21, 2026
54c7588
docs: document the dedicated service account
Oct 21, 2026
1145d66
workflow: narrow the macOS CI job to the service package
Oct 21, 2026
2a9c5ef
fix(logsink): never change the mode of an existing path
Oct 21, 2026
eadfe6a
perf(server): resolve the state directory without the config search
Oct 21, 2026
df74ef1
fix(service): remove the directories a failed installation created
Oct 21, 2026
9ead8a4
fix(service): read the state of a Windows instance without elevation
Oct 21, 2026
e7a58f6
workflow: run the race detector over the suite
Oct 21, 2026
4c75b00
fix(cmd): honour the documented exit codes of the service family
Oct 21, 2026
a6b7177
fix(service): one contract for the lifecycle of an absent service
Oct 21, 2026
1e006cc
fix(service): remove the directories a failed Windows installation cr…
Oct 21, 2026
c0e1b39
docs: state the lifecycle and the exit codes of the service family
Oct 21, 2026
4303798
fix(service): restrict only the paths a Windows installation created
Oct 21, 2026
add174e
fix(cmd): report the account the instance actually runs as
Oct 21, 2026
3291f15
tests: verify the machine-wide systemd unit as well
Oct 21, 2026
a95be96
docs: state the Windows identity and the macOS CI scope
Oct 21, 2026
50fe3e6
fix(service): name the primary group of the service account
Oct 21, 2026
743f3cb
fix(service): report a failed Windows instance as failed
Oct 21, 2026
59dfe8b
fix(service): recommend only non-recursive access to a cache tree
Oct 21, 2026
509b325
refactor(service): keep the platform facts in the service layer
Oct 21, 2026
2d0e045
fix(service): report a never-started Windows instance as stopped
Oct 21, 2026
3964cf7
fix(service): make the launchd state diagnosable and parse it defensi…
Oct 21, 2026
310362a
fix(service): align the shutdown budget and the access check
Oct 21, 2026
458d5ec
fix(service): check the access of the Windows identity with its own t…
Oct 21, 2026
51eaf8b
tests: build the launchd access fixtures where the account can reach …
Oct 21, 2026
c490b02
docs: describe the graceful stop of an instance
Oct 21, 2026
6f597c6
fix(service): restart the launchd instance through the graceful stop
Oct 21, 2026
65e90d1
fix(service): read the membership of the Windows identity from its token
Oct 21, 2026
f51ce67
fix(service): judge the resolved path of a reused directory
Oct 21, 2026
08ba5fb
fix(cmd): accept a Windows executable and name the scope in the hint
Oct 21, 2026
ad37742
fix(logsink): write the log destinations independently
Oct 21, 2026
e494e01
fix(service): make the membership of the Windows identity reliable
Oct 21, 2026
15bb3a2
tests: assert the platform facts of the command layer
Oct 21, 2026
23970e8
fix(service): decide the state-root exemption on the real location
Oct 22, 2026
bcc59f1
fix(service): judge a denial by the right it denies
Oct 22, 2026
ed2c2da
fix(service): check an existing directory inside the state root
Oct 22, 2026
d789fb9
fix(service): judge the individual rights of a Windows directory
Oct 22, 2026
38cb8c3
tests: compare a path with the real location the message names
Oct 22, 2026
7391102
fix(service): run the instance on the shared logging pipeline
Oct 22, 2026
5e8a3f7
tests: assert the interactive flags through the shared registration
Oct 22, 2026
1c686ae
docs: describe the log of an instance
Oct 22, 2026
aab4c94
tests: stop the planned transfer before the gate is released
Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ jobs:
- name: Test
run: go test ./...

# The service mode adds process-global mutable state (the installed
# runtime path model behind a lock, and the standard logger's output
# being redirected by the service host), so the race detector must run
# over the whole suite on the Linux runner.
- name: Test (race detector)
run: go test ./... -race

# The Go jobs never touch the Docker surface. Exercise the real entrypoint
# image so PUID/PGID, the non-root drop, the --user path, symlink refusal, and
# argument validation cannot regress silently.
Expand Down Expand Up @@ -85,6 +92,7 @@ jobs:
- name: Test selected-cache path locality
run: go test ./internal/server -run '^TestCacheSelectionRejectsNonLocalRepoBeforeJoining$' -count=1 -v


# The Windows desktop host is a second binary of HFDesk. Its defining
# property is the PE subsystem of the produced file: the console host
# stays a console subsystem executable and the desktop host is a GUI
Expand Down Expand Up @@ -135,3 +143,62 @@ jobs:
# skip themselves on a machine that cannot host them.
- name: Test the desktop host lifecycle
run: go test ./internal/desktop -count=1 -v

# The service tests skip cleanly when the runner is not elevated, so the
# SCM registration itself is only exercised where the runner allows it.
- name: Test service package
run: go test ./internal/service -v

# The command layer is platform independent, so it is exercised on
# Windows too: a check of the command line that only holds on Unix
# would otherwise stay unnoticed until a Windows installation.
- name: Test command layer
run: go test ./cmd/hfdesk -v

# The launchd manager and the property list rendering are darwin-only code,
# so they need a macOS runner. The run executes the plist validation
# (plutil -lint) and the launchctl integration cycle where the runner
# allows it.
#
# The repository has never run its full suite on macOS, so the job covers
# the darwin-only code only: the launchd manager, the property list
# rendering, the plutil validation and the launchctl lifecycle. Widening
# it to the full suite is a separate, deliberate step.
go-macos:
name: Go checks (macOS)
runs-on: macos-latest
# A macOS runner is slow and a stalled lifecycle test would occupy it
# until the default limit of the platform. The limit is comfortably
# above the duration of the job.
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache: true

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...


# The service package covers the launchd manager, the plutil -lint
# validation of the generated property list and the launchctl
# lifecycle where the runner allows it.
- name: Test service package
run: go test ./internal/service -v


# The command layer is platform independent, so it is exercised on
# macOS too: a check of the command line that only holds on another
# platform would otherwise stay unnoticed until a macOS installation.
- name: Test command layer
run: go test ./cmd/hfdesk -v

22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ browser-only presentation belongs in `internal/assets/static`.
| `internal/server/` | HTTP API, jobs, settings, cache browser, websocket state |
| `internal/assets/` | embedded frontend assets |
| `internal/assets/static/` | browser UI files |
| `internal/service/` | service mode: scopes, lifecycle, and the systemd/launchd/SCM managers |
| `internal/runtimepaths/` | runtime path model for the config file, state root and log dir |
| `pkg/hfdownloader/` | Hugging Face scan/download/cache/mirror logic |
| `pkg/smartdl/` | model analysis and selectable download helpers |
| `docs/` | API docs and screenshots |
Expand All @@ -52,6 +54,7 @@ Read relevant Markdown before changing behavior:

- General project behavior: `README.md`
- API contracts: `docs/API.md`
- Service mode reference: `docs/SERVICE.md`
- Contribution and commit conventions: `CONTRIBUTING.md`
- Release history and user-visible changes: `CHANGELOG.md`
- Current project notes: `TODO.md`
Expand Down Expand Up @@ -83,10 +86,13 @@ change is documentation-only.
## Runtime entry points

- Main process: `cmd/hfdesk/main.go`
- Service command family: `cmd/hfdesk/service.go`
- Server routes: `internal/server/server.go`, `internal/server/api.go`
- Job manager: `internal/server/jobs.go`
- WebSocket hub: `internal/server/websocket.go`
- Config persistence: `internal/server/config.go`
- Service layer: `internal/service/service.go`, `internal/service/manager_linux.go`, `internal/service/manager_darwin.go`
- Runtime paths: `internal/runtimepaths/runtimepaths.go`
- Frontend app: `internal/assets/static/js/app.js`
- Frontend styles: `internal/assets/static/css/style.css`
- Downloader: `pkg/hfdownloader/downloader.go`
Expand Down Expand Up @@ -187,6 +193,22 @@ Downloader bugs often produce silent corruption, so safety beats clever resume.
- Exact matching and filter behavior must remain predictable for GGUF quants,
sharded files, mmproj companions, safetensors, and datasets.

## Service mode rules

- The supervisor owns the lifecycle: HFDesk stays a foreground process, with
no self-daemonization, no PID file, no shell wrapper, and no startup
shortcut.
- The service is headless: it never opens a browser and never shows a tray
icon.
- Service mode binds loopback by default; a non-loopback address must be
requested explicitly, and `install` and `status` report it.
- Credentials never enter a supervisor registration, a command line, or a unit
environment; they live in the service-owned config file.
- `uninstall` removes the registration only and never deletes models, cache,
config, jobs/history, or logs.
- Platform-specific supervisor code stays in `internal/service`; the shared
layer must not branch on the operating system.

## Testing expectations

- Downloader changes: run `go test ./pkg/hfdownloader` at minimum.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

### Added


- Add a service mode: `hfdesk service install|uninstall|start|stop|restart|status` registers HFDesk with the native supervisor of the platform (systemd on Linux, launchd on macOS, the Windows Service Control Manager on Windows). The supervisor owns the lifecycle and HFDesk stays a headless foreground process, the instance resolves deterministic runtime paths of its scope (its own config file, state root and log directory), and it binds loopback by default; exposing it to the network needs an explicit `--listen`. Credentials stay in the service-owned config file and are never written into a supervisor registration (#142).

- A second Windows artifact, `hfdesk-desktop_windows_amd64_v1.3.0.exe`, is now built and published next to the console binary: it is a GUI program without a console window that runs the same HFDesk server in-process behind a notification-area icon and a small Runtime / Logs window showing the server status, the active job count and the live log records. A left click on the icon opens that window, the tray menu offers Open HFDesk, Runtime / Logs and Quit HFDesk (a graceful shutdown), and the host is single-instance per user and session: a second launch detects the running instance and opens the web UI of it. The Runtime / Logs window needs the Microsoft Edge WebView2 Evergreen runtime, and a missing runtime produces an actionable message while the server and the tray keep working. The desktop binary is a separate download and the console binary is unchanged (#141).
- A structured logging pipeline with the new `--log-file` and `--log-level` options: every record carries a level, a timestamp, the component that produced it and structured attributes, the console sink writes to standard error so redirecting standard output keeps working, and the file sink rotates. The desktop host always writes a log file, because a GUI program has no console, and it lands in `<user cache dir>/HFDesk/logs/hfdesk.log` unless `--log-file` names another path (#141).
- Local-only runtime and log endpoints: `GET /api/runtime` reports the version, the status, the URL, the active job counts, the run id of the server and the log cursor, `GET /api/runtime/logs` returns a bounded page of the retained records, `GET /api/runtime/logs/stream` streams them as Server-Sent Events, and `POST /api/runtime/open-logs` and `POST /api/runtime/open-ui` open the log folder and the web UI of the running instance. They take no path and no URL from the caller, and three rules keep them local: the peer address has to be a loopback address, an `Origin` header of another site is rejected, and the `Host` header has to name the local machine, so a name of another site that is pointed at `127.0.0.1` cannot read the runtime data of the local machine. A request that breaks a rule is rejected with `403` and never reaches the handler (#141).
- The requests of the runtime endpoints are written to the log at `debug` level and not at `info` level, so the polling of the Runtime / Logs window (every four seconds) does not fill the bounded log history that the window exists to show. Every other request keeps the `info` level, including a request that answers with an error (#141).

- Add a read-only Community feature for repository Discussions and Pull Requests: `GET /api/community/{owner}/{repo}` lists both types with paging and type/status filters, while `?num=N` returns a thread with sanitized comments and human-readable event summaries. The Model Details view now has Overview and Community tabs; the read-only Community tab is lazy-loaded, lists Discussions and Pull Requests, opens a thread with comments and events, and supports model and dataset repositories. The proxy uses the configured endpoint, proxy, and HF token without exposing the token to the browser (#136).
- Report gated Hugging Face repositories as a dedicated access state instead of a partial analysis: the API returns HTTP 403 with error code `gated_repo`, the repository type, an accept link for the configured endpoint, and boolean-only token state, and the Model Details view shows configured-token and repository-agreement guidance without exposing the token.
- Model Details now opens README, lineage, and related-download repository links in-place, with Back/Forward history across models, datasets, and revisions (#135).
Expand Down
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ hfdesk --port 9090
hfdesk --token hf_xxx
```

**install the service** — working directory `.`; evidence: `README.md`

```bash
hfdesk service install
hfdesk service status
```

The `hfdesk service` command family (`install`, `uninstall`, `start`, `stop`,
`restart`, `status`) registers HFDesk with the native supervisor of the
platform. See `docs/SERVICE.md` for the full reference.

## Before Submitting
**run all tests** — working directory `.`; evidence: `README.md`

Expand Down Expand Up @@ -167,6 +178,8 @@ Components and important paths observed in the repository:
- **hfdesk binary** — Compiled Go binary built in Dockerfile (paths: `Dockerfile`; evidence: `Dockerfile`)
- **cmd/hfdesk/main.go** — Application entrypoint CLI/server (paths: `cmd/hfdesk/main.go`; evidence: `README.md`)
- **internal/server package** — HTTP API, job scheduling, settings handling (paths: `internal/server`; evidence: `AGENTS.md`)
- **internal/service package** — Service mode: installation scopes, lifecycle, and the systemd/launchd/Windows SCM managers (paths: `internal/service`; evidence: `AGENTS.md`)
- **internal/runtimepaths package** — Runtime path model for the config file, state root, and log directory (paths: `internal/runtimepaths`; evidence: `AGENTS.md`)
- **pkg/hfdownloader package** — Hugging Face cache scan and resumable download logic (paths: `pkg/hfdownloader`; evidence: `AGENTS.md`)
- **pkg/smartdl package** — Model analysis and download recommendation utilities (paths: `pkg/smartdl`; evidence: `AGENTS.md`)

Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,24 @@ Proxy example:
}
```

## Service mode

HFDesk can also run as a supervised background service instead of an
interactive process: systemd on Linux, launchd on macOS, the Windows Service
Control Manager on Windows. The supervisor owns the lifecycle, and HFDesk stays
a headless foreground process inside it.

```bash
hfdesk service install
hfdesk service status
```

Service mode binds loopback by default (`127.0.0.1:8080`). Exposing the
dashboard to the network needs an explicit `--listen 0.0.0.0:8080`.

See [docs/SERVICE.md](docs/SERVICE.md) for the full reference: scopes, runtime
paths, credentials, logs, and per-platform notes.

## API

HFDesk's web UI is backed by a local JSON API. See [docs/API.md](docs/API.md) for endpoints, request shapes, and response formats.
Expand Down
8 changes: 8 additions & 0 deletions cmd/hfdesk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ import (
var Version = "dev"

func main() {
// The service command family is a command of its own. It is dispatched
// before the interactive startup, so "hfdesk" without a "service" token
// keeps its exact behaviour: same flags, same defaults, same browser
// launch.
if isServiceCommand(os.Args[1:]) {
os.Exit(runServiceCommand(os.Args[2:], os.Stdout, os.Stderr))
}

if err := run(); err != nil {
os.Exit(1)
}
Expand Down
Loading
Loading