From 6277cd24cf6fc9ab3fb03ea92d90456db9a24390 Mon Sep 17 00:00:00 2001 From: RedStar Date: Sun, 9 Aug 2026 22:59:08 +0200 Subject: [PATCH 01/11] feat(server): productionize the control plane Add durable task history, approval workflows, and sandbox runner pools. Add oRPC 2, native provider adapters, and a Nuxt 5-compatible UnoCSS dashboard. Closes #10 --- .agent-zero.example.yml | 6 + .gitignore | 5 +- .skills/agent-zero-safety/SKILL.md | 3 + .skills/orpc-server/SKILL.md | 9 +- README.md | 44 +- apps/server/app/app.vue | 7 + apps/server/app/assets/css/main.css | 94 + apps/server/app/components/AppSidebar.vue | 59 + apps/server/app/components/RunnerMetrics.vue | 38 + apps/server/app/components/TaskInspector.vue | 141 + apps/server/app/components/TaskStatus.vue | 23 + apps/server/app/components/TaskTable.vue | 85 + apps/server/app/components/TaskTimeline.vue | 43 + apps/server/app/layouts/default.vue | 8 + apps/server/app/pages/index.vue | 139 + apps/server/nuxt.config.ts | 35 + apps/server/package.json | 23 +- apps/server/server/api/dashboard.get.ts | 10 + .../server/api/tasks/[id]/approval.post.ts | 21 + apps/server/server/routes/rpc/[...].ts | 17 + apps/server/shared/dashboard.ts | 31 + apps/server/src/control-plane.test.ts | 108 + apps/server/src/control-plane.ts | 202 + apps/server/src/dashboard.ts | 17 + apps/server/src/index.ts | 18 +- apps/server/src/router.test.ts | 40 +- apps/server/src/router.ts | 259 +- apps/server/src/rpc.ts | 41 + apps/server/tsconfig.json | 4 +- apps/server/uno.config.ts | 27 + apps/server/uno.theme.ts | 24 + docs/architecture.md | 10 + docs/sandbox-providers.md | 24 + package.json | 2 +- packages/agent/package.json | 2 +- packages/agent/src/agent.test.ts | 28 + packages/agent/src/agent.ts | 26 +- packages/cli/package.json | 2 +- packages/cli/src/index.ts | 8 +- packages/config/package.json | 2 +- packages/config/src/index.test.ts | 38 + packages/config/src/index.ts | 37 +- packages/github/package.json | 2 +- packages/models/package.json | 5 +- packages/models/src/index.test.ts | 97 +- packages/models/src/index.ts | 234 +- packages/runner/package.json | 2 +- packages/runner/src/index.ts | 10 + packages/runner/src/sandbox.test.ts | 90 + packages/runner/src/sandbox.ts | 149 + packages/shared/package.json | 2 +- packages/shared/src/evidence.test.ts | 9 + packages/shared/src/index.ts | 4 + packages/shared/src/types.ts | 46 + pnpm-lock.yaml | 6345 ++++++++++++++++- turbo.json | 2 +- 56 files changed, 8316 insertions(+), 441 deletions(-) create mode 100644 apps/server/app/app.vue create mode 100644 apps/server/app/assets/css/main.css create mode 100644 apps/server/app/components/AppSidebar.vue create mode 100644 apps/server/app/components/RunnerMetrics.vue create mode 100644 apps/server/app/components/TaskInspector.vue create mode 100644 apps/server/app/components/TaskStatus.vue create mode 100644 apps/server/app/components/TaskTable.vue create mode 100644 apps/server/app/components/TaskTimeline.vue create mode 100644 apps/server/app/layouts/default.vue create mode 100644 apps/server/app/pages/index.vue create mode 100644 apps/server/nuxt.config.ts create mode 100644 apps/server/server/api/dashboard.get.ts create mode 100644 apps/server/server/api/tasks/[id]/approval.post.ts create mode 100644 apps/server/server/routes/rpc/[...].ts create mode 100644 apps/server/shared/dashboard.ts create mode 100644 apps/server/src/control-plane.test.ts create mode 100644 apps/server/src/control-plane.ts create mode 100644 apps/server/src/dashboard.ts create mode 100644 apps/server/src/rpc.ts create mode 100644 apps/server/uno.config.ts create mode 100644 apps/server/uno.theme.ts create mode 100644 docs/sandbox-providers.md create mode 100644 packages/runner/src/sandbox.test.ts create mode 100644 packages/runner/src/sandbox.ts diff --git a/.agent-zero.example.yml b/.agent-zero.example.yml index faacfbc..8579095 100644 --- a/.agent-zero.example.yml +++ b/.agent-zero.example.yml @@ -57,5 +57,11 @@ runner: maxOutputBytes: 200000 model: + # ai-gateway, anthropic, google, openai, or openai-compatible. + # Credentials are read only from the provider's documented environment variable; they are + # never accepted from repository configuration or persisted with task evidence. provider: openai-compatible name: gpt-5 + # Optional explicit pricing enables deterministic cost accounting. + # inputCostPerMillionTokens: 1.25 + # outputCostPerMillionTokens: 10 diff --git a/.gitignore b/.gitignore index 634875b..e0a002c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,13 @@ node_modules/ .turbo/ dist/ .output/ +.nuxt/ *.tsbuildinfo coverage/ .env .env.* !.env.example .agent-zero/ +.data/ *.log .DS_Store - -# Nitro build output -.output/ diff --git a/.skills/agent-zero-safety/SKILL.md b/.skills/agent-zero-safety/SKILL.md index 42d7e69..cdf81e6 100644 --- a/.skills/agent-zero-safety/SKILL.md +++ b/.skills/agent-zero-safety/SKILL.md @@ -22,6 +22,9 @@ Safety properties are behavior, not documentation. Back every change with determ - A failed verification cannot be represented as success. `verified` is derived once, where the terminal result is built. - A run that cannot verify does not write. No checks means no change. - Isolation is never approximated. Requesting a sandbox that cannot be provided must fail. +- Remote sandbox credentials stay private to the provider adapter. Requests, leases, snapshots, task state, and logs remain credential-free. +- Runner pools enforce active, per-repository, and lease-duration ceilings before provisioning and stop expired leases. +- Persistent task records omit review input and checkout paths, and recursively redact every string before storage. - A reviewer's claim is not evidence. Reject what the repository does not support, and keep the reasons. ## Review workflow diff --git a/.skills/orpc-server/SKILL.md b/.skills/orpc-server/SKILL.md index aac8264..2d4afc1 100644 --- a/.skills/orpc-server/SKILL.md +++ b/.skills/orpc-server/SKILL.md @@ -9,8 +9,9 @@ description: Use when changing apps/server procedures, oRPC contracts, handlers, ## Rules -- Keep procedure contracts and router composition in `apps/server/src/router.ts`. -- Keep Node HTTP startup in `apps/server/src/index.ts`. +- Keep domain composition in `apps/server/src/router.ts` and oRPC procedures in `apps/server/src/rpc.ts`. +- Nitro v3 scans `apps/server/routes/` because `nitro.config.ts` sets `serverDir: './'`; keep route files as thin transport adapters. +- Pass persistence through oRPC context. Production routes construct `PersistentTaskStore` from Nitro storage; unit tests use `MemoryTaskStore`. - Infer client types from the router; do not duplicate request or response interfaces. - Validate inputs at the procedure boundary and return stable domain-shaped results. - Procedures call the agent runtime through typed APIs. They do not execute shell commands or mutate checkouts directly. @@ -20,8 +21,8 @@ description: Use when changing apps/server procedures, oRPC contracts, handlers, ## Workflow 1. Read the router, its tests, and the runtime method being exposed. -2. Define or adjust the oRPC procedure contract. +2. Define or adjust the oRPC procedure contract in `src/rpc.ts`. 3. Keep the handler thin: validate, authorize, delegate, translate. -4. Add router tests without opening a real network port. +4. Add router/store tests without opening a real network port, then verify the built Nitro route once. 5. Update the README client example when the public router shape changes. 6. Run `aube run test --filter @agent-zero/server`, typecheck, and build. diff --git a/README.md b/README.md index 29c2e4c..b27c3fa 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Feedback is never treated as truth merely because it came from a human or an AI GitHub webhook / CLI │ ▼ - Server API ─── task events + Nitro + oRPC control plane ─── durable task events / approvals │ ▼ Agent state machine @@ -99,22 +99,56 @@ The CLI parses arguments with [`@bomb.sh/args`](https://github.com/bomb-sh/args) ## Control plane -The type-safe oRPC API starts on `http://localhost:4040` and exposes `health`, `tasks.list`, `tasks.get`, and `tasks.create`. Call it from a dashboard or another service with `@orpc/client`: +`aube run dev` starts the Nuxt control plane, backed by Nitro, on `http://localhost:3000`. The operational dashboard is served at `/`; the type-safe oRPC endpoint at `/rpc` exposes `health`, `tasks.list`, `tasks.get`, `tasks.create`, and `approvals.decide`. + +Task history, structured lifecycle events, evidence, model/token/latency/cost usage, and approval decisions are stored through Nitro storage. The development mount uses `.data/agent-zero`; production deployments can replace it with Redis or provider KV without changing task logic. Checkout paths, review input, provider credentials, and raw provider responses are not persisted, and all persisted strings are redacted before storage. + +Call the API from another service with `@orpc/client`: ```ts import { createORPCClient } from '@orpc/client'; import { RPCLink } from '@orpc/client/fetch'; import type { RouterClient } from '@orpc/server'; -import type { AppRouter } from '@agent-zero/server/router'; +import type { RpcRouter } from '@agent-zero/server'; -const zero: RouterClient = createORPCClient( - new RPCLink({ url: 'http://localhost:4040' }), +const zero: RouterClient = createORPCClient( + new RPCLink({ url: 'http://localhost:3000/rpc' }), ); await zero.tasks.create({ repository: '.', feedback: 'Check error handling', mode: 'observe' }); await zero.tasks.create({ repository: '.', trigger: 'proactive', mode: 'observe' }); ``` +The scheduler admits at most four tasks globally, one active task per repository, and 100 queued tasks by default. Hosted execution is injected as a provider-neutral `RunnerPool`: every lease has a maximum lifetime, quota checks run before provisioning, expired sandboxes are stopped, and the agent receives only the ordinary `Runner` contract. See [the sandbox provider evaluation](./docs/sandbox-providers.md). + +Agent Zero supports native OpenAI, Anthropic, and Google Generative AI adapters, Vercel AI +Gateway, and arbitrary OpenAI-compatible endpoints. Select the transport in repository policy and +provide its credential through the environment: + +| `model.provider` | Credential environment variable | Model example | +| ------------------- | -------------------------------------------------------- | ----------------------------- | +| `ai-gateway` | `AI_GATEWAY_API_KEY` or Vercel OIDC | `anthropic/claude-sonnet-4.5` | +| `anthropic` | `ANTHROPIC_API_KEY` | `claude-sonnet-4-5` | +| `google` | `GOOGLE_GENERATIVE_AI_API_KEY` | `gemini-2.5-pro` | +| `openai` | `OPENAI_API_KEY` | `gpt-5` | +| `openai-compatible` | `OPENAI_COMPATIBLE_API_KEY` (or legacy `OPENAI_API_KEY`) | provider-specific | + +`AGENT_ZERO_MODEL_BASE_URL` is an optional operator environment variable for custom gateways and +self-hosted endpoints. Endpoint URLs and credentials cannot be named or embedded in +`.agent-zero.yml`, so untrusted repository policy cannot redirect a provider secret. The AI Gateway +accepts `provider/model` identifiers and exposes the broader AI SDK provider catalog without adding +provider-specific logic to the Agent Zero runtime. + +To record cost, configure explicit rates; Agent Zero never guesses provider pricing: + +```yaml +model: + provider: openai-compatible + name: gpt-5 + inputCostPerMillionTokens: 1.25 + outputCostPerMillionTokens: 10 +``` + `observe` is the safe default and never writes files. Proactive pull-request webhooks are ignored until `proactive.enabled` is true. Automatic changes additionally require `mode: fix` or `autonomous`, `autofix.enabled`, sufficient confidence, an allowed change-risk class, repository-native checks, and (by default for proactive/autonomous work) an isolated runner. High-impact changes always require human approval. --- diff --git a/apps/server/app/app.vue b/apps/server/app/app.vue new file mode 100644 index 0000000..13e62d3 --- /dev/null +++ b/apps/server/app/app.vue @@ -0,0 +1,7 @@ + diff --git a/apps/server/app/assets/css/main.css b/apps/server/app/assets/css/main.css new file mode 100644 index 0000000..21d40a1 --- /dev/null +++ b/apps/server/app/assets/css/main.css @@ -0,0 +1,94 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +:root { + color-scheme: dark; + --az-canvas: #080c0e; + --az-panel: #0d1215; + --az-raised: #11181c; + --az-line: #273137; + --az-ink: #edf2ef; + --az-muted: #94a0a7; + --az-accent: #82e849; + --az-warning: #f5bd38; + --az-danger: #ff675f; + --az-link: #57a7ff; + --az-font-sans: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-family: var(--az-font-sans); + background: var(--az-canvas); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + scrollbar-gutter: stable; +} + +body { + margin: 0; + min-width: 320px; + background: + linear-gradient(rgb(130 232 73 / 0.018) 1px, transparent 1px), + linear-gradient(90deg, rgb(130 232 73 / 0.018) 1px, transparent 1px), var(--az-canvas); + background-size: 32px 32px; + color: var(--az-ink); + line-height: 1.5; +} + +button, +textarea { + font: inherit; +} + +button { + cursor: pointer; +} + +:focus-visible, +:-moz-focusring { + outline: 2px solid var(--az-accent) !important; + outline-offset: 2px !important; +} + +::selection { + color: var(--az-canvas); + background: var(--az-accent); +} + +* { + scrollbar-color: var(--az-line) var(--az-canvas); +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--az-canvas); +} + +::-webkit-scrollbar-thumb { + background: var(--az-line); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--az-muted); +} + +@media (prefers-contrast: more) { + :root { + --az-line: #aab3b8; + --az-muted: #d7ddda; + } +} + +@media screen and (max-width: 767px) { + input, + select, + textarea { + font-size: 16px !important; + } +} diff --git a/apps/server/app/components/AppSidebar.vue b/apps/server/app/components/AppSidebar.vue new file mode 100644 index 0000000..1bd92be --- /dev/null +++ b/apps/server/app/components/AppSidebar.vue @@ -0,0 +1,59 @@ + + + diff --git a/apps/server/app/components/RunnerMetrics.vue b/apps/server/app/components/RunnerMetrics.vue new file mode 100644 index 0000000..fd98dad --- /dev/null +++ b/apps/server/app/components/RunnerMetrics.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/server/app/components/TaskInspector.vue b/apps/server/app/components/TaskInspector.vue new file mode 100644 index 0000000..a01d310 --- /dev/null +++ b/apps/server/app/components/TaskInspector.vue @@ -0,0 +1,141 @@ + + +