omadiaomadia.ai
registry
extensiondev-platform

Dev Platform

@omadia/dev-platform

Runs autonomous coding jobs against your GitHub repositories. An agent clones a repo into an isolated runner, drives an LLM through an analyze/plan/implement/review pipeline, and opens a pull request — bounded at every step by human approval gates, a diff policy, a per-job cost budget and an egress allowlist. Ships its own operator UI, HTTP routes, chat tools, background workers and nine database tables it migrates itself. Runners execute on Fly Machines or on local Docker; neither ever sees a long-lived credential. Extracted from omadia core into its own installable plugin (epic byte5ai/omadia#470).

latest
v0.3.5
license
MIT
versions
5
author
byte5 GmbH

install

In your omadia instance, open Admin → Registries and add this registry, then install Dev Platform from Admin → Plugins → Store.

registryhttps://hub.omadia.ai

setup guide

Dev Platform

Runs coding jobs against your repositories: an agent clones the repo in an isolated runner, works the job, and opens a pull request. Human gates, diff policy and per-job LLM budgets bound what it can do.

Before you install

omadia core 1.5 or newer. The plugin activates against extension points that landed across five core releases; the newest is ctx.sql.seedLedger (the migration handoff). On an older core the handoff silently does not happen and the migrations re-run against a schema core already created.

A Postgres-backed knowledge graph is required. The job spine, repo, gate and artifact tables live there. With an in-memory graph the plugin refuses to activate rather than pretending to work — set DATABASE_URL and install the Neon knowledge-graph plugin first.

Two operator grants — the next step after this form

This plugin declares two permissions that a manifest alone cannot give it. A human has to agree, separately, and neither is implied by installing.

You are asked on the next step. Once this form is accepted the wizard shows Permissions, listing exactly what is below and nothing else. Grant & activate records your consent and activates the plugin in the same request. Skip grants nothing and does not lose the install — the same choices live on the plugin page afterwards, under Permissions, with a toggle per grant. Either way the plugin is re-activated in place: no middleware restart.

1. The SQL permission (permissions.sql). The plugin owns nine tables and migrates them itself into its own ledger, plg_omadia_dev_platform_migrations. It cannot read or write omadia's tables or another plugin's. The ledger name comes from this manifest and the granting identity from your session — neither can be supplied by a request.

This plugin reaches for the database in activate(), so skipping this grant leaves it errored rather than half-working, and the wizard says so instead of guessing. Granting it later from the plugin page brings it up; no reinstall.

2. The public-path grants (permissions.public_paths). Three URL prefixes are served without a kernel session, because the caller has no session to present. Each is still authenticated, just not by one:

  • /api/v1/dev-runner — a per-job bearer token, verified in the router.
  • /api/webhooks/github — HMAC-SHA256 over the raw request body.
  • /api/v1/dev-platform — the GitHub App manifest-conversion callback, bound to a kernel-signed, plugin-audience state token.

Declining one is not an activation failure: the prefix simply stays behind the login and answers 401. Without them the runner cannot phone home, webhooks 401, and GitHub App onboarding cannot complete.

Consent can never exceed the declaration — core rejects anything this manifest does not ask for, so the consent surface cannot itself be used to hand a plugin your database or make an arbitrary URL public.

For automation, one route carries both, behind operator auth:

GET  /api/v1/admin/runtime/installed/@omadia%2Fdev-platform/grants
PUT  /api/v1/admin/runtime/installed/@omadia%2Fdev-platform/grants
     { "sql": true,
       "public_paths": ["/api/v1/dev-runner",
                        "/api/webhooks/github",
                        "/api/v1/dev-platform"] }

An absent key leaves that grant alone; a present public_paths is the COMPLETE set, so omitting a prefix revokes it. The older …/public-paths route still works unchanged.

On a core older than byte5ai/omadia#824 none of the above exists: GET …/grants answers 404, the SQL grant is a hand-written row in plugin_sql_grants followed by a middleware restart, and only the …/public-paths route is available. The plugin's OPERATOR-GUIDE Appendix A has that procedure in full.

What this plugin does without, and what you lose

Four capabilities are declared optional. The plugin installs and runs when core does not provide them, and each absence is logged rather than silent. This table is the only place you can read it before installing — the registry does not carry optional capabilities.

CapabilityAbsent means
turnContext@1The chat dev-job tools are not registered at all, rather than registered and always refusing. They authorize per call against the human driving the turn; with no envelope there is nothing to authorize against. Use the operator UI instead.
githubAppJwt@1The plugin falls back to a local RS256 signer. Functionally equivalent; the App private key is held by the plugin rather than by core.
usageTelemetry@1No rows in the operator cost dashboard's ledger. Per-job budgets are unaffected — they meter this plugin's own tables and keep enforcing.
conductorRoles@1Repositories whose approver is a ROLE rather than a person cannot have their gates approved. Fail-CLOSED — the safe direction, but a regression you should know about. Assign named approvers instead.

Only graphPool@1 is mandatory. If core cannot provide it the install is refused with install.missing_capability (HTTP 409).

Where your runners execute

Two backends, chosen by what you configure in the deployment — not here:

  • Fly Machines — one ephemeral Machine per job, in a dedicated Fly app named by DEV_FLY_RUNNER_APP. This is the production path. The app must exist before the first job.
  • Local Docker — the compose overlay shipped with this repository (docker-compose.dev-platform.yaml), which brings up the runner daemon, the egress proxy and one privileged docker-in-docker service. Best for evaluation and self-hosting.

Either way the runner reaches this middleware at the phone-home URL below, so a loopback address only works when the runner shares the host.

What lives here, and what lives in your deployment

The fields below are policy — the decisions only you can make: how much a job may spend, how long it may run, how many run at once, which models it may call, how long its history is kept.

Infrastructure is not asked for here. The runner image, the runner daemon URL and token, and the dedicated Fly app are set in the compose overlay alongside the sidecars themselves (DEV_RUNNER_IMAGE, DEV_RUNNER_DAEMON_URL, DEV_RUNNER_DAEMON_TOKEN, DEV_FLY_RUNNER_APP). A second copy in this form would be a second thing to keep in sync.

Two switches that need a second confirmation

Subscription mode runs jobs against a CLI subscription credential inside the runner. Unsafe local backend runs the shim as a local process instead of in a container. Each demands its paired acknowledgment field, and activation is refused if you turn one on without it. That refusal is deliberate: both modes put a credential or your host filesystem within reach of code an agent wrote.

Upgrading from the Dev Platform that ran inside core

Migration slots 0022–0030 are already applied on your database and recorded in CORE's ledger. This plugin adopts them instead of re-applying them — but only where a WITNESS proves the schema object each file creates is actually present. The case that makes this necessary is rows present, tables absent: a restore from an older snapshot, a version-skewed rollback, a table dropped during an incident.

handoff-plan.json ships inside the ZIP so you can see the plan against your real database BEFORE installing. Dry run is the default:

cd <omadia>/middleware && npm run build
DATABASE_URL=… node scripts/plugin-ledger-handoff.mjs \
  --plan <zip>/handoff-plan.json

Read skippedNoWitness in the output. Empty is healthy. Non-empty means core's ledger and your live schema disagree — not a failure of the handoff, but a reason to confirm the database is the one you think it is.

After installing — you re-enter the credentials

Connect a repository under Dev Platform → Repositories. A GitHub App is the recommended credential — the runner then receives a freshly minted, single-repo, read-only token that is revoked when the job ends.

GitHub App keys and repository tokens are not migrated for you. This plugin stores credentials in its own vault namespace, so App signing material, webhook secrets and per-repo clone tokens are entered here once, by you. That is a deliberate choice over a one-time migration hook: the right moment to see that a GitHub App private key has moved into a plugin's namespace is while it is happening. The same applies after a reinstall. See docs/SECRETS.md for the full list and for rotation.

versions

v0.3.5
>=1.5 <2.0525.9 KB2026-09-08
.zipb23f1a13e150
v0.3.4
>=1.5 <2.0525.8 KB2026-08-21
.zipcec222fb95cb
v0.3.3
>=1.5 <2.0525.8 KB2026-08-21
.zipd22022bca7d9
v0.3.2
>=1.5 <2.0524.5 KB2026-08-21
.zip80eec66e32f5
v0.3.1
>=1.5 <2.0524.5 KB2026-08-21
.zip618d8b60b95a

setup fields

Values the operator fills in at install-time.

runner_base_urlurl
Runner phone-home base URLThe base URL a runner reaches this middleware at, e.g. https://bot.example.com. Runners are launched OUTSIDE this process and call back to it, so a loopback address only works when the runner shares the host. Leave empty and the plugin derives it from the incoming request, which is wrong behind a proxy that rewrites Host.
optional
commit_authorstring
Commit authorIdentity on commits the platform pushes, as `Name <email>`. This is what shows up in your repository history and in blame — pick something that is obviously a bot.
optional
max_concurrent_jobsinteger
Max concurrent jobsHow many jobs may run at once. Each one holds a runner, so this is also the ceiling on how many Fly Machines or containers exist at peak — size it against your runner budget, not against your queue.
optional
job_wall_clock_msinteger
Job wall-clock limit (ms)Hard ceiling on one job's total runtime. Exceeding it terminates the runner and fails the job. Default 2700000 = 45 minutes.
optional
heartbeat_timeout_msinteger
Heartbeat timeout (ms)A runner silent for longer is treated as stalled and reaped. Set it below the wall-clock limit or a hung runner burns the full budget before anything notices. Default 300000 = 5 minutes.
optional
llm_allowed_modelshost_list
Allowed modelsModel ids a job's agent may call through the proxy. EMPTY MEANS NO JOB CAN RUN — with no policy the proxy answers 500, which surfaces inside the runner as a total LLM outage. A model with no entry in the price table cannot have its cost budget enforced; the plugin logs a warning naming it at activation rather than silently metering it at zero.
optional
llm_default_budget_usdinteger
Default budget per job (USD)Cost ceiling for one job's LLM spend, unless the repository overrides it. Crossing it marks the job budget_exceeded and terminates the runner mid-call — enforcement is not deferred to the end of the turn.
optional
llm_max_output_tokensinteger
Max output tokens per callClamp on max_tokens. Bounds how far a single response can overshoot the budget before enforcement sees it — the budget is checked between calls, so one very large response is the blind spot this closes.
optional
event_retention_daysinteger
Event retention (days)Age at which low-value job telemetry is pruned by the nightly sweep. Audit-grade events are NOT covered by this — they follow the audit retention below.
optional
audit_retention_daysinteger
Audit retention (days)Outer bound for audit-grade events: gate decisions, policy verdicts, token mint and revoke. These are the records that answer 'who approved this change, and what was it allowed to touch' — set it to your compliance retention, not to your disk budget.
optional
max_events_per_jobinteger
Max events per jobPer-job event cap, enforced at write time. A chatty runner is truncated rather than allowed to fill the table.
optional
artifact_max_bytesinteger
Max inline artifact size (bytes)Ceiling on a single stored artifact — a diff, a brief, a review verdict. Anything larger is truncated with a marker, so raising this trades database size for the ability to read very large diffs in the UI. Default 1048576 = 1 MiB.
optional
webhooks_enabledboolean
Accept GitHub webhooksLet labelled issues create jobs automatically. Off means the webhook route is not registered at all — not registered-and-refusing, so `/api/webhooks/github` simply 404s and GitHub shows the delivery as failed.
optional
webhook_max_jobs_per_repo_hourinteger
Webhook jobs per repo per hourRate ceiling per repository. A webhook is a PUBLIC trigger — anyone who can apply a label can start a job that spends money. This is what bounds a label-spam incident.
optional
webhook_max_jobs_per_sender_hourinteger
Webhook jobs per sender per hourRate ceiling per GitHub user, applied on top of the per-repo ceiling. The per-repo limit alone does not stop one account from consuming a repository's whole hourly allowance.
optional
egress_base_allowlisthost_list
Extra egress hosts for runnersHosts a runner may reach beyond the defaults (package registries and the like). Everything not on the list is refused by the egress proxy. Private and link-local addresses are rejected outright — an allowlist entry cannot be used to reach your internal network.
optional
subscription_modeboolean
Subscription modeRun jobs against a CLI subscription credential inside the runner instead of a metered API key. REQUIRES the acknowledgment below — activation is REFUSED without it, the plugin does not start degraded.
optional
subscription_ackstring
Subscription acknowledgmentType your name to acknowledge that subscription jobs run your CLI credential inside a runner that is executing code an agent wrote. Required whenever subscription mode is on. A free-text name rather than a checkbox on purpose: the point is that a person, nameable afterwards, agreed to it.
optional
unsafe_localboolean
Unsafe local backendRun the shim as a local process instead of in a container. DEVELOPMENT ONLY — agent-written code then executes on the middleware host itself, with only the uid below between it and your filesystem. REQUIRES that uid; activation is refused without it.
optional
unsafe_local_uidinteger
Local runner uidDedicated unprivileged uid the jailed shim runs as. NEVER 0, and never the uid the middleware itself runs as — that would give the job write access to the middleware's own files. Required whenever the unsafe local backend is on.
optional
tracker_polling_enabledboolean
Tracker polling (not yet available)Poll a Jira or Linear tracker for labelled tickets. NOT WIRED in this release — six hardening fixes gate switch-on (cold-start budget, sender allowlist, label-application trigger, cross-source dedupe, ticket-id namespacing, frozen provider contract). Turning it on records the intent and logs a warning; nothing polls. It is shown rather than hidden so the roadmap is visible and the key is stable when it does land.
optional

permissions

sql

migrations: migrations · ledger: plg_omadia_dev_platform_migrations · handoff: handoff-plan.json

public_paths

0: /api/v1/dev-runner · 1: /api/webhooks/github · 2: /api/v1/dev-platform

secrets

runtime_write: true

network

outbound: [4]