+ * This value is never serialized inside the result JSON; the SDK forwards it as + * a sibling of {@code result} when responding to the runtime. + * + * @return the decision context, or {@code null} if none was attached + * @since 1.3.0 + */ + public PermissionDecisionContext getDecisionContext() { + return decisionContext; + } + + /** + * Sets provenance describing how and where this decision was made, so the + * runtime can attribute {@code auto_approval_decision} telemetry. + *
+ * Calling this method more than once replaces any previously set context. The
+ * context is never serialized inside the result; the SDK forwards it as a
+ * sibling of {@code result}.
+ *
+ * @param decisionContext
+ * the decision context, or {@code null} to attach none
+ * @return this result for method chaining
+ * @since 1.3.0
+ */
+ public PermissionRequestResult setDecisionContext(PermissionDecisionContext decisionContext) {
+ this.decisionContext = decisionContext;
+ return this;
+ }
}
diff --git a/java/src/test/java/com/github/copilot/rpc/PermissionRequestResultDecisionContextTest.java b/java/src/test/java/com/github/copilot/rpc/PermissionRequestResultDecisionContextTest.java
new file mode 100644
index 0000000000..395ad50ad6
--- /dev/null
+++ b/java/src/test/java/com/github/copilot/rpc/PermissionRequestResultDecisionContextTest.java
@@ -0,0 +1,97 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.copilot.generated.rpc.PermissionDecisionContext;
+import com.github.copilot.generated.rpc.PermissionDecisionOutcome;
+import com.github.copilot.generated.rpc.PermissionDecisionSource;
+import com.github.copilot.generated.rpc.PermissionDecisionSurface;
+import com.github.copilot.generated.rpc.SessionPermissionsHandlePendingPermissionRequestParams;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that {@link PermissionRequestResult} carries an optional
+ * {@link PermissionDecisionContext} as a sibling of {@code result} — never
+ * nested inside the serialized result — when the SDK forwards a permission
+ * response to the runtime.
+ */
+class PermissionRequestResultDecisionContextTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static PermissionDecisionContext sampleContext() {
+ return new PermissionDecisionContext(PermissionDecisionOutcome.AUTO_APPROVED,
+ PermissionDecisionSource.HOST_POLICY, PermissionDecisionSurface.SDK);
+ }
+
+ @Test
+ void setDecisionContextForwardsContextAsSiblingOfResult() throws Exception {
+ var result = PermissionRequestResult.approveOnce().setDecisionContext(sampleContext());
+ var params = new SessionPermissionsHandlePendingPermissionRequestParams("session-1", "req-1", result,
+ result.getDecisionContext());
+
+ JsonNode json = MAPPER.valueToTree(params);
+
+ assertTrue(json.has("decisionContext"), "decisionContext must be a top-level sibling of result");
+ assertEquals("host_policy", json.get("decisionContext").get("source").asText());
+ assertEquals("auto_approved", json.get("decisionContext").get("outcome").asText());
+ assertEquals("sdk", json.get("decisionContext").get("surface").asText());
+ assertFalse(json.get("result").has("decisionContext"), "decisionContext must NOT be nested inside result");
+ }
+
+ @Test
+ void withoutContextOmitsDecisionContextKey() throws Exception {
+ var result = PermissionRequestResult.approveOnce();
+ assertNull(result.getDecisionContext());
+
+ var params = new SessionPermissionsHandlePendingPermissionRequestParams("session-1", "req-1", result,
+ result.getDecisionContext());
+
+ JsonNode json = MAPPER.valueToTree(params);
+
+ // Generated params record is @JsonInclude(NON_NULL), so a null
+ // decisionContext is omitted entirely — byte-identical to legacy behavior.
+ assertFalse(json.has("decisionContext"), "decisionContext key must be absent when no context is supplied");
+ }
+
+ @Test
+ void setDecisionContextTwiceReplacesRatherThanNests() {
+ var first = sampleContext();
+ var second = new PermissionDecisionContext(PermissionDecisionOutcome.PROMPTED_USER,
+ PermissionDecisionSource.HUMAN_RESPONSE, PermissionDecisionSurface.TUI);
+
+ var result = PermissionRequestResult.approveOnce().setDecisionContext(first).setDecisionContext(second);
+
+ assertSame(second, result.getDecisionContext(), "second setDecisionContext must replace the first, not nest");
+ }
+
+ @Test
+ void serializingResultWithContextDoesNotEmitContextInsideResult() throws Exception {
+ var result = PermissionRequestResult.approveOnce().setDecisionContext(sampleContext());
+
+ JsonNode resultJson = MAPPER.valueToTree(result);
+
+ assertFalse(resultJson.has("decisionContext"),
+ "@JsonIgnore must keep decisionContext out of the serialized result");
+ assertEquals(PermissionRequestResultKind.APPROVED.getValue(), resultJson.get("kind").asText());
+ }
+
+ @Test
+ void setDecisionContextAcceptsNullAsNoContext() {
+ var result = PermissionRequestResult.approveOnce().setDecisionContext(sampleContext());
+
+ result.setDecisionContext(null);
+
+ assertNull(result.getDecisionContext(), "null must clear the context rather than throwing");
+ }
+}
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index 5ab53471a6..0dfbb5ff72 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -27,6 +27,7 @@ export {
export {
defineTool,
approveAll,
+ createAttributedPermissionResult,
convertMcpCallToolResult,
createSessionFsAdapter,
CopilotRequestHandler,
@@ -123,6 +124,11 @@ export type {
PermissionRequestedData,
PermissionRequestedEvent,
PermissionRequestResult,
+ AttributedPermissionResult,
+ PermissionDecisionContext,
+ PermissionDecisionOutcome,
+ PermissionDecisionSource,
+ PermissionDecisionSurface,
ProviderConfig,
ProviderModelConfig,
ProviderTokenArgs,
diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts
index ed575a5154..996189cf25 100644
--- a/nodejs/src/session.ts
+++ b/nodejs/src/session.ts
@@ -24,6 +24,7 @@ import type {
import { type Canvas, CanvasError } from "./canvas.js";
import type { OpenCanvasInstance } from "./generated/rpc.js";
import { getTraceContext } from "./telemetry.js";
+import { isAttributedPermissionResult } from "./types.js";
import type {
CommandHandler,
AutoModeSwitchHandler,
@@ -43,6 +44,7 @@ import type {
McpAuthRequest,
PermissionHandler,
PermissionRequest,
+ PermissionRequestResult,
ContextTier,
ReasoningEffort,
ReasoningSummary,
@@ -1124,17 +1126,26 @@ export class CopilotSession {
permissionRequest: PermissionRequest
): Promise