Repository navigation
feat: per-program status, opt-in reconcile tick, llmGateway.whenProxyUnavailable - #146
Merged
peterj merged 5 commits intoOct 7, 2026
Conversation
Each apply reports every managed program's outcome with a detail: applied,
unchanged, removed, conflict, inactive (the program uses the gateway but the
local proxy is not running), blocked (not applied because another program
conflicted) or failed. The daemon logs one line per program that was not
applied and sends the list with its ConfigStatus (additive proto fields
`programs`, `programs_reported`).
The controller stores the latest list per device (migration 0003,
`device_program_status`), returns it in `GET /api/v1/devices/{id}`, and the
device page shows a "Managed programs" card. Upgrade the controller first
(migration 0003 is one-way).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`daemon.reconcileInterval` (local startup setting, off when unset; zero or more than 30 days is rejected) re-applies the current configuration, which repairs drift between pushes: a deleted or hand-edited managed file, a fixed conflict, a loosened file mode. - The current configuration is the startup configuration, replaced by each pushed configuration that parses, and cleared on logout (also between controller sessions and while waiting to retry). - Applies are serialized: a push, the startup apply and a tick never overlap. - A tick reports to the controller only when its outcome differs from what was last sent on the stream; a pushed configuration whose apply failed is saved once a tick applies it. Behaviour change for every program: an apply no longer rewrites a file that already holds the planned bytes with no mode bit beyond the planned mode, so unchanged files and sidecars are no longer rewritten on each apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ired for every managed file Review on agentdesktop-dev#137. - Reconcile tick: the revision in a tick's status is the one read together with the configuration under the apply lock, not the one the tick woke up with. Before, a push between the two published the new revision's outcome under the old revision; the connection loop dropped it and delayed saving the pushed configuration by one interval. A tick that applies nothing (after a logout) publishes nothing. - Mode repair: Codex, Grok, OpenCode, Claude Code and Claude Desktop now treat a managed file with identical bytes but a mode looser than planned as an update and rewrite it at the planned mode (before, they returned `unchanged` before planning a write, so the plan's mode check never saw them). For Claude Code and Claude Desktop such a file is rewritten even without the owner marker, as the unchanged path already claimed it. One predicate, `reconcile::grants_beyond`, decides "looser than planned" for the plan's skip check, json_merge and the VS Code settings file and sidecar; Codex, Grok, OpenCode and Claude Code name their file mode once. Only the permission bits are compared (setuid/setgid/sticky are not); a managed path that is a symlink is replaced by a regular file on such a repair, as on any rewrite. - Schema: `daemon.reconcileInterval` states the 30-day maximum. Tests: 16 new (2 tick, 4 plan, 10 provider; the 10 provider tests and 2 of the plan tests are unix-only); the five existing tick tests take the closure's new return type. The file-mode tests assume umask 022, as the existing ones do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proxy-unavailable policy
- While the daemon runs without its local LLM proxy, the programs behind it
used to remove their managed entries; for VS Code on GitHub's models that
removes the CAPI override, so Copilot Chat talks to GitHub directly, past
the gateway.
- `llmGateway.whenProxyUnavailable` now decides for `programs.copilot` and
`programs.vscode`: `failClosed` (default) leaves the managed files as they
are, so the tools stay pointed at the loopback port and fail; `failOpen`
removes them until the proxy is back. The program is `inactive` with a
reason that names the policy.
- Program removal, a mode switch, `useLlmGateway: false` and a missing
`proxyUrl` remove as before. The controller's configuration view offers the choice ("Fail open
without the local proxy") when a Copilot program is selected. Older daemons and controllers reject the key
(`deny_unknown_fields`); the default needs no key.
Example
- `examples/copilot/`: Dex, agentgateway, a daemon config and a step-by-step
guide for the Copilot CLI, VS Code on own models and VS Code on GitHub's
models, with what to check at each step (drift, conflicts, removal, the
proxy gone), plus the settings the GitHub organisation owner configures
(`github-side.md`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mes its policy and cleans up without a sidecar Review on agentdesktop-dev#138. - failClosed without the proxy: the program reports "no managed entries are in place, so the tool keeps its own settings" when nothing of the daemon's is in the file (a first apply without the proxy, or entries removed by hand), and keeps "the managed entries stay and the tool fails" when they are. In place = a managed provider or chat-model entry with a pairing header, or a VS Code override pointing at an agentdesktop loopback route; the file is read for this only under failClosed and never written. A file that cannot be read is logged and counts as not in place; one that does not parse counts as not in place (the merge reports it once the proxy is back). Before, the status claimed the tool fails while VS Code on GitHub's models went to GitHub directly. - failOpen: all three planners report "the managed entries are removed until the proxy is back (whenProxyUnavailable: failOpen)" instead of the generic proxy-absent reason, which is removed. - Cleanup without a sidecar while the daemon has no pairing (proxy off, failOpen or program removed): Copilot CLI providers and VS Code chat-model entries under the managed names that carry any pairing header are removed; user entries stay; with a known pairing only that value counts, as before. Before, such entries were left pointing at the unavailable port. - README (when failClosed has nothing to keep, the status row, the any-pairing rule), schema (`llmGateway.whenProxyUnavailable`), the controller's toggle text and a Storybook fixture. Tests: 13 new (Copilot CLI 5, VS Code chat models 5, VS Code settings 3; 2 of them guard existing behaviour). The two "keeps its mode" tests assume umask 022, as the existing file-mode tests do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Collaborator
|
@LutzLange there are some test/lint failures -- can you take a look? |
peterj
approved these changes
Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #86. Replaces #136, #137 and #138 as one PR on main (they were stacked on #135, now merged). The review on #137 and #138 is addressed in the commits
fix(agent): tick reports the revision ...andfix(agent): failClosed says whether anything is in place ...; the earlier threads are answered on those PRs.Why
Change
Per-program status
ConfigStatus; the controller stores the latest list per device, returns it inGET /api/v1/devices/{id}and shows a "Managed programs" card.Opt-in reconcile tick
daemon.reconcileInterval(local startup setting, off when unset, at most 30 days) re-applies the current configuration. A push, the startup apply and a tick never overlap; a tick reports the revision it applied, and only when its outcome changed.llmGateway.whenProxyUnavailableforprograms.copilotandprograms.vscodefailClosed(default) leaves the managed entries in place, so the tools fail instead of bypassing the gateway; when nothing of the daemon's is in place yet, the status says so and the tool keeps its own settings.failOpenremoves the entries until the proxy is back; without a sidecar, entries carrying a pairing header are removed while the daemon has no pairing of its own.examples/copilot/adds Dex, agentgateway, a daemon config and a step-by-step guide, plusgithub-side.md(what the GitHub organisation owner sets).Risk and compatibility
device_program_status) is one-way, and older controllers and daemons rejectwhenProxyUnavailable. The proto fieldsprogramsandprograms_reportedare additive.How to review
Five commits, one per part plus the review fixes:
reconcile/mod.rs,reconcile/plan.rs,controller/src/database.rs+ migration,DeviceView.tsx;tick.rs,remote.rs, the apply lock inreconcile/mod.rs, the identical-write skip inreconcile/plan.rs;whenProxyUnavailable:fail_closed_without_proxyinreconcile/mod.rsand the three planners, the toggle inConfigurationView.tsx, the example;Tests
Unit tests cover outcome precedence and reporting, controller storage, the tick (dedup, lock ordering, logout, revision), mode repair per program, and both policies in the three planners; Storybook stories cover the card and the toggle. Lab-tested on a Linux VM: the device page shows each program's outcome; with a 20 s interval a deleted file is back and a loosened mode is tightened within one interval, quiet ticks write nothing; both failClosed reasons, failOpen on all three files, and cleanup without sidecars. With a Copilot Business seat, VS Code on GitHub's models answers through the proxy, and with the proxy off under failClosed the chat fails with the requests refused at the loopback port, no fallback to GitHub. The guide's steps 1 to 4 and 6 were run from the guide alone.
Follow-ups
🤖 Generated with Claude Code