Product
RAICOF / AI ContentOps
Doc version
v1.0
Owner
Platform Architect
Status
For review
1 · Executive summary
Two delivery tracks share one control plane. Native track: control plane → native renderer → managed hosting. Adapter track: control plane → WordPress plugin → draft-only post in wp-admin; publishing remains a human action inside WordPress. Both tracks share the same plan/guardrail/audit pipeline. AI output is JSON only and is never executed.
2 · Target architecture
Dashboard (TanStack Start)
│ signed session
▼
Control plane (Fastify, Node 22)
├── Postgres 16 (state of record)
├── Redis 7 (queues, locks, rate limits)
├── Object store (S3 / R2)
├── AI orchestrator ── packages/ai-orchestrator
├── Guardrail engine ── packages/guardrails
└── Deployment worker ── packages/deployment (15 stages)
│
├──► Native renderer ──► Staging ──► Production
│
└──► WordPress adapter plugin (PHP)
│ HMAC-signed REST
▼
wp-admin: draft post + preview URL
3 · Environments
| Env | URL shape | Data | Promotion |
|---|---|---|---|
| local | localhost | compose: pg / redis / minio | n/a |
| staging | https://staging.<host> | managed pg / redis / object | auto on merge to main |
| prod | https://app.<host> | managed pg / redis / object | human gate in dashboard |
4 · Phased rollout — step by step
- Initialise the monorepo: bun workspaces, shared
tsconfig.base.json, root scriptsdev/build/typecheck/test. - Lift
platform/packages/*intopackages/*unchanged; update path aliases. - Lift the dashboard into
apps/dashboard; addapps/wordpress-pluginandapps/rendererstubs. - Scaffold
apps/control-plane(Fastify, TS strict) with/healthzand/readyz; structured JSON logs with request IDs. - Add
docker-compose.ymlwith Postgres 16, Redis 7, MinIO; add.env.example. - Write Postgres migrations: tenants, users, memberships, projects, sites, site_credentials, content_requests, content_plans, page_documents, page_versions, approvals, deployments, wordpress_operations, audit_events, idempotency_keys.
- Auth: email+password, OIDC-ready; signed httpOnly cookies; CSRF on state-changing routes.
- Tenancy middleware: every authed request resolves
tenantIdand scopes every query. - RBAC roles
owner,editor,operator,viewer;requireRole()helper. - BullMQ worker process with shared Redis connection; jobs typed as a discriminated union.
- Object-storage wrapper:
media/{tenant}/{site}/...,builds/{deployment}/...,tmp/{request}/...; signed-URL helper. - Audit writer: every state transition writes one row with actor, tenant, target, before/after hash.
- Seed script:
tenant_demo,project_chase4heat, onesite, an owner user; load CHASE4HEAT manifest intosite_credentials.manifest_cache. - CI: GitHub Actions runs typecheck / lint / test / build on PR; branch protection on
main. - Cloud bootstrap: provision managed pg + redis + object storage on the chosen host; wire secrets via the host's secret manager.
Exit checks
- Control plane boots locally and in staging.
- Seeded owner can log in, see their tenant, switch tenants.
- Audit log records login + tenant switch.
- CI green on a clean PR; staging URL serves the dashboard.
- Pick plugin distribution channel (manual zip vs self-hosted update server) — tracked in §10.
- CI job produces a versioned
agent-assisted-content-{version}.zipfromapps/wordpress-plugin; pin the bundled SPA build id. - Plugin metadata:
Plugin Name,Version,Requires at least,Requires PHP,Update URI; PSR-4 autoload. - On
register_activation_hook: create theaac_auditcustom table, scheduleaac_refresh_manifestdaily. - Admin page registered with
manage_options; SPA bundle enqueued only on the plugin screen. - Settings stored server-side via
update_option; API key never echoed to the browser;Test connectionhitsaac/v1/diagnostics. - Register REST namespace
aac/v1viaregister_rest_route; every route has apermission_callbackcheckingmanage_options+ validX-WP-Nonce. - Server-to-server signer (PHP): HMAC-SHA256 over
METHOD + PATH + BODY_SHA256 + TIMESTAMP + NONCEwithADAPTER_SIGNING_SECRET; 5-min replay window; nonces in a transient. - Pairing flow: dashboard mints a Site Key → admin pastes it → plugin calls
/api/v1/sites/pair→ long-lived signed site identity cached server-side. - Manifest collector: scheduled job calls every WP read function in §6 and POSTs a normalised
SiteCapabilityManifest; also triggered on activation, theme change, and on demand. - Draft writer: receives a
ContentPlanfrom the control plane, executes the allowed write functions in §6 in one PHP request, returnspost_id+ preview URL. All writes arepost_status=draft; no publish path exists in code. - Media sideloader: pulls media from a signed URL, uses
media_sideload_image/wp_handle_sideload, attaches to the target post. - Audit logger: every plugin op writes one row to
aac_audit(operation, actor, post_id, payload hash, result, timestamp); mirrored to the control plane via/api/v1/audit. - Fixed error taxonomy:
NOT_PAIRED,SIGNATURE_INVALID,CAPABILITY_DENIED,WP_INSERT_FAILED,MEDIA_SIDELOAD_FAILED,MANIFEST_DRIFT,RATE_LIMITED. - Rate limiting + circuit breaker per site in the control plane (token bucket, opens after N consecutive 5xx).
- Dashboard Connection screen shows plugin version, manifest version, last sync, last error,
Refresh manifestbutton. - E2E smoke against a throwaway WP: pair → manifest → create draft page → preview URL renders in
wp-admin.
Exit checks
- A real WP site pairs in under 5 minutes.
- A draft post created from the dashboard appears in wp-admin with correct preview URL.
- Tampered or replayed requests are rejected and logged.
- Manifest refresh runs on schedule and on demand; drift surfaces in the dashboard.
- Provider adapter behind
AI_PROVIDER/AI_API_KEY; default Lovable AI Gateway. - Implement the orchestrator contract: JSON-only output — either
ClarificationQuestion[]or aContentPlan. Non-JSON or schema-failing output is rejected. - Structured prompt assembly from
ContentRequest, cachedSiteCapabilityManifest, target schema, allowed operations, allowed block contracts. Never raw HTML. - Hard guard: only
allowedBlockContracts/editableSectionsmay be referenced; first violation regenerates with tightened prompt; second becomesneeds_clarification. - Wire
packages/guardrails: schema validation, block-registry validation, media resolution, SEO fields, slug/route collision, destructive-action detector. - Request state machine:
received→plan_pending→ (plan_ready|needs_clarification|rejected); transitions persisted, audit row per transition. - Clarification loop UI: questions render, user answers, orchestrator re-invoked with answers appended.
- Plan rendering in dashboard: field-level diff against current
page_document; only safe fields are user-editable. - Plan approval = first human gate; transitions to
awaiting_build. - Per-tenant quotas: prompt tokens, output tokens, requests/hour, retries.
- Observability: per-request trace row with
prompt_hash,tool_calls,validation_outcomes,latency_ms,cost_estimate. - Prompt-injection defense: manifest text fields JSON-escaped and delimited; orchestrator instructed to treat as data.
- Hard rule at the call site: orchestrator output is never executed. No
eval, no template hydration with raw output, no shell.
Exit checks
- A CHASE4HEAT request produces a valid ContentPlan reviewable in the dashboard.
- Invalid plans never persist; user sees a clear error.
- Clarification loop works end to end.
- No code path allows AI output to be executed.
| # | Stage | Gate | Native track | Adapter track |
|---|---|---|---|---|
| 1 | validate_page_schema | — | guardrails | guardrails |
| 2 | validate_blocks | — | block registry | block registry |
| 3 | validate_media | — | resolve references | resolve references |
| 4 | validate_seo | — | title / desc / OG / heading order | title / desc / OG / heading order |
| 5 | validate_routes | — | slug + collisions | slug + collisions |
| 6 | build_preview | — | renderer → object store | wp_insert_post (draft) |
| 7 | seo_checks | — | HTML lint, OG, Twitter, headings | skipped (WP renders) |
| 8 | generate_sitemap | — | sitemap.xml | skipped (WP) |
| 9 | generate_robots | — | robots.txt | skipped (WP) |
| 10 | generate_jsonld | — | per schemaType | skipped (WP) |
| 11 | deploy_staging | — | atomic upload + URL | WP preview URL |
| 12 | await_promotion | human | side-by-side diff | disabled in MVP |
| 13 | deploy_production | — | atomic swap | disabled in MVP |
| 14 | log_deployment | — | deployments + audit_events | audit row |
| 15 | preserve_rollback | — | previous prod = rollback_target | last-known-good draft id |
- BullMQ
deploymentworker consumes{ deployment_id }jobs; idempotent and resumable. - Each stage implements
Stagefrompackages/deployment/src/pipeline.tsand returnsStageResult. - Per-stage timeout + retry policy: exponential backoff with jitter, max 3 retries on transient errors; fatal errors halt immediately.
- Stage 12 timeout policy: 7 days →
cancelled. - One-click rollback in the dashboard restores the previous artifact via
rollback_target. - Observability: per-stage duration, success rate, gate wait, queue depth; alerts on consecutive failures of the same stage.
Exit checks
- An approved plan reaches production through all 15 stages, exercising both human gates.
- An adapter plan produces a draft in wp-admin with preview URL, audit row, rollback ref.
- A failed stage halts the pipeline with a clear remediation message.
- Rollback restores the previous production deployment in one click.
5 · WordPress contact surface — every function we call
All WordPress reads and writes happen inside the adapter plugin. The control plane never speaks to WordPress directly; it only speaks to the plugin's signed REST channel.
| Function | Purpose | Constraint we enforce |
|---|---|---|
register_activation_hook | Create plugin tables, schedule cron | Idempotent |
register_deactivation_hook | Unschedule cron, keep data | No data loss on deactivate |
add_menu_page | Register Agent Content admin page | Capability manage_options |
current_user_can('manage_options') | Permission gate | Required in every REST handler |
wp_verify_nonce / wp_create_nonce | CSRF for admin → plugin REST | All state-changing routes |
register_rest_route | Expose aac/v1/* endpoints | permission_callback required |
wp_enqueue_script / wp_enqueue_style | Load SPA bundle on plugin screen only | Screen check guard |
get_option / update_option / delete_option | Persist plugin settings | API key never sent to browser |
wp_remote_post / wp_remote_get | S2S calls to control plane | HMAC-signed, 10s timeout |
wp_schedule_event / wp_unschedule_event | Daily manifest refresh | Single recurring slot |
wp_insert_post(post_status=draft) | Create draft Page / Post / CPT | Draft only — never publish |
wp_update_post | Update existing draft | Status pinned to draft |
update_post_meta / get_post_meta / delete_post_meta | Structured field values per section | Keys whitelisted by manifest |
wp_set_object_terms / wp_get_object_terms | Taxonomy assignment on CPTs | Only registered taxonomies |
get_post / get_posts / WP_Query | Read content for manifest + planning | Read-only |
get_pages | Enumerate Pages | Read-only |
get_post_types(['public' => true]) | Enumerate CPTs | Read-only |
get_taxonomies | Enumerate taxonomies | Read-only |
get_terms | Enumerate terms | Read-only |
register_block_type | List allowed blocks for manifest | Read-only |
wp_get_nav_menus / wp_get_nav_menu_items | Read menus for manifest | Read-only |
wp_update_nav_menu_item | Edit menu items | Allowed locations only, gated by approval |
wp_delete_post(force_delete=false) | Move post / menu item to trash | Never force_delete=true |
wp_get_sidebars_widgets / the_widget introspection | Read widgets for manifest | Read-only |
update_option('widget_*', …) | Update widget fields | Gated by approval |
wp_get_theme / wp_get_themes | Active theme + version for manifest | Read-only |
media_sideload_image | Pull media from signed URL | MIME + size allowlist |
wp_handle_sideload | Lower-level media intake | MIME + size allowlist |
wp_insert_attachment / wp_generate_attachment_metadata / wp_update_attachment_metadata | Persist attachment | Linked to target post |
set_post_thumbnail | Featured image | Attachment must belong to this site |
get_preview_post_link | Return preview URL | Returned only to authenticated admin |
add_query_arg / home_url / rest_url | Build URLs returned to dashboard | No raw concatenation |
wp_kses / wp_kses_post / wp_kses_allowed_html | Sanitize text rendered server-side | Allowlist from block registry |
sanitize_text_field / sanitize_textarea_field / esc_url_raw / absint | REST boundary input sanitisation | Applied per field type |
$wpdb->prepare | Writes to aac_audit only | Never used on core tables |
wp_get_current_user | Actor identity for audit | Required on every write |
__() / esc_html__() | i18n + escape | All UI strings |
6 · Infrastructure checklist
| Component | Choice (default) | Why |
|---|---|---|
| Hosting (control plane) | Cloud Run / Fly / Render | TS Node service, autoscale |
| Hosting (native frontend) | CDN-backed static + edge SSR | TanStack Start output |
| Database | Managed Postgres 16 | State of record |
| Queue / cache | Managed Redis 7 | BullMQ + rate limits |
| Object storage | S3-compatible (R2 / S3) | Media + build artifacts |
| Secrets | Host secret manager | No secrets in env files |
| Logs / traces | OpenTelemetry → managed backend | Per-request trace id |
| Provider behind one adapter | Approval + alert notifications |
7 · Secrets & configuration matrix
| Key | Scope | Used by |
|---|---|---|
DATABASE_URL | server | control plane, worker |
REDIS_URL | server | control plane, worker |
OBJECT_STORE_* | server | control plane, worker |
SESSION_SECRET | server | auth cookies |
ADAPTER_SIGNING_SECRET | server + plugin | HMAC for plugin ⇄ control plane |
AI_PROVIDER | server | orchestrator |
AI_API_KEY | server | orchestrator |
DEPLOY_TARGET_* | worker only | stage 11 / 13 |
SENTRY_DSN | server | error reporting |
8 · Security & compliance gates
- Two human gates: plan approval, production promotion.
- AI output is JSON-only and never executed; no
eval, no shell, no template hydration of raw output. - Plugin enforces capability + nonce on every REST route; control plane verifies HMAC on every plugin call.
- Audit row per state transition, per WP operation, per deployment stage.
- Tenant scoping applied in a single middleware; queries cannot skip it.
- Rate limits + circuit breaker per site; quotas per tenant.
- No publish path in adapter code; no force-delete; no privileged role mutation.
9 · Go-live checklist
- Staging green for 7 days under synthetic load (1 op/sec/site).
- Disaster-recovery drill: restore pg from backup, replay last 24h of audit.
- Rotate
ADAPTER_SIGNING_SECRETonce before launch; verify rotation procedure. - Pen-test the plugin REST surface and the control-plane public endpoints.
- Status page + on-call rota in place.
- Runbooks for: failed deployment, manifest drift, signature reject storm, AI quota exhausted.
10 · Open decisions
- Plugin distribution: manual zip vs self-hosted update server.
- Primary host for control plane (Cloud Run vs Fly vs Render).
- AI provider lock-in vs adapter (Lovable AI Gateway default).
- Whether the adapter track ever gains an automated publish path post-MVP (currently: no).