# ISkillScriptExecutor

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/api/tracon.iskillscriptexecutor/
> Summary: ISkillScriptExecutor: Starts a skill script that already passed every execution gate, and reports what it did. Register your own implementation to run scripts somewhere other than the.

<div class="api-provenance">Namespace Tracon · Assembly <code>Tracon.Abstractions.dll</code></div>

Starts a skill script that already passed every execution gate, and reports
what it did. Register your own implementation to run scripts somewhere other
than the Tracon host: a container, a micro-VM, or a remote runner service.

```csharp
public interface ISkillScriptExecutor
```

## Remarks

**What stays with Tracon.** Before the call, Tracon decides whether
a script may run: the feature switch, the tenant's [SkillScriptGrant](/api/tracon.skillscriptgrant/)
and its content pin, the interpreter allow-list, the argument size and schema,
a plain file name for a stored script, the content size limit for a file
script, and the audit trail entry. A request that fails one of them never
reaches an executor. Tracon then takes a concurrency slot and holds it until
the call returns or the wait limit passes. After the call, it measures the
duration, enforces the output limit again, and writes the span, the metric, and
the tool call record. An executor cannot skip a gate, and it cannot write or
suppress the audit entry.

**What the executor owns.** Where and how the script runs:
the process, its operating-system identity, its file system, its network, and
its CPU and memory limits. Tracon cannot see or check any of this. The script
runs where your executor runs it. The built-in default starts a local process
under the host's own operating-system identity.

**Lifetime: singleton.** Tracon resolves one instance when it
builds its script runner and calls it concurrently from many runs. An
implementation must be thread-safe. Register it before or after
`AddTracon` with `AddSingleton`; both orders win over the
built-in default. `RequireCustomBinding<ISkillScriptExecutor>`
stops the host when the built-in default is what resolves.

**Tenant mode: expected tenant.** The request carries the tenant
whose grant opened the gates ([SkillScriptExecutionRequest.TenantId](/api/tracon.skillscriptexecutionrequest/#Tracon_SkillScriptExecutionRequest_TenantId)).
An implementation uses that value and never reads `ITenantContext`: the
call can run on a thread that belongs to no request.

**Delivery: no delivery guarantee applies.** Tracon calls the
executor once for each approved run and never retries. A failure is reported
to the model as a failed tool call; the model may call the script again, and
that new call passes every gate again.

**Guarantee limit.** Tracon does not isolate the script process
and cannot verify that an executor does. Cancellation is cooperative: Tracon
cancels the token when the caller stops or when [SkillScriptExecutionRequest.Timeout](/api/tracon.skillscriptexecutionrequest/#Tracon_SkillScriptExecutionRequest_Timeout)
plus a fixed grace period has passed, and then stops waiting. It does not
force the work to stop. A script whose executor ignores the token can keep
running after Tracon recorded it as timed out.

Register it with `builder.Services.AddSingleton<ISkillScriptExecutor, RunnerServiceScriptExecutor>`
and keep `UseSkillScripts`: the gates, the grant, and the interpreter
allow-list still decide what reaches the executor. With a remote executor an
allow-list value can be a runtime name, for example
`o.Interpreters["py"] = "python-3.12"`.

```csharp
public sealed class RunnerServiceScriptExecutor(System.Net.Http.HttpClient runner) : ISkillScriptExecutor
{
    private static readonly JsonSerializerOptions Web = new(JsonSerializerDefaults.Web);

    public async ValueTask<SkillScriptExecutionResult> ExecuteAsync(
        SkillScriptExecutionRequest request, CancellationToken cancellationToken = default)
    {
        // The runner service gets the content, never a host path, and stops
        // the script at the timeout. The tenant lets it pick an isolated runner.
        using var response = await runner.PostAsync(
            "runs",
            System.Net.Http.Json.JsonContent.Create(new
            {
                tenant = request.TenantId,
                runtime = request.Interpreter,
                source = request.Content,
                sourceSha256 = request.ContentSha256,
                standardInput = request.ArgumentsJson,
                timeoutSeconds = request.Timeout.TotalSeconds,
                maxOutputBytes = request.MaxOutputBytes,
            }),
            cancellationToken);

        response.EnsureSuccessStatusCode();

        var outcome = JsonSerializer.Deserialize<RunnerOutcome>(
            await response.Content.ReadAsStringAsync(cancellationToken), Web)
            ?? throw new InvalidOperationException("The runner service returned no outcome.");

        return new SkillScriptExecutionResult
        {
            ExitCode = outcome.TimedOut ? null : outcome.ExitCode,
            StandardOutput = outcome.StandardOutput,
            StandardError = outcome.StandardError,
            Truncated = outcome.Truncated,
            TimedOut = outcome.TimedOut,
        };
    }

    private sealed record RunnerOutcome(
        int? ExitCode, string StandardOutput, string StandardError, bool Truncated, bool TimedOut);
}
```

## Methods

### <a id="Tracon_ISkillScriptExecutor_ExecuteAsync_Tracon_SkillScriptExecutionRequest_System_Threading_CancellationToken_"></a> ExecuteAsync\(SkillScriptExecutionRequest, CancellationToken\)

Runs one script and reports its outcome.

```csharp
ValueTask<SkillScriptExecutionResult> ExecuteAsync(SkillScriptExecutionRequest request, CancellationToken cancellationToken = default)
```

#### Parameters

`request` [SkillScriptExecutionRequest](/api/tracon.skillscriptexecutionrequest/)

The script to run. Every gate already passed and the audit entry is
already written when this method is called.

`cancellationToken` [CancellationToken](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken)

Cancelled when the caller stops the run, or when the timeout plus the
grace period has passed and Tracon stops waiting.

#### Returns

 [ValueTask](https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask\-1)<[SkillScriptExecutionResult](/api/tracon.skillscriptexecutionresult/)\>

The outcome. When the script runs past [SkillScriptExecutionRequest.Timeout](/api/tracon.skillscriptexecutionrequest/#Tracon_SkillScriptExecutionRequest_Timeout),
stop it and return [SkillScriptExecutionResult.TimedOut](/api/tracon.skillscriptexecutionresult/#Tracon_SkillScriptExecutionResult_TimedOut) set to
[true](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/bool) and no exit code. Keep at most
[SkillScriptExecutionRequest.MaxOutputBytes](/api/tracon.skillscriptexecutionrequest/#Tracon_SkillScriptExecutionRequest_MaxOutputBytes) UTF-8 bytes of each
output stream and set [SkillScriptExecutionResult.Truncated](/api/tracon.skillscriptexecutionresult/#Tracon_SkillScriptExecutionResult_Truncated) when
you cut one.

#### Remarks

Any other exception fails the tool call: Tracon records it on the span, the
metric, and the tool call record, and the model sees a failed call. Its
message is not shown to the model or written to the tool call record.

#### Exceptions

 [OperationCanceledException](https://learn.microsoft.com/dotnet/api/system.operationcanceledexception)

The caller cancelled `cancellationToken`. Do not report a
caller's cancellation as a timeout.
