# Runner control plane The runner control plane delivers work from Neotask Site to an enrolled local or hosted runner. It is separate from the public Agent API operation registry. ## Authentication Every request uses both credentials: 1. A short-lived Agent API bearer token for the tenant and the `neotask:runners:execute` scope. 2. A per-installation `NEOTASK-RUNNER-HMAC-V1` proof. The proof signs the exact method, path and query, timestamp, nonce, and SHA-256 hash of the raw request body. Nonces are single-use. Site rejects stale generations, altered bytes, replayed nonces, and unavailable replay protection. The HMAC secret is never sent in an envelope or returned by the API. Keep bearer tokens, HMAC material, tenant identifiers, and dispatch inputs out of logs. ## Operations All paths are relative to `https://neotask.ai/api/agent-runner/v1`. - `POST /runners/{runnerId}/recovery/company-disposition` checks whether a company-local cache must be retained or its company has been erased. - `POST /runners/{runnerId}/recovery/company-export-authorize` checks current owner/admin authority for an attended local company export. - `POST /runners/{runnerId}/recovery/export-authorize` checks that authority for an exact run recovery binding. - `POST /runners/{runnerId}/recovery/disposition` reads the exact run's retention, company-erasure or delivered-result disposition. - `POST /runners/{runnerId}/confirm` confirms an enrollment challenge. - `POST /runners/{runnerId}/heartbeat` records liveness and safe capability metadata. Runners should send a heartbeat every 30 seconds; Site marks a runner offline after 90 seconds without one. - `POST /runners/{runnerId}/lifecycle/drain` starts a durable drain and returns the lifecycle token for that exact intent and generation pair. - `POST /runners/{runnerId}/lifecycle/drain/status` returns the current drain state and server-derived work counts. - `POST /runners/{runnerId}/lifecycle/drain/resume` releases a completed drain after the local lifecycle transaction succeeds. - `POST /runners/{runnerId}/lifecycle/drain/abort` releases a completed drain after the local transaction is abandoned safely. - `POST /runners/{runnerId}/leases/claim` atomically claims the oldest queued dispatch that matches the runner capability set. A response with no work is `204`. - `POST /runners/{runnerId}/leases/{leaseId}/operation-authorize` issues one short-lived online authorization for one `agent.turn` or `tool.execute` operation. The runner must advertise the `live-operation-authorization-v2` capability. For a selected-agent run, Site rereads its runtime tool policy before each receipt. A denied tool returns `403 tool_policy_denied`, with a source `reason` when available. It uses the same reason as the prepared-run tool projection, such as `tool_not_prepared`, `tool_policy_denied`, or `integration_connection_required`. A malformed stored policy returns `503 temporarily_disabled`. Approval and recovery do not bypass this check. - `POST /runners/{runnerId}/controls/poll` returns the one queued autonomy steering or cancellation command matching the exact dispatch, lease, run, task, and state-sequence fence. A response with no command is `204`. - `POST /runners/{runnerId}/leases/{leaseId}/tool-inventory` records the selected root execution's complete prepared tool identities under its live lease and version-2 runtime profile. - `POST /runners/{runnerId}/leases/{leaseId}/prepared-tools/read` reads that run's complete current tool and approval-policy view without granting execution. - `POST /runners/{runnerId}/leases/{leaseId}/approvals/request` requests a human review for one plugin or gateway command call. - `POST /runners/{runnerId}/leases/{leaseId}/approvals/read` reads that request under the current lease. - `POST /runners/{runnerId}/leases/{leaseId}/approvals/consume` claims the exact human decision once for the waiting call. - `POST /runners/{runnerId}/leases/{leaseId}/approvals/checkpoint` retains the complete pending approval references and transcript digest for restart recovery. - `POST /runners/{runnerId}/controls/{commandId}/ack` acknowledges or rejects one delivered autonomy control under the same exact delivery fence. Repeating the same acknowledgement is idempotent; a stale delivery is rejected. - `POST /runners/{runnerId}/leases/{leaseId}/ack` acknowledges or rejects a delivery. A pre-execution rejection requeues the same dispatch with a new lease fence. - `POST /runners/{runnerId}/leases/{leaseId}/renew` extends a live lease. A lease waiting for approval is retained for at least eight hours; approval and model execution do not time out. - `POST /runners/{runnerId}/leases/{leaseId}/events` appends one bounded event with a monotonic sequence and idempotent `eventKey`. - `POST /runners/{runnerId}/leases/{leaseId}/result` writes one bounded terminal result. Repeating the same result hash is idempotent; a different result is rejected. - `POST /runners/{runnerId}/leases/{leaseId}/abandon` requeues a delivery abandoned before acknowledgement. After acknowledgement, Site marks the dispatch `execution_unknown` and requires explicit recovery. Selected dispatch creation and claim check the current task, agent profile and tool policy inside their write transaction. First policy creation serializes through the same agent record, so a missing policy cannot bypass a concurrent restriction. Recovery requires an exact active-run proof and commits the dispatch and run lease epochs together before issuing workload authority. A failed pre-commit run or authority check leaves both previous epochs intact. Approval-wait recovery retains the eight-hour lease floor. A drain stops new claims while existing acknowledged work keeps its exact pre-drain runner and lease generations. Continuation requires the recorded service-lifecycle or Electron-adoption transition, with each current generation exactly one above its previous value. Recovery preserves that run's generation pair. An unrelated, malformed, resumed, offline or revoked runner state cannot reuse the old binding. ## Packaged tool inventory A heartbeat may include `runtimeToolInventory` with `schemaVersion: 1`, `runtimeFlavor`, `catalogDigest`, and `toolNames`. The flavor must match the enrolled runner type: Electron uses `electron-managed`; CLI and cloud runners use `standalone-agent-runner`. Send the complete packaged-name list, preserving case. Names must be unique, nonempty, free of surrounding whitespace and ASCII controls, and at most 512 UTF-8 bytes each. The list allows at most 4,096 names and 256 KiB of serialized JSON. Omit the report to preserve stored metadata; send an empty `toolNames` list to remove all previously reported names. Site retains only names present in its reviewed core/plugin catalog for that flavor. It derives the inventory digest and advances the stored generation when the reviewed report changes. Identical reports keep their generation. A concurrent change returns `503 temporarily_disabled`; send a fresh signed heartbeat. Clients cannot supply the stored inventory digest or generation. This report describes package contents. Agent configuration, plan, scope, connection grants, approvals, and live execution checks still govern use. The report does not make a tool available or grant permission to execute it. New selected-agent dispatches require `agent-runtime-tool-inventory-v1` and a current reviewed report. Claim records the report's flavor, catalog digest, inventory digest and generation in both the dispatch and canonical run. The signed workload lease carries the same binding. Live operations and recovery reject a changed or missing binding, a changed report, or a missing required capability. They do not silently move an existing run to a newer inventory. Generic legacy dispatches without this requirement retain their existing path. ## Prepared execution inventory After preparing a selected root execution's tools, the runner sends `dispatchId`, `leaseEpoch`, `runtimeProfileDigest`, and `inventory` to `tool-inventory`. Capture the tools after the ordinary runtime filters and before tool-search compacts the prompt. Child executions are separate and must not replace the root observation. The inventory contains `schemaVersion: 1`, a UUID-v4 `preparationId`, `runtimeFlavor`, `catalogDigest`, and `tools`. Each tool has its exact `name`, `source` (`core`, `plugin`, or `mcp`), `pluginId`, `serverId`, and `originalName`. Core tools have three null identity fields; plugin tools have a plugin ID and two null MCP fields; MCP tools have all three identities from their runtime materializer. Do not infer identity from an alias or send descriptions, schemas, paths, arguments, or credentials. Each non-null name is limited to 512 UTF-8 bytes. The inventory allows 4,096 unique tool names and 512 KiB of serialized JSON. Send an empty list when no tools were prepared. Invalid or oversized reports are rejected as a whole. Site checks current principal and membership, account, runner and HMAC generation, run, task, lease, and selected profile before storing the report. The catalog must match Site's reviewed catalog and the selected profile. The response contains `accepted: true` and the server-derived `inventoryDigest`. For dispatches requiring `agent-prepared-capability-binding-v1`, it also contains `capabilityDigest`. The runner must verify the inventory digest and require a valid capability digest when the dispatch requires it. This local requirement is not an additional request field. Site binds the complete prepared source decisions to the current principal, run, lease, runner inventory, selected profile, and admitted origin/resource authority. It stores the snapshot and the canonical run's matching digest in one transaction. Repeating the current preparation is idempotent only when its contents, epoch, and current source decisions still match. A new preparation or recovered lease needs a new acknowledgement; an old digest cannot resume it. The stored observation shares the dispatch's retention, export, and erasure lifecycle. Its inventory/capability digests and count enter the audit record; tool and server names do not. The report grants no tool access. Consumers must recheck the current lease, profile, grants, policies, and service state before presenting availability. Roll out the Site endpoint before a runner that requires this report for version-2 selected profiles. ## Live operation authorization New selected-agent dispatches carry a version-2 runtime profile and require `agent-runtime-profile-v1`, `agent-runtime-tool-policy-v1`, `agent-runtime-tool-inventory-v1`, and `agent-prepared-capability-binding-v1`. The profile contains the current tool-policy revision, reviewed catalog digest, and runtime policy. Its signed digest binds those fields with the selected agent's identity, generation, workspace and execution preferences. Site reloads that binding before delivery, claim, approval continuation and workload use. A changed policy revision invalidates the old run even if someone restores the previous policy values. Gateway rejects a version-2 profile built against a different reviewed catalog. Retained version-1 profiles can continue only while the additional runtime policy remains unrestricted. Gateway applies the runtime policy after its existing tool filters. An empty `allow` list exposes no tools; `null` leaves the selected policy profile's allowance unchanged. Names are case-sensitive and wildcards are literal. Nested managed execution retains its parent's restrictions. Coding and messaging profiles retain bundled MCP tools through the runtime's verified materializer metadata. An alias or a tool-owned plugin label cannot establish that membership. Exact allowlists and denials still apply, and self-service narrowing accounts for future names in the dynamic group. For every tool in a new inventory-bound selected run, Site checks the exact current prepared source, including under the full profile or an explicit allowlist. Core and static plugin tools must match both the runner's reviewed inventory and the catalog's owning source. MCP tools require the exact scoped server and provider-local tool, connected grant, runtime readiness, and current agent/company policies. Both native and bundled MCP paths require a claimed service-auth principal; a local connection without the matching scoped Site grant does not qualify. Native MCP tools do not inherit core or bundled profile membership from an alias. The reviewed catalog artifact is version 2. Its digest covers static tools and the built-in LSP factory's three name families. LSP calls require that reviewed factory, an exact prepared tool, and current profile authority. Their local server configuration remains the Gateway's responsibility. Native MCP and LSP tools remain full-profile-only, with exact allow/deny restrictions applied. The four existing tool-search controls are recorded without expanding any restricted profile. Packaged and prepared report envelopes remain version 1. Drain selected work before activating a changed catalog. Deploy matching Site and Gateway catalog artifacts, obtain a fresh runner heartbeat, and materialize new selected profiles. An old digest cannot authorize a new catalog. Site rechecks current policy and the prepared digest on each tool authorization, including after approval or resume. Invocation and approval owners retain their separate checks. Legacy no-inventory runs keep their existing path. The public agent-tools MCP projection applies the same current company access, agent/company MCP policies and runtime restriction. Runtime denials appear as `tool_policy_denied` in the tool's reasons, alongside any connection or provider policy denial. Canonical MCP names do not establish bundled profile membership; that requires the exact prepared-source checks above. The projection does not replace live authorization or report the full core/plugin/skill tool set yet. The operation-authorization route is online-only. Call it immediately before one live operation after the runner has acknowledged the lease. Site checks the current tenant, principal, account, runner capability, HMAC generation, dispatch, run, lease epoch, and expiry fences. It does not accept a prompt, tool arguments, credentials, or an offline grant. Site requires `live-operation-authorization-v2` for new and previously queued dispatches. Update an older runner before it can receive that work. The route prefix remains `/api/agent-runner/v1`; the capability versions the live authorization contract. The request body is closed and contains these six fields, plus `runtimeProfileDigest` when the dispatch selected an agent profile and `capabilityDigest` when it requires prepared capability binding: ```json { "dispatchId": "550e8400-e29b-41d4-a716-446655440000", "leaseEpoch": 7, "action": "tool.execute", "toolName": "read", "operationId": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg", "resourceHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } ``` `runtimeProfileDigest`, when required, is the selected profile's lowercase 64-character SHA-256 digest. `capabilityDigest` is the lowercase 64-character digest from the latest prepared-inventory acknowledgement. Site checks it before model turns, exact tool calls, and approval request/read/consume. It rechecks the current tool source and rejects a replaced preparation before returning authorization. The runner cannot execute during preparation or use a previous acknowledgement after a failed replacement. `dispatchId` is a UUID v4. `leaseEpoch` is an integer from 1 through 1,000,000. `action` is `agent.turn` or `tool.execute`. `operationId` is a fresh identifier generated from 32 cryptographically random bytes, encoded as 43 base64url characters. Never reuse it. `toolName` is `null` for `agent.turn` and the exact runtime name for `tool.execute`. Tool names must contain 1–512 UTF-8 bytes, with no leading or trailing whitespace or ASCII control characters. Preserve case, company suffixes and bundle aliases. `resourceHash` is the lowercase 64-character SHA-256 hash of Gateway's canonical JSON for `{ toolName, resource }`, where `resource` contains the operation's private resource data. Site receives the tool name and hash, without the resource contents. Resource-specific permissions and approval checks still apply. A successful response contains exactly seven fields: ```json { "authorized": true, "operationId": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg", "action": "tool.execute", "toolName": "read", "resourceHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "authorizedAt": "2026-09-05T12:00:00.000Z", "authorizationExpiresAt": "2026-09-05T12:00:05.000Z" } ``` The receipt contains the same action, tool name, operation ID and resource hash. Gateway rejects a receipt with a missing or different tool name, including hash-only v1 receipts. The authenticated request and runner execution context bind it to the dispatch and lease; those identifiers are not response fields. Its expiry is no later than five seconds after `authorizedAt`, or earlier when the agent credential, account license, dispatch lease, or run lease expires. Treat it as single-use and discard it after the operation or expiry. Do not cache or replay it. Keep this operation in the internal runner protocol. The public Agent API and MCP tool registry exclude it. All requests still require the Agent bearer scope `neotask:runners:execute` and the per-installation runner HMAC proof. ## Human approval for runner operations The three approval routes require a claimed, trusted agent account, current account membership, and the bearer and installation-HMAC credentials above. Site checks the tenant, billing, principal generation, runner, dispatch, run, task and selected profile before each request, read or claim. Send the same closed body to all three routes: `dispatchId`, `leaseEpoch`, the required selected `runtimeProfileDigest`, the required prepared `capabilityDigest`, and `operation`. Omit digest fields only when the dispatch does not require their binding. The operation contains `toolCallId`, `kind` (`plugin` or `exec`), `toolName`, `operationHash`, `title`, optional `description`, `parameters`, `riskLevel`, and `allowedDecisions`. Parameters are complete redacted JSON encoded as a string. Site rejects bodies above 768 KiB and individual content strings above 512 KiB; do not truncate a request to fit. Gateway hashes the exact, unredacted execution arguments with the separate `NEOTASK-RUNNER-OPERATION-V1` domain. It sends only the hash and redacted review content. Site binds the stored request to the authenticated principal, task, run, lease and tool-call occurrence. Reusing that occurrence with changed arguments or review content returns a conflict. A pending request returns an `approvalId`, matching `operationHash`, `humanReviewUrl` and `handoffExpiresAt`. The link opens the existing signed-in account approval page. An owner or admin reviews the full request there; the runner cannot submit a human decision. The handoff expires after 15 minutes. Requesting another handoff for the same unchanged pending occurrence preserves its approval record. Approval records have no automatic expiry, and cancelling a local wait does not approve or delete them. A read returns status only. After approval or denial, consume returns the selected decision and an `authorizationExpiresAt` no more than five seconds away, capped by current credential, license and lease expiry. Site checks that the reviewed request and policy are unchanged, then records one claim and its audit in the same transaction. The claim permits the waiting call to continue; it does not prove that the call executed. Keep the original call waiting and continue lease renewal. Retry transport failures on request and read with a fresh HMAC nonce. Never retry a lost consume response or replay the operation: report the uncertain continuation and start a new reviewed occurrence. Recheck live operation authority and the claim deadline immediately before execution. MCP calls retain their separate sealed workload approval path. ## Local recovery retention and export Recovery requests use the same bearer scope and installation HMAC as other runner operations. The runner must still belong to the principal and registration, with the current HMAC generation and an active, offline or draining status. Site checks that ownership before and after reading authority. Company recovery requests contain only `companyId`. Run recovery requests contain only `dispatchId`, `runId`, `taskId`, `companyId`, `leaseId` and `leaseEpoch`; the OpenAPI schemas define their exact accepted formats. A disposition of `retain` means keep the local data. Missing rows, an expired lease, elapsed time or a company sealed for deletion are not disposal authority. `company_erased` comes from the committed company-erasure record. `result_delivered` includes the SHA-256 hash of the terminal result for the exact saved binding; compare it with the local receipt before cleanup. A company cache has no unrelated run receipt and uses the company-disposition route. Export authorization requires a claimed account and current owner/admin access to the account and company. A standalone run must belong to the principal's own tenant. The response contains the verified binding, not recovery contents, credentials or execution permission. Local attended consent is still required. An authorization response cannot replay work or override a retained approval. ## Lifecycle fencing Begin a drain with the generations from the latest runner record: ```json { "expectedRunnerGeneration": 1, "expectedLeaseGeneration": 1, "intentId": "lifecycle-intent-unique-0001", "purpose": "service_lifecycle" } ``` `purpose` is `service_lifecycle` for a standalone service transaction or `electron_adoption` when Electron adopts the installation. A successful begin increments both generations, changes the runner to `draining`, and returns a `lifecycle.token`. Retrying the same begin request while the drain is active returns the same token. A retry after release returns the completion receipt without the token. A different intent or stale generation returns `409`. Use this exact body for status, resume, and abort: ```json { "runnerGeneration": 2, "leaseGeneration": 2, "intentId": "lifecycle-intent-unique-0001", "lifecycleToken": "rdr_<43 base64url characters>" } ``` Treat the lifecycle token as a credential. Do not put it in a URL, log, audit payload, process list, or service definition. The token is bound to the tenant, principal, runner, intent, and both generations. Site derives `activeRunCount`, `approvalWaitCount`, `activeLeaseCount`, `providerReferenceCount`, and `mailIdentityCount` from tenant-scoped records. The runner is ready to release only when the first three counts are zero. Provider and Mail counts remain visible so the local transaction can preserve or migrate those references deliberately. Draining has no timeout. A crash leaves the runner in `draining`, blocks new normal claims, and preserves recovery of work the runner already owns. Resume and abort both require a ready drain. They move the runner to `offline`; a new heartbeat is required before Site marks it active again. `runnerGeneration` and `leaseGeneration` are lifecycle fences. They are separate from the HMAC key version and from each dispatch's `leaseEpoch`. Each lease request includes `leaseEpoch` in its JSON body. Site checks the runner, lease ID, epoch, tenant, and expiration in one fenced update. Runner dispatch records and the AutoTaskRun ledger remain tenant-scoped MongoDB records; the runner does not become a source of truth. Terminal dispatches set an `expiresAt` value 90 days after completion, and MongoDB's TTL index removes them after that retention window. The same model is registered with the Site tenant-erasure service for account deletion and DSAR requests. An `execution_unknown` dispatch remains tenant-retained until an explicit recovery or administrative cleanup decision; it is never silently expired and reassigned. Committed control transitions emit tenant-scoped `agent.runner.*` audit events for enrollment, heartbeat, dispatch creation, claim, acknowledgement, requeue, renewal, event append, result, abandonment, execution-unknown, and revocation convergence. Drain begin, resume, and abort events record only the purpose, generations, and server-derived counts. Autonomy control poll and acknowledgement are also tenant-scoped and HMAC-protected; their audit payloads contain identifiers and bounded state metadata only. They do not contain prompts, tool input, secrets, or result bodies. Machine-readable schemas and response examples are in the [runner OpenAPI contract](./openapi.yaml). The canonical operation list is the `AGENT_RUNNER_PROTOCOL_REGISTRY` in the Site server source.