@gea-ai/extensions-sdk
v0.1.260715-alpha.0
Published
Public frontend SDK for GEA extensions.
Readme
@gea-ai/extensions-sdk
Public frontend SDK for GEA extensions.
Install
npm install @gea-ai/extensions-sdkWhen testing unpublished SDK changes from this repository, build the SDK and link the workspace package from the standalone extension repo:
pnpm --filter @gea-ai/extensions-sdk build
npm install @gea-ai/extensions-sdk@file:/path/to/gea/packages/extensions-sdkMinimal Shape
import {
document,
defineExtension,
field,
table,
wiki,
} from "@gea-ai/extensions-sdk";
export const extensionDeclaration = defineExtension({
frontend: {
entry: "src/index.tsx",
},
name: "Customer Research",
resources: {
workspace: wiki({
children: {
notes: table({
fields: {
note: field.text({
indexed: true,
required: true,
title: "Note",
}),
},
title: "Notes",
}),
summary: document({
initialContent: {
format: "markdown",
markdown: "# Customer Research Summary\n",
},
title: "Summary",
}),
},
title: "Customer Research",
visibility: "workspace",
}),
},
slug: "customer-research",
});// src/extension-runtime.ts
import { createExtensionRuntime } from "@gea-ai/extensions-sdk/react";
import extensionDeclaration from "../gea.extension";
export const { mount, useExtensionApp, useLocale, useTranslations } =
createExtensionRuntime({
declaration: extensionDeclaration,
});// src/index.tsx
import { useExtensionApp } from "./extension-runtime";
export default function CustomerResearch() {
const app = useExtensionApp();
async function submitNote(note: string) {
await app.resources.notes.records.create({
note,
});
}
return <button onClick={() => void submitNote("Follow up")}>Add</button>;
}Frontend code calls host APIs through SDK helpers. The SDK uses the GEA host
postMessage bridge by default, so extension authors do not need to write
message plumbing or know the host ORPC transport.
Actions And Backend Packages
Curated backend-capable extension packages can share one typed action contract between frontend and backend code. The frontend calls the host transport, while the backend exports handlers that the host loads through ORPC after install, membership, and runtime resource checks.
// src/contract.ts
import { defineActions } from "@gea-ai/extensions-sdk/actions";
import z from "zod";
export const actions = defineActions({
ping: {
inputSchema: z.object({ value: z.string() }),
outputSchema: z.object({ echoed: z.string() }),
},
});// src/backend/index.ts
import { defineBackendActions } from "@gea-ai/extensions-sdk/backend";
import { actions } from "../contract";
export const backend = defineBackendActions(actions, {
ping: async ({ input }) => ({ echoed: input.value }),
});// src/frontend/index.tsx
import {
createExtensionActionClient,
type ExtensionActionTransport,
} from "@gea-ai/extensions-sdk/actions";
import { actions } from "../contract";
declare const callExtensionAction: ExtensionActionTransport;
const client = createExtensionActionClient({
actions,
extensionSlug: "example-extension",
transport: callExtensionAction,
});
await client.call("ping", { value: "hello" });Private organization extension packages and curated marketplace packages run
backend actions through the trusted in-process executor. This is the current
demo/development trust model; package authors and organization operators are
responsible for the uploaded code. Remote backend files are checked against
manifest sha256 and sizeBytes metadata before import, and the host dispatches
only declaration-listed backend actions. The public backend SDK surface is
intentionally narrow:
sdk.state.records.*for extension-private workflow state.sdk.connectors.execute(...)for host-managed connector calls.sdk.artifacts.path(...),writeText(...), andwriteJson(...)for extension artifacts.sdk.resources.records.create/update(...)andsdk.resources.records.findByIndexedText(...)for idempotent lookup on a declared indexed text field, plussdk.resources.documents.update(...)for declared GEA resources.sdk.agents.call(slug, input)and generated agent handles for declared package-internal agents.sdk.chats.messages.list(...)for reading the scoped agent chat produced by an extension run.
Any broader host helpers are reserved for internal built-in workflows and are not a third-party SDK contract.
Nested-agent calls may include runtimeReminders: string[]. The host passes
these reminders unchanged into the agent runtime, where they are assembled as
system reminders outside the structured user input. Extension backends should
derive reminder content from server-resolved action context such as
context.extension.appResourceId and context.extension.resources; frontend
input must not be trusted to identify extension-owned resources.
resources.records.findByIndexedText(...) accepts only a field declared by the
current extension table as type: "text" and indexed: true. The lookup stays
inside the current organization, workspace, declared table, and optional scope
resource. It returns null for no match and rejects duplicate matches instead
of selecting one silently.
state.records.update({ data }) merges the provided object into the existing
record data. Use this for partial progress updates; model full replacement as a
future explicit API if an extension needs it.
Backend actions execute external systems through host-managed connectors:
const result = await sdk.connectors.execute({
connector: "bocha-search",
params: {
method: "GET",
operation: "request",
path: "/search",
query: { q: "consumer trend" },
},
});Connector credentials are resolved by normal GEA connector credential scope
rules. Extensions should declare every connector dependency in
connectorRequirements so install and setup flows can surface missing
connectors or credentials before backend actions run. Provider-specific request
details should live in API, MCP, or native connector implementations; backend
actions should call those connectors instead of embedding provider credentials or
direct network integration code.
A builtin_connector requirement activates the host-owned connector from the
published extension package. It does not add a connector export or create a
standalone connector app resource. The host still requires the connector to be
enabled in the deployment environment and resolves any system or user
credentials at execution time.
export const extensionDeclaration = defineExtension({
// ...
connectorRequirements: [
{
slug: "bocha-search",
source: { type: "app_resource", slug: "bocha-search" },
},
{
slug: "wechat-official-account",
source: {
type: "builtin_connector",
slug: "wechat-official-account",
},
},
],
});Installed extension summaries expose each declared connector as connected,
missing_connector, or missing_credentials. Backend action dispatch preflights
the same setup state before creating the connector runtime and fails with
connector_setup_required when setup is incomplete.
Host Context, Theme, And Locale
The local-dev host sends a host-context message to the extension iframe on
load and when the host document theme or locale attributes change. React
extensions should normally use createExtensionRuntime from
@gea-ai/extensions-sdk/react; it wires host-context sync into
ExtensionRuntimeProvider and exposes:
useExtensionApp()for the typed host SDK client and resolved resource handles.useLocale()for the current resolved locale.useTranslations(namespace)for extension-owned local dictionaries.mount(Component)for rendering the default no-props React component into#root.
The context includes locale, text direction, resolved color scheme, resolved
theme, and CSS custom properties such as --background, --foreground, and
--border. The CLI scaffold wires this into src/extension-runtime.ts,
includes a small local i18n dictionary, and configures Tailwind CSS so utilities like
bg-background, text-foreground, and border-border follow the host theme.
Extension messages remain extension-owned; the host only supplies the active
locale.
Upload And Publish
Use gea ext package for local package inspection, then gea ext deploy to
upload the package to the hosted server as an extension app-resource draft.
Deployment does not publish. Review and publish that app resource from the GEA
web UI, using the same lifecycle as other app resources.
The generated artifact is a canonical gea.resource_package.v1 directory. Its
root manifest.json exports the extension, while private agents and skills stay
under agents/<slug> and skills/<slug> as non-exported components. The
extension frontend and backend stay under extensions/<slug>. Deployment
uploads this complete generated directory; source repositories, tests, and
node_modules are not part of the runtime package. The CLI bundles backend
runtime dependencies into the generated backend entrypoint, so hosted execution
does not depend on packages installed in the GEA server image.
The app-resource publish version is the canonical hosted version.
gea.extension.ts version metadata is stored in the normalized manifest as package
metadata. Published extension frontend assets are served by the host through a
sandboxed iframe asset route, and SDK calls still go through the same
postMessage bridge.
Resource Handles
Extensions declare durable resources in defineExtension. Draft uploads store
the normalized declaration only. Publishing and local-dev registration provision
real GEA resources and bind them to the extension scope. The iframe URL carries
only extension identity. The host sends lifecycle.stage = "ready" plus
resolved resource handles through the host-context postMessage, and the React
runtime exposes them as typed app.resources when the frontend uses
useExtensionApp() from a runtime created with the declaration.
Use those handles for extension-owned durable data:
await app.resources.notes.records.create({
note: "Follow up with the design team",
});The full demo lives at examples/research-queue in the GEA repo. It declares a
workspace-visible extension wiki, stores submitted URLs in a declared context
table inside that wiki, declares one aggregate summary document next to the
table, and starts a hosted agent task with only the resolved table and document
locations in the prompt.
Third-party iframe extensions should not persist private UI state through
production extension_record. Use browser-local or dev-runtime-local state for
UI-only state. Durable user-visible data should be modeled as declared GEA
resources so users and admins can inspect, recover, export, permission, and
delete it through normal resource flows.
Current table APIs are not yet optimized for large database workloads. Treat 10k to 100k row scenarios, server-side filtering/sorting, relation joins, and benchmarks as a separate table performance track.
The root SDK avoids private monorepo imports so extension projects can
type-check and build outside the GEA monorepo. Public context resource result
types are exported from @gea-ai/extensions-sdk/context. The portable schemas
and TypeScript types are owned by @gea-ai/contract and re-exported by the SDK.
Local-dev extensions are trusted developer code. They do not declare manifest permissions yet. The host bridge exposes only curated SDK methods, but it does not enforce extension-specific grants. SDK calls run as the current signed-in user and remain constrained by normal hosted ORPC user, workspace, and resource permissions.
