Agent Center is a Bun monorepo for a self-hosted control plane around background coding runs, with a realtime web UI for starting, watching, steering, and reviewing agent work.
This is no longer just a backend slice. The repo now includes:
- a product web surface in
apps/web - task detail pages with live run activity, follow-up prompts, and multi-tab updates
- a local runner that provisions workspaces, optionally clones repositories, and executes Codex or Claude-backed runs
- persisted run events, command traces, and a docked diff viewer for workspace changes
- queueing and automation infrastructure through the API, worker, and runner services
It is still intentionally early, but it is now a working local product rather than a placeholder scaffold.
- Postgres-backed CRUD APIs for workspaces, projects, repo connections, tasks, runs, and automations
- A web UI for browsing tasks, opening task detail pages, sending follow-ups, cancelling runs, and reviewing diffs
- A worker that polls Postgres for queued runs and due automations
- A host-local runner that provisions a workspace, optionally clones a GitHub repository, and executes configured shell commands or agent-backed runs
- Realtime task and run updates over WebSockets, plus browser-local sync to keep open tabs in step
- A docked desktop diff viewer for run workspace changes, including newly created files
- Credential support for provider API keys and Codex-style OAuth-backed auth material
- A
@agent-center/githubpackage for GitHub repo access checks and clone URL construction - A
@agent-center/sdk-tspackage for HTTP + realtime access from TypeScript
- No secure sandbox or VM/container isolation yet
- No tenant isolation or production-grade RBAC
- GitHub support is still repository-access oriented, not full GitHub App automation
- The local runner is powerful, but it still runs on the host machine
- Final Codex assistant prose is not yet token-by-token streamed; run state and command activity are realtime, but the final assistant reply is still emitted at turn completion
clients / scripts / SDK
|
v
apps/api
- REST API for workspaces, projects, repo connections, tasks, runs, automations
- WebSocket endpoint for run event subscriptions
- Reads from and writes to Postgres
|
v
Postgres
- source of truth for tasks, runs, run_events, automations, repo connections, projects, workspaces
|
+--> apps/worker
| - polls queued runs
| - polls due automations
| - dispatches accepted runs to the runner over HTTP
|
+--> apps/runner
- loads run state from Postgres
- creates a local workspace on disk
- optionally clones a GitHub repo
- executes configured shell commands
- writes status, logs, and events back to Postgres
apps/api: Bun + Hono API onhttp://127.0.0.1:3000by default. Exposes/api/*,/health, and/ws.apps/worker: background poller. It claims queued runs, marks dispatch results, and materializes due automations into tasks + runs.apps/runner: local execution service onhttp://127.0.0.1:3002by default. It exposes internal-only run control routes and executes shell commands on the host machine.packages/db: Drizzle schema, migrations, and shared Postgres client.packages/github: GitHub provider helpers for repo access checks, token fallback handling, and authenticated clone URLs.packages/sdk-ts: typed client for the API plus a realtime stream client for run events.
The prompt field is stored on tasks and runs, but it is not interpreted by an LLM in this phase.
Actual execution comes from config.commands. If a run has no commands, the runner fails it.
Fields such as commitMessage, prTitle, and prBody are also stored for future phases, but the current runner does not commit, push, or open pull requests.
- Bun
1.3.5or newer - Postgres
16+, or Docker Desktop if you want to use the included compose setup gitavailable on your machine for repository-backed runs
bun installThe Bun services load the root .env file and then .env.local, with .env.local taking precedence.
cp .env.example .envThe example file includes the current env surface for:
- database access
- API host/port
- worker polling + runner dispatch
- runner host/port + local workspace path
- Convex URL + service-token auth
- optional GitHub token fallbacks
Generate AGENT_CENTER_CONVEX_SERVICE_TOKEN with openssl rand -hex 32 and put it in .env for shared local setup. If you run the Convex control plane, set the same value on that Convex deployment too:
bunx convex env set AGENT_CENTER_CONVEX_SERVICE_TOKEN "$(grep '^AGENT_CENTER_CONVEX_SERVICE_TOKEN=' .env | cut -d= -f2-)"Also set RUNNER_INTERNAL_TOKEN with another openssl rand -hex 32 value. The worker and runner both read this token for local internal dispatch.
If you want runner workspaces in a deterministic location, set RUNNER_WORKSPACE_ROOT to an absolute path. The example uses a relative path for local convenience.
If you want to use the included Postgres container:
bun run db:upApply the current migrations:
bun run db:migrateStop the container later with:
bun run db:downRun all three backend services together:
bun run devOr run them separately:
bun run dev:api
bun run dev:worker
bun run dev:runnerCurrent resource groups:
/api/workspaces/api/projects/api/repo-connections/api/tasks/api/runs/api/automations
Supporting endpoints:
GET /healthGET /ws
The API is intentionally unauthenticated in this phase. Do not expose it directly to untrusted networks.
End-to-end run flow today:
- A client creates a task and then creates a run for that task.
- The API stores the run in Postgres with status
queued. - The worker polls for queued runs, claims one, marks it
provisioning, and dispatches therunIdto the runner overPOST /internal/runs/execute. - The runner loads the run, task, project, workspace, and repo connection from Postgres.
- The runner creates a local workspace directory for the run.
- If the run has a repo connection, the runner clones the repository and checks out the target branch.
- The runner executes
config.commandssequentially with/bin/zsh -lc. - Stdout, stderr, status transitions, and lifecycle events are written back to Postgres as
run_events. - The API exposes those events over REST and WebSocket subscriptions.
Execution details that matter:
- Commands run as the same local user that started the runner.
- The runner inherits the host process environment.
permissionMode: "safe"is a blocklist, not a sandbox.permissionMode: "yolo"skips command blocking entirely.permissionMode: "custom"only blocks commands listed inpolicy.blockedCommands.RUNNER_CLEANUP_MODE=retainkeeps workspaces after completion.delete_on_completionremoves successful or cancelled workspaces, but failed workspaces are still retained for debugging.
GitHub support in this phase is repository access only.
What is implemented:
- provider: GitHub only
- auth shape: PAT-style tokens
- repo connection creation and persistence
- repo access test via the GitHub API
- clone URL generation for the runner
- private repo cloning when a token is available
What is not implemented yet:
- GitHub App auth
- OAuth connect flow
- webhook ingestion
- automatic commit, push, or PR creation from runner output
Token resolution order for GitHub operations:
- direct token passed to the provider
- repo connection metadata such as
token,accessToken,pat, orpersonalAccessToken - process env fallback from
GITHUB_TOKEN,GITHUB_PAT, orGH_TOKEN
For local development, the usual path is:
- Put a PAT in
.envasGITHUB_TOKEN. - Create a repo connection through
/api/repo-connections. - Optionally call
/api/repo-connections/:id/testto verify access before creating tasks.
Public repositories can work without a token for some operations, but private repository access requires a token.
This path proves the queue, worker, runner, and event pipeline without cloning a repository.
- Create a workspace:
curl -s http://127.0.0.1:3000/api/workspaces \
-H 'content-type: application/json' \
-d '{
"slug": "local-dev",
"name": "Local Dev",
"description": "Local smoke test",
"metadata": {}
}'- Copy the returned
workspace.id, then create a task with commands:
curl -s http://127.0.0.1:3000/api/tasks \
-H 'content-type: application/json' \
-d '{
"workspaceId": "REPLACE_WITH_WORKSPACE_ID",
"title": "Runner smoke test",
"prompt": "Stored for future agent work; commands do the real work in this phase.",
"permissionMode": "safe",
"config": {
"commands": [
{ "command": "pwd" },
{ "command": "echo hello-from-agent-center" },
{ "command": "ls -la" }
]
},
"metadata": {}
}'- Copy the returned
task.id, then queue a run:
curl -s http://127.0.0.1:3000/api/runs \
-H 'content-type: application/json' \
-d '{
"taskId": "REPLACE_WITH_TASK_ID"
}'- Watch the run over REST:
curl -s http://127.0.0.1:3000/api/runs/REPLACE_WITH_RUN_ID/events
curl -s http://127.0.0.1:3000/api/runs/REPLACE_WITH_RUN_ID/logsIf you want the runner to clone a repository first:
- Set
GITHUB_TOKENin.envif you need private repository access. - Create a workspace.
- Create a project for that workspace.
- Create a GitHub repo connection with the project id.
- Optionally test the repo connection.
- Create a task that references
repoConnectionIdand includesconfig.commands. - Create a run for that task.
The repository-backed SDK example in packages/sdk-ts/examples/basic.ts exercises that full path.
bun run packages/sdk-ts/examples/basic.tsUseful env vars for the example:
AGENT_CENTER_BASE_URLAGENT_CENTER_GITHUB_TOKEN
The API exposes a WebSocket endpoint at ws://127.0.0.1:3000/ws.
Client messages:
{"type":"subscribe_run","runId":"..."}{"type":"unsubscribe_run","runId":"..."}{"type":"ping"}
Server messages:
{"type":"subscribed","runId":"..."}{"type":"run_event","runId":"...","event":{...}}{"type":"pong"}{"type":"error","message":"..."}
Current implementation details:
- subscriptions are in-memory inside the API process
- the API polls Postgres for new
run_eventsevery second - events are delivered in sequence order per run
- there is no resume token, pub/sub bus, or multi-node fanout yet
If you are consuming from TypeScript, use client.runs.stream(runId) from @agent-center/sdk-ts.
Automations are Postgres-backed cron definitions processed by the worker.
Current behavior:
- the worker polls for due automations on an interval
- cron parsing is local to the worker and expects a standard 5-field expression
- if an automation has
nextRunAt = null, the worker initializes it on first poll instead of firing immediately - when an automation is due, the worker creates a task and a queued run
- the automation can attach a repo connection and can generate a timestamped branch name from
branchPrefix
What automations do not do yet:
- no GitHub webhook triggers
- no external scheduler
- no distributed locking beyond the current database transaction flow
- no commit/push/PR step after command execution
- The runner is host-local and executes shell commands directly on your machine.
- There is no container, VM, namespace, or filesystem isolation boundary.
- Command blocking is lightweight and easy to bypass compared with real sandboxing.
- The API and runner internal endpoints do not require authentication.
- Secrets are simple environment variables or stored repo connection metadata.
- WebSocket delivery is polling-based and single-process.
- Worker scheduling is polling-based, not queue-broker-based.
Treat this phase as a local backend slice and integration harness, not a production-secure control plane.
- real agent orchestration from prompts
- secure runner isolation
- multi-runner scheduling and heartbeats
- Git commit / push / PR execution flow
- GitHub App and OAuth-based auth
- frontend product experience
- production auth, billing, and org management
- hardened observability, auditing, and operational controls
bun run dev
bun run typecheck
bun run lint
bun run format:check
bun run build
bun run db:generate
bun run db:migrate