Skip to content

feat(agent,agent-installer): add transactional policy store - #1963

Draft
Benoît Cortier (CBenoit) wants to merge 45 commits into
masterfrom
cbenoit-phase-2-policy-store
Draft

feat(agent,agent-installer): add transactional policy store#1963
Benoît Cortier (CBenoit) wants to merge 45 commits into
masterfrom
cbenoit-phase-2-policy-store

Conversation

@CBenoit

@CBenoit Benoît Cortier (CBenoit) commented Aug 29, 2026

Copy link
Copy Markdown
Member

Adds authenticated transactional package-policy management to Devolutions Agent, including deterministic validation, optimistic concurrency, crash-safe JSON persistence, secure path and ACL enforcement, and stable old-or-new runtime snapshots.

Policy writes require the actual named-pipe client token to be elevated with the Administrators group enabled. Read, management, and validation remain available to an authenticated standard user. Structured tracing, Windows Event Log auditing, fail-closed watcher reconciliation, and split unelevated/LocalSystem end-to-end coverage make policy changes observable and recoverable.

Caller authentication binds a retained executable candidate to the process main-image section with NtQueryInformationProcess(ProcessImageFileMapping), rejects network/provider image paths, verifies Devolutions Authenticode through the retained handle, and permits only trusted writers to tamper with the executable or its pinned, reparse-safe ancestor path. The same-stream regression proves that class 44 and a current valid signature are insufficient alone: a user-writable rewritten stream is rejected by the retained file and ancestor security policy.

This authenticates approved image and file provenance at connection time, not runtime memory integrity or historical bytes. Same-integrity injection or hollowing of an approved non-PPL process is outside signed-image authentication. Such control grants no additional read, management, or validation authority beyond running signed UniGetUI, and a standard or medium-integrity user still cannot replace policy because the pipe token must independently be elevated and Administrators-enabled. SYSTEM and elevated-Administrator injection are inside the write trust boundary; stronger runtime integrity requires an appropriate WDAC policy or compatible PPL protection levels.

Current ACL verification cannot reconstruct historical write access. Trust assumes the approved binary and each ancestor were secure when created and were never untrusted-writable. Operators must reinstall or remediate after any ACL or path compromise before trusting the deployment again; installer and package verification establish secure deployment, while runtime checks fail closed on present insecurity.

Non-Create replacement retains the exact token-observed file and directory path, writes a durable secure transaction marker and replacement, moves the observed target to a tombstone by handle, and publishes the replacement by a no-replace handle-relative rename. Concurrent external content is never overwritten. Startup recovery restores the exact verified tombstone when the final leaf is absent, preserves any raced-in or published final leaf, and fails closed on malformed, ambiguous, or insecure remnants.

The default policy tree is traversed under retained no-delete-share handles from ProgramData. Existing shared Devolutions directories may retain compatible create-only sibling grants, but path-tampering grants are rejected; the dedicated PackageBroker directory always requires strict SYSTEM/Administrators-only security. Missing components receive protected security at creation, and race winners are reopened without following reparses and verified before traversal continues.

The store preserves structured API failures, binds ConfirmOverwrite to the exact observed store token, restricts committed revisions and rule priorities to 1..=2,147,483,647 where applicable, and migrates eligible legacy policy files transactionally. Migration commit and rollback retain verified marker handles through deletion so path replacement or in-place marker rewrites cannot delete newer content.

Uses official crates.io releases now-policy 0.3.0, now-policy-api 0.4.0, and now-policy-server-template 0.4.0 with no now-libraries git source. Committed documents use https://devolutions.net/schemas/now-policy.schema.1.0.json; editable drafts use https://devolutions.net/schemas/now-policy-draft.schema.1.0.json.

Local validation includes Rust formatting, locked default and development broker suites, warnings-denied workspace Clippy, the full locked workspace suite, the i686 Windows broker check, Agent and policy-tester builds, the unelevated Agent policy end-to-end suite, direct Win32/native handle-relative rename tests, transaction crash/race recovery tests, and hostile directory creation tests. Exact-head CI provides the LocalSystem Repair and full end-to-end legs.

Stacked on #1937.
Shared contract: Devolutions/now-libraries#99 with the corrective follow-up in Devolutions/now-libraries#101.
Registry publication: Devolutions/now-libraries#98.

Expose the validated active package-broker policy through the shared
authenticated GET /v1/policy route. Return a structured unavailable
error without leaking policy source or file-security details.

This requires now-policy-api and now-policy-server-template 0.4.0 from
Devolutions/now-libraries#93 before the change can ship.

Issue: Devolutions/now-libraries#93

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Return a generic policy-unavailable message so clients cannot infer
whether the active policy is file-backed, missing, or corrupt.

Issue: Devolutions/now-libraries#93

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adopt the final shared server trait and keep policy-domain conversions owned by the broker after the compatibility feature removal.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Record the registry graph after removing the obsolete policy compatibility features so locked CI can resolve the manifest consistently.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise the policy HTTP contract from the repository integration tests while keeping authentication and snapshot-locking invariants beside the broker implementation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Launch the Agent through its CLI and issue HTTP requests over a real Tokio named-pipe client. Build the test Agent with the development signature bypass while requiring the matching debug configuration opt-in.

Remove the in-process broker test harness and its testsuite dependencies; retain authentication and snapshot-locking invariants as broker unit tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move privileged policy endpoint coverage into a dedicated tester so the active-policy path cannot silently skip in normal test runs. Run it as LocalSystem in CI with a development-only signature bypass.

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

Copy link
Copy Markdown
Member Author

Implementation notes:

  • Replaces the watcher-owned policy mutation path with one serialized PolicyStore that publishes immutable Active/Missing/Invalid snapshots.
  • Uses the exact ec592a91f95a66c8a79b7d612ed73d931ef74aeb now-libraries contract, keyed validation receipts, opaque disk-state tokens, and explicit Create/Update/Repair/ReplaceIdentity semantics.
  • Restricts writable policy storage to securely resolved JSON paths with SYSTEM/Administrators-only protection, same-directory atomic publication, post-write verification, and synchronous post-publication reconciliation.
  • Requires the existing caller-signature validation plus actual elevation and enabled Administrators membership for writes; the development signature bypass does not bypass authorization.
  • Audits attempts and outcomes through structured tracing and the Agent Windows Event Log without policy content.
  • Validation completed: diff check and rustfmt; 409 default and 413 dev-skip broker tests passed except the existing host-specific Winget AppExecAlias DACL baseline; sysevent-codes (1), sysevent-winevent (8), and win-api-wrappers (25) tests passed; workspace check and Clippy with warnings denied passed; Agent debug and release message-resource builds passed; unelevated Agent policy E2E passed. LocalSystem E2E is wired into Windows CI but could not run in this medium-integrity shell because PsExec could not install PSEXESVC.

Note

Human-tuned, LLM-assisted content.

@CBenoit

Copy link
Copy Markdown
Member Author

CI repair update (be3a9edf):

  • Resolves the verified policy parent through its held handle before comparing the leaf, so 8.3 parent aliases and case-only leaf differences are accepted without weakening location checks.
  • Rejects policy files with multiple hard links using FILE_STANDARD_INFO.NumberOfLinks instead of relying on which link name GetFinalPathNameByHandleW reports.
  • Runs the unelevated E2E suite through PsExec -l; the tester now fails immediately unless its token is both non-elevated and lacks enabled Administrators membership. LocalSystem remains a separate mode.
  • Selects and validates one deterministic mc.exe result before exporting WindowsSdkVerBinPath.

Focused validation passed: 37 policy-store Windows tests, 3 token-wrapper tests, agent-policy-tester check, direct unelevated E2E with token assertion, rustfmt, diff check, and targeted Clippy with warnings denied. Workflow assertions for restricted launch and single message-compiler selection also passed; actionlint is not installed locally.

Note

Human-tuned, LLM-assisted content.

@CBenoit

Copy link
Copy Markdown
Member Author

Follow-up CI repair (9c85810a): the restricted PsExec process could not create its transcript in the protected checkout. The wrapper now uses the user's low-integrity LocalLow\Temp directory for test artifacts and streams stdout/stderr back through the parent CI process, preserving the explicit non-elevated/non-admin token assertion and expected 403 behavior.

Local script parsing, workflow assertions, diff/rustfmt, agent-policy-tester check, and targeted Clippy passed.

Note

Human-tuned, LLM-assisted content.

@CBenoit

Copy link
Copy Markdown
Member Author

Final CI status: run 33254105745 passed at c1b72fe33b36dc1d80d79ee6d8d759d76ef9e275, including the restricted unelevated and LocalSystem Agent policy E2E modes, Windows path/hardlink tests, and Agent release message-resource build. The worktree is clean and the pushed upstream SHA matches locally.

Note

Human-tuned, LLM-assisted content.

@CBenoit

Copy link
Copy Markdown
Member Author

Shared-contract refresh provenance for 78f7c6bbb399f146245c7f693221786110e647d7 (2026.8.29-phase2.78f7c6bb):

  • OpenAPI SHA-256: 759481ac915fb0d48a602f90ad037261d5045c9a3ae906d293fef7ff3c74d469
  • Policy schema SHA-256: cd156264ed107890e0eeb9046d19fe0caf9bbc3f270235d434533bb325626fe8
  • API nupkg: c0d25dd367bba2e119a62987cc45fdfe61bd3837096d5872554b9d0c943fecac
  • API snupkg: c2b20126ffd8d63713b844f38cf920e356c5547d87306d1786aa8bd2fe800ab1
  • Client nupkg: 9bd84dc16ac05bb97ff579d48f6be55c70ed83a71cc021586deae6d32a945e34
  • Client snupkg: cab11a1cc045f26c0e79bf3733cf64f093fc771e2297e2e35c12f91489a9a972
  • Model nupkg: 0c3eb06d14bcc9fcd3374fb87a18e7bf56faa54d5978a9552897d299c1d6e04d
  • Model snupkg: 18a0088876e4f5968b59d7d9502046e281d508bfc8e3af5ade2731a7cf2d75be

All six local artifacts were rehashed and matched provenance.json. They remain unpublished. The reviewed shared delta changes direct .NET DTO invariant enforcement and diagnostic wording only; Rust signatures, OpenAPI, and policy schema are unchanged, so Gateway required only the exact three-crate revision/lock refresh.

Local validation: rustfmt and diff checks passed; locked broker suites passed 409/413 tests respectively with only the documented host Winget AppExecAlias DACL baseline; locked targeted check and Clippy with warnings denied passed; Agent and tester builds passed; unelevated Agent policy E2E passed.

Note

Human-tuned, LLM-assisted content.

@CBenoit

Copy link
Copy Markdown
Member Author

Authoritative shared-contract refresh at 44210762f7eb3a947d322e8732d6ae7c86941154 (2026.8.29-phase2.44210762):

  • OpenAPI SHA-256: 759481ac915fb0d48a602f90ad037261d5045c9a3ae906d293fef7ff3c74d469
  • Policy schema SHA-256: cd156264ed107890e0eeb9046d19fe0caf9bbc3f270235d434533bb325626fe8
  • API nupkg: 8058bc671bc286db7f165b3dea6b49c09c7fbe167cf9b2ea423aec477fd5384e
  • API snupkg: 9dadb0385b1becd57166266296d495d4f844b36510401af2ab878c4c3fd1eeb1
  • Client nupkg: af38db5b2fc77763d696a8f94e2c79bd29ba1d7fe3baee16ae8650cbbccd4be1
  • Client snupkg: 623ab2d87361002f2f340096bf156dd2aecec67a7193f9df74b83167ca14fd24
  • Model nupkg: 014a27ea541b662dd3f2b8323481e399b7569a317d9923a90fcb12ff1ed6162e
  • Model snupkg: f7d7779c647aa9cbd6dd1208c337e7583b218f7fb1c8f7ab31c1c3c831ebfe8f

All six unpublished artifacts were rehashed and match provenance.json. Wire formats, DTOs, OpenAPI, and policy schema are unchanged. Gateway now uses the reviewed explicit PolicyDocument::to_draft() projection and pins all three Rust policy crates and lock sources to the exact head.

Local validation: rustfmt and diff checks passed; locked broker suites passed 409/413 tests respectively with only the documented host Winget AppExecAlias DACL baseline; locked targeted check and Clippy with warnings denied passed; Agent and tester builds passed; unelevated Agent policy E2E passed. Exact-head CI: 33764438462.

Note

Human-tuned, LLM-assisted content.

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved upgrade, filesystem-concurrency, authorization, and installer-security issues undermine the transactional guarantees.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds authenticated transactional JSON policy management to Devolutions Agent, including secure persistence, validation, auditing, and end-to-end coverage.

Changes:

  • Replaces the legacy loader/watcher with a transactional policy store.
  • Adds elevation, ACL, concurrency-token, and Windows Event Log protections.
  • Expands Windows CI and privileged/unelevated testing.
File summaries
File Description
.github/workflows/ci.yml Adds SDK discovery and policy test modes.
Cargo.lock Locks new contract and security dependencies.
Cargo.toml Pins Phase 2 policy crates.
crates/agent-policy-tester/Cargo.toml Adds Windows policy-test dependencies.
crates/agent-policy-tester/run-as-system.ps1 Selects elevated test mode.
crates/agent-policy-tester/run-unelevated.ps1 Adds restricted-token test runner.
crates/agent-policy-tester/src/windows.rs Tests management lifecycle and authorization.
crates/now-package-broker/Cargo.toml Adds receipts and audit dependencies.
crates/now-package-broker/src/audit.rs Implements structured policy auditing.
crates/now-package-broker/src/auth.rs Captures elevation and administrator membership.
crates/now-package-broker/src/evaluator/mod.rs Exposes wildcard validation internally.
crates/now-package-broker/src/evaluator/wildcard.rs Shares wildcard compilation checks.
crates/now-package-broker/src/lib.rs Registers policy-store modules.
crates/now-package-broker/src/policy_loader.rs Removes the legacy loader.
crates/now-package-broker/src/policy_security.rs Strengthens policy filesystem security.
crates/now-package-broker/src/policy_store/mod.rs Implements transactional store coordination.
crates/now-package-broker/src/policy_store/receipt.rs Adds keyed validation receipts.
crates/now-package-broker/src/policy_store/validation.rs Implements deterministic policy validation.
crates/now-package-broker/src/policy_store/windows.rs Implements secure Windows persistence.
crates/now-package-broker/src/policy_watcher.rs Removes the legacy watcher.
crates/now-package-broker/src/scenario_tests.rs Makes scenarios JSON-only.
crates/now-package-broker/src/server/mod.rs Adds policy-management routes and authorization.
crates/now-package-broker/src/server/responses.rs Maps management errors and snapshots.
crates/now-package-broker/src/task.rs Integrates the transactional store.
crates/now-package-broker/src/test_support.rs Adds shared SID test support.
crates/sysevent-codes/src/lib.rs Defines policy audit events.
crates/sysevent-codes/tests/message_catalog_parity.rs Checks event-catalog parity.
crates/sysevent-winevent/src/lib.rs Corrects null-handle detection.
crates/win-api-wrappers/src/token.rs Adds token group-membership checks.
devolutions-agent/build.rs Embeds the Agent message catalog.
devolutions-agent/devolutions-agent.mc Adds the Agent event catalog.
devolutions-gateway/devolutions-gateway.mc Mirrors policy audit messages.
package/AgentWindowsManaged/Actions/AgentActions.cs Schedules policy-directory actions.
package/AgentWindowsManaged/Actions/CustomActions.cs Creates and secures the policy directory.
package/AgentWindowsManaged/Program.cs Registers the Agent event source.
package/AgentWindowsManaged/Resources/Includes.cs Defines the policy-directory SDDL.
Review details
  • Files reviewed: 34/36 changed files
  • Comments generated: 10
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/now-package-broker/src/auth.rs
Comment thread crates/now-package-broker/src/policy_store/windows.rs
Comment thread crates/now-package-broker/src/policy_store/windows.rs Outdated
Comment thread Cargo.toml Outdated
Comment thread crates/now-package-broker/src/policy_store/mod.rs
Comment thread crates/now-package-broker/src/task.rs
Comment thread devolutions-agent/build.rs Outdated
Comment thread package/AgentWindowsManaged/Actions/AgentActions.cs Outdated
Comment thread package/AgentWindowsManaged/Actions/CustomActions.cs Outdated
Comment thread crates/sysevent-codes/tests/message_catalog_parity.rs Outdated

Copilot AI left a comment

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.

🟡 Changes recommended

Migration cleanup uses an incorrect Win32 field ABI, and the dependency-publication gate remains unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 38/40 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread package/AgentWindowsManaged/Actions/WinAPI.cs Outdated

Copilot AI left a comment

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.

🟡 Changes recommended

Production resource embedding and legacy ACL migration are defective, and registry dependency publication remains incomplete.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

devolutions-agent/build.rs:128

  • Master builds and packages the Agent with the custom production profile (.github/workflows/ci.yml:135-144, 1011, 1036), but this gate embeds the message catalog only when PROFILE is literally release. The shipped production binary will therefore lack the message-table resource that the installer registers as EventMessageFile, so Event Viewer cannot format these audit events. Include the production profile in this gate (or detect release-like profiles without relying on the profile name).
  • Files reviewed: 38/40 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread package/AgentWindowsManaged/Actions/CustomActions.cs Outdated
Keep Phase 1's function-level large-error expectations and remove the now-redundant module-level expectation after Phase 2 route integration.

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

Copilot AI left a comment

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.

🟡 Changes recommended

Default-directory creation can follow an attacker-controlled junction before ancestor verification.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 39/42 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/now-package-broker/src/policy_store/windows.rs Outdated
Verify the complete existing ancestor chain before the runtime fallback creates the protected policy directory, preventing privileged side effects through a pre-created junction.

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

Copilot AI left a comment

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.

🟡 Changes recommended

Create transactions lack retained-directory anchoring, and capability-only updates produce false policy-change audits.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/now-package-broker/src/policy_security.rs:861

  • This paragraph describes ancestor verification, but it is attached to final_path_from_handle, which only resolves a path. Remove the stale paragraph so the function's documentation matches its behavior.
  • Files reviewed: 39/42 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/now-package-broker/src/policy_store/mod.rs Outdated
Create default policy components under retained verified parents, permit compatible shared-vendor ACLs while keeping PackageBroker strict, and use tested handle-relative rename transitions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Create default policy components under retained verified parents, support compatible shared-vendor ACLs, and use tested handle-relative file transitions.

Avoid policy-change audit events for capability-only snapshot updates.

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

Copilot AI left a comment

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.

🟡 Changes recommended

Legacy migration can delete a policy file that remains explicitly configured as the Agent’s active path.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 39/42 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread package/AgentWindowsManaged/Actions/CustomActions.cs

Copilot AI left a comment

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.

🟡 Changes recommended

Client-token impersonation occurs before any pipe message is read, causing real connections to fail authentication.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 39/42 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/now-package-broker/src/auth.rs Outdated
Capture authorization from the retained connector process token without premature pipe impersonation, and reject inherited connections after the original process exits.

Preserve an explicitly configured legacy policy during installer migration using strict config parsing and pinned file identity.

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

Copilot AI left a comment

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.

🔵 Needs a closer look

The security-critical 15,000-line change requires final human review despite substantial automated coverage.

Review details
  • Files reviewed: 39/42 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@CBenoit
Benoît Cortier (CBenoit) marked this pull request as ready for review September 4, 2026 13:43
@CBenoit
Benoît Cortier (CBenoit) marked this pull request as draft September 4, 2026 16:11
Base automatically changed from cbenoit-plan-agent-policy-endpoint to master September 8, 2026 13:47
Benoît Cortier (CBenoit) added a commit that referenced this pull request Sep 8, 2026
Exposes the validated active package-broker policy through the
authenticated `GET /v1/policy` route. Clients receive a stable snapshot,
while an unavailable policy returns the shared structured 404 response
without exposing policy source or file-security details.

Uses the official `now-policy` 0.3.0, `now-policy-api` 0.4.0, and
`now-policy-server-template` 0.4.0 releases. Phase 1 remains read-only:
policy management, validation, and replacement routes are not exposed.

Package policy files are now JSON-only. Deployments using `.yaml` or
`.yml` policy files must convert them to `.json`; unsupported extensions
and YAML content are rejected rather than silently parsed.

Coordinated stack: Devolutions/now-libraries#93 defines the shared
contract, this PR implements the Agent endpoint, and
Devolutions/UniGetUI#5292 consumes it. Keep this PR open while Phase 2
#1963 is restacked and the full stack is finalized.

BREAKING CHANGE: Package broker policy files must use JSON; YAML and YML
are no longer discovered or parsed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

2 participants