Skip to documentation
Open Workflow Studio
On this page

Approved JIT execution environments

JIT tools remain run-scoped capabilities. A model supplies a bounded tool definition and JSON input; it never chooses a pool, subscription, credential, session identifier, role, or deployment tier. Existing Functions profiles remain separate. Dynamic Sessions is an explicitly configured, Python-only, prepared backend—not a silent replacement or paid fallback.

Prepare once; execute separately

An operator prepares a dedicated Azure Container Apps PythonLTS session pool and approves its host configuration. Creating or invoking a logical tool does not run ARM deployments, build a runtime, install packages, or execute generated code on the workflow coordinator. The first invocation allocates a session through the data-plane execution API.

The session pool is persistent and billable. Workflow cleanup never deletes the pool or its resource group. Pool updates and removal are separate operator actions.

Operator prerequisites

  • An existing dedicated resource group/subscription and a supported region with appropriate session-pool capacity. Register Microsoft.App separately if necessary.
  • A reviewed PythonLTS pool with EgressDisabled, no session-accessible identity or secrets, and a bounded allocation limit and inactivity cooldown. The preparation template permits 1–16 allocated sessions and 300–3,600 seconds of cooldown.
  • A refreshable host identity authorized to use the pool. The documented data-plane role is Azure ContainerApps Session Executor, scoped to this pool. ARM Reader access, when independently available, enables configuration inspection. The application does not require subscription Contributor or silently grant it.
  • A dedicated HTTP transport with automatic redirects and cookies disabled, no default credentials/headers, and Timeout = Timeout.InfiniteTimeSpan. The adapter applies its own deadlines. Injecting an arbitrary HttpClient cannot prove its hidden handler settings; host construction must enforce them.
  • Protected durable lease storage supplied as WorkflowJitHostOptions.LeaseStore. The shared file store requires an absolute persistent path, not container-local ephemeral storage; the shared blob store is another host option. Journal access must be restricted to trusted host/cleanup operators because session identifiers are sensitive. Do not put tokens, source, task data, or inputs into the journal.

The Session Executor token has audience https://dynamicsessions.io (scope https://dynamicsessions.io/.default) and can access any session in the pool. Do not expose it to generated source, users, model context, logs, or browsers that are not trusted runtime hosts. Random identifiers are additional routing controls, not a substitute for protecting this pool-wide credential. The application sends source/input only, not the authorization token, to the interpreter program.

Preparation files

File Purpose
infra\jit\main.bicep Explicit resource-group-scoped preparation entry point.
infra\jit\session-pool.bicep Microsoft.App/sessionPools@2025-01-01; PythonLTS, disabled egress, no identity, timed lifecycle. No role assignments.
infra\jit\session-pool.parameters.example.json Bounded example preparation parameters.
infra\jit\prepare-jit-sessions.ps1 Read-only check/offline plan, or explicitly approved preparation.
infra\jit\jit-sessions-profile.example.json Host-owned profile example matching the C# configuration contract.

The script does not install CLI extensions, register providers, create a resource group, change the selected subscription, or escalate permissions as a fallback.

# Offline and read-only: displays intended settings; no Azure requests or writes.
.\infra\jit\prepare-jit-sessions.ps1 -Mode Plan `
    -SubscriptionId '<approved-subscription-guid>' `
    -ResourceGroupName 'rg-jit-approved' -SessionPoolName 'jitpython'

# Read-only ARM inspection of an already prepared pool; never executes a probe.
.\infra\jit\prepare-jit-sessions.ps1 -Mode Check `
    -SubscriptionId '<approved-subscription-guid>' `
    -ResourceGroupName 'rg-jit-approved' -SessionPoolName 'jitpython'

# OPERATOR ONLY: creates/updates billable infrastructure after review and confirmation.
# Omission of GrantExecutorRole means NO permission grants.
.\infra\jit\prepare-jit-sessions.ps1 -Mode Prepare -ApproveBillablePreparation `
    -SubscriptionId '<approved-subscription-guid>' `
    -ResourceGroupName 'rg-jit-approved' -SessionPoolName 'jitpython'

Only an explicitly approved Prepare -GrantExecutorRole -ExecutorPrincipalId '<service-principal-object-id>' can grant the pool-scoped Session Executor role; there is a separate ShouldProcess confirmation for that grant. Default Check, offline Plan, and -WhatIf do not deploy or grant anything. Plan is deliberately not an ARM what-if or a guarantee of deployability. None of these examples were deployed as part of this implementation.

Host profile and policy

Install the profile example only after replacing its subscription, region, exact endpoint/resource ID, and storage path with the approved prepared resource. On Linux/container hosts, replace the Windows path with an absolute protected persistent mount or configure LeaseBlobContainerUri instead. A settings path alone does not create durable storage or grant storage access.

The configuration section is Workflow:Jit, with profiles keyed by name. A JIT node opts in with ExecutionProfile = "approved-python-sessions" and explicit deployment/execution consent. The host resolves and intersects the workflow limits with the trusted profile. Backend is DynamicSessions; SessionApiVersion must be explicitly 2025-10-02-preview.

Required effective constraints:

  • Only Python; AllowLongRunning = false; short execution budget at most 200 seconds.
  • A separate remote session slot for every concurrent execution: MaximumConcurrentInvocations <= MaximumRuntimeEnvironments.
  • Bounded unique tools, total executions (including examples), concurrency, repairs, and wall-clock run lease. The parent enforces the invocation budget; the adapter independently enforces local tool admission, capacity, lifetime, and execution deadlines.
  • SessionCooldownSeconds must match the prepared pool's timed inactivity setting.
  • SessionPoolEndpoint must be the exact documented HTTPS regional endpoint for SessionPoolResourceId. No user information, alternative ports, encoded paths, queries, fragments, model-provided IDs, or model-provided destinations are accepted.

InvocationTimeoutSeconds remains in the shared configuration contract for long Functions execution and must retain its valid structural value. It does not authorize long interpreter execution: Policy.ShortInvocationTimeoutSeconds is the effective session execution budget. AllowAsyncOperations, if separately enabled by policy, is parent orchestration of bounded calls; this adapter submits only synchronous interpreter requests.

Adapter seam and execution contract

Workflows\WorkflowJitSessionsHost.cs implements IWorkflowJitToolHost, IWorkflowJitPreflightHost, and IWorkflowJitHostDescription:

new WorkflowJitSessionsHost(
    client, resolveCredential, resolvedConfiguration, runId, leaseId, hostOptions);

resolvedConfiguration is the shared WorkflowJitResolvedConfiguration(Configuration, Profile). Inputs are snapshotted and rejected before requests if invalid. ProvisionAsync validates/registers the content-addressed logical tool; it performs no cloud calls. WorkflowJitToolDeployment truthfully reports:

  • Backend = "DynamicSessions" and the logical run LeaseId.
  • Empty FunctionAppName: no Function App is created or implied.
  • ArtifactVersion = "dynamic-sessions-python-json-v1": the fixed adapter wrapper protocol, not an immutable Azure interpreter image/version claim.
  • Reused = true only when that logical tool was already registered by this adapter.

InvokeAsync safely JSON-serializes and base64-encodes the source and input into a fixed Python interpreter program. Inside the remote interpreter it compiles the source, finds run, calls run(tool_input), and serializes the returned value with allow_nan=False. The final interpreter expression is JSON text, decoded once by the host. This preserves objects, arrays, strings, booleans, numbers, and null without treating a JSON string as an object or coercing scalars.

The wrapper discards ordinary generated stdout/stderr. This is an output protocol, not an in-interpreter security sandbox; untrusted Python can modify its interpreter and files. Security isolation depends on the prepared Azure session boundary and its settings. Output remains untrusted and is size/JSON-validated by the host.

Limits are 128 KiB source, 256 KiB input-object UTF-8, and 256 KiB output-JSON UTF-8. The complete provider response is independently stream-bounded at 2 MiB to allow escaped JSON overhead. Provider errors and raw stdout/stderr are not returned as diagnostics. The HTTP/queue/execution budget is at most 200 seconds; independently bounded release attempts run after execution (15 seconds, plus at most five seconds for final journal persistence). The documented service synchronous runtime ceiling is 220 seconds.

Exact preview API contract

This adapter pins the generated REST reference, not an inferred endpoint or a mixture of versions:

POST {approvedPoolEndpoint}/executions?api-version=2025-10-02-preview&identifier={hostGeneratedIdentifier}
Authorization: Bearer {poolScopedToken}
operation-id: {hostGeneratedOperationId}
Content-Type: application/json

{
  "codeInputType": "Inline",
  "executionType": "Synchronous",
  "code": "<fixed wrapper plus encoded data>",
  "timeoutInSeconds": 200,
  "useEmptyKernel": true,
  "returnJupyterFormatExecutionResult": false,
  "outputStreamsMaxLength": 4096
}

The successful response is flat: { "id", "identifier", "executionType", "status": "Succeeded", "result": { "executionResult", "stdout", "stderr", ... } }. The adapter verifies execution/session correlation and requires the wrapper's JSON-text string in result.executionResult. operation-id is correlation, not a claim of idempotent POST retry support. Ambiguous POSTs are never retried.

Documentation/version caveat: The conceptual code-interpreter page currently shows a properties envelope despite naming the 2025-10 API, and the session-pool overview still lists the legacy code/execute route with 2024-02-02-preview. Those are not this adapter's contract. The current REST reference specifies /executions with a flat request/result. Legacy or unknown API versions are rejected rather than guessed. The data-plane API remains explicitly preview, even though the separate preparation ARM schema is stable.

No returned Location, Operation-Location, nextLink, or output URL is followed. Every execution/release URI is derived exclusively from the approved endpoint and a host-generated identifier. Responses that redirect or identify another request URI are rejected. A caller-supplied auto-redirecting/cookie-enabled transport violates the constructor contract even if a later response check would detect it.

Fresh sessions, capacity, and honest cleanup

Every invocation—even the same tool in the same run—gets a fresh cryptographically random identifier and requests an empty kernel. Parallel executions never issue concurrent commands to one stateful interpreter. Identifiers, globals, files, and remote capabilities are never reused across runs. No reset routine is trusted to remove hostile state.

The current preview API does document individual release:

DELETE {approvedPoolEndpoint}/session?api-version=2025-10-02-preview&identifier={ownedIdentifier}

Only documented 204 No Content is treated as confirmed release. There is no invented cancel route. This is a session delete, not a pool or resource-group delete.

  1. Before the execution POST, persist a revision-zero WorkflowJitLease with CreationUncertain, random ownership, run/profile, exact pool resource ID, and host-generated session metadata. No task payload is stored.
  2. On a correlated terminal interpreter response, retire the identifier permanently, fence a ReleaseRequested journal update, and attempt the documented session DELETE. A 204 records Released; a lost/failing response leaves ExpiryPending.
  3. If the execution POST is canceled, times out, redirects, or otherwise has an uncertain outcome, do not immediately DELETE and claim success. Allocation could race that deletion. Stop local waiting, retire the identifier, and retain its local capacity reservation.
  4. The independently recorded conservative fallback expiry is fixed before dispatch: allocation time + application execution budget + 220-second service-execution allowance + configured cooldown. It is an operator recovery estimate, not an observed server expiry or a hard service TTL. Unknown sessions are eligible for a later targeted release attempt only after this bound. Their capacity is freed only after a confirmed 204, not simply because a timer elapsed.
  5. DeleteAsync stops admission, cancels/drains local operations, retries eligible owned session releases, and fails with cleanup_pending while any allocation/release is unconfirmed. CleanupDescription explicitly reports expiry-pending state; cancellation is never displayed as proven remote termination.

The pool's documented Timed lifecycle deletes sessions after inactivity, and each session API request can reset that timer. It does not support maxAlivePeriodInSeconds. The run lease bounds admission and local waiting, not an independently guaranteed remote kill deadline: an uncertain session can outlive the run lease. Deletion failures, background service behavior, transport uncertainty, or later authorized requests can prevent an exact wall-clock destruction guarantee. The adapter never polls a retired interpreter to keep it alive.

The shared lease ResourceId is the persistent pool ARM ID, while Tags.sessionIdentifier, Tags.poolEndpoint, and Tags.sessionApiVersion identify the owned data-plane session. Never interpret that ResourceId as authority to delete the pool. ExpiresAt and ownership are immutable; journal changes advance the revision. A failed ownership fence prevents a deletion request and blocks new admission.

Recovery limitation: The shared independent WorkflowJitReaper currently reconciles disposable Functions resource groups only; it deliberately refuses session-pool deletion. It does not independently release these session IDs or confirm TTL expiry. After host loss, the configured pool inactivity lifecycle is the service-side backstop. Operators must inspect/reconcile expiry-pending session records using the protected identifier and the documented session APIs. Passing the estimated expiry never marks a lease Released. Qualify service expiry, cancellation races, shared-pool capacity and cleanup operationally before unattended/high-volume use.

An unsuccessful release consumes local environment capacity. At capacity the next execution fails closed, rather than reusing a dirty interpreter or allocating beyond the profile ceiling. The pool-wide cap independently includes other runs and retiring sessions; per-run concurrency is not a pool reservation or fairness guarantee.

Read-only readiness and qualification

CheckReadinessAsync:

  • Validates the host profile and fixed transport/endpoint requirements.
  • Calls allocation-free GET {poolEndpoint}/metadata?api-version=2025-10-02-preview with the Dynamic Sessions scope and checks for a versioned Python runtime.
  • Attempts only GET https://management.azure.com{poolResourceId}?api-version=2025-01-01 with an ARM-scoped credential. If accessible, verifies pool identity, endpoint, provisioning, PythonLTS/Dynamic type, disabled egress, no identity/secrets, capacity and exact cooldown.
  • Reports denied/unavailable ARM access as unverified with an operator-validation warning, not as verified network isolation. A confirmed missing or incompatible pool fails the corresponding check.
  • Does not create a session, execute code, change roles, test-write a journal, or prove execution/deletion authorization, future capacity, token lifetime, or actual network isolation.

Azure manages the PythonLTS image and its packages; this implementation does not pin an image digest or establish package reproducibility. The helper protocol version is not a runtime artifact attestation. Keep Functions/Flex artifact qualification separate.

Offline fake-transport coverage is in AspireDemo.Web.Tests\WorkflowJitSessionsTests.cs: lazy/warm execution without ARM/build, exact route/audience, flat schema, payload escaping, JSON kinds/UTF-8 limits, bounded parallel independent slots, cancellation/release uncertainty, persistent ownership fences, cross-run isolation, invalid profiles, redirects and read-only preflight. Live Azure behavior, costs, latency and service isolation have not been measured by these tests.

First-party references

Verified against the following Microsoft Learn pages:

An unhandled error has occurred. Reload 🗙