Function Mode Lifecycle and Invoke Dataplane
Status: Accepted; implementation in progress
This document refines the function-mode target in Function Mode and Agent Sandboxes . It defines the Run lifecycle, status API, gateway boundary, recovery behavior, and invocation semantics that govern implementation.
Problem
The existing design establishes a function-mode Run and a Runtime gateway, but it leaves several correctness and security questions unresolved:
- whether
Readyis a terminal phase and how generic Run controllers classify it; - what happens when registration fails or the assigned Runtime Pod disappears;
- how cancellation, deletion, total timeout, and idle timeout release capacity;
- how an invoke request reaches the owning runtimed without a Kubernetes API lookup on the hot path;
- how callers are authenticated and authorized;
- whether a transport retry may execute an invocation twice;
- how invocation concurrency, payloads, outputs, and logs remain bounded;
- which state belongs in Run status and which state remains in the dataplane.
Adding only a Ready phase and an endpoint field would leave these behaviors
undefined and make later compatibility harder.
Control Plane and Dataplane
Kubernetes remains the durable lifecycle control plane:
- a function-mode
Rundeclares source, Runtime, handler, timeouts, and retry policy; - the scheduler assigns Runtime capacity exactly as it does for task Runs;
- runtimed prepares and registers the function, updates bounded Run status, and releases the reservation;
- controllers recover or terminate the Run when its Runtime Pod disappears.
Invocation is a dataplane operation:
- clients invoke through the Runtime gateway Service;
- runtimed routes from an in-memory ownership/readiness cache;
- the owning runtimed calls its local Runtime Server;
- individual invocations do not create Kubernetes objects and do not append history to Run status.
Scheduler and Runtime Server remain unaware of agents, sandboxes, Workflows, and SDK sessions.
Run Status API
The Run phase enum gains Ready:
const RunReady RunPhase = "Ready"
Ready is active and non-terminal. Generic phase helpers, capacity accounting,
stale-pod recovery, cancellation, metrics, CLI waits, TTL cleanup, and completed
Run GC must all classify it explicitly. Only Succeeded, Failed, Timeout,
and Cancelled remain terminal.
Function-mode status gains one stable endpoint reference:
type RunEndpointProtocol string
const (
RunEndpointProtocolHTTP RunEndpointProtocol = "HTTP"
RunEndpointProtocolHTTPS RunEndpointProtocol = "HTTPS"
)
type RunEndpoint struct {
Protocol RunEndpointProtocol `json:"protocol"`
URL string `json:"url"`
CABundle []byte `json:"caBundle,omitempty"`
}
type RunStatus struct {
// Existing fields omitted.
AssignedPodUID string `json:"assignedPodUID,omitempty"`
Endpoint *RunEndpoint `json:"endpoint,omitempty"`
}
assignedPodUID disambiguates Pod-name reuse and is set and cleared with
assignedPod. The endpoint URL is limited to 2048 bytes and the PEM CA bundle
to 16 KiB. These bounds are part of CRD validation.
The first public invoke protocol is HTTPS. Runtime Server communication remains
internal gRPC and is not exposed through this field. caBundle contains a
bounded PEM trust bundle when the Runtime gateway uses the controller-managed
CA; it is omitted when the certificate chains to a trust root configured by
the client. Supporting another public protocol requires a later API review.
The endpoint path includes namespace, Run name, and immutable Run UID so a deleted Run name cannot address a newly created Run accidentally:
status:
phase: Ready
assignedPod: runtime-python-7f587b4668-njcks
endpoint:
protocol: HTTPS
url: https://runtime-gateway.kruntimes-system.svc.cluster.local/v1/namespaces/kruntimes-demo/runtimes/python/runs/2c24c1f0-9f8f-4f80-82d5-3dd16a12d1e6:invoke
caBundle: <base64-encoded-PEM>
conditions:
- type: Ready
status: "True"
reason: FunctionRegistered
Ready=True means the assigned runtimed has registered the Run UID with the
local Runtime Server and can accept invokes. During scheduling, registration,
retry, cancellation, and terminal phases it is false with a specific reason.
The endpoint may remain stable while a Run is temporarily unavailable during
recovery; clients must use phase or the Ready condition, not endpoint presence,
as the readiness signal.
Opaque Runtime Server registration IDs, ownership-cache entries, invocation counters, and invocation history do not belong in Run status. The Run UID is the stable registration identity across runtimed and Runtime Server.
Spec Transition Rules
Execution inputs must not change after creation. CRD transition validation
should make runtime, source, mode, env, timeout, and retryPolicy
immutable for both task and function Runs. Otherwise a reconciler can observe a
different program or execution mode after assignment.
cancelRequested may transition only from false to true. The existing
ttlSecondsAfterFinished field may remain mutable because it controls retention
after execution rather than execution behavior.
These are breaking validation changes to the experimental API and were reviewed before the CRD was regenerated.
Lifecycle State Machine
The normal function lifecycle is:
Pending -> Scheduled -> Running (preparing/registering) -> Ready
Run.status.startTime is set when runtimed claims the Run. In function mode,
Run.status.attempt counts registration lifecycle attempts, including the
initial attempt, using the existing shared retry engine. Retrying an uncertain
Pod-local registration RPC for the same attempt is idempotent and does not
increment status.
Terminal transitions are:
- cancellation before assignment ends as
Cancelledin the scheduler; - cancellation after assignment makes the owning runtimed unregister, clean
local state, release capacity, and then set
Cancelled; - a registration error retries according to
retryPolicy; exhaustion ends asFailed; - assigned Runtime Pod loss makes the Run unavailable and enters fenced stale
recovery; after the old assignment is fenced, a new attempt may register on
another ready Pod, while exhaustion ends as
Failed; spec.timeout, when set, is the maximum reservation lifetime fromstartTime; expiry unregisters and ends asTimeout;mode.function.idleTimeoutSeconds, when set, expires one registration epoch after no completed or in-flight invocation activity and ends asTimeoutwith reasonIdleTimeout.
Invocation-level handler errors, request timeouts, and rejected concurrency do not change Run phase and do not consume Run retry attempts. They are results of one invocation, not failures of the registered function lifecycle. A fatal Runtime Server error that invalidates registration makes the Run not ready and enters registration recovery.
The Runtime Server owns the precise last-activity clock for a registration
generation and exposes it through FunctionStatus; it is not checkpointed on every
invoke to etcd. If the Runtime Server process recovers its registration, it
also recovers that clock. If the Runtime Pod is replaced, registration creates
a new epoch and resets the idle timer. This avoids early expiry and avoids a
high-frequency Run status write path.
Ownership Epoch and Fencing
Run UID is the stable external function identity. A new registration is fenced
by status.attempt, assignedPod, and assignedPodUID. RegisterFunction
carries the Run UID and registration attempt, then returns an opaque local
registration ID. FunctionStatus, InvokeFunction, and UnregisterFunction
carry that registration ID instead of repeating the attempt. A local gateway
invokes only when its active assignment matches the newest cache entry.
Pod readiness or a stale heartbeat alone is not proof that an old function has stopped. Recovery first makes the old assignment unreachable through the gateway Service and confirms one of these fences:
- the exact assigned Pod UID no longer exists;
- the Pod is deleting and has been removed from Service endpoints; or
- the old owning runtimed acknowledged unregister for that epoch.
Only then may shared retry clear assignment and schedule a new attempt. Peer routing requests carry the expected Run UID, registration attempt, and assigned Pod UID and are rejected on mismatch. This prevents normal recovery from routing to two assignments at once. kruntimes still does not promise exactly-once invocation when a network fails after dispatch; that separate ambiguity is defined by the invoke contract below.
Cleanup and Finalization
Before registering a long-lived Run, runtimed adds the shared
kruntimes.io/registration-cleanup finalizer. Function and Session Runs use
the same Kubernetes deletion protocol because both hold opaque, Pod-local
Runtime Server state that must not outlive their Run object. A deleting
function Run is reconciled even though normal execution creation has stopped:
- stop accepting new invokes;
- wait for or cancel bounded in-flight invokes;
- call
UnregisterFunctionidempotently; - remove function-local workspace and retained invocation state;
- release the active capacity entry;
- remove the shared finalizer.
If the assigned Pod no longer exists, the stale-recovery controller may remove the finalizer after confirming that the Pod-local registration cannot still be serving. PersistentWorkspace and ArtifactStore cleanup remain owned by their existing controllers and policies; the registration-cleanup finalizer must not delete shared persistent data implicitly.
Cancellation keeps the Run object and records a terminal status. Deletion is a separate user request and uses the finalizer path. Both operations are idempotent across controller and runtimed restarts.
Runtime Gateway
The Helm chart optionally installs one shared runtime-gateway Deployment and
ClusterIP Service for all Runtimes. gateway.enabled controls installation.
The Deployment runs stateless HTTP gateway servers. The Service forwards client
traffic only to gateway Pods, never directly to Runtime Pods. A gateway server
reads the namespace, Runtime, and Run UID from the endpoint path, calls the
Kubernetes Service for that Runtime on its dedicated gRPC port, and translates
the HTTP invoke request to the internal runtimed gRPC request.
Chart values configure replicas, RBAC, and the serving certificate Secret mounted into gateway Pods. The certificate covers the shared gateway Service DNS name. Certificate issuance and rotation are installation concerns, for example through a user-managed Secret or optional cert-manager chart resources; the Runtime controller does not manage gateway resources or certificates.
client
-> shared runtime-gateway Service
-> runtime-gateway Pod
-> Kubernetes Service for the requested Runtime
-> ready Runtime Pod's runtimed
-> owning runtimed, directly or through one peer proxy hop
-> local Runtime Server
Every runtimed maintains a watch-backed cache for function Runs in its Runtime and namespace:
Run namespace/name/UID -> assigned Pod address -> lifecycle readiness
The cache is populated asynchronously from informer events. The invoke handler must not synchronously read Runs or Pods from the Kubernetes API. Requests are handled as follows:
- UID or current-object mismatch:
404 Not Found; - known Run but not ready, recovering, or terminating:
503 Service Unavailable; - local owner: resolve its private registration reference and invoke the local Runtime Server;
- remote owner: proxy the same
FunctionRuntime.InvokeFunctionrequest once to the owning Pod’s runtimed port; - stale or unreachable owner:
503 Service Unavailableand enqueue recovery; - local concurrency limit reached:
429 Too Many Requests.
The peer request retains the original caller credential and is authorized by the owner too. Peer connections use mTLS with workload identities; a hop marker prevents proxy loops. Gateway selection and cache recovery must tolerate a request reaching a runtimed whose watch cache has not yet observed the newest assignment.
Authentication and Authorization
The initial gateway is HTTPS and ClusterIP-only; it does not claim to be an
Internet ingress. Plaintext HTTP is not supported because every client request
carries a Kubernetes bearer token. The receiving runtimed uses that token to
submit a SelfSubjectAccessReview and requires the caller to be allowed to
get the exact Run being invoked. The token must be valid for the Kubernetes
API audience.
Authorization decisions may be cached for at most 30 seconds and never beyond the token’s known expiry. Cache keys use a token digest plus Run namespace, name, and UID; raw tokens are never logged or stored in Run status. A proxied request forwards the original token and the owner repeats or reuses the same bounded authorization decision.
This introduces Kubernetes API traffic on authorization cache misses, but not for ownership or readiness routing. It preserves namespace RBAC without granting a custom Runtime service account broad Secret access or TokenReview privileges.
External exposure requires a separately reviewed authenticated ingress or gateway. NetworkPolicy should restrict direct gateway access to expected agent and platform namespaces even when Kubernetes authorization is enabled.
Invoke Contract
The first HTTPS request is deliberately bounded:
POST /v1/namespaces/{namespace}/runtimes/{runtime}/functions/{uid}:invoke
Authorization: Bearer <kubernetes-token>
Content-Type: application/json
X-Kruntime-Invocation-ID: <caller-generated-id>
Initial limits are:
- request body: 1 MiB;
- invocation ID: 128 bytes;
- outputs: the same key/count/value limits as bounded Run outputs;
- one in-flight invocation per function Run in v0.x;
- no unbounded server-side queue.
Invocation artifacts are out of scope for v0.x. Existing task-mode artifact storage and cleanup remain unchanged.
The Runtime gateway never automatically retries an invocation after dispatch to an owner or Runtime Server. A connection failure may have an unknown execution outcome. The SDK also defaults to no execution retry. A future deduplication or idempotency contract may use the caller-generated invocation ID, but v0.x does not promise exactly-once execution.
Successful and failed responses include the invocation ID. Structured runtimed
logs include Runtime, Run UID, and invocation ID. Invocation stdout/stderr is
exposed through log collection or a bounded response field defined by the
Runtime Server contract; it is never appended to Run.status.message.
Runtime Server Boundary
The internal gRPC API gains idempotent lifecycle operations keyed by Run UID:
RegisterFunction: register working directory, handler, and environment;FunctionStatus: report registered/readiness state, fatal errors, in-flight count, and last activity for the current registration generation;InvokeFunction: execute one bounded request with an invocation ID and timeout;UnregisterFunction: stop new work and release the registration.
Registering the same Run UID and registration attempt with the same immutable configuration succeeds idempotently and returns the same opaque registration ID. Registering that attempt with different configuration fails. A higher registration attempt creates a new generation and invalidates the old ID. Unregistering an absent registration succeeds. Exact protobuf fields and Bash/Python adapter behavior are a separate implementation PR, but they must preserve these semantics.
Capacity and Concurrency
Runtime runs capacity controls how many task executions or registered
function Runs may occupy one Runtime Pod. A Ready function Run continues to
consume one unit until terminal cleanup or deletion releases it.
Invocation concurrency is not scheduler capacity. In v0.x, runtimed and the
Runtime Server allow one in-flight invocation per function Run and reject an
overlapping request with 429. A later design may add per-function and
Runtime-wide invocation concurrency or queue controls without teaching the
scheduler about individual invokes.
Agent sandbox deployments should continue to use Runtime runs: "1" when they
need exclusive Pod and workspace ownership. The SDK may validate or warn about
that deployment choice, but it must not change scheduler semantics.
Component Boundaries
| Component | Responsibility |
|---|---|
| Helm chart | Optionally install the shared runtime-gateway Deployment, Service, RBAC, and TLS configuration. |
| Scheduler | Assign generic Run capacity; treat Ready as active and non-terminal. |
| runtimed | Register/unregister, own lifecycle status, maintain routing caches, authorize and proxy invokes, enforce local limits, and clean local state. |
| Runtime Server | Hold registration and activity state, execute invokes, and expose idempotent local lifecycle APIs. |
| stale recovery | Detect lost owning Pods, release impossible local cleanup, and enter shared Run retry/exhaustion semantics. |
SDK and krt | Watch readiness, discover endpoint, supply credentials, invoke without implicit execution retries, and expose typed errors. |
Workflow controllers remain unaware of function registration and invocation. They may create function Runs later, but they use only the public Run API.
Implementation Plan
- Add API prerequisites:
Ready, assigned Pod UID, endpoint status, transition validation, finalizer constant, generated CRDs, and generic phase-classification tests. - Add Helm templates, values, RBAC, and smoke coverage for the optional shared runtime-gateway Deployment and Service.
- Define chart-managed TLS configuration, including an existing Secret and optional cert-manager integration.
- Add Runtime Server register/status/invoke/unregister protobuf APIs and built-in Bash/Python adapters.
- Add runtimed registration lifecycle, finalization, shared retry integration, timeout handling, and restart recovery.
- Add the HTTPS gateway, watch-backed routing cache, local/peer dispatch, SelfSubjectAccessReview authorization, limits, and metrics.
- Add
krt invoke, then the Go and Python sandbox SDK connection layer. - Add E2E coverage for registration, TLS rotation, authorization, local and proxied invoke, concurrency rejection, cancellation, deletion, idle/lifetime timeout, Runtime Pod loss, and retry exhaustion.
- Complete the agent sandbox demo only after the supported path passes E2E.