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).
Why it exists
Section titled “Why it exists”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).
Architecture overview
Section titled “Architecture overview”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})D1 — the widened BlogConnector contract
Section titled “D1 — the widened BlogConnector contract”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/-artifacteven if the name shape matches)checkConnectorRealpathMatch()— package-name↔realpath equality underrealpath(extensionsRoot); rejects symlink-escape + vendor/slug mismatchcinatra.visibilitydefaultadmin(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).
F2 — transport-only binding
Section titled “F2 — transport-only binding”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 fromexistingwhen no override passedsaveWordPressInstanceFromNangoConnection— unconditionally preservesexisting?.blogConnectorId(Nango never carries it; without this a disconnect→reconnect silently drops the binding)getWordPressAPISettingsnormalizer 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.
Key files
Section titled “Key files”| File | Role |
|---|---|
extensions/cinatra-ai/social-media-connector/src/{contract,registry,facade,index}.ts + mcp/module.ts | Social facade (5-part). social_media_publish MCP primitive |
extensions/cinatra-ai/linkedin-connector/src/connector.ts | linkedInSocialMediaConnector — wraps host publishLinkedInPost |
extensions/cinatra-ai/blog-connector/src/{contract,registry,facade,index}.ts + default-connector.ts + mcp/module.ts | Blog facade (5-part) + generic defaultBlogConnector + blog_connector_list primitive |
extensions/ossflywheel/blog-connector/src/{elementor,connector,index}.ts | First 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.ts | Boot 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.ts | F1 generic-vendor regex + visibility + realpath guards |
Adding a new vendor connector
Section titled “Adding a new vendor connector”- Create
extensions/<vendor>/<slug>-connector/withpackage.jsondeclaringcinatra.kind:"connector"(+ optionalcinatra.visibility). - Implement
BlogConnector/SocialMediaConnector(import typethe contract only). registerBlogConnector(...)/registerSocialMediaConnector(...)in the matchingsrc/lib/register-*-providers.ts.- Add the
tsconfig.jsonpath alias. Theextensions/*/*-connectorworkspace glob picks it up automatically. - Bind it: set
WordPressInstanceSettings.blogConnectorId(blog) or passconnectorId(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.
Faithful-inline OAS-RUNTIME-005 allowlist
Section titled “Faithful-inline OAS-RUNTIME-005 allowlist”@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.