Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .eslint-plugin-local/code-no-telemetry-common-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const commonTelemetryProperties = new Set([
'common.copilottrackingid',
'common.copilotsdkversion',
'common.copilotruntimeversion',
'copilotsku',
'common.isagentswindow',
]);

Expand Down
29 changes: 17 additions & 12 deletions .github/skills/policy-and-managed-settings/local-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,29 @@ Only managed settings is mocked by default. Use the switch beside each endpoint
tab to choose mock or passthrough. Presets apply immediately; response edits
auto-save.

Valid endpoint state persists atomically in
`~/.mock-policy-server/state.json` and response-body drafts are also retained in
browser storage. On the first restart after upgrading from an in-memory-only
server, the GUI restores valid browser drafts into the server-side state file.
Use `--state-file` or `MOCK_POLICY_STATE_FILE` for isolated test instances.

Agents should use the JSON control API: start with `GET /api` for discovery and
`GET /api/state` for endpoint IDs and presets, then use `POST /api/state` for a
single update or an atomic endpoint array. Prefer known preset IDs over copying
preset bodies.
preset bodies. Use `GET /api/file-deployment` to generate platform-specific
install and removal commands from the current Managed Settings response.

Choose the client setup in the GUI:

- **Code OSS from sources:** apply `product.overrides.json`, reload, sign in, and
run **Developer: Sync Account Policy**.
- **Stable, Insiders, CLI, or other clients:** configure the displayed system
proxy mapping and enable Proxyman's platform proxy toggle (**Tools > macOS
Proxy** or **Tools > Override Windows Proxy**). VS Code clients must also add
the displayed `http.proxy` property to `settings.json`.
- **File-based settings (no proxy):** expand **Deploy as a file** under the
Managed Settings response body and run the copied per-platform command to write
the current body to `managed-settings.json` on the device. Restart the client to
load it. Use it to skip proxying or to test precedence against a server-managed
response. See [Deploying file-based settings](https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-for-enterprise/manage-agents/configure-enterprise-managed-settings#deploying-file-based-settings).
- **Code OSS, Stable, Insiders, CLI, or other clients:** configure the displayed
system proxy mapping and enable Proxyman's platform proxy toggle (**Tools >
macOS Proxy** or **Tools > Override Windows Proxy**). VS Code clients must
also add the displayed `http.proxy` property to `settings.json`.
- **File-based settings (no proxy):** use **File Deployment** in the right
sidebar and run the copied per-platform command to write the current body to
`managed-settings.json` on the device. Restart the client to load it. Use it
to skip proxying or to test precedence against a server-managed response. See
[Deploying file-based settings](https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-for-enterprise/manage-agents/configure-enterprise-managed-settings#deploying-file-based-settings).

Use **Clear SDK Policy Cache**, expand the macOS or Windows section, and run the
copied command when the runtime's fresh managed-settings cache prevents a network
Expand Down
2 changes: 1 addition & 1 deletion build/azure-pipelines/product-build-ado-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ extends:

if [ "${#trailer_values[@]}" = "0" ]; then
echo "##vso[task.logissue type=error]Commit $head_sha is missing the required 'Msrc-Case-Id' trailer."
printf "Add a trailer to the commit message, for example:\n\n Msrc-Case-Id: 12345\n\n"
printf "Add a trailer to the commit message, for example:\n\n Msrc-Case-Id: 12345\n\nIf there is no associated case ID, use N/A:\n\n Msrc-Case-Id: N/A\n\n"
exit 1
fi

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface DiagnosticNotificationParams {
message: string;
severity: string;
source?: string;
code?: string | number;
code?: string | number | null;
range: {
start: { line: number; character: number };
end: { line: number; character: number };
Expand Down Expand Up @@ -197,6 +197,26 @@ describe('diagnosticsChanged push notification', () => {
expect(notifiedDiag.range.end.character).toBe(20);
});

it('should preserve a null diagnostic code without throwing', async () => {
registerDiagnosticsChangedNotification(logger, httpServer as unknown as InProcHttpServer);

const uri = createMockUri('/test/file.ts');
// A diagnostic with no structured code surfaces as `null` at runtime (out of contract
// with the `string | number | { value; target }` type). `typeof null === 'object'` used
// to take the object branch and dereference `null.value`, throwing inside the delayed task.
const diag = createMockDiagnostic('No code', 1, 0, 0, 0, 5, 'test-source', null as unknown as undefined);

mockGetDiagnostics.mockReturnValue([diag]);

registeredCallback!({ uris: [uri] });
await vi.advanceTimersByTimeAsync(250);

const params = httpServer.broadcastNotification.mock.calls[0][1] as unknown as DiagnosticNotificationParams;
const notifiedDiag = params.uris[0].diagnostics[0];

expect(notifiedDiag.code).toBe(null);
});

it('should handle multiple URIs in a single change event', async () => {
registerDiagnosticsChangedNotification(logger, httpServer as unknown as InProcHttpServer);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function getDiagnosticsForUri(uri: vscode.Uri): DiagnosticInfo {
message: d.message,
severity: severityToString(d.severity),
source: d.source,
code: typeof d.code === 'object' ? d.code.value : d.code,
code: typeof d.code === 'object' && d.code !== null ? d.code.value : d.code,
})),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class ExecutionSubagentPrompt extends PromptElement<ExecutionSubagentProm
// Check if we're at the last turn (to align with training where we coax final answer)
const currentTurn = toolCallRounds?.length ?? 0;
const isLastTurn = currentTurn >= this.props.maxExecutionTurns - 1;
const remainingTurns = Math.max(this.props.maxExecutionTurns - currentTurn, 1);

return (
<>
Expand Down Expand Up @@ -82,11 +83,9 @@ export class ExecutionSubagentPrompt extends PromptElement<ExecutionSubagentProm
toolCallResults={toolCallResults}
toolCallMode={CopilotToolMode.FullContext}
/>
{isLastTurn && (
<UserMessage priority={900}>
OK, your allotted iterations are finished. Show the &lt;final_answer&gt;.
</UserMessage>
)}
<UserMessage priority={900}>
You have {remainingTurns} of {this.props.maxExecutionTurns} allotted iterations remaining. When one iteration remains, do not call tools; return only the &lt;final_answer&gt;.
</UserMessage>
{!isLastTurn && this.props.hasBackgroundCommand && (
<UserMessage priority={900}>
One or more commands are running in the background. You do not have the ability to monitor them. Show the &lt;final_answer&gt;.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ const retiredAutoModeTiers: Readonly<Record<string, AutoModeTier>> = {

/**
* Maps a retired tier name to its current one, leaving anything else untouched.
* Only raw inputs that bypass the picker schema need this, notably the override setting.
* Needed for raw inputs that predate the rename, such as the override setting or a persisted picker
* value restored before its model's schema has loaded.
*/
export function normalizeAutoModeTier(value: string): string {
return retiredAutoModeTiers[value] ?? value;
export function normalizeAutoModeTier(value: unknown): unknown {
return typeof value === 'string' ? retiredAutoModeTiers[value] ?? value : value;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,19 +404,17 @@ export class AutomodeService extends Disposable implements IAutomodeService {
private _resolveTier(chatRequest: IAutoModeRoutingRequest | undefined): AutoModeTier | undefined {
const override = this._configurationService.getConfig(ConfigKey.Advanced.AutoModeTierOverride);
if (override) {
// Normalized because the override is a raw string setting, so unlike a picker value it never
// passes through the schema filter that drops retired names.
const normalized = normalizeAutoModeTier(override);
// The override is internal, so unlike the picker it may select `fast`.
if ((autoModeTiers as readonly string[]).includes(normalized)) {
if (autoModeTiers.some(tier => tier === normalized)) {
return normalized as AutoModeTier;
}
this._logService.warn(`[AutomodeService] Ignoring auto tier override '${override}' — not one of [${autoModeTiers.join(', ')}].`);
}
if (!this.areAutoModeTiersSupported()) {
return undefined;
}
const configured = chatRequest?.modelConfiguration?.[AUTO_MODE_TIER_PROPERTY];
const configured = normalizeAutoModeTier(chatRequest?.modelConfiguration?.[AUTO_MODE_TIER_PROPERTY]);
if (isSelectableAutoModeTier(configured) && configured !== defaultAutoModeTier) {
return configured;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,32 @@ describe('AutomodeService', () => {

expect(autoRequestBodies()).toEqual([{ prompt: 'panel turn', tier: 'efficiency' }]);
});

// A picker value stored before the rename can be restored unfiltered while its model's
// schema is still loading, so it must upgrade rather than silently fall back.
it('maps a retired tier name in a persisted picker value to its current one', async () => {
enableTiers();
const gpt4oEndpoint = createEndpoint('gpt-4o', 'OpenAI');
mockAuto(autoResponse('gpt-4o'));

automodeService = createService();
await automodeService.resolveAutoModeEndpoint({
location: ChatLocation.Panel,
prompt: 'panel turn',
sessionId: 'session-persisted-retired',
modelConfiguration: { tier: 'max' },
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
// `balanced` upgrades to the current default, which reads as "never picked", so the inline
// pin applies instead of being treated as an explicit selection.
await automodeService.resolveAutoModeEndpoint({
location: ChatLocation.Editor,
prompt: 'inline turn',
sessionId: 'session-persisted-retired-default',
modelConfiguration: { tier: 'balanced' },
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);

expect(autoRequestBodies().map(b => b.tier)).toEqual(['intelligence', 'fast']);
});
});

describe('session cache', () => {
Expand Down
106 changes: 79 additions & 27 deletions scripts/mock-policy-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ Open `http://127.0.0.1:3000`. Managed settings is mocked by default. Use the
switch beside each endpoint tab to choose mock or passthrough. Presets apply
immediately; response behavior, status, and JSON edits auto-save.

Valid endpoint bodies and response configuration are stored redundantly: the
browser keeps each response-body draft in `localStorage`, and the server
atomically writes its complete state to `~/.mock-policy-server/state.json`.
Both copies survive server restarts. Invalid JSON remains in browser storage
until corrected because the server cannot serve it. Use `--state-file` or
`MOCK_POLICY_STATE_FILE` to select a different server-side state file.

The GUI opens on the **Policies** workspace. Select **Setup** in the header to
open a modal that guides you through any of these connection methods:

- **System proxy (recommended):** works with Code OSS, Stable, Insiders, Copilot
CLI, and SDK/runtime clients. The page recommends Proxyman on macOS and Windows
and provides a **Map Remote** rule, along with the per-platform toggle that
routes system traffic through Proxyman (**Tools > macOS Proxy** on macOS,
CLI, and SDK/runtime clients. Any HTTP debugging proxy that can rewrite HTTPS
requests works; [Proxyman](https://proxyman.com/) is the suggested option on
macOS and Windows. The page provides a **Map Remote** rule, along with the
per-platform toggle that routes system traffic through Proxyman (**Tools > macOS Proxy** on macOS,
**Tools > Override Windows Proxy** on Windows). VS Code clients must also add
the displayed `http.proxy` property to `settings.json`; the copy action copies
only the property, without surrounding object braces.
- **Code OSS overrides:** the quicker option for Code OSS from this checkout.
Select **Apply Overrides**, reload, and sign in. This option does not redirect
SDK/runtime requests.
- **File-based settings:** skip proxying altogether by writing the enterprise
`managed-settings.json` to the client device. The client reads it from disk at
startup — before sign-in and with no server round trip — so these requests never
Expand All @@ -38,30 +43,44 @@ After connecting, open the VS Code Command Palette and run **> Developer: Sync
Account Policy**. To refresh the policy used by Local Agent Host, also run
**> Developer: Restart Local Agent Host**.

For file-based settings there is nothing to connect: expand **Deploy as a file**
under the Managed Settings response body on the Policies page. It builds a
per-platform one-liner (macOS, Windows PowerShell, Linux) that writes the current
response body to `managed-settings.json` at its documented location. Copy it, run
it on the client device, and restart the client. On macOS and Linux the command
uses `sudo` so the file is the root-owned, non-writable regular file Copilot CLI
requires.
For file-based settings there is nothing to connect: use **File Deployment** in
the right sidebar on the Policies page. It provides per-platform commands
(macOS, Windows PowerShell, Linux) that write the current response body to
`managed-settings.json` at its documented location. Copy one, run it on the
client device, and restart the client. On macOS and Linux the command uses
`sudo` so the file is the root-owned, non-writable regular file Copilot CLI
requires. Each platform section also provides a removal command to unset the
file-based policy.

| Operating system | `managed-settings.json` location |
| --- | --- |
| macOS | `/Library/Application Support/GitHubCopilot/managed-settings.json` |
| Windows | `%ProgramFiles%\GitHubCopilot\managed-settings.json` |
| Linux | `/etc/github-copilot/managed-settings.json` |

The Setup dialog checks Code OSS overrides directly. It tests the system proxy by
sending a request without credentials to the managed settings URL and confirming
that the response came from this local server. It does not inspect Proxyman or
the operating system's proxy configuration. The test runs automatically, and the
global header always shows a green or red connection indicator.

If no real request appears in **Live Requests**, open **Clear SDK Policy Cache**,
expand the section for the client platform, and run the copied command in a
terminal. A fresh managed-settings cache entry can prevent the client from making
a request for up to one hour. Then run the commands above again.
The Setup dialog tests the system proxy every five seconds by sending an
unauthenticated request shaped like
`GET <configured upstream>/copilot_internal/managed_settings?mockPolicySetupProbe=<random UUID>`
(`https://api.github.com` is the default upstream).
The probe must traverse the same mapping as real policy traffic: the page only
reports a successful connection when the mapped response carries this mock
server's identifying header. It does not inspect Proxyman or the operating
system's proxy configuration. The global header always shows a green or red
connection indicator.

To keep those automatic probes out of Proxyman's traffic list, use the display
filter regex `^(?!.*mockPolicySetupProbe).*managed_settings.*`. This should only
filter the displayed traffic; keep the probe included in the Map Remote rule so
the connection check can reach this server. The mock server recognizes the probe
query parameter and excludes those requests from **Live Requests**.

If no real request appears in **Live Requests**, open **Troubleshooting** in the
right sidebar, expand the client platform under **Clear SDK policy cache**, and run
the copied command in a terminal. The Copilot SDK maintains this cache outside of
VS Code; these commands delete the SDK cache file for the selected platform so a
fresh managed-settings request can be made. Without clearing it, a fresh cache
entry can prevent the client from making a request for up to one hour. Then run
the commands above again.

macOS:
```sh
Expand Down Expand Up @@ -98,6 +117,11 @@ curl "$BASE/api/state"
`GET /api/state` returns endpoint IDs, presets, current bodies, statuses, and
mock/passthrough state.

`GET /api` is intended for agents as well as humans. It documents update field
types, valid response modes, atomic update semantics, route side effects, and
the common JSON error shape. Unknown routes point back to this discovery
document, and unsupported methods return `405` with an `Allow` header.

Apply a known preset:

```sh
Expand Down Expand Up @@ -133,6 +157,31 @@ in the same update. Explicit `status`, `body`, or `active` values override the
preset. Invalid requests are rejected before any endpoint changes. Supported
response modes are `json`, `malformed-json`, `disconnect`, and `timeout`.

Generate file-based Managed Settings commands from the current
`managedSettings` response:

```sh
curl "$BASE/api/file-deployment"
```

The response includes the destination path plus install and removal commands
for macOS, Windows, and Linux. The server does not run these commands; run one
on the client machine and restart the client. Run the corresponding removal
command, or delete `managed-settings.json`, to unset file-based Managed Settings.

Other control operations:

```sh
curl "$BASE/api/schema"
curl "$BASE/api/log"
curl -X DELETE "$BASE/api/log"
curl -X DELETE "$BASE/api/cache"
curl -X POST "$BASE/api/reset"
```

`DELETE /api/cache` modifies files on the machine running the server. Inspect
each route's `sideEffects` value in `GET /api` before invoking it.

### Test fail-closed managed-settings refresh

First serve a successful policy that enables the forced-refresh requirement and
Expand Down Expand Up @@ -164,13 +213,12 @@ request does not appear in **Live Requests**.
| --- | --- | --- |
| `GET` | `/api` | Discover request shapes and routes |
| `GET` | `/api/state` | Read definitions, presets, and current state |
| `POST` | `/api/state` | Apply one update or an atomic endpoint array |
| `POST` | `/api/reset` | Restore startup endpoint state |
| `POST` | `/api/state` | Apply and persist one update or an atomic endpoint array |
| `POST` | `/api/reset` | Restore and persist default endpoint state |
| `GET` | `/api/schema` | Read the managed-settings schema |
| `GET` | `/api/file-deployment` | Generate file install and removal commands |
| `GET`, `DELETE` | `/api/log` | Read or clear the request log |
| `DELETE` | `/api/cache` | Clear the managed-settings disk cache |
| `POST` | `/api/wire` | Apply `product.overrides.json` |
| `POST` | `/api/unwire` | Restore `product.overrides.json` |

## Schema and options

Expand All @@ -182,11 +230,15 @@ VS Code checkout, including from a Git worktree. Override it at startup with
```sh
npm run mock-policy-server -- --upstream https://api.ghe.example.com
npm run mock-policy-server -- --schema /path/to/managed-settings-schema.json
npm run mock-policy-server -- --port 3001
npm run mock-policy-server -- --state-file /path/to/mock-policy-state.json
npm run mock-policy-server -- --help
```

| Flag | Environment variable | Default |
| --- | --- | --- |
| `--host` | — | `127.0.0.1` |
| `--port` | — | `3000` |
| `--upstream` | `MOCK_POLICY_UPSTREAM` | `https://api.github.com` |
| `--schema` | `MANAGED_SETTINGS_SCHEMA` | Auto-detected sibling checkout |
| `--state-file` | `MOCK_POLICY_STATE_FILE` | `~/.mock-policy-server/state.json` |
Loading
Loading