Skip to content

Commit fd100bb

Browse files
carloriveraCopilot
andcommitted
feat: expose Policy Helper failures in Node and Rust
Generate the structured failure and scoped recheck surface from runtime contract b7b80ebc085, with capability, compatibility, and RPC error preservation coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4bd5ad7 commit fd100bb

12 files changed

Lines changed: 753 additions & 23 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu
77

88
## [Unreleased]
99

10+
### Feature: structured Policy Helper failures and scoped recheck
11+
12+
Node and Rust managed-settings snapshots now expose bounded structured Policy Helper failures, including actionable messages, remediation, details, explicit help links, and secondary diagnostics. The same DTO is preserved in applicable `session.create`, `session.resume`, and policy-dependent activation RPC error data without replacing the operation's primary code or message.
13+
14+
Use `policyHelperCapabilities` to detect support. Older runtimes omit the capability object; the absence of `policyHelperFailure` alone does not prove recovery or support. When `recheck` is true, the new session-scoped RPC performs a fresh helper evaluation for the current policy context.
15+
16+
```ts
17+
const snapshot = await session.rpc.managedSettings.get();
18+
if (snapshot.policyHelperCapabilities?.recheck) {
19+
await session.rpc.managedSettings.recheckPolicyHelper();
20+
}
21+
```
22+
23+
```rust
24+
let snapshot = session.rpc().managed_settings().get().await?;
25+
if snapshot
26+
.policy_helper_capabilities
27+
.as_ref()
28+
.is_some_and(|capabilities| capabilities.recheck)
29+
{
30+
session
31+
.rpc()
32+
.managed_settings()
33+
.recheck_policy_helper()
34+
.await?;
35+
}
36+
```
37+
1038
## [v1.0.14](https://github.com/github/copilot-sdk/releases/tag/v1.0.14) (2026-09-16)
1139

1240
### Feature: typed message provenance for user, system, and agent sources

‎nodejs/README.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,33 @@ security checks, user permissions, or sandbox policy. `permissions.limitTo` neit
14381438
enables the sandbox nor changes `sandbox.userPolicy.network`; configure sandbox
14391439
network restrictions explicitly.
14401440

1441+
### Policy Helper failures and scoped recheck
1442+
1443+
Managed-settings snapshots may include a structured `policyHelperFailure` and
1444+
the explicit `policyHelperCapabilities` supported by the runtime. Treat helper
1445+
text as untrusted plain text, keep `diagnostic` secondary, and sanitize an
1446+
explicit `helpLink` again before navigation.
1447+
1448+
```typescript
1449+
const snapshot = await session.rpc.managedSettings.get();
1450+
1451+
if (snapshot.policyHelperFailure) {
1452+
console.warn(snapshot.policyHelperFailure.message);
1453+
}
1454+
1455+
if (snapshot.policyHelperCapabilities?.recheck === true) {
1456+
const refreshed = await session.rpc.managedSettings.recheckPolicyHelper();
1457+
// Only a current capable snapshot without policyHelperFailure establishes recovery.
1458+
console.log(refreshed.policyHelperFailure ?? "Policy Helper recovered");
1459+
}
1460+
```
1461+
1462+
Older runtimes omit `policyHelperCapabilities`. Do not infer support or recovery
1463+
only because `policyHelperFailure` is absent. Applicable `session.create`,
1464+
`session.resume`, and policy-dependent activation failures preserve the same DTO
1465+
under JSON-RPC `error.data.policyHelperFailure` while retaining the primary
1466+
operation error code and message.
1467+
14411468
### Permission Result Kinds
14421469

14431470
The handler must return one of the `PermissionDecision` shapes (or `{ kind: "no-result" }`). Approval scopes are present-tense — they describe the decision to apply, not the outcome reported back on session events:

‎nodejs/src/generated/rpc.ts‎

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎nodejs/src/generated/session-events.ts‎

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎nodejs/test/client.test.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
type GitHubTelemetryNotification,
2727
type ManagedSettings,
2828
type ModelInfo,
29+
type PolicyHelperFailure,
2930
type SubagentStartHookOutput,
3031
type SubagentStopHookOutput,
3132
type SessionFsProvider,
@@ -5483,12 +5484,17 @@ describe("RPC error data preservation", () => {
54835484
it.each(["session.create", "session.resume"] as const)(
54845485
"preserves structured data from %s",
54855486
async (method) => {
5486-
const data = {
5487-
context: {
5488-
category: "structured-runtime-failure",
5489-
message: "The runtime preserved structured operation context.",
5490-
},
5491-
};
5487+
const policyHelperFailure = {
5488+
authority: "future-authority",
5489+
category: "future-category",
5490+
code: "helper.future-code",
5491+
failureMode: "future-mode",
5492+
message: "The Policy Helper rejected the current configuration.",
5493+
previousPolicyRetained: true,
5494+
reason: "future-reason",
5495+
severity: "future-severity",
5496+
} satisfies PolicyHelperFailure;
5497+
const data = { policyHelperFailure };
54925498
const responseError = new ResponseError(-32001, "session operation failed", data);
54935499
const client = new CopilotClient({ autoStart: false });
54945500
(client as any).connection = {
@@ -5515,6 +5521,7 @@ describe("RPC error data preservation", () => {
55155521
expect(responseError.code).toBe(-32001);
55165522
expect(responseError.message).toBe("session operation failed");
55175523
expect(responseError.data).toBe(data);
5524+
expect(responseError.data.policyHelperFailure).toBe(policyHelperFailure);
55185525
}
55195526
);
55205527
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { ResponseError, type MessageConnection } from "vscode-jsonrpc/node.js";
2+
import { describe, expect, it, vi } from "vitest";
3+
import { createServerRpc, createSessionRpc } from "../src/generated/rpc.js";
4+
import type { ManagedSettingsResolvedData, PolicyHelperFailure } from "../src/index.js";
5+
6+
const policyHelperFailure = {
7+
authority: "future-authority",
8+
category: "future-category",
9+
code: "helper.future-code",
10+
details: [
11+
{
12+
path: "allowedRepositories",
13+
message: "Expected an array.",
14+
},
15+
],
16+
diagnostic: "bounded diagnostic",
17+
failureMode: "future-mode",
18+
helpLink: {
19+
url: "https://docs.example.com/policy-helper",
20+
label: "Review configuration",
21+
},
22+
message: "The Policy Helper rejected the current configuration.",
23+
previousPolicyRetained: true,
24+
reason: "future-reason",
25+
remediation: "Update the configuration and recheck managed policy.",
26+
severity: "future-severity",
27+
} satisfies PolicyHelperFailure;
28+
29+
const managedSettingsSnapshot = {
30+
bypassPermissionsDisabled: true,
31+
deviceManaged: false,
32+
failClosed: true,
33+
managedKeys: ["permissions"],
34+
policyHelperCapabilities: {
35+
recheck: true,
36+
structuredFailure: true,
37+
},
38+
policyHelperFailure,
39+
policyHelperManaged: true,
40+
serverManaged: true,
41+
source: "policyHelper",
42+
} satisfies ManagedSettingsResolvedData;
43+
44+
describe("Policy Helper failure contracts", () => {
45+
it("round trips the complete DTO and preserves future string values", () => {
46+
const roundTripped = JSON.parse(
47+
JSON.stringify(managedSettingsSnapshot)
48+
) as ManagedSettingsResolvedData;
49+
50+
expect(roundTripped.policyHelperCapabilities).toEqual({
51+
recheck: true,
52+
structuredFailure: true,
53+
});
54+
expect(roundTripped.policyHelperFailure).toEqual(policyHelperFailure);
55+
expect(roundTripped.policyHelperFailure?.category).toBe("future-category");
56+
expect(roundTripped.policyHelperFailure?.failureMode).toBe("future-mode");
57+
expect(roundTripped.policyHelperFailure?.severity).toBe("future-severity");
58+
expect(roundTripped.policyHelperFailure?.reason).toBe("future-reason");
59+
expect(roundTripped.policyHelperFailure?.authority).toBe("future-authority");
60+
expect(roundTripped.policyHelperFailure?.code).toBe("helper.future-code");
61+
});
62+
63+
it("keeps older runtime snapshots compatible", () => {
64+
const legacySnapshot: ManagedSettingsResolvedData = {
65+
bypassPermissionsDisabled: false,
66+
deviceManaged: false,
67+
failClosed: false,
68+
managedKeys: [],
69+
serverManaged: false,
70+
source: "none",
71+
};
72+
73+
expect(legacySnapshot.policyHelperCapabilities).toBeUndefined();
74+
expect(legacySnapshot.policyHelperFailure).toBeUndefined();
75+
});
76+
77+
it("binds the session-scoped recheck RPC and returns its authoritative snapshot", async () => {
78+
const sendRequest = vi.fn().mockResolvedValue(managedSettingsSnapshot);
79+
const rpc = createSessionRpc(
80+
{ sendRequest } as unknown as MessageConnection,
81+
"policy-session"
82+
);
83+
84+
const result = await rpc.managedSettings.recheckPolicyHelper();
85+
86+
expect(sendRequest).toHaveBeenCalledWith("session.managedSettings.recheckPolicyHelper", {
87+
sessionId: "policy-session",
88+
});
89+
expect(result.policyHelperCapabilities?.recheck).toBe(true);
90+
expect(result.policyHelperFailure).toEqual(policyHelperFailure);
91+
});
92+
93+
it("preserves Policy Helper context on policy-dependent activation errors", async () => {
94+
const errorData = {
95+
code: "plugin_activation_failed",
96+
policyHelperFailure,
97+
};
98+
const responseError = new ResponseError(-32001, "plugin activation failed", errorData);
99+
const sendRequest = vi.fn().mockRejectedValue(responseError);
100+
const rpc = createServerRpc({ sendRequest } as unknown as MessageConnection);
101+
102+
const error = await rpc.managedSettings
103+
.resolve({
104+
workingDirectory: "/workspace",
105+
policyHelperReason: "pluginActivation",
106+
})
107+
.then(
108+
() => undefined,
109+
(reason: unknown) => reason
110+
);
111+
112+
expect(sendRequest).toHaveBeenCalledWith("managedSettings.resolve", {
113+
workingDirectory: "/workspace",
114+
policyHelperReason: "pluginActivation",
115+
});
116+
expect(error).toBe(responseError);
117+
expect(responseError.code).toBe(-32001);
118+
expect(responseError.message).toBe("plugin activation failed");
119+
expect(responseError.data).toBe(errorData);
120+
expect(responseError.data.policyHelperFailure).toBe(policyHelperFailure);
121+
});
122+
});

0 commit comments

Comments
 (0)