Extensions
Cinatra extends through extensions: versioned, published packages that an admin installs into the workspace. The platform owns their lifecycle — install, update, uninstall, archive, restore, and (under explicit admin gate) force-delete — and tracks where they came from, who can see them, and how to read or publish them back to the right registry.
This document is the reference for the extension lifecycle and the registry routing it depends on. For the conventions around building an extension as a TypeScript package, see Building packages. For the authoring loop on the OAS Flow file inside an agent extension, see Developing agents.
What an extension is, today
Section titled “What an extension is, today”The platform registers four extension type handlers at startup: agent packages, skill packages, connector packages, and artifact packages.
-
Agent extension — an OAS Flow agent published as an npm-style package. Carries
oas.json(the agent definition), a siblingpackage.json(npm metadata + Cinatra metadata undermetadata.cinatra), and any inline skill bundles the agent declares. Installing it upserts anagent_templatesrow, creates anagent_template_versionssnapshot, registers any object types the agent declares, and (when present) installs declared package skills. -
Skill extension — a GitHub-hosted
SKILL.mdpackage. Installing it persists askill_packagesrow and triggers a skill-match recompute so the new skills are reachable from agents that match their criteria. -
Artifact extension — a metadata-only package declaring a content type. Carries
cinatra.kind: "artifact"plus anartifactblock describing its MIME allowlist and the matcher/authoring skills that resolve it (artifact.skills.matchers,artifact.skills.authoring). Registered withArtifactExtensionTypeHandler, which forbidscinatra/oas.jsonso the agent loader never mounts it. See artifacts-architecture.md. -
Connector extension — a transport / external-system integration package living at
extensions/<vendor>/<domain>-<capability>-connector/withcinatra.kind: "connector"in itspackage.json. Registered with the extension registry viaConnectorExtensionTypeHandler(Phase 302/303; v4.6 Phase 306 widened the vendor scope). Today connectors are workspace-compiled: they ship in the build, are linked via pnpm workspace symlinks, and register at boot (src/lib/register-transport-connectors.tsfor transport providers;src/lib/register-email-providers.ts/register-social-providers.ts/register-blog-providers.tsfor the provider-neutral facades). The handler’svalidate()enforces the kind-at-end naming +cinatra.kinddeclaration + (v4.6 Phase 306 F1) the generic-vendor policy boundary: any@<vendor>/<slug>-connectoris admissible but gated byGENERIC_VENDOR_CONNECTOR_NAME_RE+cinatra.kind+ package-name↔realpath (symlink-escape reject) + defaultadminvisibility + static loader entries only.archive/restoreare audit-only;install/update/uninstallthrowConnectorWorkspaceCompiledError— runtime/marketplace install is a future v2. The connector packages today: 10 transport (gmail,apify,apollo,google-calendar,github,linkedin,wordpress,drupal,youtube,media-feeds), 3 LLM-provider (openai,gemini,claude—LlmProviderAdapternotEmailConnector; the registry treatsconnectoras a broad category), and 3 provider-neutral facades —email-connector(v4.5 Phase 302/303),social-media-connector+blog-connector(v4.6 Phases 304–305). The first non-@cinatra-ai-scope connector is@ossflywheel/blog-connector(v4.6 Phase 306 — the F1 generic-vendor proof case; owns the ossflywheel Elementor logic). Seedocs/ai/blog-and-social-connectors.md.
Asset and entity packages are still not registered as separate extension kinds — their MCP primitives and UI surfaces ship as workspace packages compiled into the platform. The extension registry remains a deliberately small surface; adding a new kind still means writing and registering a new ExtensionTypeHandler (the connector handler is the reference example: packages/extensions/src/connector-handler.ts).
ObjectSyncAdapteris NOT a connector (Phase 302 rename). The object-store outbound-sync framework was historically calledObjectConnectorAdapter; it was renamed toObjectSyncAdapter(interface, registry,cinatra.object_sync_adapter_configstable,adapter_idcolumn) precisely to disambiguate it from these transport “connector” extension packages. They are orthogonal: a connector extension is an inbound/outbound integration to an external system; anObjectSyncAdaptermirrors a Cinatra object out to an external CRM/CMS.
Naming conventions
Section titled “Naming conventions”Extension packages are published as npm-style scoped packages. Their directory names equal their unscoped package names (1:1, kebab-case), and the kind goes at the end so the registry reads as a noun phrase (“email test delivery agent”, “assistant skills”) and sorts by functional domain. These rules apply to new and renamed extensions; legacy in-tree names that don’t conform are tracked in the drift-gate’s grandfatherList (advisory) and transitionalAllowlist (must shrink to zero).
Grammar
Section titled “Grammar”StandardPackageName = "@" Scope "/" Slug "-" KindVendoredPackageName = "@" Scope "/" Slug // vendored carve-outCatalogRef = ( StandardPackageName | VendoredPackageName ) ":" LocalIdScope = "cinatra-ai" | <vendor-scope>Kind = "agent" | "connector" | "artifact" | "skills"Slug = kebab-caseLocalId = kebab-casecinatra.kind in package.json (singular: "agent" | "connector" | "artifact" | "skill") is the authoritative signal for lifecycle, purge, and dispatch. The directory suffix is a strong hint and is validated by the drift-gate, but the manifest wins on disagreement.
The VendoredPackageName form (no -<kind> suffix) is allowed only for kind:"skill" bundles that mirror an upstream identity which does not follow Cinatra’s <slug>-<kind> convention — today that is @anthropics/skills. The drift-gate’s vendoredSkillScopeAllowlist and VENDORED_PACKAGE_NAME_ALLOWLIST enumerate the exact-name carve-outs; nothing else may use this shape.
Kind scope policy
Section titled “Kind scope policy”| Kind | Allowed scopes |
|---|---|
| Agent | @cinatra-ai/ |
| Connector | Any scope (Phase 306 generic-vendor policy) |
| Artifact | @cinatra-ai/ |
| Skill | @cinatra-ai/ OR an entry in the explicit vendored-scope allowlist (today: @anthropics) |
Per-kind shapes
Section titled “Per-kind shapes”| Kind | Dir pattern | cinatra.kind | Naming rule | Required files | Has src/? |
|---|---|---|---|---|---|
| Agent | <slug>-agent/ | "agent" | Role noun. Name the role the agent plays (author, planner, code-reviewer, security-reviewer, lint-policy, skill-recommender, email-outreach). Domain-prefixed role nouns are allowed (email-outreach-agent, blog-draft-writer-agent, context-selection-agent). Orchestrator-topology names are forbidden as suffix OR prefix: no -pipeline, -orchestrator, -handler, -child, -stage-N. Names where the role itself is “curator”, “coordinator”, or “router” are allowed (list-curator-agent). Grandfathered names that pre-date this rule (blog-pipeline-agent, the workflow-coupled email-*/blog-* families) are tracked by name in the drift-gate’s grandfatherList and removed as each is generalized-then-renamed. | package.json, LICENSE, cinatra/oas.json, .cinatra-published.json, skills/<slug>/SKILL.md | No |
| Connector (provider) | <provider>-connector/ | "connector" | <provider> is the third-party service slug. Has a workspace-mounted setup page: setup-page.tsx is ≤20 lines, impl named <Slug>ConnectorPageImpl; the route /connectors/<scope>/<slug>-connector/setup must match the directory name. Phase 306 widens scopes — any @<vendor>/<slug>-connector is admissible. | package.json, src/index.ts, src/setup-page.tsx, src/<provider>-setup-impl.tsx, src/deps.ts, vitest.config.ts | Yes |
| Connector (facade) | <domain>-connector/ | "connector" | Provider-neutral abstraction over multiple provider connectors (email-connector, blog-connector, social-media-connector, mcp-client-registry-connector). NO setup-page (the route is per-provider). Carries a contract type + facade implementation + provider registry. | package.json, src/index.ts, src/contract.ts, src/facade.ts, src/registry.ts | Yes |
| Artifact | <content-type>-artifact/ | "artifact" + artifact:{...} | Name MUST match @cinatra-ai/<slug>-artifact (validator enforced). <slug> describes the content type, not the producer or workflow. MUST NOT contain cinatra/oas.json. Matcher skill is conventionally <slug>-matcher, authoring skill <slug>-author. Cross-extension skill refs use the @<scope>/<pkg>:<skill-slug> colon syntax. | package.json, src/index.ts | Yes (re-exports only) |
| Skill (bundle) | <theme>-skills/ (plural dir, singular kind) | "skill" | <theme> groups by consumer: assistant-skills for the chat assistant, blog-skills for asset-blog generation, drupal-skills for the Drupal widget, skill-creator-skills for skill-authoring meta-skills. Individual skill slugs inside skills/<slug>/ are verb-noun (generate-blog-ideas, chat-campaign-creation). | package.json, skills/<slug>/SKILL.md (one directory per skill) | No |
Reserved singleton —
@cinatra-ai/context-selection-agent(formerly@cinatra-ai/context-agent, renamed in v5.7 / Phase 435). The agent owns the ContextSelector HITL renderer and the version-pinning that happens at context-selection time. Consumers reference it verbatim undercinatra.agentDependencies; only declared when the parent itself declarescontextSlots(transitive declaration is forbidden). The pre-rename legacy renderer-id@cinatra-ai/context-agent:context-selectorwas removed early in the v5.7.1 close-out (originally scoped for v5.9) after telemetry proved zero paused runs referenced it — the renderer id is composed at render-time from the migrated template packageName and never persisted.
Vendored skill bundles
Section titled “Vendored skill bundles”For kind:"skill" bundles that mirror an upstream identity not following Cinatra’s <slug>-<kind> convention (today: @anthropics/skills), the package name follows VendoredPackageName. The bundle is declared in the app-root package.json under cinatra.vendoredSkillBundles[] — this is the single source of truth for { packageName, destination, source: { type: "github", owner, repo, sha, url }, license, include[], skills.<slug>.matchWhen, skills.<slug>.level, skills.<slug>.patches[] }.
The lifecycle is dev fetches, prod consumes:
- Development (
CINATRA_RUNTIME_MODE === "development"): thepnpmpostinstall runsscripts/vendor-anthropic-skills.mjs, which downloads the SHA-pinned tarball, extracts only theinclude[]paths intodestination, applies the declarativepatches[]to vendored SKILL.md files (fail-closed on missing anchors), and writes a synthesizedpackage.jsoncarryingcinatra.kind: "skill"andcinatra.vendoredFrom: { owner, repo, sha, url }. The fetcher self-manages a sentinel-bounded section of the repo’s root.gitignoreso vendored content is never committed. - Production (any non-dev
CINATRA_RUNTIME_MODE): the same package is consumed viaextensions_install("@anthropics/skills@<semver>")from Cinatra’s Verdaccio. The CI release-prep job is the only allowed source of the publish tarball — the maintainer downloads the CI artifact, SHA256-verifies, and runspnpm publish <downloaded-tarball>. SemVer is written into the generatedpackage.jsonby CI; the maintainer never edits version locally and never runs the fetcher locally.
Consumer pinning (which agents are allowed to call which vendored skill) is declared per-skill in cinatra.vendoredSkillBundles[].skills.<slug>.matchWhen in the app-root package.json; the loader merges these into the catalog at registration time via a sidecar cinatra-matchers.json written by the fetcher.
Cross-cutting rules
Section titled “Cross-cutting rules”- Directory name == unscoped package name (1:1, kebab-case) for
StandardPackageName; forVendoredPackageName, the directory matches the<slug>part (extensions/anthropics/skills/, notextensions/anthropics/@anthropics-skills/). package.json.nameandmetadata.cinatra.packageName(when present) MUST match — drift breaks publish.cinatra.kindis singular per thePluginTypeenum in@cinatra-ai/registries;"connector"and"artifact"are the extension-only additions.- Skill references across extensions use
@<scope>/<pkg>:<skill-slug>colon syntax, not paths. - The prefix form (
@cinatra-ai/agent-email-test-delivery,@cinatra-ai/skill-assistant) groups by package type rather than domain and is forbidden for new extensions. - The drift-gate at
packages/extensions/src/__tests__/naming-conformance.test.tsenforces the naming, scope, and kind rules on everyextensions/<scope>/<pkg>/package.json. It does NOT yet enforce the per-subtype required-files contract from the per-kind table above (provider connector setup-page, facade connector contract/facade/registry, etc.) — that gate is a follow-up. It carries three explicit lists:transitionalAllowlist(known violators with named fix-phases — must shrink to zero; the bookkeeping test fails when a fix lands but the allowlist entry isn’t removed),grandfatherList(legacy advisory-warn names deferred to v5.8+ generalize-then-rename), andoldNameDenylist(post-rename forbidden names; package-name-surface scope only, not repo-wide string grep).
Legacy holdouts (v5.7 fix-targets)
Section titled “Legacy holdouts (v5.7 fix-targets)”@cinatra-ai/skill-creator— non-conformant directory (no-skillssuffix) and partial vendor of a multi-skill upstream. Replaced by@anthropics/skills(Phase 434.1).@cinatra-ai/auditor-agent— emptycinatra: {}block; gainsapiVersion+kind: "agent"in Phase 432.@cinatra-ai/email-test-delivery-agent— missingcinatra.kind; gainskind: "agent"in Phase 432.@cinatra-ai/context-agent— fixed singleton renamed to@cinatra-ai/context-selection-agent(Phase 435); the legacy renderer-id alias was removed in the v5.7.1 close-out (telemetry-verified dead).@cinatra-ai/claude-connector— misleading name (it’s an MCP-client-registry tracker for clients that connect TO Cinatra, not an outbound Claude integration). Renamed to@cinatra-ai/mcp-client-registry-connector(Phase 434.2).
For the per-kind specs see agent-packaging.md (agents), connector-route-extraction.md (provider-connector setup-page contract), artifacts-architecture.md (artifact validator gate), and context-slots.md (context-selection-agent integration). For per-kind authoring procedure see the .agents/skills/develop-{agent,connector,artifact,skill}/SKILL.md files.
Origin and visibility
Section titled “Origin and visibility”Every installed extension carries an origin JSONB column on its primary row (agent_templates.origin or skill_packages.origin). This is the contract the rest of the lifecycle reads from:
{ "packageName": "@<scope>/<slug>", // npm-style scoped name as installed "version": "1.4.2", // semver of the installed release "scope": "@<scope>", // npm scope; derived from the publishing instance's namespace "destinationId": "<opaque-id> | null",// null = public registry; otherwise an opaque key into extension_destinations "registryUrl": "https://...", // self-contained registry URL used for install/update/migration "visibility": "public" | "private", "importedFrom": { // optional — present when imported from outside the marketplace flow "source": "github" | "zip" | "chat", "url": "https://...", // optional "license": "Apache-2.0", // optional "licenseAcknowledged": true, // optional "updatePolicy": "manual" | "auto", "lastSyncedAt": "2026-05-11T09:00:00Z" // optional }}No secrets live in origin. The destinationId is an opaque pointer into the encrypted extension_destinations table, where per-field-AAD AES-256-GCM ciphertexts hold the read and publish tokens.
Visibility is a server-side contract, not UI styling. Public rows are universally visible. Private rows are visible only when their origin.scope matches the reading instance’s vendor scope. The filter runs at every server read path that touches extension tables — the marketplace screen, MCP extensions_search, the active/archived catalog readers, and any caller of listAgentPackages. UI-only filtering is forbidden.
The instance’s vendor scope comes from readInstanceIdentity() — it is the @<instanceNamespace> chosen during platform setup. An instance with no namespace cannot publish, and cannot see another instance’s private extensions.
Lifecycle states
Section titled “Lifecycle states”Every workspace extension carries a extension_lifecycle_status enum ('active' | 'archived'):
- active — installed and reachable. Agents run, skills match, UI mounts.
- archived — installed-but-suspended. The DB rows that depend on the extension (
agent_runs,agent_versions,agent_template_versions,agent_registry_entries,agent_forks) remain intact, but the extension itself is treated as unavailable. Re-activation is non-destructive.
There is no “draft” or “pending” state for an installed row — the install is either committed or not.
The five lifecycle operations
Section titled “The five lifecycle operations”The extensionRegistry in @cinatra-ai/extensions dispatches every operation to the right type handler. The MCP primitives are admin-gated; the same surface is reachable from the UI.
install
Section titled “install”extensions_install (admin-only). Routes to the handler for the package’s kind:
- Agent handler. Resolves the dependency tree, fetches each package from its registry, extracts to a temp dir, validates the manifest and payload, upserts the
agent_templatesrow, creates a newagent_template_versionssnapshot, registers declared object types, installs declared package skills, and cleans up the temp dir. - Skill handler. Persists a
skill_packagesrow and queues a skill-match recompute.
Both handlers write the origin record on their primary row.
update
Section titled “update”extensions_update (admin-only). Has its own handler entry point, not delegated through install. Replaces the installed version with a newer one, applies anything the new version declares (new object types, new skill bundles), and bumps the persisted version inside origin. Existing run history and HITL state are preserved.
uninstall
Section titled “uninstall”extensions_uninstall (admin-only). The dispatcher checks dependents and “has been used” before choosing a path:
| Condition | Result |
|---|---|
| An active dependent extension still references this one | Refuses with ActiveDependentError, naming the dependent. Uninstall the dependent first. |
| An archived dependent still references this one | Forces archive to preserve dependency closure. |
| No dependents, extension has been used (at least one persisted run exists) | Forces archive so run history stays valid. |
| No dependents, extension unused | Calls the handler’s uninstall for a hard delete. |
The extensionHasBeenUsed(ref) predicate is centralized so future handler kinds cannot drift on the definition of “used.”
archive
Section titled “archive”extensions_archive (admin-only). Soft-uninstall. Flips extension_lifecycle_status to 'archived'. Idempotent.
restore
Section titled “restore”extensions_restore (admin-only). Reverse of archive. Flips status back to 'active'. Idempotent.
force-delete
Section titled “force-delete”extensions_force_delete (admin-only). The audited destructive escape hatch. Pre-cleans the five FK source tables that would otherwise block deletion (agent_runs, agent_versions, agent_template_versions, agent_registry_entries, agent_forks), writes a snapshot to extension_lifecycle_audit before deletion (actor, operation, package, the destroyed row, dangling references, the supplied reason), and only then removes the primary row. Requires confirmDestructive: true. The dispatcher owns this path — type handlers are not asked to implement it; the dispatcher invokes the handler’s uninstall after the audit is written and the FK sources are cleared. Scope: DB + on-disk dir for ONE version. It does NOT touch the Verdaccio registry — the package stays re-installable. For full removal everywhere, use purge.
registry-only — unpublish / delete (Phase 312)
Section titled “registry-only — unpublish / delete (Phase 312)”extensions_registry_unpublish and extensions_registry_delete (admin-only). Pure Verdaccio package-name + version operations — kind-agnostic, with no DB / disk / extensionRegistry / deriveTypeId semantics (kind only matters for the installed-extension paths above). unpublish deprecates/yanks one version (history retained); delete hard-removes one version (deleted: true / notFound: true). These were generalized + renamed from the old agent-scoped agent_registry_unpublish / agent_registry_delete (Phase 312 — registry ops are not agent-specific, so they live under the extension-lifecycle namespace). They mutate the registry only — combine with uninstall/force_delete for installed state, or use purge for the all-in-one.
purge (Phase 312)
Section titled “purge (Phase 312)”The only “gone everywhere” path: Verdaccio (all versions) + DB rows + on-disk dir + WayFlow reload, for any kind (agent / skill / connector). Split deliberately so destruction is never model-invokable (an MCP primitive is reachable by the chat LLM; a model-set confirm flag is theater):
extensions_purgeMCP tool — DRY-RUN ONLY. Admin-gated (ADMIN_REQUIRED_TOOLS). Returns the blast radius (resolved kind, every published version, installed template id, active dependents that BLOCK) plus adigest. It never mutates; the handler can only callplanExtensionPurge.cinatra extensions purge <pkg> --confirm <pkg> --digest <d> --yes— the destructive path. A thin loopbackfetchto admin+loopback+dev-modePOST /api/extensions/purge(same defense pattern as/api/skills/reset-repo); the human-origin path, not in the LLM tool surface.
Pipeline (packages/extensions/src/purge.ts, fail-closed, ordered): re-plan → production-host refusal (CINATRA_DB_PROD_HOSTS) → active-dependents hard block (3 edges: agentDependencies, compiled-plan childAgent.packageName, on-disk oas.json) → mandatory digest match (the dry-run handshake) → quarantine to data/extension-quarantine/<pkg>-<ts>/ (every version tarball + packument + agent_templates row snapshot; aborts before Verdaccio if any tarball can’t be snapshotted) → audit row (operation: "purge") before Verdaccio → unpublish all versions (per-version; on any partial failure STOPS before DB/disk, idempotently re-runnable; notFound = success) → TOCTOU re-plan → kind-branched DB/disk: force_delete for agent/skill, skip for connector (workspace-compiled — Verdaccio + audit + quarantine only; a connector with an unexpected DB row is refused, not half-purged). Kind is resolved authoritatively from the registry packument’s package.json cinatra.kind (getRegistryPackageKind) — never guessed; an unresolvable/unsupported kind refuses before any destruction. Quarantine is the recovery hedge: re-publish the tarballs until the operator deletes the quarantine dir, after which removal is irreversible.
The marketplace
Section titled “The marketplace”The marketplace lives at Administration → Marketplace (/administration/marketplace). It reads from the configured Verdaccio registry, applies the server-side visibility filter, and renders a CTA per row.
CTAs render against the workspace’s current extension table — Install Now, Update Now, Installed, Restore — depending on whether the package is absent, present at an older version, present at the current version, or archived.
If the instance has no remote registry connected, the marketplace’s empty-state CTA points to the registry-configuration tab at /administration/environment?tab=registries.
Registry routing
Section titled “Registry routing”Cinatra speaks to two registry kinds:
- Private — the destination the publishing instance writes its own packages to. Typically a self-hosted Verdaccio. Credentials live in
extension_destinationskeyed bydestinationId. Tokens are stored encrypted with per-field AAD bindings (destination.<id>.publish-token,destination.<id>.read-token). - Public — the multi-tenant shared registry every Cinatra instance can read from once connected. Connection is set up under Administration → Environment (
/administration/environment?tab=registries); the instance submits a request, receives a single-use npm token after admin approval, and the token is stored in Nango — never in Cinatra’s own database.
The runtime resolves which registry handles a given operation through DeploymentRegistryConfig:
type DeploymentRegistryConfig = { publicRegistryUrl: string; publicReadToken: string; publicPublishToken: string | null; privateRegistryUrl: string | null; privateReadToken: string | null; privatePublishToken: string | null; privateDestinationConfigured: boolean; privateDestinationId: string | null; routingMode: "scope-based" | "shared-acl";};routingMode selects the topology adapter:
scope-based— emits--@<scope>:registry=<url>so each scope routes to its own registry. Used when the instance’s private destination uses a distinct npm scope.shared-acl— emits plain--registry=<url>so a single registry vhost serves multiple instances with ACL-based isolation. Used when the private destination shares the vhost with the public registry.
routingMode is required; an unset value is treated as a configuration error rather than a fallback.
resolvePublishDestination("private" | "public") and resolveInstallEnvironment(extensionId) are the canonical entry points. Direct callers of the lower-level Verdaccio config helpers are blocked by a regression test that scans for the old call sites.
Publishing
Section titled “Publishing”Admin-only. The flow:
- Author the extension as a versioned package (TypeScript workspace package or an OAS Flow agent directory under
agents/). - Bump the package version.
- Pick a publish destination —
privateorpublic. Private is the default. - Run the publish action. The platform packs the tarball, resolves credentials from
extension_destinationsbydestinationId, and pushes through the topology adapter for the chosenroutingMode. - The destination’s
extension_lifecycle_statusbecomesactiveon the publisher’s row; the package is now installable elsewhere through the normal marketplace flow.
For OAS Flow agents specifically, agent_source_publish runs the same deterministic review gate that agent_source_review surfaces (runDeterministicReview(...)) before pushing — a publishable agent must pass the gate.
Promotion (private → public)
Section titled “Promotion (private → public)”promoteExtensionToPublicAction(extensionId) is the admin-gated path. It:
- Requires
requireAdminSession(). - Refuses if the extension is already public (
origin.visibility === "public"). - Republishes the package to the public destination using its current version.
- Updates
origin.visibilityto"public". - Fire-and-forget writes a
promoteoperation row toaudit_events(withresourceType: "extension_registry"). It does not write toextension_lifecycle_audit— that table is reserved for destructive operations.
Promotion is one-way in v1. Public-to-private demotion is intentionally blocked; the demotion button is rendered disabled-but-visible with a locked tooltip ("Demotion not supported in v1 — contact ops"). The disabled-visible pattern is preferred over hidden so the limit is discoverable. Do not stub a half-built demotion flow.
Where to go next
Section titled “Where to go next”- Build a new extension as a TypeScript package: Building packages
- Build a new agent (the OAS Flow side of an agent extension): Developing agents
- The objects layer extensions write into: Objects layer
- How marketplace-installed agents talk to the rest of the platform: Open standards in Cinatra