Skip to content

Blog + Social-Media Connectors — Provider-Neutral Transport Facades (v4.6)

Documents the @cinatra-ai/social-media-connector + @cinatra-ai/blog-connector facades and the @ossflywheel/blog-connector vendor connector introduced in v4.6 Phases 304–306. Read alongside email-connector.md (the proven v4.5 pattern these mirror), mcp-patterns.md, and docs/developer/extensions.md (the kind:"connector" extension kind).

packages/asset-blog/ was a 38-file monolith owning transport + generation + project state. v4.6 splits the transport spine along the v4.5 connector-facade pattern: generic facades own the routing/contract; concrete + vendor-scoped connectors own the provider/site specifics. Agentization + the asset-blog archive are v4.6.1 (Phases 307–309).

LinkedIn publish (asset-blog generation.ts)
└─ publishSocialMediaPostThroughSystem(post, opts) @cinatra-ai/social-media-connector/facade
├─ resolveConnectorId(opts) src/lib/register-social-providers.ts
│ 1. explicit connectorId 2. first registered
└─ connector.publish(post) linkedInSocialMediaConnector → publishLinkedInPost (host @/lib/linkedin-api)
WordPress draft-write (asset-blog wordpress.ts)
└─ buildBlogDraftPayloadThroughSystem(input, {instanceBlogConnectorId}) @cinatra-ai/blog-connector/facade
├─ resolveConnectorId src/lib/register-blog-providers.ts
│ 1. explicit connectorId
│ 2. WordPressInstanceSettings.blogConnectorId
│ 3. "default" generic connector
└─ connector.buildDraftPayload(input) → { createPayload, postMeta? }
• defaultBlogConnector → markdown→HTML, postMeta:undefined (NO injection, NO Elementor)
• ossflywheelBlogConnector → Elementor node-tree swap + ossflywheel template selectors
host: createWordPressDraft(createPayload); if postMeta → updateWordPressDraftMeta({meta:postMeta})

The legacy WordPressContentConverterFn shape ({title?, excerpt?, content, contentIsHtml?}) could not express the Elementor node-tree injection (a 2-call WP sequence: create-draft THEN meta-update). D1 widens the contract so a connector returns BOTH the create payload AND an optional postMeta map:

buildDraftPayload(input): Promise<{
createPayload: { title; content; excerpt; status: "draft"; featured_media?; ... };
postMeta?: Record<string, unknown>; // omit → host skips the meta-update call
}>

The host owns the 2-call orchestration; the connector just declares what to write. Generic connectors omit postMeta; ossflywheel returns the swapped _elementor_data node-tree.

F1 — generic-vendor extension-management policy boundary

Section titled “F1 — generic-vendor extension-management policy boundary”

Owner directive 2: extension-management must accept ANY @<vendor>/<slug>-connector, not an @ossflywheel-only carve-out. Codex round-3 F1: the widening must be stricter, not a permissive wildcard. The boundary (packages/extensions/src/connector-handler.ts):

  • Regex GENERIC_VENDOR_CONNECTOR_NAME_RE = /^@[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*-connector$/ (exported, single source of truth)
  • cinatra.kind === "connector" semantic gate (rejects -agent/-skill/-artifact even if the name shape matches)
  • checkConnectorRealpathMatch() — package-name↔realpath equality under realpath(extensionsRoot); rejects symlink-escape + vendor/slug mismatch
  • cinatra.visibility default admin (defaultConnectorVisibility()); validate() rejects unknown values
  • Static setup-page loader entries only — no dynamic vendor import

The 8 extension-mgmt surfaces: connector-handler regex, pnpm-workspace.yaml glob (extensions/*/*-connector), tsconfig.json alias, the boot scanner (already generic since Phase 304’s loadAllExtensionPackages), and boot registration were changed; the catalog-descriptor / connector-policy-preflight / publish-purge-validation surfaces were already vendor-agnostic (key on cinatra.kind, zero @cinatra-ai literals — documented in the validate() docstring with grep proof).

ONLY the actual provider/transport call routes through a facade. The asset-blog blog_post_publish_linkedin_* / blog_post_publish_wordpress_* MCP primitives drive draft-COPY generation + project/HITL state — NOT pure transport — so they remain asset-blog compat wrappers through v4.6. They migrate in v4.6.1 Phase 307 (LinkedIn-copy parity) + Phase 308 (store rehome).

ABC-08 — blogConnectorId binding non-regression

Section titled “ABC-08 — blogConnectorId binding non-regression”

WordPressInstanceSettings.blogConnectorId?: string is stored in the existing connector_config:wordpress JSON blob (no schema migration). Both save constructors build the row field-by-field (no ...existing spread), so the field is explicitly preserved:

  • saveWordPressInstance — inherits from existing when no override passed
  • saveWordPressInstanceFromNangoConnectionunconditionally preserves existing?.blogConnectorId (Nango never carries it; without this a disconnect→reconnect silently drops the binding)
  • getWordPressAPISettings normalizer extracts it on re-read

Pinned by src/__tests__/wordpress-blog-connector-id-roundtrip.test.ts.

ABC-12 — idempotent ossflywheel self-heal

Section titled “ABC-12 — idempotent ossflywheel self-heal”

selfHealOssflywheelBlogConnectorBinding() runs best-effort at boot from registerBlogProviders(). It reads the WP connector_config blob directly (NOT saveWordPressInstance — that does network revalidation + Nango sync, unacceptable at boot) and binds blogConnectorId="ossflywheel" on instances with an EMPTY binding whose siteUrl host matches /(^|\.)ossflywheel\.com$/i (or an explicit allowlist hook). Idempotent — already-bound instances are skipped, so an explicit user choice is never overwritten.

FileRole
extensions/cinatra-ai/social-media-connector/src/{contract,registry,facade,index}.ts + mcp/module.tsSocial facade (5-part). social_media_publish MCP primitive
extensions/cinatra-ai/linkedin-connector/src/connector.tslinkedInSocialMediaConnector — wraps host publishLinkedInPost
extensions/cinatra-ai/blog-connector/src/{contract,registry,facade,index}.ts + default-connector.ts + mcp/module.tsBlog facade (5-part) + generic defaultBlogConnector + blog_connector_list primitive
extensions/ossflywheel/blog-connector/src/{elementor,connector,index}.tsFirst non-@cinatra-ai-scope connector. Owns ALL _elementor_data + ossflywheel template selectors (ABC-11 grep gate asserts zero refs elsewhere)
src/lib/register-social-providers.ts / src/lib/register-blog-providers.tsBoot wiring (configure facade + register connectors; auto-runs on import from instrumentation.node.ts). Blog one also runs the ABC-12 self-heal
packages/extensions/src/connector-handler.tsF1 generic-vendor regex + visibility + realpath guards
  1. Create extensions/<vendor>/<slug>-connector/ with package.json declaring cinatra.kind:"connector" (+ optional cinatra.visibility).
  2. Implement BlogConnector / SocialMediaConnector (import type the contract only).
  3. registerBlogConnector(...) / registerSocialMediaConnector(...) in the matching src/lib/register-*-providers.ts.
  4. Add the tsconfig.json path alias. The extensions/*/*-connector workspace glob picks it up automatically.
  5. Bind it: set WordPressInstanceSettings.blogConnectorId (blog) or pass connectorId (social).

v4.6.1 — Blog agentization patterns (Phase 307 / 308; reusable)

Section titled “v4.6.1 — Blog agentization patterns (Phase 307 / 308; reusable)”

These patterns were established by v4.6.1 (PR #515, Codex-gated) and apply to any future agent/orchestrator/artifact work in this area.

Explicit context-agent OAS pattern (canonical; the auto-wire replacement)

Section titled “Explicit context-agent OAS pattern (canonical; the auto-wire replacement)”

The Phase-363 autoWireContextAgent runtime auto-wiring is RETRACTED (PRE-1 deleted it). An agent that declares metadata.cinatra.contextSlots MUST wire context-agent explicitly in its own OAS: declare @cinatra-ai/context-selection-agent in the agent package.json cinatra.agentDependencies (NOT OAS metadata) + add an explicit context_<slotId> FlowNode (subflow ref to a byte-faithful inlined context-agent-<slot>-subflow) BEFORE the first slot-consuming node + a contextRefs → <consumer>.contextSlotBindings DataFlowEdge. Constants reach a FlowNode via hidden StartNode default inputs (a DataFlowEdge has no literal value field — precedent: lint-policy-agent). Child-local only — a parent orchestrator never fans context-agent out to its children. Reference impl: email-outreach-agent (PRE-2). Repo-wide invariant test: packages/extensions/src/__tests__/contextslots-require-explicit-context-agent.test.ts (ABC-30). Full contract: docs/ai/context-slots.md.

@cinatra-ai/context-selection-agent is a runtime-backed core agent: its contextRefs output is runtime-injected, not flow-graph-produced, so the shipped context-agent OAS itself inherently trips exactly one OAS-RUNTIME-005. Any byte-faithful inline of it carries the same single finding. This is intentional and Codex-adjudicated (a synthetic producer would make the canonical template diverge from the real contract). Record each such agent in KNOWN_BROKEN_AGENTS in packages/agents/src/__tests__/oas-runtime-invariants.test.ts with expectedCodes:["OAS-RUNTIME-005"] + the exact expectedBlockerCount (N inlined context subflows ⇒ N) — the allowlist is the test’s designed escape hatch; new code/count drift still fails loudly.

Parent-orchestrator child-subflow inlining + deterministic _shape seams

Section titled “Parent-orchestrator child-subflow inlining + deterministic _shape seams”

A WayFlow parent orchestrator (e.g. blog-pipeline-agent) hand-inlines each child agent’s full flow as a namespaced subflow under $referenced_components (no auto-bundler; mirrors email-outreach-agent; the compiler resolves $component_ref locally then via the global registry — it does NOT resolve a FlowNode subflow by package). Inter- agent shape gaps (e.g. idea-array → draft-object, draft-object → linkedin-strings) are bridged by an InputMessageNode (string output) → a deterministic /api/agents/passthrough ApiNode with a per-_shape input shaper (src/app/api/agents/passthrough/blog-pipeline-seam.ts; extend TOOL_INPUT_SHAPERS.objects_save via a chained pure module). The route’s result_input_passthrough echoes the shaped rawData into the node’s declared typed outputs. Never invent a transform node and never change a shipped leaf agent’s IO contract to paper over a shape gap.