Skip to content

Add no-result permission handling for extensions#802

Merged
SteveSandersonMS merged 7 commits intomainfrom
stevesa/permission-no-result
Mar 12, 2026
Merged

Add no-result permission handling for extensions#802
SteveSandersonMS merged 7 commits intomainfrom
stevesa/permission-no-result

Conversation

@SteveSandersonMS
Copy link
Contributor

Summary

  • add a public no-result permission outcome across the TypeScript, Python, Go, and .NET SDKs
  • default TypeScript extension joinSession() to a noResult permission handler so extensions can attach without actively answering permission requests
  • make v2 permission adapters fail loudly if a handler returns no-result, and add tests/docs for the new behavior

Testing

  • cd go && go test .
  • cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter PermissionRequestResultKindTests
  • cd python && uv run pytest test_client.py
  • cd nodejs && npm run typecheck && npm test -- test/client.test.ts test/extension.test.ts

Add a public no-result permission outcome across the SDKs, default TypeScript extension joinSession() to it, and make v2 permission adapters fail loudly if asked to return no-result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner March 12, 2026 14:55
Copilot AI review requested due to automatic review settings March 12, 2026 14:55
@github-actions
Copy link
Contributor

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations (TypeScript, Python, Go, and .NET). The changes maintain excellent consistency!

Verified Consistency

Type definitions: All SDKs define "no-result" using appropriate language conventions

  • TypeScript: NoResultPermissionRequestResult interface
  • Python: Added to Literal type union
  • Go: PermissionRequestResultKindNoResult constant
  • .NET: PermissionRequestResultKind.NoResult property

Helper functions: All SDKs provide equivalent helpers following naming conventions

  • TypeScript: noResult (camelCase)
  • Python: PermissionHandler.no_result() (snake_case)
  • Go: PermissionHandler.NoResult (PascalCase)
  • .NET: PermissionHandler.NoResult (PascalCase)

v3 behavior: All SDKs skip responding when handler returns no-result

v2 error handling: All SDKs throw/return an error with the same message when no-result is used with protocol v2 servers

Tests: All SDKs include appropriate test coverage

Documentation: TypeScript docs updated to explain the new default behavior for joinSession()

Language-Specific Features

The TypeScript joinSession() function defaulting to noResult is appropriate and well-documented. This is specific to the Node.js extension pattern and doesn't require equivalents in other SDKs.

No consistency issues found. Great work maintaining API parity across all four implementations! 🎉

Generated by SDK Consistency Review Agent for issue #802 ·

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a cross-SDK “no-result” permission outcome so clients (notably TypeScript extensions via joinSession()) can attach to an existing session without actively answering permission requests, while ensuring protocol v2 adapters fail loudly if no-result is returned.

Changes:

  • Introduces a new no-result permission outcome + helper handlers across TypeScript, Python, Go, and .NET.
  • Updates v3 permission handling to “do nothing” when no-result is returned, and updates v2 adapters to throw/fail loudly instead of silently converting it.
  • Updates TypeScript extension joinSession() to default onPermissionRequest to noResult, and adds/updates tests + docs around the new behavior.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
python/test_client.py Adds Python unit tests for no-result helper + v2 adapter rejection.
python/copilot/types.py Adds "no-result" to permission result kinds and a PermissionHandler.no_result helper.
python/copilot/session.py Skips responding to v3 permission requests when handler returns no-result.
python/copilot/client.py Makes v2 permission adapter reject no-result with a specific error.
nodejs/test/extension.test.ts Adds tests validating joinSession() defaults onPermissionRequest to no-result.
nodejs/test/client.test.ts Adds tests for v3 “no response” behavior and v2 “fail loudly” behavior.
nodejs/src/types.ts Adds noResult helper + extends permission result union with { kind: "no-result" }.
nodejs/src/session.ts Implements v3 “no response” and v2 “throw” behaviors for no-result.
nodejs/src/index.ts Exposes noResult helper from the main package entrypoint.
nodejs/src/extension.ts Makes joinSession() accept optional permission handler and defaults it to noResult.
nodejs/src/client.ts Propagates v2 “no-result” errors instead of converting to deny.
nodejs/docs/extensions.md Updates extension docs to reflect the new default permission behavior.
nodejs/docs/agent-author.md Updates agent authoring docs to reflect the new default permission behavior.
go/types_test.go Extends Go tests for the new kind constant + adds PermissionHandler.NoResult test.
go/types.go Adds PermissionRequestResultKindNoResult.
go/session.go Skips responding to v3 permission requests when handler returns NoResult.
go/permissions.go Adds PermissionHandler.NoResult.
go/client.go Makes v2 permission adapter fail loudly on NoResult.
dotnet/test/PermissionRequestResultKindTests.cs Extends .NET tests for the new kind constant.
dotnet/src/Types.cs Adds PermissionRequestResultKind.NoResult and documents it in PermissionRequestResult.
dotnet/src/Session.cs Skips responding to v3 permission requests when handler returns NoResult.
dotnet/src/PermissionHandlers.cs Adds PermissionHandler.NoResult.
dotnet/src/Client.cs Makes v2 permission adapter throw when handler returns NoResult.

Comment on lines 70 to 73
// Optional — provide this if your extension should actively answer
// permission requests instead of leaving them pending for another client.
await joinSession({ onPermissionRequest: approveAll });
```
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This skeleton example calls joinSession() twice; the second call reads like an in-place override but actually attaches a new client again. It would be clearer to present the onPermissionRequest override as an alternative snippet or as part of the initial joinSession({ ... }) options.

Copilot uses AI. Check for mistakes.
"github.com/github/copilot-sdk/go/rpc"
)

const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server"
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The v2 error message here is inconsistent with the other SDKs in this PR (casing + missing trailing period). Aligning the exact string across languages makes cross-SDK docs and tests easier to keep consistent.

Suggested change
const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server"
const noResultPermissionV2Error = "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."

Copilot uses AI. Check for mistakes.
go/client.go Outdated
Comment on lines +1536 to +1538
if result.Kind == PermissionRequestResultKindNoResult {
return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error}
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

New behavior: v2 permission adapters now fail loudly when a handler returns no-result, but there is no Go test covering this path. Adding a unit test that exercises handlePermissionRequestV2 with a PermissionHandler.NoResult session would prevent regressions and confirm the JSON-RPC error message/code.

Copilot uses AI. Check for mistakes.
Comment on lines 1399 to 1404
var result = await session.HandlePermissionRequestAsync(permissionRequest);
if (result.Kind == PermissionRequestResultKind.NoResult)
{
throw new InvalidOperationException(NoResultPermissionV2ErrorMessage);
}
return new PermissionRequestResponseV2(result);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The v2 adapter now throws when a permission handler returns NoResult, but the .NET test suite here only validates the new kind constant. Adding a test that drives OnPermissionRequestV2 (or the underlying session handler) and asserts that NoResult causes an exception would lock in the intended “fail loudly” behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +56
});

// Optional: override the default "no result" behavior if this extension
// should actively answer permission requests itself.
await joinSession({ onPermissionRequest: approveAll });
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The example calls joinSession() a second time to override permission handling, which is misleading: it creates a second client/session attachment rather than changing the options of the already-joined session. Consider showing this as an alternative snippet (e.g., "// or") or include onPermissionRequest in the original joinSession({ ... }) call.

Suggested change
});
// Optional: override the default "no result" behavior if this extension
// should actively answer permission requests itself.
await joinSession({ onPermissionRequest: approveAll });
// Optional: override the default "no result" behavior if this extension
// should actively answer permission requests itself.
onPermissionRequest: approveAll,
});

Copilot uses AI. Check for mistakes.
SteveSandersonMS and others added 4 commits March 12, 2026 15:10
Avoid serializing absent optional session.send fields as JSON null so resumed Python-created sessions do not fail CLI validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove extension doc suggestions to pass onPermissionRequest: approveAll when using joinSession(), since extensions now default to no-result permission handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep no-result handling internal to joinSession and raw permission result values instead of exposing simplified PermissionHandler helper APIs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the remaining public named no-result PermissionRequestResultKind conveniences and use raw no-result values internally instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS self-assigned this Mar 12, 2026
@github-actions
Copy link
Contributor

Cross-SDK Consistency Review ✅

This PR adds the "no-result" permission outcome across all four SDKs (TypeScript, Python, Go, .NET) to allow permission handlers to leave requests unanswered. The implementation is largely consistent, with the following areas for improvement:

What's Consistent

  1. Type definitions: All SDKs add "no-result" as a valid permission result kind
  2. V3 behavior: All SDKs correctly skip responding when handlers return no-result
  3. V2 error handling: All SDKs throw/error when no-result is returned to v2 protocol
  4. Extension default: TypeScript's joinSession() correctly defaults to no-result handler
  5. Documentation: Updated docs show the new default behavior

⚠️ Consistency Gaps (non-blocking suggestions)

I've added inline comments on the following:

  1. Go SDK: Missing a named constant PermissionRequestResultKindNoResult (other SDKs have well-known constants)
  2. Test coverage:
    • Go: Missing behavioral tests for both v2 error and v3 no-response behavior
    • .NET: Missing behavioral tests for both v2 error and v3 no-response behavior
    • Python: Missing test for v3 no-response behavior (has v2 error test)
    • TypeScript: ✅ Has comprehensive tests for both behaviors

📊 Summary

The core functionality is implemented consistently across all languages. The gaps are primarily in test coverage and one missing constant in Go. These are good candidates for follow-up improvements but don't block this PR since the actual behavior is correct.

Recommendation: Approve with suggestions for follow-up test additions to match TypeScript's coverage level.

Generated by SDK Consistency Review Agent for issue #802 ·

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #802

// PermissionRequestResultKindDeniedInteractivelyByUser indicates the permission was denied interactively by the user.
PermissionRequestResultKindDeniedInteractivelyByUser PermissionRequestResultKind = "denied-interactively-by-user"

)
Copy link
Contributor

Choose a reason for hiding this comment

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

Cross-SDK Consistency: Go is missing a named constant for "no-result".

Other SDKs provide well-known constants/static properties for all permission result kinds:

  • .NET has PermissionRequestResultKind.Approved, .DeniedByRules, .DeniedInteractivelyByUser, etc.
  • Python has "no-result" in the PermissionRequestResultKind literal
  • TypeScript uses a union type that includes { kind: "no-result" }

Suggestion: Add a named constant for consistency:

// PermissionRequestResultKindNoResult indicates the permission handler chose not to respond
PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result"

This would align with Go's existing pattern and make the constant available for developers (e.g., in extensions that want to explicitly return this kind).

{"DeniedByRules", PermissionRequestResultKindDeniedByRules, "denied-by-rules"},
{"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser, "denied-no-approval-rule-and-could-not-request-from-user"},
{"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser, "denied-interactively-by-user"},
{"NoResult", PermissionRequestResultKind("no-result"), "no-result"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Cross-SDK Consistency: Go is missing behavioral tests for the no-result permission outcome.

TypeScript has comprehensive tests:

  • test/client.test.ts: "does not respond to v3 permission requests when handler returns no-result"
  • test/client.test.ts: "throws when a v2 permission handler returns no-result"

Python has:

  • test_client.py: "test_v2_permission_adapter_rejects_no_result"

Suggestion: Add tests in Go to verify:

  1. When a v3 permission handler returns "no-result", HandlePendingPermissionRequest is NOT called
  2. When a v2 permission adapter receives "no-result", it returns a JSON-RPC error with the message "permission handlers cannot return 'no-result' when connected to a protocol v2 server"

This ensures the behavior is tested and won't regress.

Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.Value);
Assert.Equal("denied-no-approval-rule-and-could-not-request-from-user", PermissionRequestResultKind.DeniedCouldNotRequestFromUser.Value);
Assert.Equal("denied-interactively-by-user", PermissionRequestResultKind.DeniedInteractivelyByUser.Value);
Assert.Equal("no-result", new PermissionRequestResultKind("no-result").Value);
Copy link
Contributor

Choose a reason for hiding this comment

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

Cross-SDK Consistency: .NET is missing behavioral tests for the no-result permission outcome.

TypeScript has comprehensive tests:

  • test/client.test.ts: "does not respond to v3 permission requests when handler returns no-result"
  • test/client.test.ts: "throws when a v2 permission handler returns no-result"

Python has:

  • test_client.py: "test_v2_permission_adapter_rejects_no_result"

Suggestion: Add tests to verify:

  1. When a v3 permission handler returns new PermissionRequestResultKind("no-result"), HandlePendingPermissionRequestAsync is NOT called
  2. When a v2 permission adapter receives no-result, it throws InvalidOperationException with message CopilotClient.NoResultPermissionV2ErrorMessage

Example test structure:

[Fact]
public async Task V3PermissionNoResult_DoesNotRespond()
{
    // Similar to nodejs test: verify HandlePendingPermissionRequestAsync is not called
}

[Fact]
public async Task V2PermissionAdapter_RejectsNoResult()
{
    // Verify OnPermissionRequestV2 throws InvalidOperationException when handler returns no-result
}

"permissionRequest": {"kind": "write"},
}
)
finally:
Copy link
Contributor

Choose a reason for hiding this comment

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

Cross-SDK Consistency: Python is missing a test for v3 no-result behavior.

TypeScript has a test verifying that when a v3 permission handler returns no-result, the SDK does NOT call handlePendingPermissionRequest:

  • nodejs/test/client.test.ts: "does not respond to v3 permission requests when handler returns no-result"

Suggestion: Add a test to verify that _execute_permission_and_respond does NOT call handle_pending_permission_request when the handler returns {"kind": "no-result"}:

`@pytest`.mark.asyncio
async def test_v3_permission_no_result_does_not_respond(self):
    client = CopilotClient({"cli_path": CLI_PATH})
    await client.start()
    try:
        session = await client.create_session({
            "on_permission_request": lambda request, invocation: {"kind": "no-result"}
        })
        # Spy on handle_pending_permission_request to ensure it's not called
        from unittest.mock import AsyncMock
        session._client.rpc.permissions.handle_pending_permission_request = AsyncMock()
        
        await session._execute_permission_and_respond("request-1", {"kind": "write"})
        
        session._client.rpc.permissions.handle_pending_permission_request.assert_not_called()
    finally:
        await client.force_stop()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Contributor

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations (TypeScript, Python, Go, .NET), and the changes look excellent! The PR successfully adds the no-result permission outcome with consistent implementation across all SDKs.

What Was Checked

1. Type Definitions

All four SDKs properly define the new "no-result" permission kind:

  • TypeScript: Added to PermissionRequestResult union type (nodejs/src/types.ts)
  • Python: Added to PermissionRequestResultKind Literal type (python/copilot/types.py)
  • Go: Uses string type PermissionRequestResultKind - can accept any string including "no-result" (go/types.go)
  • .NET: Documented in PermissionRequestResult XML comments (dotnet/src/Types.cs)

2. V3 Permission Handling (Broadcast Events)

All SDKs correctly handle no-result by returning early without calling handlePendingPermissionRequest:

  • TypeScript: session.ts:406 - returns early if result.kind === "no-result"
  • Python: session.py:391 - returns early if result.kind == "no-result"
  • Go: session.go:565 - returns early if result.Kind == "no-result"
  • .NET: Session.cs:470 - returns early if result.Kind == new PermissionRequestResultKind("no-result")

3. V2 Permission Handling (RPC Adapters)

All SDKs throw/return errors when no-result is used with v2 protocol, with consistent error messages:

  • TypeScript: Throws Error with message "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."
  • Python: Raises ValueError with identical message
  • Go: Returns jsonrpc2.Error with message "permission handlers cannot return 'no-result' when connected to a protocol v2 server"
  • .NET: Throws InvalidOperationException with identical message

The error message has minor casing differences in Go ("permission" vs "Permission") but this is acceptable for cross-language error strings.

4. Extension API Enhancement

The TypeScript SDK appropriately:

  • Makes onPermissionRequest optional in joinSession()
  • Defaults to no-result handler for extensions that don't want to answer permission requests
  • Updates all documentation examples to remove the now-optional onPermissionRequest: approveAll boilerplate

This is TypeScript-specific and aligns with the extension use case - other SDKs don't have an equivalent extension API, so no changes needed there.

5. Test Coverage

All SDKs include tests for the new behavior:

  • TypeScript: test/client.test.ts + test/extension.test.ts (new file)
  • Python: test_client.py
  • Go: types_test.go
  • .NET: PermissionRequestResultKindTests.cs

6. Documentation

TypeScript docs properly updated to reflect the new optional permission handler:

  • docs/agent-author.md, docs/examples.md, docs/extensions.md - all updated

Summary

No consistency issues found. This PR maintains excellent cross-SDK parity:

✅ Feature exists in all four SDKs
✅ API naming follows language conventions
✅ Behavior is equivalent across implementations
✅ Error handling is consistent
✅ Test coverage is comprehensive
✅ Documentation is updated where applicable

Great work on maintaining consistency across all SDK implementations!

Generated by SDK Consistency Review Agent for issue #802 ·

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Contributor

SDK Consistency Review ✅

This PR implements the no-result permission outcome consistently across all four SDKs (TypeScript, Python, Go, .NET). Great work on maintaining feature parity! 🎉

Summary of Changes

The PR adds three key features uniformly across all SDKs:

  1. New no-result permission outcome - Allows extensions to leave permission requests unanswered
  2. TypeScript extension default - joinSession() now defaults to a no-result handler (removing the onPermissionRequest requirement)
  3. V2 protocol validation - V2 adapters properly reject no-result with clear error messages

Consistency Analysis

Type Definitions: All SDKs expose the new permission kind

  • TypeScript: Union type | { kind: "no-result" }
  • Python: Literal "no-result" in PermissionRequestResultKind
  • .NET: Documented in XML comments, usable via new PermissionRequestResultKind("no-result")
  • Go: String literal "no-result" (see suggestion below)

V2 Adapter Error Handling: Consistent error message across all SDKs

  • Node: Throws Error with message
  • Python: Raises ValueError
  • Go: Returns jsonrpc2.Error
  • .NET: Throws InvalidOperationException
  • All use the same error message: "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."

V3 Handler Behavior: Early return (no response sent) when no-result is returned

Test Coverage: All SDKs include tests for the new behavior

Documentation: TypeScript examples updated to reflect optional onPermissionRequest

Minor Suggestion

Go SDK API Design: For consistency with other permission kinds, consider adding an exported constant for no-result:

const (
    // ... existing constants ...
    
    // PermissionRequestResultKindNoResult indicates the handler chose not to answer the request.
    PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result"
)

This would match the pattern used for PermissionRequestResultKindApproved, PermissionRequestResultKindDeniedByRules, etc. Currently "no-result" is only used as a string literal in the implementation, which differs from the other permission kinds that have exported constants.

This is a minor API design consistency point - the current implementation works correctly, but having the constant would provide better discoverability and IDE support for Go developers.

Generated by SDK Consistency Review Agent for issue #802 ·

@SteveSandersonMS SteveSandersonMS merged commit df59a0e into main Mar 12, 2026
35 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesa/permission-no-result branch March 12, 2026 16:02
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