diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs
new file mode 100644
index 000000000..6a6134e18
--- /dev/null
+++ b/dotnet/src/Canvas.cs
@@ -0,0 +1,288 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using GitHub.Copilot.Rpc;
+
+namespace GitHub.Copilot;
+
+///
+/// Declarative metadata for a single canvas, sent over the wire on
+/// session.create / session.resume.
+///
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasDeclaration
+{
+ /// Canvas identifier, unique within the declaring connection.
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// Human-readable name shown in host UI and canvas pickers.
+ [JsonPropertyName("displayName")]
+ public string DisplayName { get; set; } = string.Empty;
+
+ /// Short, single-sentence description shown to the agent in canvas catalogs.
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ /// JSON Schema for the input payload accepted by canvas.open.
+ [JsonPropertyName("inputSchema")]
+ public JsonElement? InputSchema { get; set; }
+
+ /// Agent-callable actions this canvas exposes.
+ [JsonPropertyName("actions")]
+ public IList? Actions { get; set; }
+}
+
+///
+/// Stable extension identity for session participants that provide canvases.
+///
+[Experimental(Diagnostics.Experimental)]
+public sealed class ExtensionInfo
+{
+ /// Extension namespace/source, e.g. "github-app".
+ [JsonPropertyName("source")]
+ public string Source { get; set; } = string.Empty;
+
+ /// Stable provider name within the source namespace.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// Response returned from .
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasOpenResponse
+{
+ /// URL the host should render. Optional for canvases with no visual surface.
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
+
+ /// Provider-supplied title shown in host chrome.
+ [JsonPropertyName("title")]
+ public string? Title { get; set; }
+
+ /// Provider-supplied status text shown in host chrome.
+ [JsonPropertyName("status")]
+ public string? Status { get; set; }
+}
+
+/// Host capabilities passed to canvas provider callbacks.
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasHostContext
+{
+ /// Host capability details.
+ [JsonPropertyName("capabilities")]
+ public CanvasHostCapabilities Capabilities { get; set; } = new();
+}
+
+/// Host capability details passed to canvas provider callbacks.
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasHostCapabilities
+{
+ /// Whether the host supports canvas rendering.
+ [JsonPropertyName("canvases")]
+ public bool Canvases { get; set; }
+}
+
+/// Context handed to .
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasOpenContext
+{
+ /// Session that requested the canvas.
+ public string SessionId { get; init; } = string.Empty;
+
+ /// Owning provider identifier.
+ public string ExtensionId { get; init; } = string.Empty;
+
+ /// Canvas id from the declaring .
+ public string CanvasId { get; init; } = string.Empty;
+
+ /// Stable instance id supplied by the runtime.
+ public string InstanceId { get; init; } = string.Empty;
+
+ /// Validated input payload.
+ public JsonElement Input { get; init; }
+
+ /// Host capabilities supplied by the runtime.
+ public CanvasHostContext? Host { get; init; }
+}
+
+/// Context handed to .
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasActionContext
+{
+ /// Session that invoked the action.
+ public string SessionId { get; init; } = string.Empty;
+
+ /// Owning provider identifier.
+ public string ExtensionId { get; init; } = string.Empty;
+
+ /// Canvas id targeted by the action.
+ public string CanvasId { get; init; } = string.Empty;
+
+ /// Instance id targeted by the action.
+ public string InstanceId { get; init; } = string.Empty;
+
+ /// Action name from .
+ public string ActionName { get; init; } = string.Empty;
+
+ /// Validated input payload.
+ public JsonElement Input { get; init; }
+
+ /// Host capabilities supplied by the runtime.
+ public CanvasHostContext? Host { get; init; }
+}
+
+/// Context handed to a canvas's close lifecycle hook.
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasLifecycleContext
+{
+ /// Session owning the canvas instance.
+ public string SessionId { get; init; } = string.Empty;
+
+ /// Owning provider identifier.
+ public string ExtensionId { get; init; } = string.Empty;
+
+ /// Canvas id from the declaring .
+ public string CanvasId { get; init; } = string.Empty;
+
+ /// Instance id this lifecycle event applies to.
+ public string InstanceId { get; init; } = string.Empty;
+
+ /// Host capabilities supplied by the runtime.
+ public CanvasHostContext? Host { get; init; }
+}
+
+/// Structured error returned from canvas handlers.
+///
+/// Throw this from implementations to surface a
+/// machine-readable error code to the runtime. Any other exception is wrapped
+/// in a generic canvas_handler_error envelope.
+///
+[Experimental(Diagnostics.Experimental)]
+public sealed class CanvasError : Exception
+{
+ /// Initializes a new .
+ /// Machine-readable error code.
+ /// Human-readable message.
+ public CanvasError(string code, string message) : base(message)
+ {
+ Code = code;
+ }
+
+ /// Machine-readable error code.
+ public string Code { get; }
+
+ ///
+ /// Default error returned when a custom action has no handler.
+ ///
+ public static CanvasError NoHandler() => new(
+ "canvas_action_no_handler",
+ "No handler implemented for this canvas action");
+}
+
+///
+/// Internal helpers used by the session runtime to translate
+/// (and other handler-thrown exceptions) into structured JSON-RPC error responses.
+///
+internal static class CanvasErrorHelpers
+{
+ private const int InternalError = -32603;
+
+ public static LocalRpcInvocationException HandlerUnset() => Build(
+ "canvas_handler_unset",
+ "No canvas handler is registered on this session");
+
+ public static LocalRpcInvocationException HandlerError(string message) => Build(
+ "canvas_handler_error",
+ message);
+
+ public static LocalRpcInvocationException ToRpcException(CanvasError error) => Build(error.Code, error.Message);
+
+ private static LocalRpcInvocationException Build(string code, string message)
+ {
+ var json = JsonSerializer.Serialize(
+ new CanvasErrorPayload { Code = code, Message = message },
+ CanvasJsonContext.Default.CanvasErrorPayload);
+ using var doc = JsonDocument.Parse(json);
+ return new LocalRpcInvocationException(InternalError, message, doc.RootElement.Clone());
+ }
+
+ internal sealed class CanvasErrorPayload
+ {
+ [JsonPropertyName("code")]
+ public string Code { get; set; } = string.Empty;
+
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+ }
+}
+
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(CanvasErrorHelpers.CanvasErrorPayload))]
+internal partial class CanvasJsonContext : JsonSerializerContext;
+
+///
+/// Provider-side canvas lifecycle handler.
+///
+///
+/// A session installs a single via
+/// SessionConfigBase.CanvasHandler. The handler receives every
+/// inbound canvas.open / canvas.close / canvas.action.invoke
+/// JSON-RPC request the runtime issues for this session and decides — typically
+/// by inspecting — which
+/// application-side canvas should handle the call.
+///
+/// The SDK does not maintain a per-canvas registry; multiplexing across
+/// declared canvases is the implementor's responsibility.
+///
+///
+/// Implementations targeting netstandard2.0 cannot rely on default
+/// interface methods; derive from to inherit
+/// sensible defaults for and .
+///
+///
+[Experimental(Diagnostics.Experimental)]
+public interface ICanvasHandler
+{
+ /// Open a new canvas instance.
+ Task OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken);
+
+ /// Canvas was closed by the user or agent. Default: no-op.
+ Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken);
+
+ ///
+ /// Handle a non-lifecycle action declared by the canvas.
+ /// Default: throws .
+ ///
+ Task