mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
Every .md under docs/{architecture,guides,reference,frameworks,routing,
security,compression,ops,diagrams} plus docs/README.md now opens with:
---
title: "<inferred from first H1>"
version: 3.8.0
lastUpdated: 2026-05-13
---
46 files updated (no docs were skipped — none had pre-existing
frontmatter). [slug]/page.tsx already reads frontmatter.version and
frontmatter.lastUpdated via gray-matter and renders a "v3.8.0" pill
plus a "Last updated" caption, so the UI picks these up automatically.
Helper: scripts/docs/add-frontmatter.mjs — idempotent (skips files that
already start with `---`), falls back to a humanized basename when no
leading H1 exists. Excludes docs/i18n/, docs/screenshots/,
docs/superpowers/, docs/diagrams/exported/. Re-runnable safely.
Also regenerated src/app/docs/lib/docs-auto-generated.ts: 44 docs across
8 sections (Architecture / Guides / Reference / Frameworks / Routing /
Security / Compression / Ops), which now includes the 14 docs that were
missing from the v3.7 sidebar (Cloud Agents, Guardrails, Memory, Skills,
Webhooks, Evals, Authz, Agent Protocols, Repository Map, Provider
Reference, Reasoning Replay, Stealth Guide, Tunnels Guide, Electron
Guide).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
383 lines
15 KiB
Markdown
383 lines
15 KiB
Markdown
---
|
|
title: "Cloud Agents"
|
|
version: 3.8.0
|
|
lastUpdated: 2026-05-13
|
|
---
|
|
|
|
# Cloud Agents
|
|
|
|
> **Source of truth:** `src/lib/cloudAgent/` and `src/app/api/v1/agents/tasks/`
|
|
> **Last updated:** 2026-05-13 — v3.8.0
|
|
|
|
OmniRoute orchestrates third-party cloud-hosted coding agents (Codex Cloud, Devin,
|
|
Jules) as long-running tasks. Each agent is wrapped behind a uniform interface so
|
|
clients can submit a prompt + repo URL and receive results without dealing with
|
|
provider-specific APIs.
|
|
|
|
A Cloud Agent task is **not** a regular chat completion. It is a durable, multi-step
|
|
unit of work that may take minutes to hours, can produce a Pull Request as its
|
|
artifact, and supports follow-up messages and (in some providers) plan approval gates.
|
|
|
|

|
|
|
|
> Source: [diagrams/cloud-agent-flow.mmd](../diagrams/cloud-agent-flow.mmd)
|
|
|
|
## Supported Agents
|
|
|
|
| Provider ID | Class | Source | Upstream Base URL | Plan Approval |
|
|
| ------------- | ----------------- | ------------------------------------ | --------------------------------------- | ------------- |
|
|
| `jules` | `JulesAgent` | `src/lib/cloudAgent/agents/jules.ts` | `https://jules.googleapis.com/v1alpha` | Yes |
|
|
| `devin` | `DevinAgent` | `src/lib/cloudAgent/agents/devin.ts` | `https://api.devin.ai/v1` | Yes |
|
|
| `codex-cloud` | `CodexCloudAgent` | `src/lib/cloudAgent/agents/codex.ts` | `https://api.openai.com/v1/codex/cloud` | No (auto) |
|
|
|
|
Registry: `src/lib/cloudAgent/registry.ts` — exports `getAgent(providerId)`,
|
|
`getAvailableAgents()`, and `isCloudAgentProvider(providerId)`. The registry is a
|
|
plain in-memory `Record<string, CloudAgentBase>` populated at module load.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Client (Dashboard / CLI / API)
|
|
→ POST /api/v1/agents/tasks (management auth required)
|
|
→ CreateCloudAgentTaskSchema validation (Zod)
|
|
→ registry.getAgent(providerId)
|
|
→ getCloudAgentCredentials(providerId)
|
|
└─ pulls from getProviderConnections({ provider, isActive: true })
|
|
(apiKey first, fallback to accessToken)
|
|
→ agent.createTask({ prompt, source, options }, credentials)
|
|
└─ HTTP POST to upstream provider API
|
|
└─ returns CloudAgentTask with internal id + externalId
|
|
→ insertCloudAgentTask(...) into cloud_agent_tasks (SQLite)
|
|
|
|
Polling (lazy sync on read):
|
|
GET /api/v1/agents/tasks/[id]
|
|
→ getCloudAgentTaskById(id)
|
|
→ agent.getStatus(externalId, credentials) // refreshes status + activities
|
|
→ updateCloudAgentTask(...) with new status, result, completed_at
|
|
→ return serialized task
|
|
|
|
Interactions:
|
|
POST /api/v1/agents/tasks/[id] body: { action: "approve" | "message" | "cancel" }
|
|
→ agent.approvePlan(externalId, credentials) for "approve"
|
|
→ agent.sendMessage(externalId, message, credentials) for "message"
|
|
→ status flips to "cancelled" for "cancel" (local-only)
|
|
```
|
|
|
|
Sync is **lazy**: status is refreshed from the upstream on every `GET /tasks/[id]`.
|
|
There is no background poller. Dashboards that need fresh state should poll the GET
|
|
endpoint at a sensible interval.
|
|
|
|
## `CloudAgentBase` Interface
|
|
|
|
Source: `src/lib/cloudAgent/baseAgent.ts`
|
|
|
|
```typescript
|
|
export interface AgentCredentials {
|
|
apiKey: string;
|
|
baseUrl?: string;
|
|
}
|
|
|
|
export interface CreateTaskParams {
|
|
prompt: string;
|
|
source: CloudAgentSource;
|
|
options: {
|
|
autoCreatePr?: boolean;
|
|
planApprovalRequired?: boolean;
|
|
environment?: Record<string, string>;
|
|
};
|
|
}
|
|
|
|
export interface GetStatusResult {
|
|
status: CloudAgentStatus;
|
|
externalId?: string;
|
|
result?: CloudAgentResult;
|
|
activities: CloudAgentActivity[];
|
|
error?: string;
|
|
}
|
|
|
|
export abstract class CloudAgentBase {
|
|
abstract readonly providerId: string;
|
|
abstract readonly baseUrl: string;
|
|
|
|
abstract createTask(p: CreateTaskParams, c: AgentCredentials): Promise<CloudAgentTask>;
|
|
abstract getStatus(externalId: string, c: AgentCredentials): Promise<GetStatusResult>;
|
|
abstract approvePlan(externalId: string, c: AgentCredentials): Promise<void>;
|
|
abstract sendMessage(
|
|
externalId: string,
|
|
message: string,
|
|
c: AgentCredentials
|
|
): Promise<CloudAgentActivity>;
|
|
abstract listSources(
|
|
c: AgentCredentials
|
|
): Promise<{ name: string; url: string; branch?: string }[]>;
|
|
|
|
protected mapStatus(raw: string): CloudAgentStatus; // heuristic upstream-string → enum
|
|
protected generateTaskId(): string; // `task_<ts>_<rand>`
|
|
protected generateActivityId(): string; // `act_<ts>_<rand>`
|
|
}
|
|
```
|
|
|
|
`CodexCloudAgent.approvePlan` intentionally throws — Codex Cloud auto-plans and has
|
|
no approval gate. `CodexCloudAgent.listSources` returns `[]`.
|
|
|
|
## Domain Types
|
|
|
|
Source: `src/lib/cloudAgent/types.ts`
|
|
|
|
```typescript
|
|
export const CLOUD_AGENT_STATUS = {
|
|
QUEUED: "queued",
|
|
RUNNING: "running",
|
|
AWAITING_APPROVAL: "awaiting_approval",
|
|
COMPLETED: "completed",
|
|
FAILED: "failed",
|
|
CANCELLED: "cancelled",
|
|
} as const;
|
|
|
|
export interface CloudAgentSource {
|
|
repoName: string;
|
|
repoUrl: string; // must be a valid URL
|
|
branch?: string;
|
|
}
|
|
|
|
export interface CloudAgentResult {
|
|
prUrl?: string;
|
|
prNumber?: number;
|
|
commitMessage?: string;
|
|
diffUrl?: string;
|
|
summary?: string;
|
|
duration?: number; // seconds, positive int
|
|
cost?: number; // positive float
|
|
}
|
|
|
|
export interface CloudAgentActivity {
|
|
id: string;
|
|
type: "plan" | "command" | "code_change" | "message" | "error" | "completion";
|
|
content: string;
|
|
timestamp: string; // ISO 8601
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface CloudAgentTask {
|
|
id: string; // internal `task_...` id
|
|
providerId: "jules" | "devin" | "codex-cloud";
|
|
externalId?: string; // upstream provider's id
|
|
status: CloudAgentStatus;
|
|
prompt: string; // 1..10000 chars
|
|
source: CloudAgentSource;
|
|
options: {
|
|
autoCreatePr?: boolean;
|
|
planApprovalRequired?: boolean;
|
|
environment?: Record<string, string>;
|
|
};
|
|
result?: CloudAgentResult;
|
|
activities: CloudAgentActivity[];
|
|
error?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
completedAt?: string;
|
|
}
|
|
```
|
|
|
|
Validation schemas (`CreateCloudAgentTaskSchema`, `UpdateCloudAgentTaskSchema`) are
|
|
exported alongside the types and are used by the route handlers.
|
|
|
|
## Database
|
|
|
|
Source: `src/lib/cloudAgent/db.ts` — table is created lazily via
|
|
`createCloudAgentTaskTable()` (also called from `src/lib/cloudAgent/index.ts` at
|
|
module import).
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS cloud_agent_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
provider_id TEXT NOT NULL,
|
|
external_id TEXT,
|
|
status TEXT NOT NULL DEFAULT 'queued',
|
|
prompt TEXT NOT NULL,
|
|
source TEXT NOT NULL, -- JSON
|
|
options TEXT DEFAULT '{}', -- JSON
|
|
result TEXT, -- JSON
|
|
activities TEXT DEFAULT '[]', -- JSON
|
|
error TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
completed_at TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_cloud_agent_tasks_provider ON cloud_agent_tasks(provider_id);
|
|
CREATE INDEX IF NOT EXISTS idx_cloud_agent_tasks_status ON cloud_agent_tasks(status);
|
|
CREATE INDEX IF NOT EXISTS idx_cloud_agent_tasks_created ON cloud_agent_tasks(created_at DESC);
|
|
```
|
|
|
|
`updateCloudAgentTask` enforces a **column whitelist** to prevent SQL injection:
|
|
`status`, `prompt`, `source`, `options`, `result`, `activities`, `error`,
|
|
`completed_at`. Any other key in the partial update is silently dropped.
|
|
|
|
## REST API — Task Lifecycle
|
|
|
|
**Auth:** All `/api/v1/agents/tasks*` endpoints require **management auth**
|
|
(`requireCloudAgentManagementAuth` wraps `requireManagementAuth` from
|
|
`src/lib/api/requireManagementAuth`). This is enforced after commit `588a0333`
|
|
(_"fix(auth): require management auth for agent and cooldown APIs"_).
|
|
|
|
| Method | Path | Purpose |
|
|
| ------- | ----------------------------- | ------------------------------------------------------ |
|
|
| OPTIONS | `/api/v1/agents/tasks` | CORS preflight |
|
|
| GET | `/api/v1/agents/tasks` | List tasks (filter: `provider`, `status`, `limit≤500`) |
|
|
| POST | `/api/v1/agents/tasks` | Create task (dispatches to upstream + persists) |
|
|
| DELETE | `/api/v1/agents/tasks?id=...` | Delete task by query id (does **not** cancel upstream) |
|
|
| OPTIONS | `/api/v1/agents/tasks/[id]` | CORS preflight |
|
|
| GET | `/api/v1/agents/tasks/[id]` | Read task + lazy-sync status from upstream |
|
|
| POST | `/api/v1/agents/tasks/[id]` | Action: `approve` / `message` / `cancel` |
|
|
| DELETE | `/api/v1/agents/tasks/[id]` | Delete task by path id |
|
|
|
|
### Create task
|
|
|
|
```bash
|
|
curl -X POST http://localhost:20128/api/v1/agents/tasks \
|
|
-H "Cookie: auth_token=..." \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"providerId": "devin",
|
|
"prompt": "Fix the bug in src/foo.ts where the parser returns null",
|
|
"source": {
|
|
"repoName": "user/repo",
|
|
"repoUrl": "https://github.com/user/repo",
|
|
"branch": "main"
|
|
},
|
|
"options": {
|
|
"autoCreatePr": true,
|
|
"planApprovalRequired": false
|
|
}
|
|
}'
|
|
```
|
|
|
|
Response `201`:
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "task_1731512345678_abc123def",
|
|
"providerId": "devin",
|
|
"externalId": "session_xyz",
|
|
"status": "queued",
|
|
"prompt": "...",
|
|
"source": { "repoName": "user/repo", "repoUrl": "...", "branch": "main" },
|
|
"options": { "autoCreatePr": true },
|
|
"createdAt": "2026-05-13T12:34:56.789Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Approve a plan
|
|
|
|
```bash
|
|
curl -X POST http://localhost:20128/api/v1/agents/tasks/<id> \
|
|
-H "Cookie: auth_token=..." \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"action":"approve"}'
|
|
```
|
|
|
|
### Send a follow-up message
|
|
|
|
```bash
|
|
curl -X POST http://localhost:20128/api/v1/agents/tasks/<id> \
|
|
-d '{"action":"message","message":"Also add a unit test for the parser"}'
|
|
```
|
|
|
|
### Cancel (local status only)
|
|
|
|
```bash
|
|
curl -X POST http://localhost:20128/api/v1/agents/tasks/<id> \
|
|
-d '{"action":"cancel"}'
|
|
```
|
|
|
|
`cancel` flips `status` to `"cancelled"` in the local DB but does **not** call the
|
|
upstream provider — there is no abort RPC in `CloudAgentBase`. To stop billing
|
|
upstream, terminate the task in the provider's own console.
|
|
|
|
## REST API — Cloud Provider Plumbing
|
|
|
|
These auxiliary endpoints under `src/app/api/cloud/` are used by remote clients
|
|
(the CLI, the Electron app, or sync workers) to read provider connection metadata
|
|
and resolve model aliases. They are authenticated with a **regular API key**
|
|
(via `validateApiKey`), not the management auth used by the task endpoints.
|
|
|
|
| Method | Path | Purpose |
|
|
| ------ | ------------------------------- | ------------------------------------------------------------------- |
|
|
| POST | `/api/cloud/auth` | Validate API key, return masked connection metadata + model aliases |
|
|
| PUT | `/api/cloud/credentials/update` | Refresh `accessToken` / `refreshToken` / `expiresAt` |
|
|
| POST | `/api/cloud/model/resolve` | Resolve a model alias to `{ provider, model }` |
|
|
| GET | `/api/cloud/models/alias` | List all model aliases |
|
|
| PUT | `/api/cloud/models/alias` | Set a model alias (and auto-sync to Cloud if enabled) |
|
|
|
|
`/api/cloud/auth` never returns raw `apiKey` / `accessToken` / `refreshToken`. It
|
|
returns `hasApiKey`, `hasAccessToken`, `hasRefreshToken`, and a masked preview
|
|
(`maskedApiKey`: first 4 + `****` + last 4).
|
|
|
|
## Credentials Resolution
|
|
|
|
`getCloudAgentCredentials(providerId)` in `src/lib/cloudAgent/api.ts`:
|
|
|
|
1. Loads active provider connections via `getProviderConnections({ provider: providerId, isActive: true })`.
|
|
2. For each connection, prefers `apiKey` (trimmed). Falls back to `accessToken`.
|
|
3. Returns the first non-empty token wrapped as `{ apiKey: token }`.
|
|
4. Returns `null` if no usable token is found — the API responds `400` with
|
|
`"No active credentials configured for cloud agent provider: <id>"`.
|
|
|
|
This means Cloud Agents reuse the same Provider Connection table as regular LLM
|
|
providers. To enable Jules, create an active connection with `provider: "jules"`
|
|
and a populated `apiKey`.
|
|
|
|
## Dashboard
|
|
|
|
Source: `src/app/(dashboard)/dashboard/cloud-agents/page.tsx`
|
|
|
|
A `"use client"` React page that:
|
|
|
|
- Lists tasks (polled via `GET /api/v1/agents/tasks`).
|
|
- Submits new tasks via a form that maps to `CreateCloudAgentTaskSchema`.
|
|
- Shows status badges (`queued`, `running`, `awaiting_approval`, `completed`,
|
|
`failed`, `cancelled`) and renders the `activities[]` timeline.
|
|
- Surfaces the `result.prUrl` / `commitMessage` / `summary` when `status === "completed"`.
|
|
|
|
## Integration with A2A
|
|
|
|
Cloud Agents can be exposed as A2A skills by registering an A2A skill that delegates
|
|
its `tasks/send` handler to `getAgent(...).createTask(...)` and translates A2A task
|
|
status events to the JSON-RPC 2.0 protocol. See [A2A-SERVER.md](./A2A-SERVER.md).
|
|
|
|
## Adding a New Cloud Agent
|
|
|
|
1. Create `src/lib/cloudAgent/agents/<name>.ts` extending `CloudAgentBase`.
|
|
2. Implement `createTask`, `getStatus`, `approvePlan` (or throw if N/A),
|
|
`sendMessage`, `listSources`. Use `this.mapStatus(...)` for status normalization.
|
|
3. Register in `src/lib/cloudAgent/registry.ts` under a stable `providerId`.
|
|
4. Extend the `providerId` literal union in `src/lib/cloudAgent/types.ts`
|
|
(`CloudAgentTask.providerId` and `CreateCloudAgentTaskSchema`).
|
|
5. Add the provider to `src/shared/constants/providers.ts` if it needs a connection
|
|
record. OAuth-based providers also need `src/lib/oauth/providers/`.
|
|
6. Add tests under `tests/unit/cloud-agent-*.test.ts`.
|
|
7. Update this doc and the dashboard's `CLOUD_AGENTS` constant.
|
|
|
|
## Configuration
|
|
|
|
| Env Var | Purpose |
|
|
| ---------------- | ----------------------------------------------------------- |
|
|
| `DATA_DIR` | Location of the SQLite database holding `cloud_agent_tasks` |
|
|
| `JWT_SECRET` | Required for management auth on task endpoints |
|
|
| `API_KEY_SECRET` | Required to encrypt provider connection credentials at rest |
|
|
|
|
No Cloud-Agent-specific env vars exist today — every secret lives in the
|
|
`provider_connections` table.
|
|
|
|
## See Also
|
|
|
|
- [A2A-SERVER.md](./A2A-SERVER.md)
|
|
- [API_REFERENCE.md](../reference/API_REFERENCE.md)
|
|
- [SKILLS.md](./SKILLS.md)
|
|
- [MEMORY.md](./MEMORY.md)
|
|
- Source: `src/lib/cloudAgent/`
|
|
- Routes: `src/app/api/v1/agents/tasks/`, `src/app/api/cloud/`
|
|
- Dashboard: `src/app/(dashboard)/dashboard/cloud-agents/page.tsx`
|