feat: add PolicyPackSelectorComponent with tests and integration
- Implemented PolicyPackSelectorComponent for selecting policy packs. - Added unit tests for component behavior, including API success and error handling. - Introduced monaco-workers type declarations for editor workers. - Created acceptance tests for guardrails with stubs for AT1–AT10. - Established SCA Failure Catalogue Fixtures for regression testing. - Developed plugin determinism harness with stubs for PL1–PL10. - Added scripts for evidence upload and verification processes.
This commit is contained in:
@@ -40,7 +40,7 @@ internal static class TaskPackPlanHasher
|
||||
var json = CanonicalJson.Serialize(canonical);
|
||||
using var sha256 = SHA256.Create();
|
||||
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(json));
|
||||
return ConvertToHex(hashBytes);
|
||||
return $"sha256:{ConvertToHex(hashBytes)}";
|
||||
}
|
||||
|
||||
private static string ConvertToHex(byte[] hashBytes)
|
||||
|
||||
@@ -22,16 +22,17 @@ public sealed class TaskPackPlanner
|
||||
this.egressPolicy = egressPolicy;
|
||||
}
|
||||
|
||||
public TaskPackPlanResult Plan(TaskPackManifest manifest, IDictionary<string, JsonNode?>? providedInputs = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
|
||||
var errors = ImmutableArray.CreateBuilder<TaskPackPlanError>();
|
||||
|
||||
var validation = validator.Validate(manifest);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
foreach (var error in validation.Errors)
|
||||
public TaskPackPlanResult Plan(TaskPackManifest manifest, IDictionary<string, JsonNode?>? providedInputs = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
|
||||
var errors = ImmutableArray.CreateBuilder<TaskPackPlanError>();
|
||||
ValidateSandboxAndSlo(manifest, errors);
|
||||
|
||||
var validation = validator.Validate(manifest);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
foreach (var error in validation.Errors)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError(error.Path, error.Message));
|
||||
}
|
||||
@@ -106,10 +107,70 @@ public sealed class TaskPackPlanner
|
||||
return new TaskPackPlanResult(plan, ImmutableArray<TaskPackPlanError>.Empty);
|
||||
}
|
||||
|
||||
private static void ValidateSandboxAndSlo(TaskPackManifest manifest, ImmutableArray<TaskPackPlanError>.Builder errors)
|
||||
{
|
||||
// TP6: sandbox quotas must be present.
|
||||
var sandbox = manifest.Spec.Sandbox;
|
||||
if (sandbox is null)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.sandbox", "Sandbox settings are required (mode, egressAllowlist, CPU/memory, quotaSeconds)."));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sandbox.Mode))
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.sandbox.mode", "Sandbox mode is required (sealed or restricted)."));
|
||||
}
|
||||
|
||||
if (sandbox.EgressAllowlist is null)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.sandbox.egressAllowlist", "Egress allowlist must be declared (empty list allowed)."));
|
||||
}
|
||||
|
||||
if (sandbox.CpuLimitMillicores <= 0)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.sandbox.cpuLimitMillicores", "CPU limit must be > 0."));
|
||||
}
|
||||
|
||||
if (sandbox.MemoryLimitMiB <= 0)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.sandbox.memoryLimitMiB", "Memory limit must be > 0."));
|
||||
}
|
||||
|
||||
if (sandbox.QuotaSeconds <= 0)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.sandbox.quotaSeconds", "quotaSeconds must be > 0."));
|
||||
}
|
||||
}
|
||||
|
||||
// TP9: SLOs must be declared and positive.
|
||||
var slo = manifest.Spec.Slo;
|
||||
if (slo is null)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.slo", "SLO section is required (runP95Seconds, approvalP95Seconds, maxQueueDepth)."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (slo.RunP95Seconds <= 0)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.slo.runP95Seconds", "runP95Seconds must be > 0."));
|
||||
}
|
||||
|
||||
if (slo.ApprovalP95Seconds <= 0)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.slo.approvalP95Seconds", "approvalP95Seconds must be > 0."));
|
||||
}
|
||||
|
||||
if (slo.MaxQueueDepth <= 0)
|
||||
{
|
||||
errors.Add(new TaskPackPlanError("spec.slo.maxQueueDepth", "maxQueueDepth must be > 0."));
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, JsonNode?> MaterializeInputs(
|
||||
IReadOnlyList<TaskPackInput>? definitions,
|
||||
IDictionary<string, JsonNode?>? providedInputs,
|
||||
ImmutableArray<TaskPackPlanError>.Builder errors)
|
||||
IDictionary<string, JsonNode?>? providedInputs,
|
||||
ImmutableArray<TaskPackPlanError>.Builder errors)
|
||||
{
|
||||
var effective = new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ public sealed class TaskPackMaintainer
|
||||
public string? Email { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TaskPackSpec
|
||||
{
|
||||
[JsonPropertyName("inputs")]
|
||||
public IReadOnlyList<TaskPackInput>? Inputs { get; init; }
|
||||
|
||||
public sealed class TaskPackSpec
|
||||
{
|
||||
[JsonPropertyName("inputs")]
|
||||
public IReadOnlyList<TaskPackInput>? Inputs { get; init; }
|
||||
|
||||
[JsonPropertyName("secrets")]
|
||||
public IReadOnlyList<TaskPackSecret>? Secrets { get; init; }
|
||||
|
||||
@@ -72,11 +72,17 @@ public sealed class TaskPackSpec
|
||||
public IReadOnlyList<TaskPackOutput>? Outputs { get; init; }
|
||||
|
||||
[JsonPropertyName("success")]
|
||||
public TaskPackSuccess? Success { get; init; }
|
||||
|
||||
[JsonPropertyName("failure")]
|
||||
public TaskPackFailure? Failure { get; init; }
|
||||
}
|
||||
public TaskPackSuccess? Success { get; init; }
|
||||
|
||||
[JsonPropertyName("failure")]
|
||||
public TaskPackFailure? Failure { get; init; }
|
||||
|
||||
[JsonPropertyName("sandbox")]
|
||||
public TaskPackSandbox? Sandbox { get; init; }
|
||||
|
||||
[JsonPropertyName("slo")]
|
||||
public TaskPackSlo? Slo { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TaskPackInput
|
||||
{
|
||||
@@ -255,11 +261,41 @@ public sealed class TaskPackFailure
|
||||
public TaskPackRetryPolicy? Retries { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TaskPackRetryPolicy
|
||||
{
|
||||
[JsonPropertyName("maxAttempts")]
|
||||
public int MaxAttempts { get; init; }
|
||||
|
||||
[JsonPropertyName("backoffSeconds")]
|
||||
public int BackoffSeconds { get; init; }
|
||||
}
|
||||
public sealed class TaskPackRetryPolicy
|
||||
{
|
||||
[JsonPropertyName("maxAttempts")]
|
||||
public int MaxAttempts { get; init; }
|
||||
|
||||
[JsonPropertyName("backoffSeconds")]
|
||||
public int BackoffSeconds { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TaskPackSandbox
|
||||
{
|
||||
[JsonPropertyName("mode")]
|
||||
public string? Mode { get; init; }
|
||||
|
||||
[JsonPropertyName("egressAllowlist")]
|
||||
public IReadOnlyList<string>? EgressAllowlist { get; init; }
|
||||
|
||||
[JsonPropertyName("cpuLimitMillicores")]
|
||||
public int CpuLimitMillicores { get; init; }
|
||||
|
||||
[JsonPropertyName("memoryLimitMiB")]
|
||||
public int MemoryLimitMiB { get; init; }
|
||||
|
||||
[JsonPropertyName("quotaSeconds")]
|
||||
public int QuotaSeconds { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TaskPackSlo
|
||||
{
|
||||
[JsonPropertyName("runP95Seconds")]
|
||||
public int RunP95Seconds { get; init; }
|
||||
|
||||
[JsonPropertyName("approvalP95Seconds")]
|
||||
public int ApprovalP95Seconds { get; init; }
|
||||
|
||||
[JsonPropertyName("maxQueueDepth")]
|
||||
public int MaxQueueDepth { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
@@ -34,6 +35,14 @@ public sealed class PackRunApprovalDecisionService
|
||||
var runId = request.RunId.Trim();
|
||||
var approvalId = request.ApprovalId.Trim();
|
||||
|
||||
if (!IsSha256Digest(request.PlanHash))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Approval decision for run {RunId} rejected – plan hash format invalid (expected sha256:<64-hex>).",
|
||||
runId);
|
||||
return PackRunApprovalDecisionResult.PlanHashMismatch;
|
||||
}
|
||||
|
||||
var state = await _stateStore.GetAsync(runId, cancellationToken).ConfigureAwait(false);
|
||||
if (state is null)
|
||||
{
|
||||
@@ -101,6 +110,14 @@ public sealed class PackRunApprovalDecisionService
|
||||
|
||||
return PackRunApprovalDecisionResult.Applied;
|
||||
}
|
||||
|
||||
private static bool IsSha256Digest(string value)
|
||||
=> !string.IsNullOrWhiteSpace(value)
|
||||
&& Sha256Pattern.IsMatch(value);
|
||||
|
||||
private static readonly Regex Sha256Pattern = new(
|
||||
"^sha256:[0-9a-f]{64}$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
public sealed record PackRunApprovalDecisionRequest(
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class PackRunApprovalDecisionServiceTests
|
||||
NullLogger<PackRunApprovalDecisionService>.Instance);
|
||||
|
||||
var result = await service.ApplyAsync(
|
||||
new PackRunApprovalDecisionRequest("missing", "approval", "hash", PackRunApprovalDecisionType.Approved, "actor", null),
|
||||
new PackRunApprovalDecisionRequest("missing", "approval", "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", PackRunApprovalDecisionType.Approved, "actor", null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("not_found", result.Status);
|
||||
@@ -107,6 +107,44 @@ public sealed class PackRunApprovalDecisionServiceTests
|
||||
Assert.False(scheduler.ScheduledContexts.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_ReturnsPlanHashMismatchWhenFormatInvalid()
|
||||
{
|
||||
var plan = TestPlanFactory.CreatePlan();
|
||||
var state = TestPlanFactory.CreateState("run-1", plan);
|
||||
var approval = new PackRunApprovalState(
|
||||
"security-review",
|
||||
new[] { "Packs.Approve" },
|
||||
new[] { "step-a" },
|
||||
Array.Empty<string>(),
|
||||
null,
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
PackRunApprovalStatus.Pending);
|
||||
|
||||
var approvalStore = new InMemoryApprovalStore(new Dictionary<string, IReadOnlyList<PackRunApprovalState>>
|
||||
{
|
||||
["run-1"] = new List<PackRunApprovalState> { approval }
|
||||
});
|
||||
var stateStore = new InMemoryStateStore(new Dictionary<string, PackRunState>
|
||||
{
|
||||
["run-1"] = state
|
||||
});
|
||||
var scheduler = new RecordingScheduler();
|
||||
|
||||
var service = new PackRunApprovalDecisionService(
|
||||
approvalStore,
|
||||
stateStore,
|
||||
scheduler,
|
||||
NullLogger<PackRunApprovalDecisionService>.Instance);
|
||||
|
||||
var result = await service.ApplyAsync(
|
||||
new PackRunApprovalDecisionRequest("run-1", "security-review", "not-a-digest", PackRunApprovalDecisionType.Approved, "actor", null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("plan_hash_mismatch", result.Status);
|
||||
Assert.False(scheduler.ScheduledContexts.Any());
|
||||
}
|
||||
|
||||
private sealed class InMemoryApprovalStore : IPackRunApprovalStore
|
||||
{
|
||||
private readonly Dictionary<string, List<PackRunApprovalState>> _approvals;
|
||||
@@ -214,7 +252,7 @@ internal static class TestPlanFactory
|
||||
metadata,
|
||||
new Dictionary<string, JsonNode?>(StringComparer.Ordinal),
|
||||
new[] { step },
|
||||
"hash-123",
|
||||
"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
new[]
|
||||
{
|
||||
new TaskPackPlanApproval("security-review", new[] { "Packs.Approve" }, null, null)
|
||||
|
||||
@@ -36,9 +36,24 @@ public sealed class TaskPackPlannerTests
|
||||
|
||||
var resultB = planner.Plan(manifest, inputs);
|
||||
Assert.True(resultB.Success);
|
||||
Assert.Equal(plan.Hash, resultB.Plan!.Hash);
|
||||
}
|
||||
|
||||
Assert.Equal(plan.Hash, resultB.Plan!.Hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanHash_IsPrefixedSha256Digest()
|
||||
{
|
||||
var manifest = TestManifests.Load(TestManifests.Sample);
|
||||
var planner = new TaskPackPlanner();
|
||||
|
||||
var result = planner.Plan(manifest);
|
||||
Assert.True(result.Success);
|
||||
var hash = result.Plan!.Hash;
|
||||
Assert.StartsWith("sha256:", hash, StringComparison.Ordinal);
|
||||
Assert.Equal(71, hash.Length); // "sha256:" + 64 hex characters
|
||||
var hex = hash.Substring("sha256:".Length);
|
||||
Assert.True(hex.All(c => Uri.IsHexDigit(c)), "Hash contains non-hex characters.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_WhenConditionEvaluatesFalse_DisablesStep()
|
||||
{
|
||||
|
||||
@@ -19,17 +19,27 @@ metadata:
|
||||
version: 1.0.0
|
||||
description: Sample pack for planner tests
|
||||
tags: [tests]
|
||||
spec:
|
||||
inputs:
|
||||
- name: dryRun
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
approvals:
|
||||
spec:
|
||||
inputs:
|
||||
- name: dryRun
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
approvals:
|
||||
- id: security-review
|
||||
grants: ["packs.approve"]
|
||||
steps:
|
||||
- id: plan-step
|
||||
steps:
|
||||
- id: plan-step
|
||||
name: Plan
|
||||
run:
|
||||
uses: builtin:plan
|
||||
@@ -57,6 +67,16 @@ spec:
|
||||
- name: sbomBundle
|
||||
type: object
|
||||
required: true
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: noop
|
||||
run:
|
||||
@@ -71,12 +91,22 @@ kind: TaskPack
|
||||
metadata:
|
||||
name: step-ref-pack
|
||||
version: 1.0.0
|
||||
spec:
|
||||
steps:
|
||||
- id: prepare
|
||||
run:
|
||||
uses: builtin:prepare
|
||||
- id: consume
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: prepare
|
||||
run:
|
||||
uses: builtin:prepare
|
||||
- id: consume
|
||||
run:
|
||||
uses: builtin:consume
|
||||
with:
|
||||
@@ -89,16 +119,26 @@ kind: TaskPack
|
||||
metadata:
|
||||
name: map-pack
|
||||
version: 1.0.0
|
||||
spec:
|
||||
inputs:
|
||||
- name: targets
|
||||
type: array
|
||||
required: true
|
||||
steps:
|
||||
- id: maintenance-loop
|
||||
map:
|
||||
items: "{{ inputs.targets }}"
|
||||
step:
|
||||
spec:
|
||||
inputs:
|
||||
- name: targets
|
||||
type: array
|
||||
required: true
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: maintenance-loop
|
||||
map:
|
||||
items: "{{ inputs.targets }}"
|
||||
step:
|
||||
id: echo-step
|
||||
run:
|
||||
uses: builtin:echo
|
||||
@@ -112,16 +152,26 @@ kind: TaskPack
|
||||
metadata:
|
||||
name: secret-pack
|
||||
version: 1.0.0
|
||||
spec:
|
||||
secrets:
|
||||
spec:
|
||||
secrets:
|
||||
- name: apiKey
|
||||
scope: packs.run
|
||||
description: API authentication token
|
||||
steps:
|
||||
- id: use-secret
|
||||
run:
|
||||
uses: builtin:http
|
||||
with:
|
||||
description: API authentication token
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: use-secret
|
||||
run:
|
||||
uses: builtin:http
|
||||
with:
|
||||
token: "{{ secrets.apiKey }}"
|
||||
""";
|
||||
|
||||
@@ -131,12 +181,22 @@ kind: TaskPack
|
||||
metadata:
|
||||
name: output-pack
|
||||
version: 1.0.0
|
||||
spec:
|
||||
steps:
|
||||
- id: generate
|
||||
run:
|
||||
uses: builtin:generate
|
||||
outputs:
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: generate
|
||||
run:
|
||||
uses: builtin:generate
|
||||
outputs:
|
||||
- name: bundlePath
|
||||
type: file
|
||||
path: artifacts/report.txt
|
||||
@@ -152,6 +212,16 @@ metadata:
|
||||
name: failure-policy-pack
|
||||
version: 1.0.0
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: build
|
||||
run:
|
||||
@@ -170,6 +240,16 @@ metadata:
|
||||
name: parallel-pack
|
||||
version: 1.1.0
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: fanout
|
||||
parallel:
|
||||
@@ -196,6 +276,16 @@ metadata:
|
||||
name: policy-gate-pack
|
||||
version: 1.0.0
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: prepare
|
||||
run:
|
||||
@@ -216,6 +306,16 @@ metadata:
|
||||
name: egress-allowed
|
||||
version: 1.0.0
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: fetch
|
||||
run:
|
||||
@@ -233,6 +333,16 @@ metadata:
|
||||
name: egress-blocked
|
||||
version: 1.0.0
|
||||
spec:
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: fetch
|
||||
run:
|
||||
@@ -252,6 +362,16 @@ spec:
|
||||
- name: targetUrl
|
||||
type: string
|
||||
required: false
|
||||
sandbox:
|
||||
mode: sealed
|
||||
egressAllowlist: []
|
||||
cpuLimitMillicores: 100
|
||||
memoryLimitMiB: 128
|
||||
quotaSeconds: 60
|
||||
slo:
|
||||
runP95Seconds: 300
|
||||
approvalP95Seconds: 900
|
||||
maxQueueDepth: 100
|
||||
steps:
|
||||
- id: fetch
|
||||
run:
|
||||
|
||||
@@ -319,6 +319,11 @@ async Task<IResult> HandleApplyApprovalDecision(
|
||||
return Results.BadRequest(new { error = "planHash is required." });
|
||||
}
|
||||
|
||||
if (!Regex.IsMatch(request.PlanHash, "^sha256:[0-9a-f]{64}$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "planHash must be sha256:<64-hex>." });
|
||||
}
|
||||
|
||||
var result = await decisionService.ApplyAsync(
|
||||
new PackRunApprovalDecisionRequest(runId, approvalId, request.PlanHash, decisionType, request.ActorId, request.Summary),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user