@gea-ai/extensions-sdk
v0.1.260923-alpha.2
Published
Public frontend SDK for GEA extensions.
Readme
@gea-ai/extensions-sdk
Public frontend SDK for GEA extensions.
Start with the public GEA extension developer docs and extension quick start.
Install
npm install @gea-ai/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>;
}SQLite Database Authoring
database.sqlite declares a migration-backed Table without repeating its
relation schema in gea.extension.ts:
import { database, defineExtension, wiki } from "@gea-ai/extensions-sdk";
export default defineExtension({
frontend: { entry: "src/index.tsx" },
name: "Customer Research",
resources: {
workspace: wiki({
children: {
appData: database.sqlite({
migrations: "drizzle",
scope: "workspace",
title: "Application Data",
visibility: "backend",
}),
},
title: "Customer Research",
visibility: "workspace",
}),
},
slug: "customer-research",
});The migration directory may contain ordered .sql files and Drizzle's
meta/ directory. gea ext validate, gea ext package, and gea ext sync
replace the directory with a checksummed migration inventory and include every
SQL file in the signed extension component.
SQLite databases are additive within declaration version gea.extension/v1.
Older CLIs still fail closed because their v1 resource parser rejects the
unknown engine field instead of silently packaging it as a native Table.
The host provisions and upgrades this resource before extension activation. A
visibility: "backend" database stays out of Context navigation; use
visibility: "context" when workspace members and Agents should also browse or
query it through the ordinary Context Table APIs. In both cases, extension code
receives a typed Drizzle SQLite Proxy handle, never a SQLite path, storage
locator, or credential:
import {
defineBackendActions,
type ExtensionBackendSdk,
} from "@gea-ai/extensions-sdk/backend";
import * as schema from "./schema";
import { actions } from "../contract";
type Sdk = ExtensionBackendSdk<never, { appData: typeof schema }>;
export const backend = defineBackendActions<typeof actions, Sdk>(actions, {
createIdea: async ({ input, sdk }) =>
await sdk.databases.appData.transaction(async (tx) => {
const idea = { id: crypto.randomUUID(), title: input.title };
await tx.insert(schema.ideas).values(idea);
return idea;
}),
});Normal Drizzle selects, writes, batches, and interactive transactions execute
against the host-owned database. The host revalidates the installed extension,
workspace, declared resource key, engine readiness, and caller on every proxy
operation. The same proxy protocol is used by gea ext dev.
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.
The frontend app.gea.chats surface starts and continues both legacy and
SDK-first Agent chats. When a legacy Agent pauses for a request Tool result,
submit only the persisted assistant message ID, exact runtime Tool call ID, and
JSON output. The host validates the pending Tool call, persists its result, and
queues the continuation Run in one transaction:
await app.gea.chats.submitToolOutput({
chatId,
messageId: pendingMessage.id,
output: { tone: "concise" },
tool: "request",
toolCallId: pendingRequest.toolCallId,
});When an SDK-first Agent pauses before a Tool call, an extension may instead respond with only the persisted assistant message ID and the approval decisions:
await app.gea.chats.respondToToolApprovals({
chatId,
messageId: pendingMessage.id,
responses: [{ id: pendingApproval.id, approved: true }],
});Runtime selection stays server-side: submitToolOutput rejects SDK-first Agent
chats, while respondToToolApprovals rejects legacy Agent chats.
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
hosted backend actions in an operating-system sandbox. The bundle is read-only,
temporary storage and the private host RPC socket are the only writable/runtime
paths, ambient environment variables are cleared, and network access is denied.
fetch is unavailable inside the backend; external calls must use explicit SDK
methods such as connectors and webSearch. Backend code also cannot start child
processes or worker threads. Remote backend files are checked against manifest
sha256 and sizeBytes metadata before execution, and the host dispatches only
declaration-listed backend actions. The public backend SDK surface is
intentionally narrow:
sdk.state.records.*for extension-private, authenticated-user-owned workflow state. Reads and mutations cannot cross principals in the same workspace.sdk.connectors.execute(...)for host-managed connector calls.sdk.webSearch(...)for provider-neutral public web search with host-owned credentials and usage metering.sdk.artifacts.path(...),writeText(...), andwriteJson(...)for extension artifacts.sdk.resources.records.create/createWithArtifact/get/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. Generated handles may receive ordered uploaded-file metadata inattachments.sdk.chats.list(...)for listing the authenticated user's ordinary top-level chats in the current workspace, plussdk.chats.messages.list(...)for reading a selected chat.sdk.databases.<handle>for typed Drizzle SQLite Proxy queries, batches, and transactions against SQLite resources declared by this extension. No path, provider locator, credential, or arbitrary resource lookup is exposed.
Legacy host helpers outside this surface are not part of the 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.
resources.records.get(...) reads one row by its stable table record ID. The
host enforces the current user's resource permissions and rejects a record that
does not belong to the declared extension table.
resources.records.createWithArtifact(...) imports one file produced by the
current extension's declared agent into a declared Context Table file field:
const asset = await sdk.resources.records.createWithArtifact({
agentRunId,
artifactPath: "assets/logo.svg",
chatId,
data: {
assetKey: `${generationId}:primary-logo`,
generationId,
},
fieldKey: "file",
resourceKey: "brandAssets",
});The host anchors the relative path under the current extension's chat output
directory, verifies the chat and declared-agent run, and creates the attachment
value itself. Persist asset.id, the declared resource key, and the field key;
do not persist a filesystem path, object-store key, or signed URL. Frontend
table handles expose records.get(recordId) for resolving that stable reference
through the same declared table boundary.
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.
Public web search is a first-class SDK capability rather than a connector:
const results = await sdk.webSearch({
maxResults: 5,
query: "site:example.com architecture intelligence",
});The host chooses the provider and returns { title, url, content } results.
Extensions do not declare a connector requirement or manage a search-provider
credential for this capability.
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 Account And Usage
Hosted Extension frontends can read the current public user profile, query that user's token usage in the active workspace, let authorized organization administrators query organization-wide usage, and invoke password-account actions without importing the host auth or ORPC clients:
const user = await app.account.getCurrentUser();
const usage = await app.usage.getSummary({
from: "2026-08-01T00:00:00.000Z",
grain: "day",
to: "2026-09-01T00:00:00.000Z",
});
const organizationUsage = await app.usage.getOrganizationSummary({
from: "2026-08-01T00:00:00.000Z",
grain: "day",
to: "2026-09-01T00:00:00.000Z",
workspaceId: "workspace-id",
});
await app.account.changePassword({
currentPassword,
newPassword,
revokeOtherSessions: true,
});
await app.account.signOut();getCurrentUser() returns only id, email, name, and image. Both usage
methods accept time, grain, model, request-type, Agent, and Project filters. For
getSummary, the host injects the authenticated user and active workspace. For
getOrganizationSummary, an authorized administrator may additionally filter
by workspace, while the host keeps organization identity from the authenticated
session. The existing usage service requires the view_enterprise_usage
permission and returns the same organization totals plus user and workspace
breakdowns used by the administration UI. Neither method accepts user or
organization identity. Password values are passed without
trimming. These account operations require a hosted browser session; the
loopback local-development helper does not own a browser session to read the
profile, change a password, or sign out. Token usage remains available in local
development under the authenticated CLI user and ordinary organization usage
authorization.
Host File Uploads
Extension frontends prepare a tenant-scoped upload through the host, then send
the bytes directly to object storage. File bytes do not pass through
postMessage:
const prepared = await app.files.prepareUpload({
contentType: file.type || "application/octet-stream",
name: file.name,
sizeBytes: file.size,
});
const response = await fetch(prepared.upload.url, {
body: file,
credentials: "omit",
headers: prepared.upload.headers,
method: prepared.upload.method,
});
if (!response.ok) {
throw new Error(`Upload failed with status ${response.status}.`);
}One file is limited to 50 MiB, and its exact name is limited to 240 UTF-8 bytes.
A declared-agent run accepts at most 20 files and 100 MiB in total. The
extension keeps the original name, contentType, and sizeBytes together
with prepared.sourceUrl and
prepared.attachmentToken in its own private state when the source must survive
a reload. The attachment token expires after 24 hours, so a later run must ask
the user to upload the source again.
A backend action passes those values to a declared package agent without constructing Lexical content or a workspace path:
await sdk.agents.brandDnaGenerator.run({
attachments: sourceFiles.map((file) => ({
attachmentToken: file.attachmentToken,
contentType: file.contentType,
name: file.name,
sizeBytes: file.sizeBytes,
sourceUrl: file.sourceUrl,
})),
chatId,
message: "Analyze the uploaded official sources.",
});The host assigns each attachment an identifier, derives
/workspace/uploads/<extensionSlug>/<attachmentId>/<filename>, and uses the
existing agent upload pipeline to materialize each source before the run.
Before materialization, the runtime verifies that the signed capability matches
the current user, workspace, installed extension, source URL, and exact
metadata. The token is not included in model input or rendered chat messages.
Host Skills, Themes, And Chat Streaming
useExtensionApp() also exposes small, permission-checked host APIs for
creating a user skill, installing an organization custom theme, and reading the
authenticated user's workspace chats:
const app = useExtensionApp();
await app.skills.create({
// Use a stable operation identifier when a retry must return the same skill.
idempotencyKey: generationId,
files: [{ path: "SKILL.md", content: generatedSkillMarkdown }],
});
await app.appearance.installCustomTheme({
...generatedTheme,
apply: true,
logo: {
fieldKey: "file",
recordId: logoAssetRecordId,
resourceKey: "brandAssets",
},
});
const chats = await app.chats.list({ limit: 30 });
const selectedChat = chats.items[0];
const summary = await app.chats.get({ chatId: selectedChat.id });
const page = await app.chats.messages.list({
chatId: selectedChat.id,
limit: 50,
});app.chats.list(...) returns only the current user's non-project, top-level,
non-deleted chats in the current workspace. Continue pagination with its opaque
nextCursor; do not inspect or persist assumptions about the cursor format.
The returned item contains the chat title, update and pin timestamps, latest
Agent identity, and isRunning. Pass the selected id to get,
messages.list, and createStreamAccess.
An organization administrator with the existing
export_organization_chat_traces permission can inspect user-owned chats
across active workspaces without receiving per-chat access:
const organizationChats = await app.chats.admin.list({
limit: 30,
workspaceId,
// userId is also optional.
});
const transcript = await app.chats.admin.messages.list({
chatId: organizationChats.items[0].id,
limit: 100,
});The administrator list includes owner and workspace identity. Its optional cursor is returned by the host and must be passed back unchanged. Message pages use the same raw chronological trace representation and audit behavior as the organization chat export API. Both methods recheck the signed-in member and the current organization permission. Owner has this permission by default; System Admin and custom roles require an explicit grant. No deployment-level system configuration or target Workspace membership is required.
Authorized administrators can use the existing role-management surface from an Extension without importing ORPC directly:
const state = await app.permissions.listRoleManagementState();
await app.permissions.roles.permissions.update({
roleId: systemAdminRoleId,
permissions: [...currentPermissions, "export_organization_chat_traces"],
});
await app.permissions.setMemberManualRoles({
userId,
roleIds: [analystRoleId],
});app.permissions mirrors the existing permission-checked Role, Member Role,
and Workspace Admin procedures. It does not add privileges: every read and
mutation is authorized and audited by the same server procedures used by the
administration UI.
app.skills.create({ idempotencyKey }) accepts an optional stable operation
key. The host combines it with the authenticated tenant, principal, installed
extension, and canonical source files. Retrying the same operation after a
reload or lost response returns the same installed skill; changing the files
produces a distinct operation.
Theme logo inputs are stable references to exact rows in declared Context Tables. The host enforces the current user's table and record-scope view permissions, validates the referenced image bytes, constrains the logo to a 2048-pixel maximum dimension, and copies it into organization-owned appearance storage. Extracted fonts remain extension content for brand guidance and generated skills; custom themes do not install font files into the host application.
These request-response calls use the SDK bridge. Live tokens do not pass
through postMessage. Ask the host for a short-lived stream capability and
give it directly to the AI SDK transport:
import { useChat } from "@ai-sdk/react";
import { EXTENSION_CHAT_STREAM_PROTOCOL } from "@gea-ai/extensions-sdk";
import { DefaultChatTransport } from "ai";
import { useMemo } from "react";
import { useExtensionApp } from "./extension-runtime";
function Transcript({ chatId }: { chatId: string }) {
const app = useExtensionApp();
const transport = useMemo(
() =>
new DefaultChatTransport({
api: "/api/chat",
prepareReconnectToStreamRequest: async ({ id }) => {
const access = await app.chats.createStreamAccess({ chatId: id });
if (access.protocol !== EXTENSION_CHAT_STREAM_PROTOCOL) {
throw new Error(`Unsupported stream protocol: ${access.protocol}`);
}
return {
api: access.url,
credentials: "omit",
headers: { Authorization: `Bearer ${access.token}` },
};
},
}),
[app],
);
const chat = useChat({ id: chatId, resume: false, transport });
return <button onClick={() => void chat.resumeStream()}>Open stream</button>;
}The capability is bound to the current user, organization, workspace,
installed extension, chat, and exact latest Agent identity. The chat may come
from a declared nested Agent or a top-level Agent task started through the
user-scoped SDK. It expires after five minutes. The direct request omits cookies
and uses the AI SDK UI-message stream protocol, so extension code keeps the
normal useChat experience while the host retains authorization and identity
ownership.
Extensions should render those messages with the public protocol renderer instead of rebuilding individual AI SDK parts:
npm install @gea-ai/chat-uiimport {
type AgentChatMessage,
type GeaAddToolOutputInput,
GeaChatUIProvider,
GeaMessages,
} from "@gea-ai/chat-ui";
import "@gea-ai/chat-ui/styles.css";
const chat = useChat<AgentChatMessage>({
id: chatId,
resume: false,
transport,
});
async function submitToolOutput(input: GeaAddToolOutputInput) {
if (!input.messageId) {
throw new Error("A persisted assistant message ID is required.");
}
await app.gea.chats.submitToolOutput({
chatId,
messageId: input.messageId,
output: input.output,
tool: "request",
toolCallId: input.toolCallId,
});
const { messageId: _messageId, ...localToolOutput } = input;
await chat.addToolOutput(localToolOutput);
await chat.resumeStream();
}
return (
<GeaChatUIProvider
actions={{ addToolOutput: submitToolOutput }}
theme={{ accent: "#d85f5f", radius: "16px" }}
>
<GeaMessages messages={chat.messages} status={chat.status} />
</GeaChatUIProvider>
);The package handles standard message parts and yaml-render, but it does not
own Chat transport or authorization. Extensions may override semantic theme
variables, stable data-slot selectors, and visual component slots without
changing message or request submission semantics.
In the useChat configuration above, AI SDK chat.addToolOutput only updates
the local message. Configuring AI SDK sendAutomaticallyWhen can make that
method invoke its own Chat transport, but it still does not call GEA's
canonical Tool-output continuation API. Call the host-owned
app.gea.chats.submitToolOutput first, then update local state and resume the
newly created stream as shown above. If this action is omitted, display-only
YAML continues to render and interactive request controls are not mounted.
Interactive YAML is read directly from tool.input.yaml without a Markdown
fence and rendered at that request tool part's position. Submission targets the
same part's runtime toolCallId and sends the complete current form state.
Legacy request-key parts and interactive YAML copied into assistant text are
not mounted. During input-streaming, partial YAML is display-only: the
renderer shows a loading placeholder or a locked last-valid preview and never
calls the host. input-available triggers final strict validation and unlocks
the form; the existing persisted messageId plus toolCallId submission path
is unchanged.
Host Context, Theme, And Locale
Packaged and local-dev hosts install the parent message bridge before activating
the extension iframe, then send a host-context message on load and when the
host document theme or locale attributes change. Calls made during the
extension's first mount therefore reach the host even when the extension
declares no resources. 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. Hosted web context also includes navigation.href, the absolute
host route URL, and navigation.search, the current route search values. Use
app.navigation.updateSearch(...) to change extension-owned route state and
app.navigation.openExternal({ url }) to open an HTTP(S) URL in a host-owned
browser tab. Sandboxed frontend code must not read window.parent.location or
use popup permissions. 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 sync before the first local session and after changing the
declaration, agents, or skills. Sync uploads an owner-private runtime draft and
provisions or reuses its declared workspace resources; it does not build or
upload frontend/backend code. Use gea ext package for local package
inspection, then gea ext deploy to upload the complete extension package as
an 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. Hosted frontends run with
sandbox="allow-scripts allow-downloads allow-forms" and the
matching CSP sandbox directive so React and other client-side form handlers can
receive submit events. The CSP keeps form-action 'none', so native form
navigation and submission remain blocked. Declared nested HTTPS frames retain a
network-scheme ancestor. Host control calls use the postMessage bridge; live
chat bytes use the scoped, cookie-free direct AI SDK stream described above.
Set Vite base to "./" and import Extension-owned images, fonts, and media
from frontend source whenever possible. A file copied beside the generated
frontend entrypoint may instead be referenced as ./file-name; never use a
root-absolute path such as /logo.png, because that resolves against the GEA
tenant site rather than the installed package.
Resource Handles
Extensions declare durable resources in defineExtension. Accepting an
owner-private runtime draft provisions or reuses real GEA resources and binds
them to the extension scope. gea ext dev verifies that synchronized draft and
its bindings only when the declaration uses package-internal agents, skills,
connector requirements, or declared resources. A stateless frontend declaration
skips Extension resource and owner authorization; its hosted API calls continue
to authenticate the current workspace member and authorize each target resource
normally. The command then prints a capability-bearing localhost URL that can be
opened directly without registration inside GEA. That bootstrap URL transfers
an in-memory loopback capability and replaces itself with the Vite frontend as
the top-level page. The SDK reads lifecycle.stage = "ready" plus any resolved
resource handles from that bootstrap and exposes them as typed app.resources
when the frontend uses useExtensionApp() from a runtime created with the
declaration.
Declared resource handles are an authoring and lifecycle convenience, not an
additional permission boundary. The curated app.gea.context methods may also
operate on existing Context resources. Every call runs as the current signed-in
user and keeps the target Context route's ordinary API-scope, organization,
workspace, resource, and row-scope authorization; an Extension never gains
permissions that the user does not already have.
Use the engine-neutral table data APIs for existing or uploaded tables. Always describe a table when the engine or relation is not already known, then browse the default relation or pass an advertised relation ID:
const descriptor = await app.gea.context.tableData.describe({ tableId });
const page = await app.gea.context.tableData.browse({
tableId,
limit: 50,
offset: 0,
});tableData.browse works for native and managed table engines. The legacy
context.records methods remain the editable-record API for native Context
Tables and must not be used to read an uploaded DuckDB or SQLite table.
Warning: Syncing a declaration that removes resources may deactivate their bindings immediately, including bindings used by the currently published extension. The underlying workspace resources and their data are not deleted. Review resource removals before running
gea ext sync.
Use those handles for extension-owned durable data:
await app.resources.notes.records.create({
note: "Follow up with the design team",
});A complete research-queue extension can declare a workspace-visible wiki, store submitted URLs in a declared context table, place an aggregate summary document next to that table, and start a hosted Agent task with only the resolved table and document locations in the prompt.
Third-party 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.
