Files
VolcanicAgenticCore/tests/Volcanic.AgenticCore.Extensions.Tests/ClaudeCode/ClaudeArgsProcessPolicyTests.cs
EugeneTes 76bda6cf55 fix(processes): address review findings
- Fill TimeZoneId only for a configured session/host zone with a resolvable IANA id
  (IExtensionHost.TimeZoneId); otherwise children inherit the host's TZ instead of TZ=Local/UTC.
- Host hardening logs a warning instead of throwing when libc/prctl can't be resolved.
- A hook or process_spawn handler returning a null or incomplete context fails the spawn with
  InvalidOperationException, so bash reports "process environment hook failed".
- Move tool-result redaction into the Agent slice with a behavioural no-change check.
- ProcessSpawnContext.ApplyTo(psi) sets cwd and replaces the environment; used by bash and ExecAsync.
- Split the Telegram process environment types into their own files; move the Extensions.Tests
  NoHostHardening under Support/; check the Claude Code assembly never references the spawn policy.
- Spec and docs: TZ inheritance, incomplete contexts, ApplyTo, async spawn handlers, root skip.
2026-09-26 19:32:47 +02:00

65 lines
2.8 KiB
C#

using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Microsoft.Extensions.DependencyInjection;
using Volcanic.AgenticCore.Abstractions.Processes;
using Volcanic.AgenticCore.Extensions.ClaudeCode;
using Volcanic.AgenticCore.Hosting;
namespace Volcanic.AgenticCore.Extensions.Tests.ClaudeCode;
/// <summary>
/// Pins down that the Claude Code provider never consults the host's process spawn policy:
/// <c>claude</c> already gets a replaced, allow-listed environment (spec ยง7).
/// </summary>
public class ClaudeArgsProcessPolicyTests
{
[Fact]
public void BuildEnvironment_is_unchanged_when_a_host_hook_is_configured()
{
var parent = new Dictionary<string, string?> { ["HOME"] = "/home/me", ["PATH"] = "/usr/bin", ["LANG"] = "C.UTF-8" };
var before = ClaudeArgs.BuildEnvironment(parent, 64_000);
var hookRuns = 0;
var services = new ServiceCollection();
services.AddAgenticCore(o =>
{
o.ProcessEnvironment.HardenHostProcess = false;
o.ProcessEnvironment.Hooks.Add(c =>
{
hookRuns++;
return c with { Environment = new Dictionary<string, string>(c.Environment) { ["INJECTED_BY_HOOK"] = "1" } };
});
});
using var provider = services.BuildServiceProvider();
_ = provider.GetRequiredService<IProcessSpawnPolicy>();
var after = ClaudeArgs.BuildEnvironment(parent, 64_000);
Assert.Equal(before.OrderBy(kv => kv.Key), after.OrderBy(kv => kv.Key));
Assert.False(after.ContainsKey("INJECTED_BY_HOOK"));
Assert.Equal(0, hookRuns);
}
/// <summary>
/// The test above can't see a spawn path outside <c>BuildEnvironment</c>, so also check the provider's IL metadata:
/// nothing in the assembly references the policy, the per-session preparation or the spawn event.
/// </summary>
[Fact]
public void Provider_assembly_never_references_the_process_spawn_policy()
{
var path = typeof(ClaudeArgs).Assembly.Location;
using var stream = File.OpenRead(path);
using var pe = new PEReader(stream);
var metadata = pe.GetMetadataReader();
var typeRefs = metadata.TypeReferences.Select(h => metadata.GetString(metadata.GetTypeReference(h).Name)).ToHashSet();
var memberRefs = metadata.MemberReferences.Select(h => metadata.GetString(metadata.GetMemberReference(h).Name)).ToHashSet();
Assert.DoesNotContain(nameof(IProcessSpawnPolicy), typeRefs);
Assert.DoesNotContain(nameof(ProcessSpawnEvent), typeRefs);
Assert.DoesNotContain("PrepareProcessAsync", memberRefs);
Assert.DoesNotContain("get_Processes", memberRefs);
Assert.DoesNotContain(nameof(IProcessSpawnPolicy.Prepare), memberRefs);
}
}