Skip to content

feat(now-policy-api): add per-operation event channel protocol - #89

Merged
Marc-André Moreau (mamoreau-devolutions) merged 3 commits into
masterfrom
feat/operation-output-polling
Aug 5, 2026
Merged

feat(now-policy-api): add per-operation event channel protocol#89
Marc-André Moreau (mamoreau-devolutions) merged 3 commits into
masterfrom
feat/operation-output-polling

Conversation

@vnikonov-devolutions

@vnikonov-devolutions Vladyslav Nikonov (vnikonov-devolutions) commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a per-operation event channel for delivering operation output and status-change notifications from the broker to the client, replacing HTTP polling for output entirely.

For each executed operation the broker (when it supports event channels) opens a dedicated channel and returns an expandable EventChannel descriptor in the execution response:

"EventChannel": {
  "Kind": "LocalPipe",
  "Path": "Devolutions.Now.PackageBroker.Operation.op-…"
}

The channel always carries status-change notifications; the CaptureOutput request flag only controls whether stdout/stderr data frames are pushed over it.

The client connects to the pipe and reads a minimal one-way binary frame protocol (NOW_BROKER frames):

Frame Kind Body
Hello 0x0000 u16 version major + u16 version minor
StatusUpdated 0x0001 empty — client should issue a GetStatus HTTP request
Finish 0x0002 empty — operation finished, pipe can be closed
Stdout 0x0003 UTF-8 data (agent ensures char boundaries)
Stderr 0x0004 UTF-8 data
StdoutOverflow 0x0005 u32 bytes skipped
StderrOverflow 0x0006 u32 bytes skipped

Frame layout is u32 body_size | u16 kind | body (little-endian, 64 KiB body cap). Decoders ignore unknown frame kinds, keeping the protocol extendable; end-of-stream mid-frame is treated as a truncated-stream error. Full spec: policies/docs/event-channel-protocol.md.

Changes

  • Rust (now-policy-api): new event_channel module — EventChannel descriptor model, EventFrame enum, encode/decode_body, incremental EventFrameDecoder; OperationSubmission.event_channel field.
  • .NET (Devolutions.Now.Policy.Api): EventChannel.cs — mirrored DTO, EventFrame hierarchy, EventFrameDecoder.
  • .NET client (Devolutions.Now.Policy.Client): BrokerClient.OpenEventChannel(ExecutionResponse) connects to the advertised pipe and returns an OperationEventChannel reader — ReadFrame() for the raw frame stream, ReadEvents() (IAsyncEnumerable) which skips unknown frames and completes after Finish or EOF.
  • Shared binary fixture (assets/samples/frames/event-channel.frames.bin) validated byte-for-byte by both Rust and .NET test suites, including unknown-frame tolerance; .NET integration tests exercise a real local named pipe, including pipe-closure and mid-frame truncation cases.
  • OpenAPI schema regenerated; docs/READMEs updated.

No crate/package or protocol version bumps (API stays 1.0; pre-release).

Copilot AI balanced review requested due to automatic review settings August 4, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds incremental polling for captured operation output across the Rust server/API and .NET client.

Changes:

  • Defines output request/response wire models and OpenAPI schema.
  • Adds server routing, mock support, and fixtures.
  • Adds .NET client polling support and cross-platform tests.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
policies/rust/now-policy-api/src/output.rs Defines output polling models.
policies/rust/now-policy-api/src/lib.rs Exports models and discriminators.
policies/rust/now-policy-api/README.md Documents the new module.
policies/rust/now-policy-api/openapi/now-policy-api.yaml Adds endpoint schemas.
policies/rust/now-policy-server-template/src/server.rs Adds trait method and route.
policies/rust/now-policy-server-template/src/mock.rs Adds mock output responses.
policies/rust/now-policy-server-template/tests/sample_documents.rs Tests fixtures and dispatch.
policies/rust/now-policy-server-template/assets/samples/requests/output-query-running.request.json Provides request fixture.
policies/rust/now-policy-server-template/assets/samples/responses/output-chunk.response.json Provides running response fixture.
policies/rust/now-policy-server-template/assets/samples/responses/output-eof.response.json Provides EOF response fixture.
policies/dotnet/Devolutions.Now.Policy.Api/OutputModels.cs Adds .NET wire DTOs.
policies/dotnet/Devolutions.Now.Policy.Api/BrokerApi.cs Adds constants and limits.
policies/dotnet/Devolutions.Now.Policy.Api/BrokerJson.cs Registers JSON metadata.
policies/dotnet/Devolutions.Now.Policy.Client/OperationOutputQuery.cs Adds client query model.
policies/dotnet/Devolutions.Now.Policy.Client/BrokerClient.cs Implements output polling.
policies/dotnet/Devolutions.Now.Policy.Client.Tests/TestData.cs Categorizes output fixtures.
policies/dotnet/Devolutions.Now.Policy.Client.Tests/SchemaValidationTests.cs Validates output schemas.
policies/dotnet/Devolutions.Now.Policy.Client.Tests/DtoRoundTripTests.cs Tests DTO round trips.
policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs Tests client requests and clamping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread policies/rust/now-policy-server-template/src/mock.rs Outdated
Comment thread policies/rust/now-policy-server-template/src/server.rs Outdated
Comment thread policies/dotnet/Devolutions.Now.Policy.Client/BrokerClient.cs Outdated
Comment thread policies/rust/now-policy-api/src/output.rs Outdated
@vnikonov-devolutions Vladyslav Nikonov (vnikonov-devolutions) changed the title feat(now-policy-api): add operation output polling API feat(now-policy-api): add operation output source API Aug 4, 2026
@vnikonov-devolutions
Vladyslav Nikonov (vnikonov-devolutions) marked this pull request as ready for review August 4, 2026 21:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (4)

policies/dotnet/Devolutions.Now.Policy.Client/OperationOutputQuery.cs:11

  • StreamKind is a required part of the wire request, but the client-facing query leaves it optional. Omitting it silently selects the enum's zero value (Stdout), so a caller can query the wrong stream without a compiler warning. Mark this member required, as is done for the other mandatory client-facing enum fields.
    public OutputStreamKind StreamKind { get; init; }

policies/dotnet/Devolutions.Now.Policy.Client/BrokerClient.cs:198

  • This local variable uses a type-style PascalCase name, unlike the camelCase locals throughout this client (including statusRequest in QueryStatus). Rename it and its uses to getOutputRequest.
        var GetOutputRequest = CreateGetOutputRequest(request);

policies/rust/now-policy-server-template/assets/samples/responses/output-local-file.response.json:5

  • This response identifies req-winget-vscode-install as its originating request, but that sample request omits CaptureOutput, which defaults to false. Under the new endpoint contract this operation must return BadRequest, so this successful output fixture contradicts the documented opt-in requirement. Update the originating request fixture to enable capture (and keep the related samples consistent).
  "RequestId": "req-winget-vscode-install",

policies/rust/now-policy-server-template/assets/samples/responses/output-http-stream.response.json:5

  • This response identifies req-winget-vscode-install as its originating request, but that sample request omits CaptureOutput, which defaults to false. Under the new endpoint contract this operation must return BadRequest, so this successful output fixture contradicts the documented opt-in requirement. Update the originating request fixture to enable capture (and keep the related samples consistent).
  "RequestId": "req-winget-vscode-install",

@vnikonov-devolutions Vladyslav Nikonov (vnikonov-devolutions) changed the title feat(now-policy-api): add operation output source API feat(now-policy-api): add per-operation event channel protocol Aug 5, 2026
Add a per-operation event channel: a one-way local pipe carrying the
NOW_BROKER binary frame protocol (Hello, StatusUpdated, Finish, Stdout,
Stderr, StdoutOverflow, StderrOverflow). The channel is opened
unconditionally when supported and always carries status change
notifications; the CaptureOutput flag controls whether stdout/stderr data
frames are pushed. Execution responses return an expandable EventChannel
descriptor (kind + path).

- Implement frame codec in Rust (now-policy-api::event_channel) and C#
  (Devolutions.Now.Policy.Api EventChannel.cs), validated against a shared
  binary fixture
- Add BrokerClient.OpenEventChannel and OperationEventChannel reader to the
  .NET client with named-pipe integration tests, including pipe-closure and
  mid-frame truncation cases
- Add protocol specification in policies/docs/event-channel-protocol.md
- Regenerate OpenAPI schema

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…th limit

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated no new comments.

Suppressed comments (6)

policies/rust/now-policy-api/src/enums.rs:110

  • This closed enum makes the advertised extendable Kind field reject any future transport during EventChannel deserialization, so even callers that do not use the new transport lose the whole execution response. Use a representation that retains unknown string kinds (and reflect it in the generated schema) so newer brokers remain readable by older consumers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, strum::Display)]
#[schemars(rename = "EventChannelKind")]
pub enum EventChannelKind {
    /// Local named pipe carrying `NOW_BROKER` event frames.
    LocalPipe,

policies/dotnet/Devolutions.Now.Policy.Api/Enums.cs:124

  • This string enum cannot represent a future transport kind: JsonStringEnumConverter throws on an unknown string while deserializing the entire ExecutionResponse. That contradicts the documented extendability of Kind and prevents OpenEventChannel from reaching its UnsupportedCapability handling. Preserve unknown wire values with a forward-compatible converter/value representation.
[JsonConverter(typeof(JsonStringEnumConverter<EventChannelKind>))]
public enum EventChannelKind
{
    /// <summary>Local named pipe carrying <c>NOW_BROKER</c> event frames.</summary>
    LocalPipe,

policies/dotnet/Devolutions.Now.Policy.Client/OperationEventChannel.cs:84

  • ReadFrame returns the first decoded frame without enforcing the channel handshake. A stream that starts with data, or a Hello with an unsupported major version, is accepted even though the protocol requires Hello first and mandates rejecting unsupported major versions (event-channel-protocol.md:83-91). Track handshake state and reject either condition before exposing frames; add integration tests for both cases.
            if (_decoder.TryReadFrame(out var frame))
            {
                return frame;

policies/dotnet/Devolutions.Now.Policy.Client/BrokerClient.cs:267

  • The explanation is inaccurate: CaptureOutput controls only stdout/stderr frames, while the descriptor is present for every operation whenever the broker supports event channels. Reporting missing output capture sends users toward the wrong fix; describe the descriptor as unavailable/unsupported instead.
                "The execution response does not carry an event channel descriptor; "
                + "the operation was likely submitted without output capture.");

policies/dotnet/Devolutions.Now.Policy.Client/README.md:19

  • This says the descriptor depends on CaptureOutput, contradicting the new API contract and the sample execution response whose request leaves capture disabled. The broker advertises the channel whenever supported; CaptureOutput only controls stdout/stderr frames.
Operations submitted with `CaptureOutput` additionally return a per-operation event channel descriptor (`OperationSubmission.EventChannel`) that carries the `NOW_BROKER` frame protocol: stdout/stderr data and status change notifications pushed by the broker. The frame codec (`EventFrame`, `EventFrameDecoder`) lives in `Devolutions.Now.Policy.Api`; see `policies/docs/event-channel-protocol.md` for the wire specification.

policies/docs/event-channel-protocol.md:60

  • The reference Encode methods reject bodies over 64 KiB; they do not split them, as their tests also assert. Clarify that producers must split output before encoding so the normative protocol text matches both implementations.
- `frame_size` MUST NOT exceed **65536** (64 KiB). Encoders split larger
  output into multiple frames; decoders MUST treat a larger value as a fatal

Address review feedback:
- OperationEventChannel now enforces the protocol handshake: the first frame
  must be Hello and its major version must be supported, otherwise
  EventFrameException is thrown; covered by integration tests
- Correct the missing-descriptor error message: descriptor presence depends
  on broker event channel support, not CaptureOutput
- Fix client README wording: the descriptor is returned whenever the broker
  supports event channels; CaptureOutput only gates stdout/stderr frames
- Clarify in the protocol spec that producers split output before encoding;
  reference Encode implementations reject oversized bodies

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me.

@mamoreau-devolutions
Marc-André Moreau (mamoreau-devolutions) merged commit d25e7eb into master Aug 5, 2026
9 checks passed
@mamoreau-devolutions
Marc-André Moreau (mamoreau-devolutions) deleted the feat/operation-output-polling branch August 5, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants