diff --git a/dotnet/src/Microsoft.Agents.AI.Hyperlight/HyperlightCodeActProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hyperlight/HyperlightCodeActProviderOptions.cs
index c6e28f68b4b..f07f609f86a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hyperlight/HyperlightCodeActProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hyperlight/HyperlightCodeActProviderOptions.cs
@@ -18,6 +18,7 @@ namespace Microsoft.Agents.AI.Hyperlight;
///
public sealed class HyperlightCodeActProviderOptions
{
+
///
/// Initializes a new instance configured for the JavaScript backend.
/// Equivalent to .
@@ -69,6 +70,20 @@ public static HyperlightCodeActProviderOptions CreateForJavaScript()
///
public string? StackSize { get; set; }
+ ///
+ /// Gets or sets the maximum execution time in seconds. Defaults to 30.
+ /// The value must be between 1 and 4,294,967 seconds.
+ ///
+ ///
+ /// The limit interrupts guest execution but cannot interrupt runtime initialization
+ /// or a host tool callback. Host tools must enforce their own cancellation policy.
+ ///
+ public int ExecutionTimeoutSeconds
+ {
+ get;
+ set => field = Throw.IfOutOfRange(value, 1, 4_294_967);
+ } = 30;
+
///
/// Gets or sets the initial set of provider-owned CodeAct tools made available
/// inside the sandbox via call_tool(...).
diff --git a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/SandboxExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/SandboxExecutor.cs
index 27f8e184e60..147c3b01f93 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/SandboxExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hyperlight/Internal/SandboxExecutor.cs
@@ -51,6 +51,7 @@ public RunSnapshot(
this.ModulePath = options.ModulePath;
this.HeapSize = options.HeapSize;
this.StackSize = options.StackSize;
+ this.ExecutionTimeout = TimeSpan.FromSeconds(options.ExecutionTimeoutSeconds);
this.HostInputDirectory = options.HostInputDirectory;
this.IncludeDetailedErrors = options.IncludeDetailedErrors;
this.ToolRegistryVersion = toolRegistryVersion;
@@ -81,6 +82,8 @@ public RunSnapshot(
public string? StackSize { get; }
+ public TimeSpan ExecutionTimeout { get; }
+
public string? HostInputDirectory { get; }
public bool IncludeDetailedErrors { get; }
@@ -224,14 +227,23 @@ public async Task ExecuteAsync(RunSnapshot snapshot, string code, Cancel
await this._executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
- this.EnsureInitialized(snapshot);
+ using var timeLimit = new CancellationTokenSource(snapshot.ExecutionTimeout);
+ using var executionCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeLimit.Token);
+ try
+ {
+ this.EnsureInitialized(snapshot, executionCancellation.Token);
+
+ if (this._warmSnapshot is not null)
+ {
+ this._sandbox!.Restore(this._warmSnapshot);
+ }
- if (this._warmSnapshot is not null)
+ return BuildResult(this._sandbox!.Run(code, executionCancellation.Token));
+ }
+ catch (OperationCanceledException) when (timeLimit.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
- this._sandbox!.Restore(this._warmSnapshot);
+ return BuildErrorResult("Code execution exceeded its time limit.");
}
-
- return BuildResult(this._sandbox!.Run(code));
}
#pragma warning disable CA1031 // Return ordinary lifecycle failures as policy-controlled execution errors.
catch (Exception ex) when (ex is not OperationCanceledException)
@@ -255,7 +267,7 @@ public async Task ExecuteAsync(RunSnapshot snapshot, string code, Cancel
}
}
- private void EnsureInitialized(RunSnapshot snapshot)
+ private void EnsureInitialized(RunSnapshot snapshot, CancellationToken cancellationToken)
{
if (this._sandbox is not null && string.Equals(this._lastConfigFingerprint, snapshot.ConfigFingerprint, StringComparison.Ordinal))
{
@@ -265,7 +277,7 @@ private void EnsureInitialized(RunSnapshot snapshot)
// Configuration changed (or first run) — dispose the previous sandbox
// so the new one picks up the current capabilities and runtime options.
this.ClearSandboxState();
- this.BuildAndWarmUp(snapshot);
+ this.BuildAndWarmUp(snapshot, cancellationToken);
}
private void ClearSandboxState()
@@ -285,7 +297,7 @@ private void ClearSandboxState()
}
}
- private void BuildAndWarmUp(RunSnapshot snapshot)
+ private void BuildAndWarmUp(RunSnapshot snapshot, CancellationToken cancellationToken)
{
var builder = new SandboxBuilder()
.WithBackend(snapshot.Backend);
@@ -336,7 +348,7 @@ private void BuildAndWarmUp(RunSnapshot snapshot)
// Backend-specific no-op used to trigger lazy guest runtime initialization
// before the warm snapshot is captured. Matches the values used by the
// upstream HyperlightSandbox.Extensions.AI CodeExecutionTool reference.
- _ = sandbox.Run(snapshot.Backend == SandboxBackend.JavaScript ? "void 0;" : "None");
+ _ = sandbox.Run(snapshot.Backend == SandboxBackend.JavaScript ? "void 0;" : "None", cancellationToken);
this._warmSnapshot = sandbox.Snapshot();
this._lastConfigFingerprint = snapshot.ConfigFingerprint;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hyperlight/README.md b/dotnet/src/Microsoft.Agents.AI.Hyperlight/README.md
index f6a19d4fe51..d6e3ed55d53 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hyperlight/README.md
+++ b/dotnet/src/Microsoft.Agents.AI.Hyperlight/README.md
@@ -54,6 +54,35 @@ The same boundary covers initialization, warm-up, snapshot restore, and executio
failures. Ordinary failures dispose and clear failed sandbox state before returning
an error; cancellation still propagates.
+## Execution limits
+
+Guest execution has a finite 30-second limit by default. Configure
+`HyperlightCodeActProviderOptions.ExecutionTimeoutSeconds` to change it; values
+must be positive and within the supported timer range. Run snapshots capture the
+limit independently of later option changes.
+
+Caller cancellation interrupts running guest code and propagates as
+`OperationCanceledException`. Reaching the configured limit returns an unsuccessful
+execution result. The next invocation restores the warm snapshot before reuse.
+Native execution finishes before another invocation can enter the sandbox.
+
+Runtime initialization and host tool callbacks cannot be interrupted by the guest
+limit. Host tools must enforce their own cancellation and execution limits.
+This behavior requires an upstream SDK version with the cancellation-aware `Run` overload.
+
+### KVM guest regression tests
+
+Ordinary unit tests retain reflection-disabled JSON settings. To run the guest
+regressions on a KVM-capable host with the corresponding native SDK installed:
+
+```powershell
+$env:HYPERLIGHT_RUN_GUEST_TESTS = "true"
+dotnet test --project dotnet\tests\Microsoft.Agents.AI.Hyperlight.UnitTests -f net10.0 -p:HyperlightGuestTests=true
+```
+
+The explicit guest configuration enables reflection-based serialization required
+by the upstream SDK; without opt-in, guest-only tests are skipped.
+
## Network access and guest packages
`HyperlightCodeActProviderOptions.AllowedDomains` controls outbound requests
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj
index 0964d4e4ada..ce104c5b947 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj
+++ b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj
@@ -4,10 +4,14 @@
$(TargetFrameworksCore)
-
+
false
+
+ true
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/SandboxExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/SandboxExecutorTests.cs
index 7e347669efc..5ab0c77b42f 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/SandboxExecutorTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hyperlight.UnitTests/SandboxExecutorTests.cs
@@ -64,6 +64,135 @@ await Assert.ThrowsAnyAsync(
() => executor.ExecuteAsync(snapshot, "void 0;", cancellation.Token));
}
+ [Fact]
+ public async Task ExecuteAsync_CancellationDuringGuestExecutionReleasesLockAsync()
+ {
+ // Arrange
+ Assert.SkipUnless(
+ string.Equals(Environment.GetEnvironmentVariable("HYPERLIGHT_RUN_GUEST_TESTS"), "true", StringComparison.OrdinalIgnoreCase),
+ "Requires explicit Hyperlight KVM guest-test configuration.");
+ using var cancellation = new CancellationTokenSource();
+ var cancelRun = AIFunctionFactory.Create(() =>
+ {
+ cancellation.Cancel();
+ return "cancelled";
+ }, name: "cancel_run");
+ var snapshot = new SandboxExecutor.RunSnapshot(
+ [cancelRun], [], [], new HyperlightCodeActProviderOptions { IncludeDetailedErrors = true });
+ using var executor = new SandboxExecutor();
+ var warmup = await executor.ExecuteAsync(snapshot, "console.log('ready');", CancellationToken.None);
+ using var warmupJson = JsonDocument.Parse(warmup);
+ Assert.True(
+ warmupJson.RootElement.GetProperty("success").GetBoolean(),
+ warmupJson.RootElement.GetProperty("stderr").GetString());
+
+ // Act / Assert
+ await Assert.ThrowsAnyAsync(
+ () => executor.ExecuteAsync(snapshot, "call_tool('cancel_run', {});", cancellation.Token));
+
+ using var recoveryCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+ var recovered = await executor.ExecuteAsync(snapshot, "console.log('recovered');", recoveryCancellation.Token)
+ .WaitAsync(TimeSpan.FromSeconds(10));
+ using var recoveredJson = JsonDocument.Parse(recovered);
+ Assert.True(
+ recoveredJson.RootElement.GetProperty("success").GetBoolean(),
+ recoveredJson.RootElement.GetProperty("stderr").GetString());
+ Assert.Contains("recovered", recoveredJson.RootElement.GetProperty("stdout").GetString(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_DefaultTimeoutInterruptsGuestAndAllowsReuseAsync()
+ {
+ // Arrange
+ Assert.SkipUnless(
+ string.Equals(Environment.GetEnvironmentVariable("HYPERLIGHT_RUN_GUEST_TESTS"), "true", StringComparison.OrdinalIgnoreCase),
+ "Requires explicit Hyperlight KVM guest-test configuration.");
+ var snapshot = new SandboxExecutor.RunSnapshot(
+ [], [], [], new HyperlightCodeActProviderOptions { IncludeDetailedErrors = true });
+ using var executor = new SandboxExecutor();
+ var warmup = await executor.ExecuteAsync(snapshot, "console.log('ready');", CancellationToken.None);
+ using var warmupJson = JsonDocument.Parse(warmup);
+ Assert.True(
+ warmupJson.RootElement.GetProperty("success").GetBoolean(),
+ warmupJson.RootElement.GetProperty("stderr").GetString());
+ var elapsed = System.Diagnostics.Stopwatch.StartNew();
+
+ // Act
+ var result = await executor.ExecuteAsync(snapshot, """
+ const end = Date.now() + 32000;
+ while (Date.now() < end) {}
+ console.log('finished');
+ """, CancellationToken.None);
+
+ // Assert
+ using var json = JsonDocument.Parse(result);
+ Assert.False(json.RootElement.GetProperty("success").GetBoolean());
+ Assert.Equal("Code execution exceeded its time limit.", json.RootElement.GetProperty("stderr").GetString());
+ Assert.True(elapsed.Elapsed < TimeSpan.FromSeconds(31), "The default execution timeout was not enforced.");
+ using var recoveryCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+ var recovered = await executor.ExecuteAsync(snapshot, "console.log('recovered');", recoveryCancellation.Token)
+ .WaitAsync(TimeSpan.FromSeconds(10));
+ using var recoveredJson = JsonDocument.Parse(recovered);
+ Assert.True(
+ recoveredJson.RootElement.GetProperty("success").GetBoolean(),
+ recoveredJson.RootElement.GetProperty("stderr").GetString());
+ Assert.Contains("recovered", recoveredJson.RootElement.GetProperty("stdout").GetString(), StringComparison.Ordinal);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ExecuteAsync_ConfiguredTimeoutUsesSnapshotAndAllowsReuseAsync(bool useWasm)
+ {
+ // Arrange
+ Assert.SkipUnless(
+ string.Equals(Environment.GetEnvironmentVariable("HYPERLIGHT_RUN_GUEST_TESTS"), "true", StringComparison.OrdinalIgnoreCase),
+ "Requires explicit Hyperlight KVM guest-test configuration.");
+ var options = useWasm
+ ? HyperlightCodeActProviderOptions.CreateForWasm(
+ Assert.IsType(Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH")))
+ : new HyperlightCodeActProviderOptions();
+ using var executor = new SandboxExecutor();
+ var warmSnapshot = new SandboxExecutor.RunSnapshot([], [], [], options);
+ var warmup = await executor.ExecuteAsync(warmSnapshot, useWasm ? "print('ready')" : "console.log('ready');", CancellationToken.None);
+ using var warmupJson = JsonDocument.Parse(warmup);
+ Assert.True(
+ warmupJson.RootElement.GetProperty("success").GetBoolean(),
+ warmupJson.RootElement.GetProperty("stderr").GetString());
+ options.ExecutionTimeoutSeconds = 1;
+ var limitedSnapshot = new SandboxExecutor.RunSnapshot([], [], [], options);
+ options.ExecutionTimeoutSeconds = 30;
+ var elapsed = System.Diagnostics.Stopwatch.StartNew();
+
+ // Act
+ var result = await executor.ExecuteAsync(limitedSnapshot, useWasm ? """
+ import time
+ end = time.monotonic() + 5
+ while time.monotonic() < end:
+ pass
+ print('finished')
+ """ : """
+ const end = Date.now() + 5000;
+ while (Date.now() < end) {}
+ console.log('finished');
+ """, CancellationToken.None);
+
+ // Assert
+ using var json = JsonDocument.Parse(result);
+ Assert.False(json.RootElement.GetProperty("success").GetBoolean());
+ Assert.Equal("Code execution exceeded its time limit.", json.RootElement.GetProperty("stderr").GetString());
+ Assert.True(elapsed.Elapsed < TimeSpan.FromSeconds(2), "The captured execution timeout was not enforced.");
+ using var recoveryCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+ var recovered = await executor.ExecuteAsync(
+ warmSnapshot, useWasm ? "print('recovered')" : "console.log('recovered');", recoveryCancellation.Token)
+ .WaitAsync(TimeSpan.FromSeconds(10));
+ using var recoveredJson = JsonDocument.Parse(recovered);
+ Assert.True(
+ recoveredJson.RootElement.GetProperty("success").GetBoolean(),
+ recoveredJson.RootElement.GetProperty("stderr").GetString());
+ Assert.Contains("recovered", recoveredJson.RootElement.GetProperty("stdout").GetString(), StringComparison.Ordinal);
+ }
+
[Fact]
public void Fingerprint_DifferentToolSets_DifferentFingerprints()
{
diff --git a/skills/agent-framework-builder-dotnet/references/integrations.md b/skills/agent-framework-builder-dotnet/references/integrations.md
index 444f2c3b88f..022b427a47b 100644
--- a/skills/agent-framework-builder-dotnet/references/integrations.md
+++ b/skills/agent-framework-builder-dotnet/references/integrations.md
@@ -46,6 +46,13 @@ stack traces and inner exceptions. This is separate from the
outer `FunctionInvokingChatClient` error policy and sensitive-data logging; outer redaction cannot repair
diagnostics already caught and returned by generated code.
+Hyperlight guest execution defaults to a finite 30-second limit, configurable through
+`HyperlightCodeActProviderOptions.ExecutionTimeoutSeconds`. Caller cancellation interrupts
+guest code, but neither cancellation nor the limit interrupts runtime initialization
+or host tool callbacks. Host tools must enforce their own limits. This requires the
+upstream SDK's cancellation-aware `Run` API; verify that it is available in the selected
+package version before using this option.
+
For AG-UI, remember that thread and run IDs are correlation identifiers, not authentication boundaries. The
application owns authorization, durable snapshots, scaling, and rendering policy.