Repository navigation
Conversation
eba5556 to
8dadbd9
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved gateway-bypass and credential-exposure risks remain in the fail-closed path.
Review effort: Balanced
Findings: 2
Open (6)
Handle port collisions without exposing tokens or enabling fallback · New Prevent unenforced direct fallback when the GitHub override is missing · New Report failOpen policy and cleanup behavior in proxy-unavailable reasons · New Preserve pairing for orphan cleanup when the proxy is unavailable · New Repair sidecar permissions even when its contents are unchanged · New Capture revision and configuration atomically under the lock · New
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
failClosedpolicy and controller controls forfailOpen. - 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(()); |
There was a problem hiding this comment.
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(()); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)?; |
There was a problem hiding this comment.
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.
| if sidecar.as_deref() != Some(state_bytes.as_slice()) { | ||
| plan.write_file(&state_path, &state_bytes, FILE_MODE)?; | ||
| } |
| let (report, result) = apply(&snapshot.config); | ||
| if let Some(statuses) = &statuses { | ||
| statuses.send_replace(Some(TickStatus { | ||
| revision: snapshot.revision, |
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>
8dadbd9 to
7cd70c9
Compare
…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>
7cd70c9 to
52d26c1
Compare
…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>


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.whenProxyUnavailableapplies toprograms.copilotandprograms.vscode:failClosed(default) leaves the managed files as they are, so the tools fail instead of bypassing the gateway.failOpenremoves the entries until the proxy is back.inactivewith a reason that names the policy. UnderfailClosedthe 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.examples/copilot/adds Dex, agentgateway, a daemon config and a step-by-step guide with a check per step, plusgithub-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.rshasfail_closed_without_proxy, and the three planners call it.ConfigurationView.tsxhas the toggle;styles.cssstops toggle checkboxes from shrinking next to long help text.Review fixes
The last commit (52d26c1) addresses the Copilot review:
failClosedwith 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.failOpenreports "the managed entries are removed until the proxy is back (whenProxyUnavailable: failOpen)" on all three paths.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.
🤖 Generated with Claude Code