# MeshX Official Actions Runner Compatibility

> Canonical path: `specs/automation/actions-runner-compat.md`
> Status: MVP compatibility contract
> Runner version: GitHub Actions runner `v2.334.0` on Linux x64

## Summary

MeshX implements a minimal server-side compatibility layer for the official
GitHub Actions runner. MeshX remains the automation control plane: AGS owns
automation definition source files, MeshX owns run and job ledgers, queue leasing, waits,
logs, artifacts, and reconciliation, and the official runner executes only
supported shell `run` steps outside MeshX.

The existing runner API under
`/api/v1/workspaces/:workspaceId/automations/runners/*` remains the canonical
AgentTeam public/control-plane protocol. Embedded MeshX may receive the corresponding
`/api/v1/automations/runners/*` suffix only as a private in-process target after
canonical authorization and prefix stripping; it is not a public URL. Official
runner compatibility is a separate adapter mounted on
GitHub-shaped endpoints and translated into the same MeshX automation runner
service transitions.

## Public URL Configuration

The official runner stores the broker URL and OAuth audience returned during
`config.sh`, and later uses the run-service URL embedded in broker job
messages. MeshX must derive these URLs from trusted configuration, not from
request `Host` or forwarded headers supplied by a client.

`MESHX_AUTOMATION_ACTIONS_RUNNER_BASE_URL` can override the public base URL used
for official runner compatibility endpoints. The value must be an absolute
`http` or `https` URL, may include a deployment path prefix, and must not
include user info, query, or fragment components. When unset, MeshX uses the
configured AGS base URL for the same GitHub-shaped HTTP surface.

## Observed Runner Protocol

The compatibility target is the official `actions/runner` release `v2.334.0`.
The observed configuration command was:

```bash
GITHUB_ACTIONS_RUNNER_FORCE_GHES=1 ./config.sh \
  --unattended \
  --url http://HOST/OWNER/REPO \
  --token REGISTRATION_TOKEN \
  --name RUNNER_NAME \
  --runnergroup Default \
  --work _work \
  --replace
```

For a GitHub Enterprise Server-shaped URL, `config.sh` uses the runner-admin
flow and sends:

| Order | Method | Path | Purpose |
| --- | --- | --- | --- |
| 1 | `POST` | `/api/v3/actions/runner-registration` | exchange the CLI token for runner registration metadata |
| 2 | `GET` | `/api/v3/repos/{owner}/{repo}/actions/runner-groups` | resolve the configured runner group |
| 3 | `GET` | `/api/v3/repos/{owner}/{repo}/actions/runners?name={name}` | check for an existing runner by name |
| 4 | `POST` | `/api/v3/actions/runners/register` | register runner name, version, labels, and public key |

The runner sends the CLI token as `Authorization: RemoteAuth <token>` during
configuration. The successful registration response records `.runner` with:

```json
{
  "useV2Flow": true,
  "useRunnerAdminFlow": true,
  "serverUrl": "http://HOST/_broker/",
  "serverUrlV2": "http://HOST/_broker/"
}
```

At runtime, `run.sh` exchanges the configured OAuth client assertion and then
uses the broker flow:

| Order | Method | Path | Purpose |
| --- | --- | --- | --- |
| 1 | `POST` | `/_oauth/token` | OAuth client credentials token exchange |
| 2 | `POST` | `/_broker/session` | create a broker session |
| 3 | `GET` | `/_broker/message?sessionId=...&status=Online&runnerVersion=2.334.0&os=Linux&architecture=X64&disableUpdate=false` | long-poll for broker messages |
| 4 | `DELETE` | `/_broker/session` | close broker session |

An empty poll response of immediate `200 null` causes the runner to tight-agentteam.
MeshX must hold empty polls open with bounded long polling before returning
`null`.

## Job Dispatch Contract

The MVP uses the V2 broker/run-service path. When MeshX claims a queue row, the
broker message is a `TaskAgentMessage` with:

- `MessageType`: `RunnerJobRequest`
- `Body`: JSON string for `RunnerJobRequestRef`

The body includes:

```json
{
  "runner_request_id": "QUEUE_JOB_ID",
  "run_service_url": "http://HOST/_runs/",
  "billing_owner_id": "meshx"
}
```

The runner then calls:

| Method | Path | MeshX transition |
| --- | --- | --- |
| `POST` | `/_runs/acquirejob` | accept the leased MeshX queue job and return an `AgentJobRequestMessage` |
| `POST` | `/_runs/renewjob` | renew the MeshX job lease |
| `POST` | `/_runs/completejob` | map runner conclusion and step-result metadata to MeshX complete/fail |

The `AgentJobRequestMessage` contains one MeshX job run and shell `run` steps.
Only `ScriptReference` action steps are emitted for the MVP. The job message
includes a typed `contextData.github` dictionary using the official runner
pipeline context-data wire shape so the runner can initialize the GitHub
context before script execution. MeshX still rejects automation expressions
outside the server-managed runner's safe `env` subset for the MVP, including
expressions embedded in `run` scripts, because the adapter does not yet
serialize official expression template tokens for step inputs. The official
runner adapter does not materialize automation/job/step `env`; if a persisted job
input contains env, including `${{ secrets.NAME }}` references, the adapter
fails closed before dispatch instead of sending an unresolved or silently
truncated job. It does not materialize `inputs`, `github`, `env`, `vars`,
`secrets`, `needs`, `jobs`, `steps`, `runner`, `matrix`, or `strategy`
expression contexts for automation authoring. The official-runner adapter does
not dispatch arbitrary checkout actions, external actions, services,
containers, matrix expansion, custom action dependency resolution, or job-output
definitions.

The server-managed runner supports explicit AgentTeam-native actions such as
`uses: agentteam/checkout`, `uses: agentteam/publish-wiki`, and
`uses: agentteam/publish-tree`. These actions require MeshX server-side handlers,
so jobs containing them are assigned to an internal server-managed runner group
and are not dispatched to official runner sessions.

`AcquireJob` embeds a separate run-service bearer token in
`SystemVssConnection` instead of reusing the broker token returned by OAuth.
The broker token has a 24-hour MVP lifetime and authorizes broker polling plus
`acquirejob`; the run-service token has a seven-day MVP lifetime and authorizes
only `renewjob` and `completejob`. The run-service token is issued only after a
MeshX queue lease is accepted and the official job payload can be built. The
adapter emits a 30-minute timeout on each shell `run` step, and caps the
official-runner job payload to 200 run steps, so supported MVP jobs have a
bounded execution budget under the run-service token lifetime.

The MVP persists terminal status and official step-result metadata from
`completejob`. The HTTP callback limit and the `automation_job_runs.output_json`
column budget must stay aligned so accepted annotations, step results, and
telemetry can be durably recorded. It does not expose the GitHub Results service
or live log upload APIs; raw runner console output remains outside the
official-runner compatibility surface until those endpoints are implemented.

## Credential And Session Mapping

MeshX registration tokens are the official `config.sh --token` values. The
official adapter verifies the token by its hash and workspace-owned runner row,
then stores official runner client metadata scoped to the MeshX runner.
Bootstrap, runner-group, and runner-list requests only validate the token. The
final runner-register request consumes the one-time MeshX registration token in
the same transaction that stores the official OAuth client metadata, so the
same token cannot register another official client after successful
configuration.

Runtime OAuth returns a JWT-shaped bearer token whose full-token hash is stored
in MeshX automation runner token storage. If the official runner already has an
active MeshX runner session, OAuth reuses that session; it creates a new session
only when no active session exists. The token is JWT-shaped because
`actions/runner` v2.334.0 parses the `SystemVssConnection` endpoint access
token before dispatching the worker. MeshX still treats the token as opaque for
authorization: broker and run-service endpoints validate the full-token hash,
expiry, server-side session-token hash, and server-side token scope before
translating the token payload back to:

- workspace id
- MeshX runner id
- MeshX runner session id
- token scope

The JWT-shaped payload must not include the native MeshX runner session token.
That native token remains server-side and is used only by hash after the
official adapter has enforced broker or run-service scope.

The broker access token expires according to the OAuth `expires_in` value,
currently a 24-hour MVP budget for broker authentication. Acquired jobs receive
a separate seven-day run-service token because the official runner turns the
`SystemVssConnection` endpoint token into a fixed credential for `renewjob` and
`completejob`; it does not refresh that credential from the OAuth client
assertion in the job payload. Token scope is enforced before heartbeat:
run-service endpoint tokens cannot create broker sessions, poll broker messages,
or acquire additional jobs, and broker tokens cannot renew or complete accepted
jobs. OAuth refreshes for the same official runner preserve the active MeshX
session so in-flight run-service tokens remain valid. A newly created session
still revokes previous active sessions for the same MeshX runner, preserving a
single active lease owner. Valid broker and run-service requests refresh the
MeshX session heartbeat before touching queue state. `DELETE /_broker/session`
revokes the MeshX runner session and session token; subsequent broker or
run-service requests with tokens bound to that session fail authentication.
Queue claim cost stays bounded by one runner session and at most one claimed job
per poll.

## Unsupported Features

The MVP rejects or fails closed for unsupported GitHub Actions features:

- non-AgentTeam `uses` actions
- arbitrary checkout actions such as `actions/checkout@v4`; use explicit
  `agentteam/checkout` for the current AGS repository snapshot
- artifact/cache GitHub service APIs
- GitHub Results service live log upload APIs
- service containers and job containers
- matrix/strategy
- environments
- automation and job concurrency
- job-level and step-level `if` conditions
- automation/job/step `env`
- automation/job `defaults`
- automation job `outputs` definitions
- all automation expressions outside the server-managed runner's safe `env`
  subset, including expressions embedded in `run` scripts
- full expression/context evaluation
- secrets masking beyond the minimal variables emitted in the job message

AgentTeam-native waits remain MeshX runtime concepts. Jobs implemented with
`agentteam/agent-task` or `agentteam/wait-event` create
`automation_waits` rows and do not occupy official runner leases. When a wait is
resolved, MeshX schedules downstream ordinary jobs through the existing
automation scheduler. A AgentTeam-native wait job must contain exactly one
AgentTeam-native wait step; combine approval/event waiting and shell work as separate
jobs linked with `needs`.

## Validation

Behavioral validation should cover:

- official runner registration endpoints preserve the existing
  `/api/v1/workspaces/:workspaceId/automations/runners/*` API behavior
- broker empty polling blocks briefly before returning `null`
- a single shell `run` step job can be acquired, renewed, completed, and mapped
  to a succeeded MeshX run
- failed runner conclusions map to MeshX job/run failure
- callback metadata persists status/log facts without storing raw log payload
  in `automation_events.metadata_json`
- automation expressions and job-output definitions are rejected before
  official-runner dispatch
- unsupported `uses` jobs are rejected or fail before official-runner dispatch
- malformed AgentTeam-native wait jobs are rejected instead of silently skipping
  runner work
- wait resolution schedules downstream jobs without a runner lease
