File: Agents\Hooks\AgentTelemetryCatalog.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.RegularExpressions;
 
namespace Aspire.Cli.Agents.Hooks;
 
/// <summary>
/// Reads telemetry allowlists from the embedded canonical hook.
/// </summary>
internal sealed partial class AgentTelemetryCatalog
{
    private static readonly Lazy<AgentTelemetryCatalog> s_canonical = new(LoadCanonical);
 
    internal static AgentTelemetryCatalog Canonical => s_canonical.Value;
 
    internal IReadOnlySet<string> Skills { get; }
    internal IReadOnlySet<string> Tools { get; }
    internal IReadOnlySet<string> References { get; }
 
    private AgentTelemetryCatalog(HashSet<string> skills, HashSet<string> tools, HashSet<string> references)
    {
        Skills = skills;
        Tools = tools;
        References = references;
    }
 
    internal static AgentTelemetryCatalog Parse(string script)
    {
        var skills = ParseIdentifiers(script, SkillsDeclaration(), "ASPIRE_SKILLS");
        var tools = ParseIdentifiers(script, McpToolsDeclaration(), "ASPIRE_MCP_TOOLS");
        var references = ParseValues(script, ReferenceFilesDeclaration(), "ASPIRE_REFERENCE_FILES");
        foreach (var reference in references)
        {
            var segments = reference.Split('/', StringSplitOptions.None);
            if (segments.Length < 3 ||
                !skills.Contains(segments[0]) ||
                !segments[1].Equals("references", StringComparison.Ordinal) ||
                reference.Contains('\\') ||
                segments.Skip(2).Any(segment => segment is "" or "." or ".."))
            {
                throw new InvalidDataException("The canonical telemetry hook contains an invalid ASPIRE_REFERENCE_FILES allowlist.");
            }
        }
 
        return new(skills, tools, references);
    }
 
    private static HashSet<string> ParseIdentifiers(string script, Regex declaration, string variableName)
    {
        var values = ParseValues(script, declaration, variableName);
        if (values.Any(value => !Identifier().IsMatch(value)))
        {
            throw new InvalidDataException($"The canonical telemetry hook contains an invalid {variableName} allowlist.");
        }
 
        return values;
    }
 
    private static HashSet<string> ParseValues(string script, Regex declaration, string variableName)
    {
        var declarations = declaration.Matches(script);
        if (declarations.Count != 1)
        {
            throw new InvalidDataException($"The canonical telemetry hook must declare {variableName} exactly once.");
        }
 
        // Canonical declarations use these raw forms:
        //   ASPIRE_SKILLS="aspire aspire-init ..."
        //   ASPIRE_MCP_TOOLS="doctor list_resources ..."
        //   ASPIRE_REFERENCE_FILES="
        //   aspire-deployment/references/azure.md
        //   ..."
        // Split on Unicode whitespace so both single-line and multiline values use the same parser.
        var values = declarations[0].Groups["values"].Value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
        var result = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
        if (values.Length == 0 || result.Count != values.Length)
        {
            throw new InvalidDataException($"The canonical telemetry hook contains an invalid {variableName} allowlist.");
        }
 
        return result;
    }
 
    private static AgentTelemetryCatalog LoadCanonical()
    {
        // Read only the compiled canonical hook. User-installed scripts and plugin contents must not
        // be able to introduce new telemetry dimensions.
        using var stream = typeof(AgentTelemetryCatalog).Assembly.GetManifestResourceStream(TelemetryHookInstaller.ShellResourceName)
            ?? throw new InvalidDataException("The canonical telemetry hook is missing.");
        using var reader = new StreamReader(stream);
        return Parse(reader.ReadToEnd());
    }
 
    [GeneratedRegex("""^ASPIRE_SKILLS="(?<values>[^"]*)"[ \t]*\r?$""", RegexOptions.Multiline)]
    private static partial Regex SkillsDeclaration();
 
    [GeneratedRegex("""^ASPIRE_MCP_TOOLS="(?<values>[^"]*)"[ \t]*\r?$""", RegexOptions.Multiline)]
    private static partial Regex McpToolsDeclaration();
 
    [GeneratedRegex("""^ASPIRE_REFERENCE_FILES="(?<values>[^"]*)"[ \t]*\r?$""", RegexOptions.Multiline)]
    private static partial Regex ReferenceFilesDeclaration();
 
    [GeneratedRegex("""\A[a-zA-Z0-9_-]+\z""")]
    private static partial Regex Identifier();
}