diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs b/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs
index 4911ce9..f4e9723 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs
@@ -7,13 +7,6 @@
namespace Devolutions.Now.Policy.Api;
-/// Canonical schema URI used in the $schema field of policy documents.
-public static class SchemaUris
-{
- public const string Policy = "https://devolutions.net/schemas/now-policy.schema.1.0.json";
- public const string PolicyDraft = "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json";
-}
-
/// Shared for broker documents.
public static class BrokerSerializer
{
diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs b/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs
index 578061e..6875840 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs
@@ -135,9 +135,8 @@ public enum PolicyFindingCode
InvalidWildcardPattern,
ContradictoryConstraints,
InvalidValidityInterval,
- UnsupportedSchema,
UnsupportedPolicyType,
- UnsupportedPolicyVersion,
+ UnsupportedPolicyFormatVersion,
AuditModeEnabled,
DefaultAllow,
SensitiveOptionAllowed,
diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/README.md b/policies/dotnet/Devolutions.Now.Policy.Api/README.md
index 287e0e5..9dc5edd 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Api/README.md
+++ b/policies/dotnet/Devolutions.Now.Policy.Api/README.md
@@ -21,6 +21,8 @@ The DTOs are used to:
- share the same JSON wire shape as the Rust source-of-truth model;
- provide compatibility conversions between package broker API enums and the `Devolutions.Now.Policy.Model` policy enums.
+Embedded policy documents and response policy projections use the software-managed `PolicyFormatVersion` field. New values are stamped as `1.0.0`; supported numeric versions in the 1.x line are accepted and preserved.
+
Architecture
------------
diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs b/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs
index 3d59925..6ec6e5f 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs
@@ -167,8 +167,9 @@ public sealed class ResponsePolicyInfo
[JsonPropertyName("Revision")]
public int Revision { get; set; }
- [JsonPropertyName("PolicyVersion")]
- public string PolicyVersion { get; set; } = "1.0.0";
+ [JsonPropertyName("PolicyFormatVersion")]
+ [JsonRequired]
+ public PolicyFormatVersion PolicyFormatVersion { get; init; } = PolicyFormatVersion.Current;
}
public sealed class OperationDiagnostics
diff --git a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs
index 00b57e6..3eb6b8c 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs
@@ -31,7 +31,7 @@ public async Task Evaluate_populates_client_context_and_missing_metadata_before_
var transport = new FakeBrokerTransport(
CapabilitiesResponse,
"""
- {"ResponseKind":"EvaluationResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"unused","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"WouldExecute":true,"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"}}
+ {"ResponseKind":"EvaluationResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"unused","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"WouldExecute":true,"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"}}
""");
var client = new BrokerClient(new BrokerClientOptions
{
@@ -77,7 +77,7 @@ public async Task Execute_normalizes_explicit_request_id_and_preserves_created_a
var transport = new FakeBrokerTransport(
CapabilitiesResponse,
"""
- {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}}
+ {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}}
""");
var client = CreateClient(transport);
var createdAt = DateTimeOffset.Parse("2026-06-29T12:00:00Z");
@@ -104,7 +104,7 @@ public async Task ExecuteAndWait_throws_typed_error_when_policy_denies_request()
var transport = new FakeBrokerTransport(
CapabilitiesResponse,
"""
- {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Deny","RuleId":"block-rule","Reason":"blocked"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"}}
+ {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Deny","RuleId":"block-rule","Reason":"blocked"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"}}
""");
var client = CreateClient(transport);
@@ -182,7 +182,7 @@ public async Task ExecuteAndWait_treats_canceled_status_as_terminal()
var transport = new FakeBrokerTransport(
CapabilitiesResponse,
"""
- {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}}
+ {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}}
""",
"""
{"ResponseKind":"StatusResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"OperationId":"operation:123","RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","Status":"Canceled","Message":"operation was canceled"}
@@ -208,7 +208,7 @@ public async Task ExecuteAndWait_requests_broker_cancelation_when_token_is_cance
var transport = new FakeBrokerTransport(
CapabilitiesResponse,
"""
- {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}}
+ {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}}
""",
"""
{"ResponseKind":"CancelResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"OperationId":"operation:123","RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","Status":"Canceling"}
@@ -412,7 +412,7 @@ public async Task GetPolicy_preserves_structured_unsupported_error(string transp
[InlineData("Server")]
[InlineData("Server.ServerVersion")]
[InlineData("Server.Transport")]
- [InlineData("Policy.$schema")]
+ [InlineData("Policy.PolicyFormatVersion")]
[InlineData("Policy.Metadata.Id")]
[InlineData("Policy.Enforcement.DefaultDecision")]
[InlineData("Policy.Rules")]
diff --git a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs
index c5dbf48..158592d 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs
@@ -169,6 +169,42 @@ public void Public_json_options_round_trip_policy_response_without_reflection()
Assert.Contains(Environment.NewLine, pretty);
}
+ [Fact]
+ public void Strict_policy_response_rejects_schema_member_and_unsupported_format_version()
+ {
+ var document = JsonNode.Parse(
+ File.ReadAllText(Path.Combine(TestData.SamplesDir, "responses", "policy.response.json")))!;
+ document["Policy"]!["$schema"] = "https://example.invalid/policy.schema.json";
+ Assert.Throws(
+ () => BrokerSerializer.DeserializeStrict(document.ToJsonString()));
+
+ document["Policy"]!.AsObject().Remove("$schema");
+ document["Policy"]!["PolicyFormatVersion"] = "2.0.0";
+ Assert.Throws(
+ () => BrokerSerializer.DeserializeStrict(document.ToJsonString()));
+ }
+
+ [Theory]
+ [InlineData("winget-vscode-install.allowed.response.json", "EvaluationResponse")]
+ [InlineData("execution-winget-vscode-install.response.json", "ExecutionResponse")]
+ public void Strict_operation_response_requires_policy_format_version(string fixture, string responseType)
+ {
+ var document = JsonNode.Parse(
+ File.ReadAllText(Path.Combine(TestData.SamplesDir, "responses", fixture)))!;
+ document["Policy"]!.AsObject().Remove("PolicyFormatVersion");
+
+ if (responseType == "EvaluationResponse")
+ {
+ Assert.Throws(
+ () => BrokerSerializer.DeserializeStrict(document.ToJsonString()));
+ }
+ else
+ {
+ Assert.Throws(
+ () => BrokerSerializer.DeserializeStrict(document.ToJsonString()));
+ }
+ }
+
[Fact]
public async Task ErrorResponse_serializes_to_schema_valid_output()
{
diff --git a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs
index cc8292c..ec91b92 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs
@@ -537,10 +537,6 @@ public async Task Public_serializer_options_enforce_policy_root_semantic_invaria
}
var committedDto = JsonSerializer.Deserialize(committed.ToJsonString(), options)!;
- committedDto.Schema = Devolutions.Now.Policy.Model.SchemaUris.PolicyDraft;
- Assert.Throws(() => JsonSerializer.Serialize(committedDto, options));
-
- committedDto = JsonSerializer.Deserialize(committed.ToJsonString(), options)!;
committedDto.Metadata.Revision = 0;
Assert.Throws(() => JsonSerializer.Serialize(committedDto, options));
@@ -553,10 +549,6 @@ public async Task Public_serializer_options_enforce_policy_root_semantic_invaria
Assert.Throws(() => JsonSerializer.Serialize(committedDto, options));
var draftDto = JsonSerializer.Deserialize(draft.ToJsonString(), options)!;
- draftDto.Schema = Devolutions.Now.Policy.Model.SchemaUris.Policy;
- Assert.Throws(() => JsonSerializer.Serialize(draftDto, options));
-
- draftDto = JsonSerializer.Deserialize(draft.ToJsonString(), options)!;
draftDto.Rules[0].Match.SkipHashCheck = [false, true];
Assert.Throws(() => JsonSerializer.Serialize(draftDto, options));
}
@@ -646,10 +638,6 @@ private static JsonNode MismatchedReplacementCanonicalDraft(JsonNode response)
private static IEnumerable InvalidCommittedPolicies(JsonNode committed)
{
- var wrongSchema = committed.DeepClone();
- wrongSchema["$schema"] = Devolutions.Now.Policy.Model.SchemaUris.PolicyDraft;
- yield return wrongSchema;
-
var zeroRevision = committed.DeepClone();
zeroRevision["Metadata"]!["Revision"] = 0;
yield return zeroRevision;
@@ -665,10 +653,6 @@ private static IEnumerable InvalidCommittedPolicies(JsonNode committed
private static IEnumerable InvalidDraftPolicies(JsonNode draft)
{
- var wrongSchema = draft.DeepClone();
- wrongSchema["$schema"] = Devolutions.Now.Policy.Model.SchemaUris.Policy;
- yield return wrongSchema;
-
var mixedBooleanMatch = draft.DeepClone();
mixedBooleanMatch["Rules"]![0]!["Match"]!["SkipHashCheck"] = new JsonArray(false, true);
yield return mixedBooleanMatch;
diff --git a/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs b/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs
index 0d236fa..ce0fa99 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs
@@ -84,21 +84,24 @@ public async Task Draft_conversion_uses_and_validates_against_draft_schema()
var schema = await JsonSchema.FromFileAsync(PolicyDraftSchema);
var json = draft.ToJson();
- Assert.Equal(SchemaUris.PolicyDraft, draft.Schema);
+ Assert.Equal(PolicyFormatVersions.Current, draft.PolicyFormatVersion.Value);
+ Assert.Null(JsonNode.Parse(json)!["$schema"]);
Assert.Empty(schema.Validate(json));
- Assert.Equal(SchemaUris.Policy, draft.ToPolicyDocument(1, DateTimeOffset.UtcNow).Schema);
+ Assert.Equal(
+ PolicyFormatVersions.Current,
+ draft.ToPolicyDocument(1, DateTimeOffset.UtcNow).PolicyFormatVersion.Value);
}
[Fact]
- public void Policy_and_draft_parsers_reject_the_other_document_schema()
+ public void Policy_and_draft_parsers_reject_schema_member()
{
var policy = PolicyDocument.Create("contoso.policy", "Contoso IT");
var policyJson = JsonNode.Parse(policy.ToJson())!;
- policyJson["$schema"] = SchemaUris.PolicyDraft;
+ policyJson["$schema"] = "https://example.invalid/policy.schema.json";
Assert.Throws(() => PolicyDocument.ParseJson(policyJson.ToJsonString()));
var draftJson = JsonNode.Parse(policy.ToDraft().ToJson())!;
- draftJson["$schema"] = SchemaUris.Policy;
+ draftJson["$schema"] = "https://example.invalid/policy-draft.schema.json";
Assert.Throws(
() => PolicySerializer.DeserializePolicyDraftDocumentStrict(draftJson.ToJsonString()));
}
@@ -161,8 +164,7 @@ public void Negative_priority_is_rejected_by_parser()
}
[Theory]
- [InlineData("$schema")]
- [InlineData("PolicyVersion")]
+ [InlineData("PolicyFormatVersion")]
[InlineData("PolicyType")]
[InlineData("Metadata")]
[InlineData("Enforcement")]
@@ -189,8 +191,7 @@ public void Missing_rust_required_property_is_rejected_by_parser(string property
}
[Theory]
- [InlineData("$schema")]
- [InlineData("PolicyVersion")]
+ [InlineData("PolicyFormatVersion")]
[InlineData("PolicyType")]
[InlineData("Metadata")]
[InlineData("Enforcement")]
@@ -238,6 +239,7 @@ public void Draft_conversion_omits_and_restores_server_metadata_without_aliasing
var draft = committed.ToDraft();
var draftJson = JsonNode.Parse(draft.ToJson())!;
+ Assert.Null(draftJson["$schema"]);
Assert.Null(draftJson["Metadata"]!["Revision"]);
Assert.Null(draftJson["Metadata"]!["PublishedAt"]);
@@ -246,11 +248,60 @@ public void Draft_conversion_omits_and_restores_server_metadata_without_aliasing
var publishedAt = DateTimeOffset.Parse("2026-08-29T00:00:00Z");
var recommitted = draft.ToPolicyDocument(7, publishedAt);
+ Assert.Equal(committed.PolicyFormatVersion, recommitted.PolicyFormatVersion);
+ Assert.Equal(committed.Metadata.Id, recommitted.Metadata.Id);
+ Assert.Equal(committed.Metadata.Publisher, recommitted.Metadata.Publisher);
+ Assert.Equal(committed.Metadata.Description, recommitted.Metadata.Description);
+ Assert.Equal(committed.Metadata.SupportUrl, recommitted.Metadata.SupportUrl);
+ Assert.Equal(committed.Metadata.ValidFrom, recommitted.Metadata.ValidFrom);
+ Assert.Equal(committed.Metadata.ValidUntil, recommitted.Metadata.ValidUntil);
Assert.Equal(7U, recommitted.Metadata.Revision);
Assert.Equal(publishedAt, recommitted.Metadata.PublishedAt);
Assert.Equal("changed", recommitted.Rules[0].Id);
}
+ [Theory]
+ [InlineData("not-semver")]
+ [InlineData("2.0.0")]
+ [InlineData("1.18446744073709551616.0")]
+ [InlineData("1.2.3-beta")]
+ [InlineData("1.0.0\n")]
+ public void Unsupported_policy_format_versions_are_rejected(string value)
+ {
+ var document = JsonNode.Parse(
+ File.ReadAllText(Path.Combine(SamplesDir, "corporate-allowlist.policy.json")))!;
+ document["PolicyFormatVersion"] = value;
+
+ Assert.Throws(() => PolicyDocument.ParseJson(document.ToJsonString()));
+ }
+
+ [Theory]
+ [InlineData("1.0.0-01")]
+ [InlineData("1.0.0-.")]
+ [InlineData("1.0.0\n")]
+ public async Task Rust_schema_rejects_unsupported_policy_format_versions(string value)
+ {
+ var document = JsonNode.Parse(
+ File.ReadAllText(Path.Combine(SamplesDir, "corporate-allowlist.policy.json")))!;
+ document["PolicyFormatVersion"] = value;
+ var schema = await JsonSchema.FromFileAsync(PolicySchema);
+
+ Assert.NotEmpty(schema.Validate(document.ToJsonString()));
+ }
+
+ [Fact]
+ public void Compatible_policy_format_version_is_preserved_by_conversion()
+ {
+ var document = JsonNode.Parse(
+ File.ReadAllText(Path.Combine(SamplesDir, "corporate-allowlist.policy.json")))!;
+ document["PolicyFormatVersion"] = "1.2.3";
+
+ var committed = PolicyDocument.ParseJson(document.ToJsonString());
+ var recommitted = committed.ToDraft().ToPolicyDocument(8, DateTimeOffset.UtcNow);
+
+ Assert.Equal("1.2.3", recommitted.PolicyFormatVersion.Value);
+ }
+
[Fact]
public void Draft_conversion_enforces_revision_bounds()
{
@@ -420,8 +471,7 @@ private static string MinimalPolicyJson(string revision, string rules)
{
return $$"""
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "test.policy",
diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs b/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs
index f371b58..09004fb 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs
@@ -3,22 +3,105 @@
namespace Devolutions.Now.Policy.Model;
-public static class SchemaUris
+public static class PolicyFormatVersions
{
- public const string Policy = "https://devolutions.net/schemas/now-policy.schema.1.0.json";
- public const string PolicyDraft = "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json";
+ public const string Current = "1.0.0";
+ public const ulong SupportedMajor = 1;
+}
+
+[JsonConverter(typeof(PolicyFormatVersionJsonConverter))]
+public sealed class PolicyFormatVersion : IEquatable
+{
+ private PolicyFormatVersion(string value)
+ {
+ Value = value;
+ }
+
+ public static PolicyFormatVersion Current { get; } = new(PolicyFormatVersions.Current);
+
+ public string Value { get; }
+
+ public static PolicyFormatVersion Parse(string value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ if (value.Length > 128)
+ {
+ throw new FormatException("PolicyFormatVersion must contain at most 128 characters.");
+ }
+
+ var components = value.Split('.');
+ if (components.Length != 3
+ || !TryParseComponent(components[0], out var major)
+ || !TryParseComponent(components[1], out _)
+ || !TryParseComponent(components[2], out _))
+ {
+ throw new FormatException(
+ "PolicyFormatVersion must contain three canonical unsigned 64-bit integer components.");
+ }
+ if (major != PolicyFormatVersions.SupportedMajor)
+ {
+ throw new NotSupportedException(
+ $"Policy format major version {major} is unsupported; supported major version is {PolicyFormatVersions.SupportedMajor}.");
+ }
+
+ return new PolicyFormatVersion(value);
+ }
+
+ private static bool TryParseComponent(string component, out ulong value)
+ {
+ value = 0;
+ return component.Length >= 1
+ && (component.Length == 1 || component[0] != '0')
+ && ulong.TryParse(
+ component,
+ System.Globalization.NumberStyles.None,
+ System.Globalization.CultureInfo.InvariantCulture,
+ out value);
+ }
+
+ public bool Equals(PolicyFormatVersion? other) =>
+ other is not null && string.Equals(Value, other.Value, StringComparison.Ordinal);
+
+ public override bool Equals(object? obj) => Equals(obj as PolicyFormatVersion);
+
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
+
+ public override string ToString() => Value;
+}
+
+internal sealed class PolicyFormatVersionJsonConverter : JsonConverter
+{
+ public override PolicyFormatVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ throw new JsonException("PolicyFormatVersion must be a string.");
+ }
+
+ try
+ {
+ return PolicyFormatVersion.Parse(reader.GetString()!);
+ }
+ catch (Exception exception) when (exception is FormatException or NotSupportedException)
+ {
+ throw new JsonException(exception.Message, exception);
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, PolicyFormatVersion value, JsonSerializerOptions options) =>
+ writer.WriteStringValue(value.Value);
}
/// A policy document governing which package operations are allowed or denied.
public sealed class PolicyDocument
{
- [JsonPropertyName("$schema")]
- [JsonRequired]
- public string Schema { get; set; } = SchemaUris.Policy;
-
- [JsonPropertyName("PolicyVersion")]
+ ///
+ /// Software-managed document-format version. Applications must not expose
+ /// this as publisher-authored editable metadata.
+ ///
+ [JsonPropertyName("PolicyFormatVersion")]
[JsonRequired]
- public string PolicyVersion { get; set; } = "1.0.0";
+ public PolicyFormatVersion PolicyFormatVersion { get; init; } = PolicyFormatVersion.Current;
[JsonPropertyName("PolicyType")]
[JsonRequired]
@@ -65,8 +148,7 @@ public PolicyDraftDocument ToDraft()
{
return new PolicyDraftDocument
{
- Schema = SchemaUris.PolicyDraft,
- PolicyVersion = PolicyVersion,
+ PolicyFormatVersion = PolicyFormatVersion,
PolicyType = PolicyType,
Metadata = PolicyModelClone.ToDraftMetadata(Metadata),
Enforcement = PolicyModelClone.Enforcement(Enforcement),
@@ -82,13 +164,13 @@ public sealed class PolicyDraftDocument
{
private const uint MaxRevision = int.MaxValue;
- [JsonPropertyName("$schema")]
- [JsonRequired]
- public string Schema { get; set; } = SchemaUris.PolicyDraft;
-
- [JsonPropertyName("PolicyVersion")]
+ ///
+ /// Software-managed document-format version. Applications must stamp the
+ /// current value for new drafts and must not expose it as authored metadata.
+ ///
+ [JsonPropertyName("PolicyFormatVersion")]
[JsonRequired]
- public string PolicyVersion { get; set; } = "1.0.0";
+ public PolicyFormatVersion PolicyFormatVersion { get; init; } = PolicyFormatVersion.Current;
[JsonPropertyName("PolicyType")]
[JsonRequired]
@@ -143,8 +225,7 @@ public PolicyDocument ToPolicyDocument(uint revision, DateTimeOffset publishedAt
return new PolicyDocument
{
- Schema = SchemaUris.Policy,
- PolicyVersion = PolicyVersion,
+ PolicyFormatVersion = PolicyFormatVersion,
PolicyType = PolicyType,
Metadata = PolicyModelClone.ToCommittedMetadata(Metadata, revision, publishedAt),
Enforcement = PolicyModelClone.Enforcement(Enforcement),
diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs b/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs
index 1392084..7ad7f7c 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs
+++ b/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs
@@ -72,14 +72,12 @@ private static void ValidateSemanticValue(object? value)
internal static void ValidateRequiredCollectionElements(PolicyDocument policy)
{
- ValidateSchemaUri(policy.Schema, SchemaUris.Policy, "$.$schema");
ValidatePolicyRevision(policy.Metadata.Revision);
ValidateRequiredCollectionElements(policy.Rules);
}
internal static void ValidateRequiredCollectionElements(PolicyDraftDocument policy)
{
- ValidateSchemaUri(policy.Schema, SchemaUris.PolicyDraft, "$.$schema");
ValidateRequiredCollectionElements(policy.Rules);
}
@@ -179,14 +177,6 @@ private static void ValidatePolicyRevision(uint revision)
}
}
- private static void ValidateSchemaUri(string actual, string expected, string path)
- {
- if (!string.Equals(actual, expected, StringComparison.Ordinal))
- {
- throw new JsonException($"The JSON string at {path} must be '{expected}'.");
- }
- }
-
private static void RejectNullElements(IReadOnlyList values, string path)
where T : class
{
diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/README.md b/policies/dotnet/Devolutions.Now.Policy.Model/README.md
index eda151c..2e552dd 100644
--- a/policies/dotnet/Devolutions.Now.Policy.Model/README.md
+++ b/policies/dotnet/Devolutions.Now.Policy.Model/README.md
@@ -25,6 +25,8 @@ Architecture
`PolicyDocument.Create` constructs a committed policy and `PolicyDraftDocument.Create` constructs an editable draft. `PolicyDocument.ToDraft` removes server-managed `Revision` and `PublishedAt`; `PolicyDraftDocument.ToPolicyDocument` requires those values when committing. `ParseJson` is the only policy parsing entry point.
+`PolicyFormatVersion` is software-managed format compatibility metadata, not a publisher release version. New documents stamp `1.0.0`; readers accept and preserve supported numeric versions in the 1.x line and reject malformed or unsupported-major values. Applications must not expose it as authored metadata. Policy documents contain no `$schema` member, and strict readers reject documents that contain one as unknown-field input.
+
Breaking change
---------------
diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml
index 61cb694..cb6f555 100644
--- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml
+++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml
@@ -1247,9 +1247,8 @@ components:
- InvalidWildcardPattern
- ContradictoryConstraints
- InvalidValidityInterval
- - UnsupportedSchema
- UnsupportedPolicyType
- - UnsupportedPolicyVersion
+ - UnsupportedPolicyFormatVersion
- AuditModeEnabled
- DefaultAllow
- SensitiveOptionAllowed
@@ -1259,6 +1258,18 @@ components:
enum:
- Error
- Warning
+ PolicyFormatVersion:
+ description: |-
+ Software-managed policy document format version.
+
+ Readers accept canonical numeric versions in the compatible 1.x line.
+ Applications must stamp the current value, `1.0.0`, for new documents and
+ must not expose this value as publisher-authored editable metadata.
+ Schemas describe the canonical shape; runtime readers additionally bound
+ each numeric component to an unsigned 64-bit integer.
+ type: string
+ maxLength: 128
+ pattern: ^1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S])
PolicyManagementResponse:
description: Response body for `GET /v1/policy/management`.
type: object
@@ -1839,10 +1850,10 @@ components:
description: Policy document identifier.
allOf:
- $ref: '#/components/schemas/ResourceId'
- PolicyVersion:
- description: Policy syntax version.
+ PolicyFormatVersion:
+ description: Software-managed policy document format version.
allOf:
- - $ref: '#/components/schemas/SemanticVersion'
+ - $ref: '#/components/schemas/PolicyFormatVersion'
Revision:
description: Policy revision number.
type: integer
@@ -1853,7 +1864,7 @@ components:
required:
- Id
- Revision
- - PolicyVersion
+ - PolicyFormatVersion
RuleId:
description: Rule ID in responses. Includes sentinel values not valid as policy rule IDs.
type: string
@@ -1865,11 +1876,6 @@ components:
enum:
- User
- Machine
- SemanticVersion:
- description: Semantic version string (SemVer 2.0.0).
- type: string
- maxLength: 128
- pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$
ServerContext:
description: Server context included in responses.
type: object
@@ -2117,10 +2123,6 @@ components:
description: A policy document governing which package operations are allowed or denied.
type: object
properties:
- $schema:
- description: Policy schema URI constant.
- allOf:
- - $ref: '#/components/schemas/PolicyModelPolicySchemaUri'
Enforcement:
description: Enforcement configuration.
allOf:
@@ -2129,14 +2131,17 @@ components:
description: Policy metadata.
allOf:
- $ref: '#/components/schemas/PolicyModelPolicyMetadata'
+ PolicyFormatVersion:
+ description: |-
+ Software-managed policy document format version.
+
+ Applications must not expose this field as publisher-authored editable metadata.
+ allOf:
+ - $ref: '#/components/schemas/PolicyModelPolicyFormatVersion'
PolicyType:
description: Must be `"PackageBrokerPolicy"`.
allOf:
- $ref: '#/components/schemas/PolicyModelPackageBrokerPolicy'
- PolicyVersion:
- description: Policy syntax version (semver).
- allOf:
- - $ref: '#/components/schemas/PolicyModelSemanticVersion'
Rules:
description: Ordered list of policy rules (may be empty; enforcement defaults apply).
type: array
@@ -2145,8 +2150,7 @@ components:
maxItems: 1024
additionalProperties: false
required:
- - $schema
- - PolicyVersion
+ - PolicyFormatVersion
- PolicyType
- Metadata
- Enforcement
@@ -2155,10 +2159,6 @@ components:
description: An editable policy document without server-managed commit metadata.
type: object
properties:
- $schema:
- description: Policy schema URI constant.
- allOf:
- - $ref: '#/components/schemas/PolicyModelPolicyDraftSchemaUri'
Enforcement:
description: Enforcement configuration.
allOf:
@@ -2167,14 +2167,18 @@ components:
description: Editable policy metadata.
allOf:
- $ref: '#/components/schemas/PolicyModelPolicyDraftMetadata'
+ PolicyFormatVersion:
+ description: |-
+ Software-managed policy document format version.
+
+ Applications must stamp the current value and must not expose this field
+ as publisher-authored editable metadata.
+ allOf:
+ - $ref: '#/components/schemas/PolicyModelPolicyFormatVersion'
PolicyType:
description: Must be `"PackageBrokerPolicy"`.
allOf:
- $ref: '#/components/schemas/PolicyModelPackageBrokerPolicy'
- PolicyVersion:
- description: Policy syntax version (semver).
- allOf:
- - $ref: '#/components/schemas/PolicyModelSemanticVersion'
Rules:
description: Ordered list of policy rules (may be empty; enforcement defaults apply).
type: array
@@ -2183,8 +2187,7 @@ components:
maxItems: 1024
additionalProperties: false
required:
- - $schema
- - PolicyVersion
+ - PolicyFormatVersion
- PolicyType
- Metadata
- Enforcement
@@ -2228,10 +2231,6 @@ components:
required:
- Id
- Publisher
- PolicyModelPolicyDraftSchemaUri:
- type: string
- enum:
- - https://devolutions.net/schemas/now-policy-draft.schema.1.0.json
PolicyModelPolicyEnforcement:
description: Enforcement configuration.
type: object
@@ -2252,6 +2251,18 @@ components:
required:
- DefaultDecision
- RulePrecedence
+ PolicyModelPolicyFormatVersion:
+ description: |-
+ Software-managed policy document format version.
+
+ Readers accept canonical numeric versions in the compatible 1.x line.
+ Applications must stamp the current value, `1.0.0`, for new documents and
+ must not expose this value as publisher-authored editable metadata.
+ Schemas describe the canonical shape; runtime readers additionally bound
+ each numeric component to an unsigned 64-bit integer.
+ type: string
+ maxLength: 128
+ pattern: ^1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S])
PolicyModelPolicyMatch:
description: |-
Match criteria for a policy rule. All specified fields must match.
@@ -2485,10 +2496,6 @@ components:
- Priority
- Decision
- Match
- PolicyModelPolicySchemaUri:
- type: string
- enum:
- - https://devolutions.net/schemas/now-policy.schema.1.0.json
PolicyModelResourceId:
description: Resource identifier (policy IDs, rule IDs, request IDs, audit IDs).
type: string
@@ -2505,14 +2512,6 @@ components:
enum:
- User
- Machine
- PolicyModelSemanticVersion:
- description: |-
- Semantic version string (SemVer 2.0.0).
-
- Validated at deserialization time using the `semver` crate.
- type: string
- maxLength: 128
- pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$
PolicyModelStringPattern:
description: Case-insensitive exact value or wildcard pattern.
type: string
diff --git a/policies/rust/now-policy-api/src/api.rs b/policies/rust/now-policy-api/src/api.rs
index 8cdc4ac..7c4150e 100644
--- a/policies/rust/now-policy-api/src/api.rs
+++ b/policies/rust/now-policy-api/src/api.rs
@@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::enums::{Architecture, Decision, Elevation, ErrorCode, ManagerName, Operation, Scope, Transport};
use super::{
ApiVersion, CommandString, CustomParameterString, ErrorResponseKind, PackageIdentifier, PackageRequestKind,
- ProcessName, ResourceId, RuleId, SemanticVersion, VersionString,
+ ProcessName, ResourceId, RuleId, VersionString,
};
/// Canonical request sent by a package broker client to the elevated broker.
@@ -254,8 +254,8 @@ pub struct ResponsePolicyInfo {
#[schemars(range(min = 1, max = 2147483647))]
pub revision: u32,
- /// Policy syntax version.
- pub policy_version: SemanticVersion,
+ /// Software-managed policy document format version.
+ pub policy_format_version: now_policy::PolicyFormatVersion,
}
/// Optional operation diagnostics.
diff --git a/policies/rust/now-policy-api/src/management.rs b/policies/rust/now-policy-api/src/management.rs
index ed0afcb..6bb7579 100644
--- a/policies/rust/now-policy-api/src/management.rs
+++ b/policies/rust/now-policy-api/src/management.rs
@@ -102,9 +102,8 @@ pub enum PolicyFindingCode {
InvalidWildcardPattern,
ContradictoryConstraints,
InvalidValidityInterval,
- UnsupportedSchema,
UnsupportedPolicyType,
- UnsupportedPolicyVersion,
+ UnsupportedPolicyFormatVersion,
AuditModeEnabled,
DefaultAllow,
SensitiveOptionAllowed,
@@ -975,8 +974,7 @@ mod tests {
"ValidatorVersion": "validator/1",
"IsValid": true,
"CanonicalDraft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": { "Id": "test", "Publisher": "test" },
"Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" },
diff --git a/policies/rust/now-policy-server-template/src/server.rs b/policies/rust/now-policy-server-template/src/server.rs
index 540b64c..65ac716 100644
--- a/policies/rust/now-policy-server-template/src/server.rs
+++ b/policies/rust/now-policy-server-template/src/server.rs
@@ -493,7 +493,6 @@ mod tests {
"Operation",
"ResourceId",
"Scope",
- "SemanticVersion",
"VersionString",
] {
assert!(
@@ -509,6 +508,10 @@ mod tests {
"component collision must not rename {name}"
);
}
+
+ assert!(schemas.contains_key("PolicyFormatVersion"));
+ assert!(schemas.contains_key("PolicyModelPolicyFormatVersion"));
+ assert!(!schemas.contains_key("PolicyFormatVersion2"));
}
#[test]
diff --git a/policies/rust/now-policy/README.md b/policies/rust/now-policy/README.md
index 484ddf0..ee3ecdb 100644
--- a/policies/rust/now-policy/README.md
+++ b/policies/rust/now-policy/README.md
@@ -6,4 +6,6 @@ This crate provides the JSON-only Rust policy model and JSON Schema helpers for
It contains committed `PolicyDocument` and editable `PolicyDraftDocument` types, explicit conversions that add or remove server-managed metadata, and schema generation utilities.
Broker request, response, server, transport, and execution types are intentionally out of scope.
+`PolicyFormatVersion` is a software-managed document-format marker, not a publisher release version. Applications stamp the current `1.0.0` value when creating documents, accept and preserve supported numeric versions in the 1.x line, reject malformed or unsupported-major values, and must not expose it as publisher-authored editable metadata. Policy documents do not contain a `$schema` member; documents that contain it are rejected as unknown-field input under the strict contract.
+
`parse_policy_yaml` was intentionally removed as a breaking change. OpenAPI YAML generation and unrelated YAML inputs are unaffected.
diff --git a/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json b/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json
index 4d50bc4..e0da844 100644
--- a/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json
+++ b/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json
@@ -1,6 +1,5 @@
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.desktop.standard-allowlist",
diff --git a/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json b/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json
index 5ddf8d1..105eebf 100644
--- a/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json
+++ b/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json
@@ -1,6 +1,5 @@
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.desktop.deny-risky-options",
diff --git a/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json b/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json
index 2b65f4c..6a744a9 100644
--- a/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json
+++ b/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json
@@ -1,6 +1,5 @@
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.invalid.failure-decision",
diff --git a/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json b/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json
index 2e2e3b9..f685df2 100644
--- a/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json
+++ b/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json
@@ -1,6 +1,5 @@
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.powershell.advanced-scenarios",
diff --git a/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json b/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json
index ddb5ae3..ccef4c1 100644
--- a/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json
+++ b/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json
@@ -1,6 +1,5 @@
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.powershell.current-user-modules",
diff --git a/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json b/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json
index 77ba46c..4ddb628 100644
--- a/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json
+++ b/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json
@@ -1,6 +1,5 @@
{
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.desktop.scenario-coverage",
diff --git a/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json b/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json
index 66bd0ea..c011677 100644
--- a/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json
+++ b/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json
@@ -1,5 +1,4 @@
{
- "$id": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"definitions": {
@@ -214,12 +213,6 @@
],
"type": "object"
},
- "PolicyDraftSchemaUri": {
- "enum": [
- "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json"
- ],
- "type": "string"
- },
"PolicyEnforcement": {
"additionalProperties": false,
"description": "Enforcement configuration.",
@@ -254,6 +247,12 @@
],
"type": "object"
},
+ "PolicyFormatVersion": {
+ "description": "Software-managed policy document format version.\n\n Readers accept canonical numeric versions in the compatible 1.x line.\n Applications must stamp the current value, `1.0.0`, for new documents and\n must not expose this value as publisher-authored editable metadata.\n Schemas describe the canonical shape; runtime readers additionally bound\n each numeric component to an unsigned 64-bit integer.",
+ "maxLength": 128,
+ "pattern": "^1\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])",
+ "type": "string"
+ },
"PolicyMatch": {
"additionalProperties": false,
"description": "Match criteria for a policy rule. All specified fields must match.\n At least one field must be present.",
@@ -515,12 +514,6 @@
],
"type": "string"
},
- "SemanticVersion": {
- "description": "Semantic version string (SemVer 2.0.0).\n\n Validated at deserialization time using the `semver` crate.",
- "maxLength": 128,
- "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$",
- "type": "string"
- },
"StringPattern": {
"description": "Case-insensitive exact value or wildcard pattern.",
"maxLength": 256,
@@ -566,14 +559,6 @@
},
"description": "An editable policy document without server-managed commit metadata.",
"properties": {
- "$schema": {
- "allOf": [
- {
- "$ref": "#/definitions/PolicyDraftSchemaUri"
- }
- ],
- "description": "Policy schema URI constant."
- },
"Enforcement": {
"allOf": [
{
@@ -590,21 +575,21 @@
],
"description": "Editable policy metadata."
},
- "PolicyType": {
+ "PolicyFormatVersion": {
"allOf": [
{
- "$ref": "#/definitions/PackageBrokerPolicy"
+ "$ref": "#/definitions/PolicyFormatVersion"
}
],
- "description": "Must be `\"PackageBrokerPolicy\"`."
+ "description": "Software-managed policy document format version.\n\n Applications must stamp the current value and must not expose this field\n as publisher-authored editable metadata."
},
- "PolicyVersion": {
+ "PolicyType": {
"allOf": [
{
- "$ref": "#/definitions/SemanticVersion"
+ "$ref": "#/definitions/PackageBrokerPolicy"
}
],
- "description": "Policy syntax version (semver)."
+ "description": "Must be `\"PackageBrokerPolicy\"`."
},
"Rules": {
"description": "Ordered list of policy rules (may be empty; enforcement defaults apply).",
@@ -616,8 +601,7 @@
}
},
"required": [
- "$schema",
- "PolicyVersion",
+ "PolicyFormatVersion",
"PolicyType",
"Metadata",
"Enforcement",
diff --git a/policies/rust/now-policy/schema/devolutions.now-policy.schema.json b/policies/rust/now-policy/schema/devolutions.now-policy.schema.json
index b4a4f85..fc1a12c 100644
--- a/policies/rust/now-policy/schema/devolutions.now-policy.schema.json
+++ b/policies/rust/now-policy/schema/devolutions.now-policy.schema.json
@@ -1,5 +1,4 @@
{
- "$id": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"definitions": {
@@ -188,6 +187,12 @@
],
"type": "object"
},
+ "PolicyFormatVersion": {
+ "description": "Software-managed policy document format version.\n\n Readers accept canonical numeric versions in the compatible 1.x line.\n Applications must stamp the current value, `1.0.0`, for new documents and\n must not expose this value as publisher-authored editable metadata.\n Schemas describe the canonical shape; runtime readers additionally bound\n each numeric component to an unsigned 64-bit integer.",
+ "maxLength": 128,
+ "pattern": "^1\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])",
+ "type": "string"
+ },
"PolicyMatch": {
"additionalProperties": false,
"description": "Match criteria for a policy rule. All specified fields must match.\n At least one field must be present.",
@@ -502,12 +507,6 @@
],
"type": "object"
},
- "PolicySchemaUri": {
- "enum": [
- "https://devolutions.net/schemas/now-policy.schema.1.0.json"
- ],
- "type": "string"
- },
"ResourceId": {
"description": "Resource identifier (policy IDs, rule IDs, request IDs, audit IDs).",
"maxLength": 128,
@@ -529,12 +528,6 @@
],
"type": "string"
},
- "SemanticVersion": {
- "description": "Semantic version string (SemVer 2.0.0).\n\n Validated at deserialization time using the `semver` crate.",
- "maxLength": 128,
- "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$",
- "type": "string"
- },
"StringPattern": {
"description": "Case-insensitive exact value or wildcard pattern.",
"maxLength": 256,
@@ -580,14 +573,6 @@
},
"description": "A policy document governing which package operations are allowed or denied.",
"properties": {
- "$schema": {
- "allOf": [
- {
- "$ref": "#/definitions/PolicySchemaUri"
- }
- ],
- "description": "Policy schema URI constant."
- },
"Enforcement": {
"allOf": [
{
@@ -604,21 +589,21 @@
],
"description": "Policy metadata."
},
- "PolicyType": {
+ "PolicyFormatVersion": {
"allOf": [
{
- "$ref": "#/definitions/PackageBrokerPolicy"
+ "$ref": "#/definitions/PolicyFormatVersion"
}
],
- "description": "Must be `\"PackageBrokerPolicy\"`."
+ "description": "Software-managed policy document format version.\n\n Applications must not expose this field as publisher-authored editable metadata."
},
- "PolicyVersion": {
+ "PolicyType": {
"allOf": [
{
- "$ref": "#/definitions/SemanticVersion"
+ "$ref": "#/definitions/PackageBrokerPolicy"
}
],
- "description": "Policy syntax version (semver)."
+ "description": "Must be `\"PackageBrokerPolicy\"`."
},
"Rules": {
"description": "Ordered list of policy rules (may be empty; enforcement defaults apply).",
@@ -630,8 +615,7 @@
}
},
"required": [
- "$schema",
- "PolicyVersion",
+ "PolicyFormatVersion",
"PolicyType",
"Metadata",
"Enforcement",
diff --git a/policies/rust/now-policy/src/markers.rs b/policies/rust/now-policy/src/markers.rs
index dcea0b5..be12479 100644
--- a/policies/rust/now-policy/src/markers.rs
+++ b/policies/rust/now-policy/src/markers.rs
@@ -51,21 +51,3 @@ fixed_string_marker! {
/// Marker type for policy type: serializes to `"PackageBrokerPolicy"`.
pub struct PackageBrokerPolicy => "PackageBrokerPolicy";
}
-
-/// Schema URI for package policy documents.
-pub const POLICY_SCHEMA_URI: &str = "https://devolutions.net/schemas/now-policy.schema.1.0.json";
-
-/// Schema URI for editable package policy draft documents.
-pub const POLICY_DRAFT_SCHEMA_URI: &str = "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json";
-
-fixed_string_marker! {
- /// Marker type for the policy `$schema` field.
- /// Serializes to the canonical policy schema URI.
- pub struct PolicySchemaUri => POLICY_SCHEMA_URI;
-}
-
-fixed_string_marker! {
- /// Marker type for the policy draft `$schema` field.
- /// Serializes to the canonical policy draft schema URI.
- pub struct PolicyDraftSchemaUri => POLICY_DRAFT_SCHEMA_URI;
-}
diff --git a/policies/rust/now-policy/src/newtypes.rs b/policies/rust/now-policy/src/newtypes.rs
index 9283f55..22292da 100644
--- a/policies/rust/now-policy/src/newtypes.rs
+++ b/policies/rust/now-policy/src/newtypes.rs
@@ -99,6 +99,110 @@ impl From<&str> for SemanticVersion {
}
}
+/// Current policy document format version emitted for new documents.
+pub const CURRENT_POLICY_FORMAT_VERSION: &str = "1.0.0";
+
+/// Software-managed policy document format version.
+///
+/// Readers accept canonical numeric versions in the compatible 1.x line.
+/// Applications must stamp the current value, `1.0.0`, for new documents and
+/// must not expose this value as publisher-authored editable metadata.
+/// Schemas describe the canonical shape; runtime readers additionally bound
+/// each numeric component to an unsigned 64-bit integer.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
+pub struct PolicyFormatVersion(
+ #[schemars(
+ length(max = 128),
+ regex(pattern = r"^1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S])")
+ )]
+ String,
+);
+
+impl PolicyFormatVersion {
+ /// Parse a supported policy document format version.
+ pub fn parse(s: &str) -> Result {
+ if s.len() > 128 {
+ return Err(ModelValidationError::Invalid {
+ type_name: "PolicyFormatVersion",
+ reason: format!("length {} exceeds maximum 128", s.len()),
+ });
+ }
+
+ let mut components = s.split('.');
+ let (Some(major), Some(minor), Some(patch), None) = (
+ components.next(),
+ components.next(),
+ components.next(),
+ components.next(),
+ ) else {
+ return Err(ModelValidationError::Invalid {
+ type_name: "PolicyFormatVersion",
+ reason: "must contain exactly three numeric components".to_owned(),
+ });
+ };
+
+ let parse_component = |component: &str| {
+ if component.is_empty()
+ || (component.len() > 1 && component.starts_with('0'))
+ || !component.bytes().all(|byte| byte.is_ascii_digit())
+ {
+ return None;
+ }
+ component.parse::().ok()
+ };
+
+ let (Some(major), Some(_minor), Some(_patch)) =
+ (parse_component(major), parse_component(minor), parse_component(patch))
+ else {
+ return Err(ModelValidationError::Invalid {
+ type_name: "PolicyFormatVersion",
+ reason: "components must be canonical unsigned 64-bit integers".to_owned(),
+ });
+ };
+
+ if major != 1 {
+ return Err(ModelValidationError::Invalid {
+ type_name: "PolicyFormatVersion",
+ reason: format!("unsupported major version {}; supported major version is 1", major),
+ });
+ }
+
+ Ok(Self(s.to_owned()))
+ }
+
+ /// Return the current version stamped on new documents.
+ pub fn current() -> Self {
+ Self(CURRENT_POLICY_FORMAT_VERSION.to_owned())
+ }
+}
+
+impl Default for PolicyFormatVersion {
+ fn default() -> Self {
+ Self::current()
+ }
+}
+
+impl<'de> Deserialize<'de> for PolicyFormatVersion {
+ fn deserialize>(deserializer: D) -> Result {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+impl std::ops::Deref for PolicyFormatVersion {
+ type Target = str;
+
+ fn deref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl std::fmt::Display for PolicyFormatVersion {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&self.0)
+ }
+}
+
/// Resource identifier (policy IDs, rule IDs, request IDs, audit IDs).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)]
pub struct ResourceId(
diff --git a/policies/rust/now-policy/src/policy.rs b/policies/rust/now-policy/src/policy.rs
index a78e437..43c4968 100644
--- a/policies/rust/now-policy/src/policy.rs
+++ b/policies/rust/now-policy/src/policy.rs
@@ -8,8 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::{
Architecture, CustomParameterString, Decision, Elevation, HttpUrl, ManagerName, ModelValidationError, Operation,
- PackageBrokerPolicy, PolicyDraftSchemaUri, PolicySchemaUri, ResourceId, Scope, SemanticVersion, StringPattern,
- VersionString,
+ PackageBrokerPolicy, PolicyFormatVersion, ResourceId, Scope, StringPattern, VersionString,
};
const MAX_POLICY_REVISION: u32 = 2_147_483_647;
@@ -20,12 +19,10 @@ const MAX_POLICY_REVISION: u32 = 2_147_483_647;
#[serde(rename_all = "PascalCase")]
#[serde(deny_unknown_fields)]
pub struct PolicyDocument {
- /// Policy schema URI constant.
- #[serde(rename = "$schema")]
- pub _schema: PolicySchemaUri,
-
- /// Policy syntax version (semver).
- pub policy_version: SemanticVersion,
+ /// Software-managed policy document format version.
+ ///
+ /// Applications must not expose this field as publisher-authored editable metadata.
+ pub policy_format_version: PolicyFormatVersion,
/// Must be `"PackageBrokerPolicy"`.
pub policy_type: PackageBrokerPolicy,
@@ -45,8 +42,7 @@ impl PolicyDocument {
/// Create an editable draft, intentionally omitting server-managed commit metadata.
pub fn to_draft(&self) -> PolicyDraftDocument {
PolicyDraftDocument {
- _schema: PolicyDraftSchemaUri,
- policy_version: self.policy_version.clone(),
+ policy_format_version: self.policy_format_version.clone(),
policy_type: self.policy_type,
metadata: self.metadata.to_draft(),
enforcement: self.enforcement.clone(),
@@ -61,12 +57,11 @@ impl PolicyDocument {
#[serde(rename_all = "PascalCase")]
#[serde(deny_unknown_fields)]
pub struct PolicyDraftDocument {
- /// Policy draft schema URI constant.
- #[serde(rename = "$schema")]
- pub _schema: PolicyDraftSchemaUri,
-
- /// Policy syntax version (semver).
- pub policy_version: SemanticVersion,
+ /// Software-managed policy document format version.
+ ///
+ /// Applications must stamp the current value and must not expose this field
+ /// as publisher-authored editable metadata.
+ pub policy_format_version: PolicyFormatVersion,
/// Must be `"PackageBrokerPolicy"`.
pub policy_type: PackageBrokerPolicy,
@@ -97,8 +92,7 @@ impl PolicyDraftDocument {
}
Ok(PolicyDocument {
- _schema: PolicySchemaUri,
- policy_version: self.policy_version,
+ policy_format_version: self.policy_format_version,
policy_type: self.policy_type,
metadata: self.metadata.into_policy_metadata(revision, published_at),
enforcement: self.enforcement,
diff --git a/policies/rust/now-policy/src/schema.rs b/policies/rust/now-policy/src/schema.rs
index 1a784cb..f6373ee 100644
--- a/policies/rust/now-policy/src/schema.rs
+++ b/policies/rust/now-policy/src/schema.rs
@@ -4,7 +4,7 @@ use schemars::generate::SchemaSettings;
use crate::{PolicyDocument, PolicyDraftDocument};
-/// Get the generated policy schema as a JSON value.
+/// Get the generated repository-local policy schema as a JSON value.
pub fn policy_schema_json() -> serde_json::Value {
let schema = SchemaSettings::draft07()
.into_generator()
@@ -12,7 +12,7 @@ pub fn policy_schema_json() -> serde_json::Value {
serde_json::to_value(&schema).expect("BUG: schema serialization failed")
}
-/// Get the generated editable policy draft schema as a JSON value.
+/// Get the generated repository-local editable policy draft schema as a JSON value.
pub fn policy_draft_schema_json() -> serde_json::Value {
let schema = SchemaSettings::draft07()
.into_generator()
diff --git a/policies/rust/now-policy/tests/policy_samples.rs b/policies/rust/now-policy/tests/policy_samples.rs
index 3314757..549e8a3 100644
--- a/policies/rust/now-policy/tests/policy_samples.rs
+++ b/policies/rust/now-policy/tests/policy_samples.rs
@@ -5,9 +5,7 @@
use std::path::PathBuf;
use chrono::{TimeZone, Utc};
-use now_policy::{
- CustomParameterString, POLICY_DRAFT_SCHEMA_URI, POLICY_SCHEMA_URI, PolicyDocument, StringPattern, VersionString,
-};
+use now_policy::{CURRENT_POLICY_FORMAT_VERSION, CustomParameterString, PolicyDocument, StringPattern, VersionString};
fn samples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/samples")
@@ -42,17 +40,22 @@ fn draft_conversion_omits_and_restores_server_metadata() {
let draft = committed.to_draft();
let draft_json = serde_json::to_value(&draft).unwrap();
- assert_eq!(draft_json["$schema"], POLICY_DRAFT_SCHEMA_URI);
+ assert!(draft_json.get("$schema").is_none());
+ assert_eq!(draft_json["PolicyFormatVersion"], CURRENT_POLICY_FORMAT_VERSION);
assert!(draft_json["Metadata"].get("Revision").is_none());
assert!(draft_json["Metadata"].get("PublishedAt").is_none());
let published_at = Utc.with_ymd_and_hms(2026, 8, 29, 0, 0, 0).unwrap();
let recommitted = draft.into_policy_document(7, published_at).unwrap();
- assert_eq!(
- serde_json::to_value(&recommitted).unwrap()["$schema"],
- POLICY_SCHEMA_URI
- );
+ let recommitted_json = serde_json::to_value(&recommitted).unwrap();
+ assert!(recommitted_json.get("$schema").is_none());
+ assert_eq!(recommitted_json["PolicyFormatVersion"], CURRENT_POLICY_FORMAT_VERSION);
assert_eq!(recommitted.metadata.id.to_string(), committed.metadata.id.to_string());
+ assert_eq!(recommitted.metadata.publisher, committed.metadata.publisher);
+ assert_eq!(recommitted.metadata.description, committed.metadata.description);
+ assert_eq!(recommitted.metadata.support_url, committed.metadata.support_url);
+ assert_eq!(recommitted.metadata.valid_from, committed.metadata.valid_from);
+ assert_eq!(recommitted.metadata.valid_until, committed.metadata.valid_until);
assert_eq!(recommitted.metadata.revision, 7);
assert_eq!(recommitted.metadata.published_at, published_at);
}
@@ -107,8 +110,7 @@ fn policy_text_newtypes_count_unicode_scalars_at_length_boundaries() {
#[test]
fn invalid_policy_unknown_field_fails_deserialization() {
let value = serde_json::json!({
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "test",
@@ -128,6 +130,68 @@ fn invalid_policy_unknown_field_fails_deserialization() {
assert!(result.is_err(), "policy with unknown field should fail deserialization");
}
+#[test]
+fn schema_field_is_rejected() {
+ let path = samples_dir().join("corporate-allowlist.policy.json");
+ let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
+ value["$schema"] = serde_json::json!("https://example.invalid/policy.schema.json");
+
+ let error = serde_json::from_value::(value).unwrap_err().to_string();
+ assert!(error.contains("unknown field `$schema`"), "unexpected error: {error}");
+}
+
+#[test]
+fn unsupported_policy_format_version_is_rejected() {
+ let path = samples_dir().join("corporate-allowlist.policy.json");
+ let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
+ value["PolicyFormatVersion"] = serde_json::json!("2.0.0");
+
+ let error = serde_json::from_value::(value).unwrap_err().to_string();
+ assert!(
+ error.contains("unsupported major version 2"),
+ "unexpected error: {error}"
+ );
+}
+
+#[test]
+fn malformed_policy_format_versions_are_rejected() {
+ let path = samples_dir().join("corporate-allowlist.policy.json");
+ let valid: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
+
+ for version in ["not-semver", "1.02.3", "1.2.3-beta", "1.0.0\n"] {
+ let mut value = valid.clone();
+ value["PolicyFormatVersion"] = serde_json::json!(version);
+ assert!(
+ serde_json::from_value::(value).is_err(),
+ "{version:?} should be rejected"
+ );
+ }
+}
+
+#[test]
+fn compatible_policy_format_version_is_preserved_by_conversions() {
+ let path = samples_dir().join("corporate-allowlist.policy.json");
+ let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
+ value["PolicyFormatVersion"] = serde_json::json!("1.2.3");
+
+ let committed = serde_json::from_value::(value).unwrap();
+ let draft = committed.to_draft();
+ assert_eq!(draft.policy_format_version.to_string(), "1.2.3");
+ let recommitted = draft
+ .into_policy_document(5, Utc.with_ymd_and_hms(2026, 8, 29, 0, 0, 0).unwrap())
+ .unwrap();
+ assert_eq!(recommitted.policy_format_version.to_string(), "1.2.3");
+}
+
+#[test]
+fn policy_format_version_rejects_components_outside_u64_range() {
+ let path = samples_dir().join("corporate-allowlist.policy.json");
+ let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
+ value["PolicyFormatVersion"] = serde_json::json!("1.18446744073709551616.0");
+
+ assert!(serde_json::from_value::(value).is_err());
+}
+
#[test]
fn invalid_policy_fixture_fails_deserialization() {
let path = samples_dir().join("invalid/policies/invalid-failure-decision.policy.json");
@@ -147,6 +211,24 @@ fn policy_schema_generates_valid_json() {
);
}
+#[test]
+fn policy_schemas_are_repository_local_and_omit_document_schema() {
+ for schema in [
+ now_policy::schema::policy_schema_json(),
+ now_policy::schema::policy_draft_schema_json(),
+ ] {
+ let properties = schema["properties"].as_object().unwrap();
+ let required = schema["required"].as_array().unwrap();
+ assert!(schema.get("$id").is_none());
+ assert!(!properties.contains_key("$schema"));
+ assert!(!required.iter().any(|value| value == "$schema"));
+
+ let version_schema = &schema["definitions"]["PolicyFormatVersion"];
+ assert_eq!(version_schema["type"], "string");
+ assert!(version_schema["pattern"].as_str().unwrap().starts_with("^1\\."));
+ }
+}
+
#[test]
fn committed_policy_enforces_revision_bounds_during_serialization_and_deserialization() {
let path = samples_dir().join("corporate-allowlist.policy.json");
diff --git a/policies/rust/now-policy/tools/generate_schema.rs b/policies/rust/now-policy/tools/generate_schema.rs
index a80a80f..ebd4cf6 100644
--- a/policies/rust/now-policy/tools/generate_schema.rs
+++ b/policies/rust/now-policy/tools/generate_schema.rs
@@ -8,39 +8,24 @@
use std::path::Path;
use now_policy::schema::{policy_draft_schema_json, policy_schema_json};
-use now_policy::{POLICY_DRAFT_SCHEMA_URI, POLICY_SCHEMA_URI};
-use serde_json::{Map, Value};
+use serde_json::Value;
fn main() {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
write_schema(
&crate_dir.join("schema").join("devolutions.now-policy.schema.json"),
policy_schema_json(),
- POLICY_SCHEMA_URI,
);
write_schema(
&crate_dir
.join("schema")
.join("devolutions.now-policy-draft.schema.json"),
policy_draft_schema_json(),
- POLICY_DRAFT_SCHEMA_URI,
);
}
-fn write_schema(path: &Path, schema: Value, id: &str) {
- let json = serde_json::to_string_pretty(&with_id(schema, id)).expect("BUG: schema serialization failed");
+fn write_schema(path: &Path, schema: Value) {
+ let json = serde_json::to_string_pretty(&schema).expect("BUG: schema serialization failed");
std::fs::write(path, &json).unwrap_or_else(|e| panic!("failed to write {}: {e}", path.display()));
println!("Wrote {}", path.display());
}
-
-fn with_id(schema: Value, id: &str) -> Value {
- let Value::Object(existing) = schema else {
- panic!("BUG: schema root is not an object");
- };
-
- let mut object = Map::new();
- object.insert("$id".to_owned(), Value::String(id.to_owned()));
- object.extend(existing);
-
- Value::Object(object)
-}
diff --git a/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json b/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json
index 5ffc01e..0503209 100644
--- a/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json
+++ b/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json
@@ -10,8 +10,7 @@
"ValidatorVersion": "gateway-policy-validator/1",
"IsValid": true,
"CanonicalDraft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
diff --git a/policies/test-data/package-broker/requests/policy-replacement.create.request.json b/policies/test-data/package-broker/requests/policy-replacement.create.request.json
index 04fd05b..c1e94e6 100644
--- a/policies/test-data/package-broker/requests/policy-replacement.create.request.json
+++ b/policies/test-data/package-broker/requests/policy-replacement.create.request.json
@@ -6,8 +6,7 @@
"ConflictHandling": "Reject",
"WarningsAcknowledged": false,
"Draft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" },
"Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" },
diff --git a/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json b/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json
index af5c350..8e08b9e 100644
--- a/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json
+++ b/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json
@@ -6,8 +6,7 @@
"ConflictHandling": "ConfirmOverwrite",
"WarningsAcknowledged": true,
"Draft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" },
"Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" },
diff --git a/policies/test-data/package-broker/requests/policy-replacement.repair.request.json b/policies/test-data/package-broker/requests/policy-replacement.repair.request.json
index f0729a3..9e7e6bb 100644
--- a/policies/test-data/package-broker/requests/policy-replacement.repair.request.json
+++ b/policies/test-data/package-broker/requests/policy-replacement.repair.request.json
@@ -6,8 +6,7 @@
"ConflictHandling": "Reject",
"WarningsAcknowledged": false,
"Draft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" },
"Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" },
diff --git a/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json b/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json
index 13668af..257cf42 100644
--- a/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json
+++ b/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json
@@ -6,8 +6,7 @@
"ConflictHandling": "Reject",
"WarningsAcknowledged": false,
"Draft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": { "Id": "fabrikam.package-policy", "Publisher": "Fabrikam IT" },
"Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" },
diff --git a/policies/test-data/package-broker/requests/policy-replacement.update.request.json b/policies/test-data/package-broker/requests/policy-replacement.update.request.json
index f4b53b9..ba20df6 100644
--- a/policies/test-data/package-broker/requests/policy-replacement.update.request.json
+++ b/policies/test-data/package-broker/requests/policy-replacement.update.request.json
@@ -6,8 +6,7 @@
"ConflictHandling": "Reject",
"WarningsAcknowledged": true,
"Draft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" },
"Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" },
diff --git a/policies/test-data/package-broker/requests/policy-validation.request.json b/policies/test-data/package-broker/requests/policy-validation.request.json
index 641c8d1..2179ea6 100644
--- a/policies/test-data/package-broker/requests/policy-validation.request.json
+++ b/policies/test-data/package-broker/requests/policy-validation.request.json
@@ -2,8 +2,7 @@
"RequestKind": "PolicyValidationRequest",
"RequestVersion": "1.0",
"Draft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
diff --git a/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json b/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json
index feea983..cf08869 100644
--- a/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json
+++ b/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json
@@ -18,7 +18,7 @@
"Policy": {
"Id": "contoso.desktop.standard-allowlist",
"Revision": 4,
- "PolicyVersion": "1.0.0"
+ "PolicyFormatVersion": "1.0.0"
},
"Operation": {
"OperationId": "op-winget-vscode-install-000001",
diff --git a/policies/test-data/package-broker/responses/policy-management.active.response.json b/policies/test-data/package-broker/responses/policy-management.active.response.json
index 5a96677..4a73225 100644
--- a/policies/test-data/package-broker/responses/policy-management.active.response.json
+++ b/policies/test-data/package-broker/responses/policy-management.active.response.json
@@ -13,8 +13,7 @@
"WriteCapability": "Writable",
"ElevationRequired": true,
"Policy": {
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
diff --git a/policies/test-data/package-broker/responses/policy-management.invalid.response.json b/policies/test-data/package-broker/responses/policy-management.invalid.response.json
index 023e84f..dd17ffa 100644
--- a/policies/test-data/package-broker/responses/policy-management.invalid.response.json
+++ b/policies/test-data/package-broker/responses/policy-management.invalid.response.json
@@ -19,13 +19,13 @@
{
"FindingVersion": "1.0",
"Severity": "Error",
- "Code": "UnsupportedPolicyVersion",
- "Path": "/PolicyVersion",
+ "Code": "UnsupportedPolicyFormatVersion",
+ "Path": "/PolicyFormatVersion",
"Arguments": {
"actual": "2.0.0",
- "supported": "1.0.0"
+ "supported": "1.x"
},
- "Message": "The policy version is not supported."
+ "Message": "The policy format version is not supported."
}
]
}
diff --git a/policies/test-data/package-broker/responses/policy-replacement.response.json b/policies/test-data/package-broker/responses/policy-replacement.response.json
index e072c1e..c1d2d6d 100644
--- a/policies/test-data/package-broker/responses/policy-replacement.response.json
+++ b/policies/test-data/package-broker/responses/policy-replacement.response.json
@@ -6,8 +6,7 @@
"Transport": "HttpNamedPipe"
},
"Policy": {
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
@@ -26,8 +25,7 @@
"ValidatorVersion": "gateway-policy-validator/1",
"IsValid": true,
"CanonicalDraft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
@@ -50,8 +48,7 @@
"WriteCapability": "Writable",
"ElevationRequired": true,
"Policy": {
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
diff --git a/policies/test-data/package-broker/responses/policy-stale-token.error.json b/policies/test-data/package-broker/responses/policy-stale-token.error.json
index 3d7cdc0..300b6e9 100644
--- a/policies/test-data/package-broker/responses/policy-stale-token.error.json
+++ b/policies/test-data/package-broker/responses/policy-stale-token.error.json
@@ -15,8 +15,7 @@
"WriteCapability": "Writable",
"ElevationRequired": true,
"Policy": {
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
diff --git a/policies/test-data/package-broker/responses/policy-validation.invalid.response.json b/policies/test-data/package-broker/responses/policy-validation.invalid.response.json
index 3b1ec8e..2924228 100644
--- a/policies/test-data/package-broker/responses/policy-validation.invalid.response.json
+++ b/policies/test-data/package-broker/responses/policy-validation.invalid.response.json
@@ -22,9 +22,8 @@
{ "FindingVersion": "1.0", "Severity": "Error", "Code": "InvalidWildcardPattern", "Path": "/Rules/0/Match/PackageIdentifiers/0", "RuleId": "duplicate", "Message": "The wildcard pattern is invalid." },
{ "FindingVersion": "1.0", "Severity": "Error", "Code": "ContradictoryConstraints", "Path": "/Rules/0/Constraints", "RuleId": "duplicate", "Message": "The constraints contradict each other." },
{ "FindingVersion": "1.0", "Severity": "Error", "Code": "InvalidValidityInterval", "Path": "/Metadata/ValidUntil", "Message": "ValidUntil must be after ValidFrom." },
- { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedSchema", "Path": "/$schema", "Message": "The policy schema is not supported." },
{ "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedPolicyType", "Path": "/PolicyType", "Message": "The policy type is not supported." },
- { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedPolicyVersion", "Path": "/PolicyVersion", "Message": "The policy version is not supported." }
+ { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedPolicyFormatVersion", "Path": "/PolicyFormatVersion", "Message": "The policy format version is not supported." }
]
}
}
diff --git a/policies/test-data/package-broker/responses/policy-validation.valid.response.json b/policies/test-data/package-broker/responses/policy-validation.valid.response.json
index ad39185..fd5703f 100644
--- a/policies/test-data/package-broker/responses/policy-validation.valid.response.json
+++ b/policies/test-data/package-broker/responses/policy-validation.valid.response.json
@@ -10,8 +10,7 @@
"ValidatorVersion": "gateway-policy-validator/1",
"IsValid": true,
"CanonicalDraft": {
- "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.package-policy",
diff --git a/policies/test-data/package-broker/responses/policy.response.json b/policies/test-data/package-broker/responses/policy.response.json
index e1efb1d..a21f34f 100644
--- a/policies/test-data/package-broker/responses/policy.response.json
+++ b/policies/test-data/package-broker/responses/policy.response.json
@@ -6,8 +6,7 @@
"Transport": "HttpNamedPipe"
},
"Policy": {
- "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json",
- "PolicyVersion": "1.0.0",
+ "PolicyFormatVersion": "1.0.0",
"PolicyType": "PackageBrokerPolicy",
"Metadata": {
"Id": "contoso.desktop.standard-allowlist",
diff --git a/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json b/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json
index ac01810..7a3f534 100644
--- a/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json
+++ b/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json
@@ -19,7 +19,7 @@
"Policy": {
"Id": "contoso.desktop.standard-allowlist",
"Revision": 4,
- "PolicyVersion": "1.0.0"
+ "PolicyFormatVersion": "1.0.0"
},
"Diagnostics": {
"CommandPreview": [
diff --git a/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json b/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json
index 3e17099..67dc132 100644
--- a/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json
+++ b/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json
@@ -19,7 +19,7 @@
"Policy": {
"Id": "contoso.desktop.standard-allowlist",
"Revision": 4,
- "PolicyVersion": "1.0.0"
+ "PolicyFormatVersion": "1.0.0"
},
"Diagnostics": {},
"Server": {