Skip to content

feat: llmGateway.whenProxyUnavailable and a GitHub Copilot example - #138

Closed
LutzLange wants to merge 8 commits into
agentdesktop-dev:mainfrom
LutzLange:upstream/s6-guide-and-policy
Closed

LutzLange wants to merge 8 commits into
agentdesktop-dev:mainfrom
LutzLange:upstream/s6-guide-and-policy

Conversation

@LutzLange

@LutzLange LutzLange commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Part of #86 (series overview: #86 overview). Stacked on #137; review the last two commits (the second addresses the review below).

Why

While the daemon runs without its proxy, the programs behind it removed their entries. For VS Code on GitHub's models, that sends Copilot Chat straight to GitHub, past the gateway. That's a silent bypass for a company that routes Copilot through the gateway.

Change

  • llmGateway.whenProxyUnavailable applies to programs.copilot and programs.vscode:
    • failClosed (default) leaves the managed files as they are, so the tools fail instead of bypassing the gateway.
    • failOpen removes the entries until the proxy is back.
  • Status. Either way, the program reports inactive with a reason that names the policy. Under failClosed the reason also says when nothing of the daemon's is in place yet (first apply without the proxy, or entries removed by hand); the tool then keeps its own settings.
  • Controller UI. The configuration view shows a "Fail open without the local proxy" toggle when a Copilot program is selected, and emits the key when it is on.
  • Unchanged removal. Removing a program, switching modes or dropping the gateway still removes its entries.
  • examples/copilot/ adds Dex, agentgateway, a daemon config and a step-by-step guide with a check per step, plus github-side.md (what the GitHub organisation owner sets).

Risk and compatibility

Older daemons and controllers reject the key. Set it only after upgrading both; the default needs no key.

How to review

  • reconcile/mod.rs has fail_closed_without_proxy, and the three planners call it.
  • ConfigurationView.tsx has the toggle; styles.css stops toggle checkboxes from shrinking next to long help text.
  • Most of the diff is the example and its guide.

Review fixes

The last commit (52d26c1) addresses the Copilot review:

  • failClosed with nothing in place reports "no managed entries are in place, so the tool keeps its own settings" instead of claiming the tool fails; the README says VS Code on GitHub's models then talks to GitHub directly. The daemon does not point VS Code at a port nothing listens on; blocking GitHub without the gateway stays with the org's Copilot policy and egress rules.
  • failOpen reports "the managed entries are removed until the proxy is back (whenProxyUnavailable: failOpen)" on all three paths.
  • Cleanup without a sidecar while the daemon has no pairing (proxy off) removes managed-name entries carrying any pairing header; user entries stay. README, schema and the controller's toggle text updated.
  • The port-collision finding is documented in the README rather than changed.
  • 13 new unit tests; lab-checked on the Linux VM (both failClosed reasons, failOpen on all three files, cleanup without sidecars keeping a user provider and the file modes).

Tests

CI green. Two Storybook stories cover the toggle: shown and emitting the key for Copilot, and absent without a Copilot program. Unit tests for fail-closed in the three planners check that the bytes and sidecar are kept, the reason is reported, the proxy returns on another port, and removal still works. Lab-tested on a Linux VM: with the proxy off, VS Code answers "Language model unavailable" and doesn't fall back to GitHub. The guide's steps 1 to 4 and 6 were run from the guide alone.

Screenshot

Controller configuration view with the fail-open toggle on and the generated key.

Controller configuration view with the fail-open toggle on and the generated key.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved gateway-bypass and credential-exposure risks remain in the fail-closed path.

Review effort: Balanced
Findings: 2 High severity · 4 Medium severity

Open (6)
What changed in this PR

Adds configurable proxy-unavailable behavior and a deployment example for routing GitHub Copilot through Agentdesktop’s LLM gateway.

Changes:

  • Adds the default failClosed policy and controller controls for failOpen.
  • Extends Copilot integration, authentication, reconciliation, and status reporting.
  • Adds a Dex/Agentgateway example and lifecycle coverage.
File Description
schema/​daemon-config.md Documents configuration options.
schema/​daemon-config.json Extends configuration schemas.
frontend/​ui/​src/​tools.tsx Adds Copilot branding.
frontend/​desktop/​src/​views/​StatusView.stories.tsx Covers proxy status display.
frontend/​desktop/​src/​types.ts Types proxy status.
frontend/​desktop/​src/​stories/​fixtures.ts Adds proxy fixtures.
frontend/​desktop/​src/​components/​DaemonInformation.tsx Displays listener status.
frontend/​controller/​src/​views/​DeviceView.tsx Displays program outcomes.
frontend/​controller/​src/​views/​DeviceView.stories.tsx Covers program status display.
frontend/​controller/​src/​views/​ConfigurationView.tsx Adds Copilot configuration controls.
frontend/​controller/​src/​views/​ConfigurationView.stories.tsx Covers policy toggle behavior.
frontend/​controller/​src/​types.ts Extends configuration and status types.
frontend/​controller/​src/​styles.css Styles status and toggles.
frontend/​controller/​src/​stories/​fixtures.ts Adds configuration and status fixtures.
examples/​copilot/​README.md Provides setup and verification steps.
examples/​copilot/​github-side.md Documents GitHub-side requirements.
examples/​copilot/​dex.yaml Configures example identity provider.
examples/​copilot/​config.yaml Configures example daemon.
examples/​copilot/​compose.yaml Runs example services.
examples/​copilot/​agentgateway.yaml Configures authenticated gateway routes.
crates/​proto/​proto/​fleet.proto Adds per-program status messages.
crates/​core/​src/​model.rs Adds proxy information model.
crates/​controller/​src/​service.rs Handles program status reports.
crates/​controller/​src/​database.rs Persists program outcomes.
crates/​controller/​migrations/​0003_program_status.sql Adds status storage.
crates/​agent/​tests/​integration.rs Registers Copilot integration tests.
crates/​agent/​tests/​common/​gateway.rs Captures forwarded token headers.
crates/​agent/​tests/​common/​container.rs Updates container lifecycle helpers.
crates/​agent/​src/​tick.rs Adds periodic reconciliation.
crates/​agent/​src/​reconcile/​plan.rs Tracks outcomes and skips identical writes.
crates/​agent/​src/​provider/​vscode/​testdata/​Dockerfile Configures user runtime directory.
crates/​agent/​src/​provider/​vscode/​testdata/​chatLanguageModels.lab.json Adds chat-model fixture.
crates/​agent/​src/​provider/​vscode/​settings.rs Manages GitHub-model endpoint settings.
crates/​agent/​src/​provider/​vscode/​reconcile.rs Manages custom chat models.
crates/​agent/​src/​provider/​vscode/​mod.rs Connects VS Code reconciliation modes.
crates/​agent/​src/​provider/​vscode/​integration_tests.rs Covers routing and reconciliation lifecycles.
crates/​agent/​src/​provider/​vscode/​discovery.rs Shares profile path resolution.
crates/​agent/​src/​provider/​README.md Updates provider capabilities.
crates/​agent/​src/​provider/​opencode/​mod.rs Exposes provider identifier.
crates/​agent/​src/​provider/​ollama/​mod.rs Exposes provider identifier.
crates/​agent/​src/​provider/​mod.rs Extends provider reconciliation context.
crates/​agent/​src/​provider/​grok/​mod.rs Exposes provider identifier.
crates/​agent/​src/​provider/​cursor/​mod.rs Exposes provider identifier.
crates/​agent/​src/​provider/​copilot/​testdata/​providers.lab.json Adds providers fixture.
crates/​agent/​src/​provider/​copilot/​testdata/​Dockerfile Adds Copilot test environment.
crates/​agent/​src/​provider/​copilot/​reconcile.rs Manages Copilot providers.
crates/​agent/​src/​provider/​copilot/​mod.rs Registers Copilot integration.
crates/​agent/​src/​provider/​copilot/​integration_tests.rs Covers provider lifecycle.
crates/​agent/​src/​provider/​copilot/​discovery.rs Discovers Copilot installations.
crates/​agent/​src/​provider/​codex/​mod.rs Exposes provider identifier.
crates/​agent/​src/​provider/​claude_desktop/​mod.rs Exposes provider identifier.
crates/​agent/​src/​provider/​claude_code/​mod.rs Adapts provider interface.
crates/​agent/​src/​oidc.rs Extends sign-in flow for GitHub.
crates/​agent/​src/​lib.rs Registers new modules.
crates/​agent/​src/​identity.rs Adds device-ID-only lookup.
crates/​agent/​src/​github_oauth.rs Implements GitHub device authorization.
crates/​agent/​src/​github_oauth.html Adds GitHub authorization page.
crates/​agent/​src/​gateway_oidc.rs Chains gateway and GitHub sign-in.
crates/​agent/​src/​api.rs Shares gateway credential acquisition.
crates/​agent/​Cargo.toml Adds implementation and test dependencies.
Cargo.toml Defines shared dependency updates.
Cargo.lock Locks dependency changes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

path = %path.display(),
"programs.vscode uses copilotChat: githubModels but the local LLM proxy is not available; whenProxyUnavailable: failClosed, so the settings are left as they are"
);
return Ok(());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is documented rather than changed: the README's "Without the proxy" paragraph says a process holding the port receives VS Code's Copilot tokens and the pairing value, and recommends the default on single-user devices, as for the proxy in general. The same holds while the daemon is stopped.

path = %path.display(),
"programs.vscode uses copilotChat: githubModels but the local LLM proxy is not available; whenProxyUnavailable: failClosed, so the settings are left as they are"
);
return Ok(());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 52d26c1 by reporting it as it is: with nothing of the daemon's in the file (first apply without the proxy, or the override removed by hand), failClosed reports "no managed entries are in place, so the tool keeps its own settings", and the README says VS Code on GitHub's models then talks to GitHub directly. Whether developers can reach GitHub without the gateway stays with the org's Copilot policy and egress rules, as the README states; the daemon does not point VS Code at a port nothing listens on. Tests: fail_closed_first_apply_reports_not_in_place (three planners), fail_closed_after_the_user_removed_the_override_reports_not_in_place.

path = %path.display(),
"programs.copilot is configured but the local LLM proxy is not available, so the Copilot CLI is not pointed at the gateway; the reason is llmProxy.error in daemon-info (or daemon.llmProxy.listen is unset); whenProxyUnavailable: failOpen, removing the managed Copilot CLI providers"
);
plan.inactive(crate::reconcile::PROXY_ABSENT_REASON);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 52d26c1: all three failOpen paths report "the managed entries are removed until the proxy is back (whenProxyUnavailable: failOpen)".

"programs.copilot is configured but the local LLM proxy is not available, so the Copilot CLI is not pointed at the gateway; the reason is llmProxy.error in daemon-info (or daemon.llmProxy.listen is unset); whenProxyUnavailable: failOpen, removing the managed Copilot CLI providers"
);
plan.inactive(crate::reconcile::PROXY_ABSENT_REASON);
remove(path, &state_path, None, plan)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 52d26c1: while the daemon runs without the proxy it has no pairing of its own, so the sidecar-less cleanup removes managed-name entries carrying any pairing header (only an agentdesktop daemon writes it); user entries stay, and with a known pairing only that value counts, as before. README updated. Tests: fail_open_without_sidecar_removes_paired_entries_and_keeps_user_entries, removal_without_proxy_and_sidecar_removes_paired_entries.

Comment on lines +563 to +565
if sidecar.as_deref() != Some(state_bytes.as_slice()) {
plan.write_file(&state_path, &state_bytes, FILE_MODE)?;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #135 (6bcbcb6): an identical sidecar looser than 0600 is written again at 0600.

Comment thread crates/agent/src/tick.rs Outdated
let (report, result) = apply(&snapshot.config);
if let Some(statuses) = &statuses {
statuses.send_replace(Some(TickStatus {
revision: snapshot.revision,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #137 (5c3e9b1), same as the tick thread there.

LutzLange and others added 2 commits October 7, 2026 01:12
Part of agentdesktop-dev#86. Two programs that point Copilot at the local LLM proxy, user
mode only:

- `programs.copilot` writes the Copilot CLI's BYOK provider registry
  (`~/.copilot/providers.json`, or `$COPILOT_HOME` / `COPILOT_PROVIDERS_CONFIG`):
  providers `agentdesktop` (OpenAI-shaped) and `agentdesktop-anthropic` on the
  proxy's `/copilot-cli` route, with the pairing header; the user's own
  providers are kept. Discovery reports the CLI and its version from the npm
  manifest.
- `programs.vscode` with `copilotChat: ownModels` (default) writes
  `chatLanguageModels.json` for Copilot Chat's built-in Custom Endpoint
  provider on `/vscode-copilot`; the user's entries are kept.
- `copilotChat: githubModels` keeps GitHub's own models and the user's
  Copilot seat: the daemon sets `github.copilot.advanced.debug.overrideCapiUrl`
  in the user `settings.json` to the proxy's `/vscode-copilot-capi/<pairing>`
  route (pairing in the path, since VS Code cannot add a header), forwards to
  `llmGateway.proxyUrl` with the client's Copilot token moved aside, tunnels
  the Responses WebSocket for agent mode, forwards `_ping` without a
  credential, and keeps the override out of Settings Sync. `settings.json` is
  edited in place (JSONC): only the managed keys change, comments, key order
  and formatting stay, and removal restores the file byte for byte.

Managed files are written 0600 and carry a sidecar that records what the
daemon added, so removal takes out exactly that. A file the daemon cannot
parse is a conflict and left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…recision

- Copilot CLI: removing our providers by their pairing header (no sidecar)
  no longer adds "models": null to a providers.json that has no models key.
- README: the CAPI WebSocket tunnel closes after 4 to 5 minutes with the
  controller's default gatewayJwt.lifetime, also mid-turn; VS Code's
  reconnect behaviour is not verified.
- README: the settings.json removal exceptions are listed in full (multi-line
  empty object, comment-only file).
- README: the providers.json sidecar holds the user's own apiKey values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LutzLange
LutzLange force-pushed the upstream/s6-guide-and-policy branch from 8dadbd9 to 7cd70c9 Compare October 6, 2026 23:19
LutzLange and others added 6 commits October 7, 2026 02:34
…idecar mode repaired

Review on agentdesktop-dev#135.

- `.settings.json.agentdesktop` that is not JSON or not a known settings
  state now fails the apply, as the providers.json and chatLanguageModels.json
  sidecars already do: like a conflict, every managed file on the device
  stays untouched, also while `programs.vscode` is not configured (removal
  reads the sidecar too), and the daemon does not start. Before, the sidecar
  was ignored and rebuilt from the sidecar-less rules, so a user's own
  `overrideCapiUrl` was lost on removal. The error names the sidecar path and,
  for a syntax error, the line and column only, never a value. A sidecar
  without `version` (the earlier whole-document form) is still upgraded; a
  leading byte-order mark is accepted.
- An identical sidecar whose mode is looser than 0600 is written again at
  0600, as settings.json is; settings.json and its sidecar use the same
  check (a 0700 settings.json is now also tightened). The rewrite of the
  sidecar alone is not shown in the plan summary, since settings.json is
  unchanged.
- README: the unreadable-sidecar behaviour and the sidecar's mode.

Behaviour change: the test `an_unreadable_sidecar_warns_and_apply_proceeds` is
replaced. Tests: 9 new, 2 of them unix-only. The file-mode tests in this
module (old and new) assume umask 022.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
@LutzLange

Copy link
Copy Markdown
Contributor Author

Combined with the rest of the series into #146, one PR on main (rebased after #135 merged). The review threads here are addressed there in the same commits.

@LutzLange LutzLange closed this Oct 7, 2026
peterj pushed a commit that referenced this pull request Oct 7, 2026
…Unavailable (#146)

* feat: report per-program configuration status to the controller

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>

* feat(agent): opt-in reconcile tick; skip identical writes

`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>

* fix(agent): tick reports the revision it applied; loosened modes repaired for every managed file

Review on #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>

* feat: llmGateway.whenProxyUnavailable and a GitHub Copilot example

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>

* fix(agent): failClosed says whether anything is in place; failOpen names its policy and cleans up without a sidecar

Review on #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>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants