mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
feat(backend): wire WhiteGloveSealingPatternIds through /api/agent/config
The V2-agent AgentConfigResponse schema has carried four fields the backend was expected to populate (Plan §M5): LatestAgentSha256, LatestAgentExeSha256, AllowAgentDowngrade, and WhiteGloveSealingPatternIds. The first three were already wired through adminConfig; the last one was declared on the DTO but never populated from storage — V2 agents always received an empty list, effectively leaving the sealing-pattern emission path dead. Adds the missing plumbing: - AdminConfiguration.WhiteGloveSealingPatternIdsJson (string, JSON-serialised List<string>) + GetWhiteGloveSealingPatternIds() helper. Defaults to empty for null/whitespace/invalid JSON so the feature stays off by default (M3-compatible, no regression on tenants that haven't opted in). Global-only, no per-tenant override (plan §M5 M4.4.5.e decision). - TableConfigRepository Store + Map entries for the new column (feedback_table_storage_serialization — new model properties MUST round-trip through the table mapping or the field silently drops on save/load). - GetAgentConfigFunction populates WhiteGloveSealingPatternIds from adminConfig.GetWhiteGloveSealingPatternIds(). ConfigVersion bumped 25 → 26 as a semantic-change marker. 5 new tests mirror the HarmlessEventIds pattern (null / whitespace / invalid JSON / valid round-trip / explicit empty array). 605/605 backend tests green. Agent-side (M4.4.4 / M4.4.5.e): the V2 agent's OnPatternMatched hook already reads WhiteGloveSealingPatternIds from the cached config and re-emits matching IME pattern hits as WhiteGloveSealingPatternDetected DecisionSignals. With this commit the data path is end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+69
@@ -0,0 +1,69 @@
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Functions.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AdminConfiguration.GetWhiteGloveSealingPatternIds"/>. Unlike the
|
||||
/// ModernDeployment helper, this feature is <b>off by default</b> — the fallback for
|
||||
/// null / empty / invalid JSON must be an empty list so M3-compatible behaviour is
|
||||
/// preserved until an operator opts in.
|
||||
/// </summary>
|
||||
public sealed class AdminConfigurationWhiteGloveSealingPatternIdsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetWhiteGloveSealingPatternIds_NullJson_ReturnsEmpty()
|
||||
{
|
||||
var cfg = new AdminConfiguration();
|
||||
|
||||
var ids = cfg.GetWhiteGloveSealingPatternIds();
|
||||
|
||||
Assert.Empty(ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWhiteGloveSealingPatternIds_WhitespaceJson_ReturnsEmpty()
|
||||
{
|
||||
var cfg = new AdminConfiguration { WhiteGloveSealingPatternIdsJson = " " };
|
||||
|
||||
var ids = cfg.GetWhiteGloveSealingPatternIds();
|
||||
|
||||
Assert.Empty(ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWhiteGloveSealingPatternIds_InvalidJson_ReturnsEmpty()
|
||||
{
|
||||
var cfg = new AdminConfiguration { WhiteGloveSealingPatternIdsJson = "not-json" };
|
||||
|
||||
var ids = cfg.GetWhiteGloveSealingPatternIds();
|
||||
|
||||
Assert.Empty(ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWhiteGloveSealingPatternIds_ValidJson_RoundTrips()
|
||||
{
|
||||
var cfg = new AdminConfiguration
|
||||
{
|
||||
WhiteGloveSealingPatternIdsJson = "[\"wg-seal-1\",\"wg-seal-2\",\"wg-seal-3\"]"
|
||||
};
|
||||
|
||||
var ids = cfg.GetWhiteGloveSealingPatternIds();
|
||||
|
||||
Assert.Equal(new[] { "wg-seal-1", "wg-seal-2", "wg-seal-3" }, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWhiteGloveSealingPatternIds_EmptyArray_ReturnsEmptyList()
|
||||
{
|
||||
// Explicit empty array means "feature opted-in but no patterns yet" — same
|
||||
// observable behaviour as the default but distinguishable in storage.
|
||||
var cfg = new AdminConfiguration { WhiteGloveSealingPatternIdsJson = "[]" };
|
||||
|
||||
var ids = cfg.GetWhiteGloveSealingPatternIds();
|
||||
|
||||
Assert.Empty(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -541,6 +541,7 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
{ "MaintenanceBlockDurationHours", config.MaintenanceBlockDurationHours },
|
||||
{ "DiagnosticsGlobalLogPathsJson", config.DiagnosticsGlobalLogPathsJson },
|
||||
{ "ModernDeploymentHarmlessEventIdsJson", config.ModernDeploymentHarmlessEventIdsJson ?? string.Empty },
|
||||
{ "WhiteGloveSealingPatternIdsJson", config.WhiteGloveSealingPatternIdsJson ?? string.Empty },
|
||||
{ "NvdApiKey", config.NvdApiKey },
|
||||
{ "VulnerabilityCorrelationEnabled", config.VulnerabilityCorrelationEnabled },
|
||||
{ "VulnerabilityDataLastSyncUtc", config.VulnerabilityDataLastSyncUtc },
|
||||
@@ -593,6 +594,7 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
MaintenanceBlockDurationHours = entity.GetInt32("MaintenanceBlockDurationHours") ?? 12,
|
||||
DiagnosticsGlobalLogPathsJson = entity.GetString("DiagnosticsGlobalLogPathsJson"),
|
||||
ModernDeploymentHarmlessEventIdsJson = entity.GetString("ModernDeploymentHarmlessEventIdsJson"),
|
||||
WhiteGloveSealingPatternIdsJson = entity.GetString("WhiteGloveSealingPatternIdsJson"),
|
||||
NvdApiKey = entity.GetString("NvdApiKey"),
|
||||
VulnerabilityCorrelationEnabled = entity.GetBoolean("VulnerabilityCorrelationEnabled") ?? true,
|
||||
VulnerabilityDataLastSyncUtc = entity.GetString("VulnerabilityDataLastSyncUtc"),
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new AgentConfigResponse
|
||||
{
|
||||
ConfigVersion = 25, // IntegrityBypassAnalyzer flag
|
||||
ConfigVersion = 26, // WhiteGloveSealingPatternIds wired from AdminConfiguration
|
||||
UploadIntervalSeconds = 10,
|
||||
SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true,
|
||||
KeepLogFile = tenantConfig.KeepLogFile ?? false,
|
||||
@@ -176,7 +176,8 @@ namespace AutopilotMonitor.Functions.Functions.Config
|
||||
SendTraceEvents = tenantConfig.SendTraceEvents,
|
||||
UnrestrictedMode = tenantConfig.UnrestrictedModeEnabled && tenantConfig.UnrestrictedMode,
|
||||
GatherRules = gatherRules,
|
||||
ImeLogPatterns = imeLogPatterns
|
||||
ImeLogPatterns = imeLogPatterns,
|
||||
WhiteGloveSealingPatternIds = adminConfig.GetWhiteGloveSealingPatternIds(),
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
@@ -307,6 +307,41 @@ namespace AutopilotMonitor.Shared.Models
|
||||
}
|
||||
}
|
||||
|
||||
// ===== WHITEGLOVE SEALING PATTERN IDS (Plan §M5, M4.4.4 / M4.4.5.e) =====
|
||||
|
||||
/// <summary>
|
||||
/// JSON-serialized list of <see cref="ImeLogPattern"/> IDs whose match is re-emitted by
|
||||
/// the V2 agent as a <c>WhiteGloveSealingPatternDetected</c> DecisionSignal (in addition
|
||||
/// to the normal <c>ime_pattern_match</c> event). Example: <c>"[\"wg-seal-1\",\"wg-seal-2\"]"</c>.
|
||||
/// <para>
|
||||
/// Default null/empty = feature off (M3-compatible, no regression risk). Only IDs in
|
||||
/// this list count as sealing signals; other IME pattern matches follow the regular
|
||||
/// event path. Global-only — no per-tenant override (plan §M5 M4.4.5.e decision).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public string WhiteGloveSealingPatternIdsJson { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the deserialized list of WhiteGlove sealing pattern IDs. Falls back to an
|
||||
/// empty list for null/empty/invalid JSON so the feature stays off by default
|
||||
/// (M3-compatible).
|
||||
/// </summary>
|
||||
public List<string> GetWhiteGloveSealingPatternIds()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(WhiteGloveSealingPatternIdsJson))
|
||||
return new List<string>();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(WhiteGloveSealingPatternIdsJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||
?? new List<string>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether vulnerability correlation is globally enabled.
|
||||
/// When false, agents still collect inventory but backend skips correlation.
|
||||
|
||||
Reference in New Issue
Block a user