This document is the current architecture map and assessment for the ProAgentStore platform repo. It is intended to be the first place to look before changing core runtime, storage, MCP, console, connector, or agent infrastructure.
Status: current as of 2026-08-06.
ProAgentStore is a Cloudflare-native marketplace and runtime platform for server-powered AI agents. The main product loop is:
- Creators publish agent templates.
- Users subscribe to private agent instances.
- Instances keep user-specific state, knowledge, memory, credentials, and runtime tasks.
- The API, MCP server, console, and local runner all operate on the same instance model.
The architecture is strong in three areas:
- Clear separation between template agents and private user instances.
- Cloudflare primitives are used well for the product shape: Workers for HTTP, Durable Objects for per-agent state, Workflows for long-running brains, D1 for account metadata, R2 for blobs, Vectorize for RAG, and WebSockets for local runner relay.
- The MCP surface is not a sidecar. It is a first-class control surface that uses the same API and safety model as the console.
The biggest architectural risk is accretion. The platform has grown from store, chat, MCP, browser automation, coding, voice, connectors, billing, and observability into one large API worker and one large console tab structure. The next phase should focus on extracting shared platform services without abstracting away agent-specific domain logic.
platform/
+-- workers/
| +-- api/ Hono API Worker, AgentDO, RelayDO, Workflows
| +-- mcp/ MCP server and OAuth provider
| +-- host/ Static host for store, console, widget, docs
+-- store/
| +-- console/ React/Vite console app
+-- packages/
| +-- sdk/ Browser/client SDK and shared UI/voice helpers
| +-- cli/ pags CLI: init, publish, login, mcp, runner
| +-- browser-runner/ Local Playwright/terminal runtime served by `pags up`
| +-- compliance/ Policy/check tooling
+-- agents/ Tier-0 first-party agent sources
+-- templates/ Agent scaffolds for worker, cron, api templates
+-- docs/ Architecture, runtime, MCP, and strategy docs
+-- assets/ Store-facing assets
Browser console / widget
|
v
workers/host -------------- static assets, docs, widget
|
v
workers/api --------------- auth, agents, instances, runtime, connectors
| | | |
| | | +-- D1: users, agents, instances, runtime rows, billing
| | +--------- R2: files, media, screenshots, agent assets
| +---------------- Vectorize: knowledge embeddings
+----------------------- Durable Objects: AgentDO, RelayDO
|
+-- Workflows: JobApplyWorkflow, CodingSessionWorkflow
MCP clients
|
v
workers/mcp --------------- OAuth + tool surface, calls workers/api
Local machine
|
v
packages/browser-runner ---- Playwright browser, terminal CLIs, local files
^
|
RelayDO WebSocket relay ---- outbound runner connection, no inbound tunnel
The control plane is the Cloudflare-hosted platform:
workers/api: product API, orchestration, auth, connector OAuth, runtime registration, instance lifecycle, billing, analytics, storage routing.workers/mcp: MCP tool interface with OAuth scopes, confirmation gates, dry-run support, and audit logging.workers/host: static web host for the store, console, docs, widget, and public assets.
The control plane is responsible for authorization, persistence, durable workflow state, and user-visible operational records.
State is split intentionally:
- D1 stores account, marketplace, instance, runtime, billing, OAuth-token ciphertext metadata, board, events, errors, and workflow/task mirrors.
- Durable Object storage stores per-agent runtime state: messages, memory, knowledge docs, tasks, collections, activity, file metadata, summaries.
- R2 stores binary files and large artifacts.
- Vectorize stores semantic vectors for knowledge and repo/file retrieval.
This split is sensible. D1 is used for relational product state and cross-object queries. Durable Objects own instance-local mutable state. R2 and Vectorize are used only where their storage shape fits.
Runtime-backed agents use pags up, which runs a local browser/CLI runtime from
packages/browser-runner. The runner connects outbound to a per-instance
RelayDO, so the cloud can call local capabilities without opening inbound
ports.
Current runtime-backed surfaces:
- Job Application Assistant: Cloudflare Workflow brain drives Playwright browser actions.
- Coder: Cloudflare Workflow brain drives local CLI sessions directly over the relay
(
callRunner→/coding/capture,/coding/act,/coding/event), not through a connector. Multiple machines can connect to the same Coder instance through node-scoped relay connections; see Coder Multi-Machine Runtime.
The key architectural pattern is "brain in cloud, hands local":
Workflow brain -> callRunner() -> RelayDO -> WebSocket -> local runner -> browser/terminal
Primary files:
workers/api/src/index.tsworkers/api/src/types.tsworkers/api/src/agent-do.tsworkers/api/src/relay-do.tsworkers/api/src/workflows/job-apply.tsworkers/api/src/workflows/coding-session.tsworkers/api/migrations/*.sql
The API worker is a Hono app. It mounts route modules for:
- Auth and profiles:
/v1/auth,/v1/profile - Agents:
/v1/agents, versions, analytics, exports, public trials - Instances:
/v1/instances - Instance storage: documents, files, collections, search, activity, summaries
- Runtime:
/v1/relay, runtime registration/status/task mirrors - Coding:
/v1/instances/:id/coding/... - Connectors:
/v1/github(GitHub App),/v1/drive(Google Drive),/v1/workdrive(Zoho WorkDrive),/v1/email(Gmail, apply-flow reads),/v1/connectors/:id/oauth/*(generic OAuth2); registry connectors (github, http, meta, web-search, terminal, tmux, browser, repo-local, supervision, mcp, google_sheets, google_drive, zoho_workdrive, gmail) are dispatched through the tool loop, not per-connector routes - Keys: BYOK key vault and key proxy
- Billing, notifications, push, dashboard, errors
The worker also exports:
AgentDO: per-agent/per-instance state and chat runtime.RelayDO: WebSocket relay between cloud and local runtime.JobApplyWorkflow: durable job application browser brain.CodingSessionWorkflow: durable coding-session brain (the Pilot).PipelineRunWorkflow: durable declarative-pipeline runner.BrowserTaskWorkflow: durable general browser-objective brain.AgentLoopWorkflow: the platform's durable, generic agent loop.
What is good:
- Route modules are mostly well-scoped.
- Runtime state is not forced into D1 when DO storage is the right owner.
- Workflows are used for long-running apply/coding flows instead of trying to stretch request lifetimes.
- Rate limits are explicit in the API entrypoint.
- Connector tokens are encrypted through the key vault instead of stored raw.
What needs attention:
workers/api/src/index.tsis now a central registry for too many unrelated product areas. This is still workable, but every cross-cutting middleware or route addition raises blast radius.- OAuth connector routes duplicate state-signing, refresh-token storage, status, disconnect, and import-to-knowledge flow.
- Runtime-backed agent logic is split across routes, workflow files, runner client helpers, board/task mirrors, and console surfaces. The boundaries are correct, but there is no single typed "runtime task service" yet.
AgentDOhas grown into a large multi-capability object. It owns chat, memory, tasks, knowledge, files, repo ingest, collections, summaries, vector search, and activity. That is the correct owner for instance-local state, but the implementation should be decomposed internally.
AgentDO is the stateful per-agent runtime. It handles:
- HTTP chat and WebSocket chat.
- Messages and summaries.
- Memory.
- Tasks.
- Markdown knowledge documents.
- URL and repo ingestion.
- Collections and records.
- Files and file registration.
- Vector search.
- Activity logs.
- State/config updates.
Storage is mediated by AgentStorageEngine, with:
- DO storage as the primary source of truth.
- R2 for large blobs.
- Vectorize for semantic retrieval.
- Optional platform-paid Workers AI for embeddings/summaries when
PLATFORM_AI_ENABLED=true.
The DO boundary is strategically correct: instance-local mutable state belongs
close to the agent. The issue is implementation size, not ownership. A good next
step is to split handlers into internal modules grouped by state area while
leaving AgentDO as the HTTP router and authority.
Recommended internal split:
agent-do-chat.tsagent-do-knowledge.tsagent-do-files.tsagent-do-collections.tsagent-do-repo.tsagent-do-activity.ts
Do not move state ownership to D1 just to make the file smaller.
Primary files:
workers/mcp/src/index.tsworkers/mcp/src/oauth-provider.tsworkers/mcp/src/safety.tsworkers/mcp/src/instance-tools/(a directory:index.tsplus per-family modules)workers/mcp/src/storage-tools.tsworkers/mcp/src/repo-tools.ts
The MCP worker exposes a browser-authenticated MCP server. It calls the API worker for actual product operations. Tool safety is enforced through:
- OAuth scopes: read, write, runtime, destructive.
- Read-only environment mode.
- Confirmation requirements for destructive operations.
- Dry-run support where useful.
- MCP audit logging.
This is a strong boundary. MCP should remain a facade over platform APIs, not a second implementation of business logic. The current direction is good.
Areas to watch:
- Tool registration is already large and will keep growing.
- Agent-specific tools are gated by instance capabilities. This is the right model, but the capability data must stay accurate in D1 and seed migrations.
- Keep all write/destructive checks in
safety.tsor a successor policy module, not scattered inside individual tools.
Primary files:
store/console/src/*store/console/src/tabs/*store/console/src/components/*packages/sdk/src/*
The console is a React/Vite app. It uses the SDK client to call workers/api.
Shared UI/voice helpers live in packages/sdk.
Major surfaces:
- Agent/instance list and store flows.
- Chat and voice.
- Knowledge: docs, memory, files, vectors, credentials, rules.
- Settings: runtime, voice, translation, connectors, billing-ish operations.
- Board and runtime task UX.
- Coder surface.
- Apply surface.
The console has become the fastest-growing part of the system. The UX is feature-rich, but tab files now contain provider-specific state, connector panels, runtime controls, and domain logic in the same component tree.
Highest-value refactors:
- Extract connector panels from
KnowledgeTabandSettingsTab. - Extract a
ConnectorStatusRowcomponent for Gmail, Google Drive, and WorkDrive. - Extract provider import panels:
DriveImportPanelWorkDriveImportPanel- future connectors should plug into the same interface.
- Keep domain-specific surfaces separate from shared tabs. Apply, Coder, and future runtime-backed agents should own their specialized panels.
Primary files:
packages/clipackages/browser-runner
The CLI is both creator tooling and runtime tooling:
pags loginpags up- agent scaffolding and publishing
- MCP proxy helpers
The browser runner provides:
- Playwright browser control.
- Runtime task endpoints.
- CAPTCHA/handoff support.
- File upload support.
- terminal-backed coding sessions.
- Terminal capture and action execution.
The outbound relay design is the right choice. It avoids tunnel setup for the normal path and supports one runner serving multiple instances.
Operational risk:
- Browser-runner tests are currently flaky/slow in the local full suite. The
root
pnpm testrun on 2026-07-11 failed in unrelated browser-runner timeout tests while API/console checks passed. This should be treated as test reliability debt, not as connector risk. - The runner is a critical dependency for flagship agents. Add a lightweight deterministic smoke test that does not depend on full browser interaction.
The D1 schema is migration-based. Important table groups:
- Identity and marketplace:
usersagentsagent_versionsagent_instancessubscriptions
- Runtime:
instance_runtimesinstance_runtime_tasksinstance_runtime_task_eventsboard_itemsagent_events
- Credentials and connectors:
user_api_keysagent_credentials- key-proxy usage tables
- Coding:
coding_reposcoding_sessionscoding_timelinegithub_installations
- Apply:
ats_apply_cacheuser_profile
- Operations:
usagenotificationspush_subscriptionserror_logmessage_gloss
The schema reflects the product. The main concern is that runtime task state is mirrored in multiple places: runner, D1 mirrors, board rows, agent events, and workflow state. That is acceptable for UX and durability, but it needs a clear source-of-truth rule.
Recommended source-of-truth rule:
- Durable Workflow owns long-running brain progress.
- Local runner owns immediate browser/terminal execution state.
- D1
instance_runtime_tasksis the user-visible mirror and query index. agent_eventsis the timeline/audit stream.- Board rows are presentation and operational workflow state.
Documenting and enforcing this rule will prevent future "which table should I update?" ambiguity.
There are two connector layers, added at different times for different jobs.
The original layer — OAuth providers whose files are imported into an instance's knowledge:
- Gmail: OAuth, email permission toggle, apply-flow email reads (
/v1/email). - Google Drive: OAuth, file search, text/document import into knowledge (
/v1/drive). - Zoho WorkDrive: OAuth, folder browsing, paginated import of supported
text-like files into knowledge (
/v1/workdrive).
Connector tokens are stored in user_api_keys with encrypted refresh tokens.
OAuth connections are account-level, but Drive/WorkDrive access is narrowed per
agent instance through instance_connector_grants (grantModel instance-resource).
A user connects Google or Zoho once, then grants individual agent instances access
to specific folders; folder grants authorize that folder and descendants.
The "recommended extraction" this doc once called for is now built: a declared
connector registry (workers/api/src/lib/connectors/registry.ts). A connector is
declared once — { id, label, auth, scopes: {read, write}, grantModel, tools } — and
everything else derives from it (the tool catalog groups, the connectorClient auth
dispatch, capability-based gating). Adding a connector = one registry entry, no bespoke
routes.
Registered connectors (CONNECTORS):
| id | auth | scopes | notes |
|---|---|---|---|
github |
app (GitHub-App installation token) | read+write | issues, issue comments + workflow runs; the three issue-lifecycle writes are github_create_issue, github_comment_issue, github_update_issue |
http |
token (vault key) | read+write | generic HTTP/REST — call any API as config |
web-search |
token (vault key) | read | Google Custom Search |
meta |
token (META_ACCESS_TOKEN) |
write | whatsapp_send_message, instagram_send_dm |
terminal |
none (runner relay) | read+write | drive tmux, kitty, or iTerm2 targets on the user's machine |
tmux |
none (runner relay) | read+write | legacy compatibility wrapper for tmux sessions |
browser |
none (runner relay) | read+write | experimental (BROWSER_TOOLS_ENABLED); browser_navigate/browser_snapshot/browser_act |
repo-local |
none (runner relay) | read | read-only tree/file/git inspection of the local checkout |
supervision |
none (internal) | read+write | delegate goals through configured supervision links |
mcp |
token (vault key) | read+write | call user-configured outbound MCP servers |
google_sheets |
oauth | read+write | read and append rows through generic OAuth2 connector flow |
google_drive |
oauth | — | connected account (#352 Stage 1): declared with tools: [], so no agent gains a tool from it |
zoho_workdrive |
oauth | — | connected account, as above |
gmail |
oauth | — | connected account, as above |
Auth is minted through the single connectorClient(env, provider, {userId, instanceId})
path (connectors/client.ts): app-installation token, OAuth refresh→access, a vault key,
or none (local relay). A connector's tools auto-register in the unified tool registry
(tool-registry.ts) and are offered to an agent only when declared in
capabilities.tools (agent-do-tools.ts toolNamesFor). The AgentDO chat loop dispatches
them via runRegistryTool (agent-think.ts), the SAME path used by pipelines and the
generic tool-call API — so auth/grant/scope are enforced identically everywhere.
Write-consent gating (#90). Every scope:"write" connector tool is refused unless the
instance has explicit write-consent for that connector (instance_connector_consent,
migration 0051, connector-consent.ts). runRegistryTool checks consent BEFORE dispatch,
fail-closed. Read-only connectors reject write-scoped token requests outright. This is what
makes github_create_issue, browser_*, and Meta messaging safe to expose — the model can
only write where the owner consented.
A declarative, no-code data-pipeline runner: configure (not code) a
source→transform→sink agent. A pipeline is an ordered list of steps, each a registry
tool dispatched through the same runRegistryTool path (so connector auth/grant/consent
are enforced identically to a direct tool call). Outputs thread between steps by named
bind; a step input references a prior output or a run parameter via a $-prefixed ref;
forEach fans a step out over an array result; the optional sink upserts final records
into an instance collection.
- Definition + validation:
lib/pipeline.ts(loadPipelinereads the named pipeline fromagent_instances.config.pipelines,validatePipeline). The pipeline is DATA, not code — a new pipeline is a config edit, never a capability-union change. - Durable runner:
workflows/pipeline-run.ts(PipelineRunWorkflow,[[workflows]] PIPELINE_RUN) walks the steps each in its ownstep.do, so a run is resumable past the 30s DO limit (same machinery as JobApply/CodingSession). Connector tokens are re-minted inside each step, never captured across steps, so a resume re-authenticates. - Kick paths: the LLM-callable
run_pipelinetool,POST /v1/instances/:id/pipelines/:name/run, and cron/webhook triggers (run_pipelinetrigger action, #92) all funnel through the onestartPipelineRunhelper (lib/pipeline-run-start.ts). - Observability (#98): every run opens a row (
pipeline_runs, migration 0052) at kick with a per-record audit trail;GET /v1/instances/:id/pipeline-runs+ MCPlist_pipeline_runs.
The intended billing/security model is:
- User-facing LLM chat and workflow brains use caller-owned credentials.
- Platform-paid Workers AI is gated by
PLATFORM_AI_ENABLEDfor internal embeddings/summaries — overridable at runtime, without a deploy, viaPUT /v1/admin/settings/platform-ai(issue #46). The override is stored inplatform_settings, wins over the env var, and is read uncached so a flip takes effect on the next AI call; the env var stays the backstop if D1 is unreachable. - Missing caller-owned AI credentials should fail clearly instead of silently billing the platform account.
This is architecturally sound and should remain a hard rule.
Risk:
PLATFORM_AI_ENABLED=truein production config is explicitly documented as acceptable only while Serge is the sole user. This must be flipped off before real multi-user onboarding unless billing and policy intentionally change.
How an agent communicates is declared configuration, resolved by the pure
lib/agent-behaviour.ts: 19 fields over Style / Reasoning / Formatting /
Interaction / Guardrails, stored sparsely as config.behaviour on the agent
(creator default) and the instance (subscriber override), merged per field.
Two properties are load-bearing:
- A field's prompt text is data, not a callback. Bands and option prose live in
the field table as strings, so
GET /v1/instances/behaviour-schemaserialises the whole table and the console renders the exact instruction the prompt will carry. A callback here would force the UI to restate the copy and the two would drift. - Absent means unconfigured, not default. A missing field emits nothing and leaves the pre-existing heuristic in charge, which is what made it safe to deploy to every live instance at once.
resolveResponseStyle deliberately separates capability (which grounding block an
agent gets - repo-chat index, live coding sessions, or neither) from preference
(language level). Conflating them told a plain chat agent it had attached
repositories and a terminal, which is the false-self-model failure those blocks
exist to prevent.
Injection order matters: behaviour goes in BEFORE the honesty/safety text, so the
free-text persona field cannot be positioned to outrank "never claim an action
succeeded when it failed".
Boundary: set_behaviour (the agent's own tool) is restricted to
SELF_WRITABLE_FIELDS, which excludes every guardrail. An agent that reads
untrusted repository files and issue bodies must not be able to widen its own
restrictions - clearing obeys the same allowlist, or the escape is just spelled
differently.
Current protections:
- Session tokens signed with
SESSION_SIGNING_KEY. - OAuth tokens encrypted with
KEY_ENCRYPTION_KEY. - CORS allowlist.
- Security headers.
- Rate limits, with stricter limits on expensive or sensitive routes.
- SSRF-safe fetch helpers for URL ingestion.
- MCP OAuth scopes and safety gates.
- Key reveal route is rate-limited.
- Runtime endpoint URL validation requires HTTPS except localhost development.
Areas to improve:
- Centralize OAuth connector state helpers and malformed-token handling.
- Add route-level tests for connector auth/ownership paths.
- Make destructive/secret-bearing route policies easier to audit from one file.
- Ensure all imported external text has size, type, and source metadata limits.
Observability surfaces:
error_logand/v1/errorsagent_events- runtime task events
- MCP
mcp_audit_log - apply/coding timelines
- console-visible activity and board state
What works:
- Errors from workflows and key operations are persisted instead of only logged.
- Unified trace concepts exist through
agent_events. - MCP exposes operational readbacks.
What needs improvement:
- Add a standard event taxonomy. Today event names are descriptive but not fully governed.
- Add correlation IDs consistently across API request, workflow run, runner task, and MCP operation.
- Add a "runbook per failure mode" document for apply, coding, connector OAuth, and runner relay.
Current checks used during recent WorkDrive work:
pnpm --filter proagentstore-api typecheckpnpm exec vitest run workers/api/src/lib/workdrive.test.ts workers/api/src/lib/drive.test.ts --reporter=verbosepnpm --filter @proagentstore/console build- GitHub CI on PRs
Known issues from local root commands:
pnpm testcan fail in unrelated browser-runner timeout tests.pnpm lintcurrently fails on existing repo-wide Biome diagnostics outside the WorkDrive change.
Assessment:
The API lib tests are useful and fast. Browser-runner integration tests need isolation or tiering so ordinary platform changes can run a dependable local suite.
Recommended test tiers:
- Tier 1: typecheck + fast unit tests + console build.
- Tier 2: API route tests and focused workflow pure logic tests.
- Tier 3: browser-runner integration tests.
- Tier 4: end-to-end deployed smoke tests.
- Large API worker surface area
The API worker is still coherent, but it is now the center of auth, marketplace, runtime, storage, coding, connectors, billing, notifications, and observability. Mitigation: extract shared service modules inside the worker before adding more route families.
- AgentDO implementation size
The DO boundary is correct, but the file should be split internally by state area. Mitigation: keep the router thin and move handler groups to modules.
- Console component growth
Settings and Knowledge tabs are accumulating connector and runtime-specific UI. Mitigation: extract panels and shared connector rows.
- Runtime task source-of-truth ambiguity
Task data exists in workflow state, runner state, D1 mirrors, board rows, and events. Mitigation: document and enforce source-of-truth rules.
- Connector duplication
Gmail, Drive, and WorkDrive have repeated OAuth and token-vault code. Mitigation: extract small connector mechanics, not provider behavior.
- Test reliability
Root tests are not currently a crisp go/no-go because browser-runner integration timeouts can mask unrelated work. Mitigation: split test tiers and stabilize the browser-runner teardown path.
- Platform AI switch
PLATFORM_AI_ENABLED=true is acceptable for single-user development but risky
for broader onboarding. Mitigation: default off before multi-user launch and add
an operational checklist. Partly addressed (#46) — an operator can now kill
platform-paid AI at runtime through PUT /v1/admin/settings/platform-ai instead
of waiting on a redeploy, so runaway spend has a same-minute stop.
- Extract connector OAuth/token-vault helpers.
- Extract WorkDrive and Drive import panels from
KnowledgeTab. - Add route tests for Google Drive and WorkDrive auth/ownership/error paths.
- Add a short runbook for connector setup and OAuth troubleshooting.
- Split CI/test scripts into fast and integration tiers.
- Split
AgentDOhandler groups into internal modules. - Introduce a runtime task service with explicit source-of-truth semantics.
- Normalize event names and correlation IDs.
- Extract Settings connector rows into a data-driven component.
- Add a durable workflow/run observability page or MCP summary tool.
- Move agent-specific surfaces into capability-owned modules.
- Formalize an extension interface for future connectors.
- Add generated schema docs from D1 migrations.
- Add API route inventory generation from Hono route registration.
Use these rules when adding new platform features:
- Keep template agents separate from private instances.
- Keep domain logic inside the agent or workflow that owns the domain.
- Share platform mechanics only after the duplication is proven.
- Durable Object storage owns instance-local mutable state.
- D1 owns relational product state and cross-instance queries.
- R2 owns blobs.
- Vectorize owns semantic retrieval indexes.
- Workflows own long-running brain progress.
- The local runner owns local browser/terminal execution.
- MCP should call platform APIs, not reimplement platform behavior.
- User-facing LLM spend must be BYOK unless the billing model explicitly changes.