SDK Reference
Complete API reference for the @scrydon/sdk-authoring/extensions/authoring SDK
Installation
bun add -d @scrydon/sdk-authoring zodImport Paths
The SDK uses explicit subpath exports — there is no root barrel import.
// Definition helpers (all define* functions)
import {
defineExtension, defineToolkit, defineTool, defineBlock, defineTriggerBlock,
defineCapabilityLLM, defineCapabilitySTT, defineCapabilityTTS,
defineCapabilityEmbedding, defineCapabilityImage, defineCapabilityVideo,
defineCapabilityOCR, defineCapabilityModeration, defineCapabilityWebSearch,
defineCapabilityWebhook, defineCapabilityDiscovery,
} from "@scrydon/sdk-authoring/extensions/authoring/define";
// Runtime types (used in execute() signatures)
import type {
PureContext, PureContextAuth, ToolResponse, ExecutorResult,
RealtimeSession, RealtimeMessage,
} from "@scrydon/sdk-authoring/extensions/authoring/context";
// Credential narrowing helpers (throw CredentialKindError on kind mismatch)
import {
requireOAuthToken, requireOAuth,
requireApiKey, requireBasicAuth, requireBotToken,
} from "@scrydon/sdk-authoring/extensions/authoring/context";
// Manifest types (for advanced use cases)
import type { ManifestSchema } from "@scrydon/sdk-authoring/extensions/authoring/manifest";defineExtension(config)
Top-level container for an extension. The entry point's default export must be a defineExtension() result.
interface VendorInput {
/** Unique vendor ID — lowercase alphanumeric + hyphens, starts with letter */
id: string;
/** Display name */
name: string;
/** Semantic version (e.g. "1.0.0") */
version: string;
/** Short description */
description?: string;
/** Hex brand color (e.g. "#FF6B35") */
color?: string;
/** SVG icon as a string */
icon: string;
/** Vendor website URL */
website?: string;
/** Documentation URL */
docsUrl?: string;
/** Categories (e.g. ["tools"], ["llm"], ["database"]) */
categories?: string[];
/** Whether this vendor is enabled by default for new orgs */
defaultEnabled?: boolean;
/** Whether this vendor supports system-mode (client_credentials) */
supportsSystemMode?: boolean;
/** Whether this vendor requires external network connectivity */
connectivity?: "cloud" | "local" | "hybrid";
/** Minimum requested isolation. Platform and organization floors may only strengthen it. */
isolation?: "memory" | "microvm";
/** Public HTTPS hosts that tool code may contact — exact, or `*.` wildcards. */
toolEgress?: {
httpsHosts: string[];
};
/** Authentication configuration */
auth: AuthConfig;
/** Admin-configurable fields (e.g. base URL for self-hosted services) */
configFields?: ConfigFieldInput[];
/** Extension-level secrets (resolved per-invocation via Dapr Secret Store) */
secrets?: SecretInput[];
/** What this extension provides — at least one toolkit, model family or content kind */
provides: {
toolkits?: ToolkitDefinition[];
models?: ModelFamilyDefinition[];
};
/** Optional protocol definitions */
protocols?: ProtocolDefinition[];
}Tool Isolation and Egress
isolation is a minimum request, not a backend selector. Exact reviewed Scrydon tools may use the
approved memory backend. Every other shipped or organization-installed tool has a platform
microvm floor, even if its manifest requests memory or omits the field. An organization policy
may strengthen an exact reviewed tool to microvm; neither setting can weaken the platform floor.
If the attested Runtime Plane backend is unavailable, the tool fails closed.
Declare every public HTTPS hostname a tool may contact:
export default defineExtension({
// ...
toolEgress: {
httpsHosts: ["api.acme.example", "uploads.acme.example"],
},
});Hostnames must be lowercase DNS names without a scheme, path, or port.
When your API host is per-tenant, prefix an entry with *.:
export default defineExtension({
// ...
toolEgress: {
// Covers acme.my.acme-crm.example and acme--dev.sandbox.my.acme-crm.example
httpsHosts: ["*.my.acme-crm.example"],
},
});A *. entry matches subdomains at any depth but not its own apex — add the apex separately if
your tools contact it. The wildcard must cover at least two labels, so *.example is rejected: no
manifest may claim an entire TLD. The *. prefix is the only wildcard form; a variable segment
inside a label (*-api.acme.example) is not expressible, so declare the parent domain instead.
Prefer exact hosts wherever the destination set is fixed — a wildcard is for hosts you genuinely
cannot know until runtime.
At execution the platform intersects this list with organization egress policy; a wildcard here never widens what the organization permits. Direct Bun/Node sockets, alternate ports, private addresses, DNS rebinding, and redirect escapes are denied at the vendor microVM boundary. Outbound DLP scans the materialized request that would leave the VM, not only the tool's input arguments.
Authentication Config
The auth field defines which credential types your vendor supports:
auth: {
credentials: {
none: { type: "none" },
},
default: "none",
}You can define multiple credential types and let the user choose:
auth: {
credentials: {
oauth: { type: "oauth", /* ... */ },
apiKey: { type: "apiKey", /* ... */ },
},
default: "oauth", // Which one to show first
}Config Fields
For vendors that need admin-configured settings (e.g. a self-hosted base URL):
configFields: [
{
key: "baseUrl",
label: "Base URL",
placeholder: "https://your-instance.example.com",
description: "The base URL of your self-hosted instance",
required: true,
type: "url",
},
],Secrets
For vendor-level secrets that are stored in the Dapr Secret Store:
secrets: [
{
key: "WEBHOOK_SIGNING_SECRET",
label: "Webhook Signing Secret",
description: "Used to verify incoming webhook payloads",
required: true,
},
],Secrets are referenced at runtime via ctx.secrets["WEBHOOK_SIGNING_SECRET"]. In the governed
vendor-microVM tool path, credential and secret strings are invocation-scoped placeholders. Pass
them unchanged to ctx.fetch; the trusted egress broker substitutes the real value only in an
allowed request location. Do not inspect, transform, persist, log, or return these values.
defineToolkit(config)
Groups tools, triggers and a block under a toolkit that can be enabled/disabled per organization. Model runtimes are NOT declared here — they belong to a model family, see defineModels() below.
interface ToolkitInput {
/** Unique provided-item ID within the extension */
id: string;
/** Display name */
name: string;
/** Short description */
description?: string;
/** Inline SVG string (falls back to the extension's logo if omitted) */
logo?: string;
/** Which credential from auth.credentials this toolkit uses */
credentialRef?: string;
/** OAuth scopes required by this toolkit */
credentialScopes?: string[];
/** Executable tools */
tools?: ToolDefinition[];
/** Triggers — rendered in the editor's "Triggers" section */
triggers?: TriggerDefinition[];
/** Main toolkit block on the canvas (omit for trigger-only toolkits) */
block?: BlockDefinition;
webhooks?: WebhookCapability;
discovery?: DiscoveryCapability;
webSearch?: WebSearchCapability;
/** Resource catalog; actions opt into exact resources separately */
resources?: Array<{
resourceType: string;
displayName: string;
description?: string;
allowMultiple?: boolean;
}>;
/** NPM package dependencies (for risk assessment at upload time) */
dependencies?: Array<{
type: "npm";
package: string;
version: string;
reason?: string;
}>;The dependencies array is for human-readable context — each entry includes a reason explaining why the dependency is needed. The CLI automatically generates a machine-readable SBOM (meta/sbom.cdx.json) that captures the complete set of archived packages with versions and licenses. Both are complementary: declared dependencies explain intent, the SBOM provides the auditable inventory.
/** System-mode (client_credentials) configuration */
systemMode?: {
supported: boolean;
permissions?: string[];
};
}System Consent Extensions
OAuth credentials can declare manifest-safe extension points for provider-specific setup flows such as Microsoft admin consent. Keep this logic owned by the extension: the manifest should describe the provider URL shape, the required organization config fields, and the callback query parameters.
For most providers, implement system consent with a declarative redirect template. This is the supported path when the platform only needs to build a URL from runtime values:
auth: {
credentials: {
oauth: {
type: "oauth",
label: "Microsoft Account",
authorizationUrl: "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize",
tokenUrl: "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token",
requiredConfig: ["tenantId"],
supportsClientCredentials: true,
clientCredentialScopes: ["https://graph.microsoft.com/.default"],
extensions: {
systemConsent: {
kind: "redirect-template",
urlTemplate: "https://login.microsoftonline.com/{tenantId}/adminconsent",
params: {
client_id: "{clientId}",
redirect_uri: "{redirectUri}",
scope: "{scope}",
state: "{state}",
},
requiredConfig: ["tenantId"],
callback: {
successParam: "admin_consent",
tenantParam: "tenant",
},
},
},
},
},
}The platform substitutes these placeholders:
| Placeholder | Value |
|---|---|
{clientId} | OAuth client ID resolved for the organization |
{redirectUri} | Platform admin-consent callback URL |
{scope} | Space-joined clientCredentialScopes |
{state} | CSRF state generated by the platform |
Any requiredConfig key, such as {tenantId} | Organization provider config value |
If a provider cannot be represented as a URL template, declare a handler reference in the manifest and implement the referenced method in the vendor runtime. The method should be pure: accept resolved runtime values, validate any provider-specific config it requires, and return the redirect URL instead of hardcoding provider logic in the platform.
extensions: {
systemConsent: {
kind: "handler",
handlerRef: "microsoft.systemConsent.buildUrl",
callback: {
handlerRef: "microsoft.systemConsent.callback",
},
},
}type SystemConsentBuildUrlInput = {
clientId: string;
redirectUri: string;
state?: string;
scopes: string[];
providerConfig: Record<string, string | undefined>;
};
export async function buildUrl(input: SystemConsentBuildUrlInput): Promise<string> {
const tenantId = input.providerConfig.tenantId;
if (!tenantId) {
throw new Error("Microsoft admin consent requires tenantId");
}
const url = new URL(`https://login.microsoftonline.com/${tenantId}/adminconsent`);
url.searchParams.set("client_id", input.clientId);
url.searchParams.set("redirect_uri", input.redirectUri);
url.searchParams.set("scope", input.scopes.join(" "));
if (input.state) {
url.searchParams.set("state", input.state);
}
return url.toString();
}
type SystemConsentCallbackInput = {
query: Record<string, string | undefined>;
providerConfig: Record<string, string | undefined>;
};
export async function callback(input: SystemConsentCallbackInput) {
return {
approved: input.query.admin_consent === "True",
tenantId: input.query.tenant,
};
}Handler references are manifest metadata. Uploaded archives should use
kind: "redirect-template" today unless the host application has explicitly
wired runtime execution for the referenced handler.
Multi-Toolkit Extensions
Extensions like Google or Microsoft provide several toolkits. Each toolkit gets its own block in the workflow editor and can be enabled independently:
export default defineExtension({
id: "acme",
// ...
provides: {
toolkits: [
defineToolkit({
id: "acme-crm",
name: "Acme CRM",
tools: [listContacts, createContact],
block: crmBlock,
triggers: [],
}),
defineToolkit({
id: "acme-email",
name: "Acme Email",
tools: [sendEmail, listInbox],
block: emailBlock,
triggers: [],
}),
],
},
});defineTool(config)
Defines the runtime logic for a tool. This is where your actual API calls, data transformations, or computations happen.
interface ToolInput<TInput, TOutput> {
/** Unique tool ID — convention: {vendor}:{product}:{action} */
id: string;
/** Display name */
name: string;
/** Semantic version */
version: string;
/** Description shown to users and LLM agents */
description?: string;
/** Zod schema for input validation */
input: z.ZodType<TInput>;
/** Zod schema for output validation */
output: z.ZodType<TOutput>;
/** Parameter metadata — controls visibility in the UI and to LLMs */
params?: Record<string, ParamDef>;
/** Exact product resources authorized before this action dispatches */
resourceAuthorization?: Array<{
required?: boolean;
oneOf: Array<{
resourceType: string;
source: { kind: "input"; key: string } | { kind: "binding" };
}>;
}>;
/** The function that runs when this tool is invoked */
execute: (input: TInput, ctx: PureContext) => Promise<ToolResponse<TOutput>>;
}
interface ParamDef {
type: string;
required?: boolean;
description?: string;
default?: unknown;
/** Controls where this parameter appears */
visibility?: "user-only" | "user-or-llm" | "hidden";
}product.resources is a catalog, not a product-wide execution requirement. Declare each
action's actual target with resourceAuthorization. Input-sourced gates are required; only a
binding-sourced gate may set required: false, which preserves delegated account-root behavior
such as /me. Delegated targets are verified live with the provider. App/system targets require
an exact workspace assignment for the same product, resource type, and resource ID.
Manifest extraction preserves Zod descriptions and supported primitive
constraints such as string length and integer minimum/maximum bounds. For
z.preprocess(...), the generated JSON Schema describes the validated output
schema rather than the preprocessing transform, which cannot be represented in
JSON Schema. Keep that output schema faithful to the values the tool accepts;
its optionality and constraints are what workflow editors and Agent tool
descriptors consume.
Parameter Visibility
| Visibility | Who can provide the value | Use case |
|---|---|---|
user-or-llm | User sets it in the UI, or an AI agent generates it | Most parameters |
user-only | Only the user can configure it (hidden from LLM) | Connection settings, credentials |
hidden | Not shown in UI or to LLM — set programmatically | Internal IDs, computed values |
Execute Function
The execute function receives validated input and a PureContext:
import { requireOAuthToken } from "@scrydon/sdk-authoring/extensions/authoring/context";
async execute(input, ctx) {
// Use the logger (visible in workflow execution logs)
ctx.logger.info(`Processing request for org ${ctx.execution.orgId}`);
// Narrow on ctx.auth.kind before reading credential fields.
// This example assumes the vendor declares an OAuth credential —
// requireOAuthToken throws a CredentialKindError if the kind doesn't match.
const accessToken = requireOAuthToken(ctx.auth);
// Make authenticated API calls
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (!response.ok) {
return {
success: false,
output: { error: `API returned ${response.status}` },
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
const data = await response.json();
return {
success: true,
output: data,
metadata: { requestId: response.headers.get("x-request-id") },
};
},PureContext
The PureContext interface is the only interface between the platform and archive code. It is injected into every execute() call at runtime:
/**
* Discriminated union of all credential shapes the platform can inject.
* Always narrow on `kind` before reading credential fields.
* Import narrowing helpers from @scrydon/sdk-authoring/extensions/authoring/context:
* requireOAuthToken(auth) → string (accessToken, throws on mismatch)
* requireOAuth(auth) → { accessToken, refreshToken?, tokenType?, expiresAt? }
* requireApiKey(auth) → string (apiKey)
* requireBasicAuth(auth) → { username, password }
* requireBotToken(auth) → string (botToken)
* Each helper throws a CredentialKindError with an actionable message on mismatch.
*/
type PureContextAuth =
| { kind: "oauth"; accessToken: string; refreshToken?: string; tokenType?: string; expiresAt?: number }
| { kind: "apiKey"; apiKey: string }
| { kind: "basicAuth"; username: string; password: string }
| { kind: "botToken"; botToken: string }
| { kind: "none" };
interface PureContext {
/** Credential resolved by the platform before invocation */
auth: PureContextAuth;
/** Execution metadata for logging and correlation */
execution: {
executionId: string; // Unique execution run ID
workflowId?: string; // Workflow ID (if running in a workflow)
nodeId?: string; // Block node ID in the workflow
orgId: string; // Organization ID
workspaceId?: string; // Workspace ID
userId?: string; // User who triggered the execution
};
/** Structured logger — console calls are intercepted in the sandbox */
logger: {
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
debug(message: string, ...args: unknown[]): void;
};
/** Vendor secrets configured by org admin (resolved per-invocation via Dapr Secret Store) */
secrets: Record<string, string>;
/** Present when executing via an Extension Profile — contains profile-specific overrides */
profileConfig?: Record<string, unknown>;
}PureContext is imported from @scrydon/sdk-authoring/extensions/authoring/context. The platform constructs it before each tool invocation — your code never needs to create one manually. All fields except auth and execution may be empty objects if not applicable. auth is a kind-discriminated union (PureContextAuth); always narrow on auth.kind or use a require* helper before accessing credential-specific fields. See Authentication Modes for the full per-mode shape reference.
ToolResponse
Every execute() must return a ToolResponse:
interface ToolResponse<T = unknown> {
/** Whether the tool execution succeeded */
success: boolean;
/** The typed output data */
output: T;
/** Stable machine-readable error code */
error?: string;
/** Human-readable failure explanation; preferred over error for display */
message?: string;
/** Optional metadata (logged but not passed to downstream blocks) */
metadata?: Record<string, unknown>;
}defineBlock(config)
Defines the workflow editor UI — how the block appears, what form fields it has, and how data flows through it.
interface BlockInput {
/** Unique block type — lowercase with underscores (e.g. "acme_crm") */
type: string;
/** Display name in the block palette */
name: string;
/** Short description */
description?: string;
/** Block category — use "extension" for archive blocks */
category: string;
/** Background color in hex */
bgColor?: string;
/** Auth mode: "none", "apiKey", "oauth", "botToken" */
authMode?: string;
/** Form fields displayed in the block panel */
subBlocks?: SubBlockInput[];
/** Which tools this block can invoke */
tools?: {
access: string[];
config?: {
/** Dynamic tool selection based on user input */
tool: (params: Record<string, any>) => string;
/** Parameter transformation before passing to tool */
params?: (params: Record<string, any>) => any;
};
};
/** Input connections from upstream blocks */
inputs?: Record<string, { type: string; description?: string }>;
/** Output connections to downstream blocks */
outputs?: Record<string, { type: string; description?: string }>;
/** Trigger configuration (for trigger blocks) */
triggers?: { enabled: boolean; available: string[] };
}SubBlock Types
SubBlocks are the form fields that appear when a user clicks on a block:
| Type | Description | Key Props |
|---|---|---|
short-input | Single-line text input | placeholder, password, defaultValue, generatedDefault, readOnly, showCopyButton |
long-input | Multi-line textarea | placeholder |
code | Code editor with syntax highlighting | placeholder, mode |
dropdown | Select menu | options: [{ label, id }] |
combobox | Searchable dropdown | options, fetchOptions |
slider | Range slider | min, max, step |
switch | Toggle switch | defaultValue |
oauth-input | OAuth credential selector | serviceId, requiredScopes |
SubBlock Definition
interface SubBlockInput {
/** Unique field ID within the block */
id: string;
/** Field label */
title?: string;
/** SubBlock type */
type: string;
/** Layout: "full" (default) or "half" */
layout?: "full" | "half";
/** Whether this field is required */
required?: boolean;
/** Placeholder text */
placeholder?: string;
/** Mask input as password dots */
password?: boolean;
/** Default value */
defaultValue?: unknown;
/** Generate a fresh value once when each new block is created */
generatedDefault?: "uuid";
/** Options for dropdown/combobox */
options?: Array<{ label: string; id: string }>;
/** Help text shown below the field */
description?: string;
/** Conditional visibility based on another field's value */
condition?: { field: string; value: string };
/** Whether the field is read-only */
readOnly?: boolean;
/** Show a copy-to-clipboard button */
showCopyButton?: boolean;
/** Hide this field from the UI */
hidden?: boolean;
}Use generatedDefault: "uuid" when every newly added block needs its own
initial value, such as a webhook secret. The declarative marker survives the
extension manifest's JSON boundary; the generated UUID is stored as the
block's ordinary field value and is not regenerated when the workflow loads.
Dynamic Tool Selection
When a block has multiple tools, use the JSON-safe tools.toolSelector to select which one runs:
defineBlock({
// ...
subBlocks: [
{
id: "operation",
title: "Operation",
type: "dropdown",
options: [
{ label: "Create", id: "create" },
{ label: "Read", id: "read" },
{ label: "Update", id: "update" },
{ label: "Delete", id: "delete" },
],
},
// ... other fields
],
tools: {
access: ["acme_create", "acme_read", "acme_update", "acme_delete"],
toolSelector: {
param: "operation",
map: {
create: "acme_create",
read: "acme_read",
update: "acme_update",
delete: "acme_delete",
},
default: "acme_read",
},
},
});Conditional Visibility
Show or hide fields based on other field values:
subBlocks: [
{
id: "authType",
title: "Auth Type",
type: "dropdown",
options: [
{ label: "API Key", id: "apiKey" },
{ label: "Username/Password", id: "basic" },
],
},
{
id: "apiKey",
title: "API Key",
type: "short-input",
password: true,
condition: { field: "authType", value: "apiKey" },
},
{
id: "username",
title: "Username",
type: "short-input",
condition: { field: "authType", value: "basic" },
},
{
id: "password",
title: "Password",
type: "short-input",
password: true,
condition: { field: "authType", value: "basic" },
},
],defineTriggerBlock(config)
Defines a trigger block — a first-class workflow member that renders in the "Triggers" sidebar section and carries runtime metadata describing how it fires (webhook, polling, or both). The category is auto-set to "trigger".
interface TriggerBlockInput {
/** Unique block type */
type: string;
/** Display name */
name: string;
/** Description */
description?: string;
/** Background color in hex */
bgColor?: string;
/** Auth mode: "none", "apiKey", "oauth", "botToken" */
authMode?: string;
/** Form fields for trigger configuration */
subBlocks?: SubBlockInput[];
/** Which tools this trigger block can invoke */
tools?: {
access: string[];
config?: {
tool?: (params: Record<string, any>) => string;
params?: (params: Record<string, any>) => any;
};
};
/** Input connections */
inputs?: Record<string, { type: string; description?: string }>;
/** Output schema — what data the trigger emits */
outputs?: Record<string, { type: string; description?: string }>;
/** Provider identifier */
provider?: string;
/** Semantic version */
version?: string;
/** Runtime configuration — how events reach the workflow engine */
runtime?: {
webhook?: {
method?: "POST" | "GET" | "PUT" | "DELETE";
headers?: Record<string, string>;
};
polling?: PollingProvider;
};
}Secrets Management
Vendors can declare secrets that are stored encrypted at rest and resolved per-invocation. Secrets are scoped to the organization and vendor, so each org has its own isolated set of credentials.
Declaring Secrets
Define secret requirements in defineExtension() using the secrets array:
export default defineExtension({
id: "acme",
name: "Acme",
version: "1.0.0",
logo: acmeIcon,
auth: { credentials: { apiKey: { type: "apiKey", label: "API Key", identityScope: "application" } }, default: "apiKey" },
secrets: [
{
key: "WEBHOOK_SIGNING_SECRET",
label: "Webhook Signing Secret",
description: "Used to verify incoming webhook payloads",
required: true,
},
{
key: "ENCRYPTION_KEY",
label: "Encryption Key",
description: "AES key for encrypting sensitive payloads",
required: false,
},
],
provides: { toolkits: [/* ... */] },
});Each SecretInput has the following shape:
interface SecretInput {
/** Unique key within the vendor — must match ^[a-zA-Z0-9_-]+$ */
key: string;
/** Human-readable label shown in the admin UI */
label: string;
/** Help text describing what this secret is used for */
description?: string;
/** Whether the extension requires this secret to function */
required: boolean;
}Secret keys must contain only alphanumeric characters, hyphens, and underscores (^[a-zA-Z0-9_-]+$). Keys that do not match this pattern are rejected at save time.
Secret Key Format
Secrets are stored using a structured key that scopes them to the organization and vendor:
extension:{orgId}:{extensionId}:{key}For example, a secret with key WEBHOOK_SIGNING_SECRET for vendor acme in organization org_abc123 is stored as:
extension:org_abc123:acme:WEBHOOK_SIGNING_SECRETThis ensures complete isolation between organizations and between vendors within the same organization.
Encryption at Rest
All secret values are encrypted with AES-256-GCM before being persisted. The encryption key strategy is configured per organisation — LOCAL, BYOK, or HYOK. Secret values are never stored in plaintext. See Security → Secrets management for the full model.
Accessing Secrets at Runtime
ctx.secrets is a flat Record<string, string> containing configured keys. In the governed
vendor-microVM path, each value is an invocation-scoped placeholder backed by a sealed broker grant:
import { defineTool } from "@scrydon/sdk-authoring/extensions/authoring/define";
import { z } from "zod";
export const verifyWebhook = defineTool({
id: "acme:webhooks:verify",
name: "Verify Webhook",
version: "1.0.0",
input: z.object({ payload: z.string(), signature: z.string() }),
output: z.object({ valid: z.boolean() }),
async execute(input, ctx) {
const signingSecret = ctx.secrets["WEBHOOK_SIGNING_SECRET"];
if (!signingSecret) {
return { success: false, output: { valid: false }, error: "Webhook signing secret not configured" };
}
const response = await ctx.fetch("https://api.acme.example/webhooks/verify", {
method: "POST",
headers: {
"content-type": "application/json",
"x-acme-signing-secret": signingSecret,
},
body: JSON.stringify(input),
});
return { success: true, output: { valid: response.ok } };
},
});Pass placeholder values unchanged to ctx.fetch. The trusted broker substitutes the reusable value
after destination authorization and before outbound DLP scans the actual request. The guest,
filesystem, model, plan, logs, traces, and tool response never receive the reusable value.
The object contains only keys set by the organization admin; absent values remain absent. Local operations that must inspect or transform a reusable secret, such as HMAC signing inside non-Scrydon Agent tool code, are intentionally unavailable on this boundary.
Admin UI
Organization administrators configure secrets under Settings > Platform > Extensions, in the vendor's sheet. The UI displays the declared secret fields from the vendor's secrets array.
Secret values are write-only -- after a value is saved, it is never displayed again. The UI only shows which keys have been configured (with a masked indicator) and allows admins to update or delete them.
API Endpoints
The platform exposes three internal admin endpoints for secret management:
| Endpoint | Purpose |
|---|---|
/extensions/authoring/admin/archive-secret-set | Create or update a secret value |
/extensions/authoring/admin/archive-secret-delete | Delete a secret |
/extensions/authoring/admin/archive-secret-list | List configured secret keys (values are never returned) |
These endpoints are internal (service-to-service) and require the caller to be an authorized internal service. They are not directly accessible from client applications.
Build CLI
# Build an archive
sdk-authoring extension build [--entry src/index.ts] [--outDir dist]
# Test an archive
sdk-authoring extension test [--level static|sandbox|live] [--tool <id>]Build Output
my-vendor-1.0.0.archive.tar.gz
├── manifest.json # Auto-generated metadata (JSON Schemas, UI defs)
├── dist/
│ └── index.js # Compiled, minified ESM (all deps inlined via esbuild)
├── meta/
│ ├── sbom.cdx.json # CycloneDX 1.6 SBOM (auto-generated)
│ └── metafile.json # esbuild dependency graph
└── assets/ # Optional icons
└── icon.svgThe build CLI inlines all npm dependencies into the single dist/index.js file and auto-generates a CycloneDX 1.6 SBOM at meta/sbom.cdx.json. There is no runtime npm install — the archive is fully self-contained and deterministic.
Vendor ID Rules
- Lowercase alphanumeric characters and hyphens only
- Must start with a letter
- Pattern:
/^[a-z][a-z0-9-]*$/ - Examples:
acme,my-vendor,hello-world
Tool ID Convention
Tool IDs use the fully qualified format {extensionId}:{providedId}:{action} at runtime:
hello-world:hello-world:say-hellogoogle:sheets:readslack:messaging:send-message
Shorthand: In authoring code you can use just the action part (e.g. "say-hello", "read"). The SDK auto-qualifies short IDs by prepending {extensionId}:{providedId}: during defineExtension(). All first-party extensions use the short form. If you use the full form, the vendor/product prefix must match — the build CLI validates this.
Capability Helpers
Capability helpers wrap runtime configurations with a type tag so the manifest extractor can identify them. Each helper returns its config with a _capability tag used internally during build.
| Helper | Tag | Purpose |
|---|---|---|
defineCapabilityLLM(config) | llm | Full LLM provider (chat, completion, streaming) |
defineCapabilitySTT(config) | stt | Speech-to-text (batch and realtime) |
defineCapabilityTTS(config) | tts | Text-to-speech (batch and realtime) |
defineCapabilityEmbedding(config) | embedding | Vector embedding generation |
defineCapabilityImage(config) | image | Image generation |
defineCapabilityVideo(config) | video | Video generation |
defineCapabilityOCR(config) | ocr | Document parsing / optical character recognition |
defineCapabilityModeration(config) | moderation | Content-safety classification |
defineCapabilityWebSearch(config) | webSearch | Provider-scoped internet search |
defineCapabilityWebhook(config) | webhook | Vendor-specific webhook handling |
defineCapabilityDiscovery(config) | discovery | Resource browsing, selectors, access scopes |
All helpers are imported from @scrydon/sdk-authoring/extensions/authoring/define:
import {
defineCapabilityLLM,
defineCapabilitySTT,
defineCapabilityTTS,
defineCapabilityEmbedding,
defineCapabilityImage,
defineCapabilityVideo,
defineCapabilityOCR,
defineCapabilityModeration,
defineCapabilityWebSearch,
defineCapabilityWebhook,
defineCapabilityDiscovery,
} from "@scrydon/sdk-authoring/extensions/authoring/define";Model capability configs are placed inside defineModels({ family: "<family>", runtime: { ... } }), one model family per entry in provides.models; webSearch is a toolkit member instead. The manifest extractor converts runtime functions to boolean flags and extracts model metadata (dimensions, benchmarks, pricing) into the manifest's capabilityMeta field. See Capabilities for full usage examples of each helper.
Manifest Schema
The build CLI auto-generates manifest.json from your defineExtension() call. The manifest is a JSON-serializable snapshot of your vendor's metadata, stripped of runtime functions. It follows the sole supported manifestVersion: 1 shape. Any other version is rejected.
Top-Level Structure
{
"$schema": "...", // Optional JSON Schema URL
"manifestVersion": 1, // Always 1
"version": "1.0.0", // Vendor version (from defineExtension)
"vendor": { ... }, // Vendor metadata
"auth": { ... }, // Authentication config
"products": [ ... ], // Product definitions
"protocols": [ ... ], // Protocol definitions (optional)
"llm": { ... } // LLM provider config (optional)
}Vendor Section
Contains display metadata. Runtime code is excluded.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Vendor ID (/^[a-z][a-z0-9-]*$/) |
name | string | Yes | Display name |
description | string | No | Short description |
color | string | No | Hex brand color |
icon | string | Yes | SVG icon as string |
website | string | No | Vendor website URL |
docsUrl | string | No | Documentation URL |
categories | string[] | No | Category tags |
defaultEnabled | boolean | No | Auto-enable for new orgs (default: false) |
supportsSystemMode | boolean | No | Supports client_credentials (default: false) |
connectivity | "cloud" | "local" | "hybrid" | No | Network requirements |
configFields | ConfigField[] | No | Admin-configurable fields |
secrets | Secret[] | No | Vendor-level secrets |
Product Section
Each product in the products array contains:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Product ID |
name | string | Yes | Display name |
capabilities.tools | Tool[] | No | Tool definitions with JSON Schema input/output |
capabilities.triggers | Trigger[] | No | Trigger definitions |
capabilities.runtimes | object | No | Boolean flags for every selectable runtime: llm, embedding, stt, tts, image, video, ocr, moderation, webSearch |
capabilities.capabilityMeta | object | No | Extracted model metadata (embedding dimensions, STT/TTS models, benchmarks) |
capabilities.webhooks | boolean | No | Whether the product handles webhooks |
capabilities.discovery | boolean | No | Whether the product supports resource discovery |
workflow.block | Block | No | Workflow editor block definition |
workflow.triggers | TriggerBlock[] | No | Workflow trigger blocks |
resources | Resource[] | No | Product resource catalog; never a product-wide action gate |
dependencies | Dependency[] | No | Declared NPM dependencies with reasons |
systemMode.permissions | string[] | No | App/system consent permissions |
How Runtime Code Becomes Manifest Data
The build CLI transforms runtime definitions into manifest-safe JSON:
| Source (defineToolkit) | Manifest Output |
|---|---|
runtimes.llm (with executeRequest function) | runtimes.llm: true |
runtimes.embedding (with embed function, models with dimension) | runtimes.embedding: true + capabilityMeta.embeddingModels |
runtimes.stt (with transcribe function, models) | runtimes.stt: true + capabilityMeta.sttModels |
runtimes.tts (with synthesize function, models) | runtimes.tts: true + capabilityMeta.ttsModels |
runtimes.image (with generation function and models) | runtimes.image: true + image model metadata |
runtimes.video (with generation function and models) | runtimes.video: true + video model metadata |
runtimes.ocr (with extraction function and models) | runtimes.ocr: true + OCR model metadata |
runtimes.moderation (with classification function and models) | runtimes.moderation: true + moderation model metadata |
runtimes.webSearch (with search function) | runtimes.webSearch: true; provider-scoped, with no model policy |
Tool input/output (Zod schemas) | JSON Schema objects |
Tool execute function | Omitted (not serializable) |
Block tools.toolSelector | Preserved as JSON-safe routing metadata |
Block tools.config.tool / tools.config.params functions | Omitted; the build emits an author warning |
The manifest is what the platform reads at boot time to populate the extension catalog. It contains no executable code -- only metadata, JSON Schemas, and UI definitions. The executable archive is loaded lazily through the canonical backend. Capability/reviewed paths may use the Worker Thread compatibility backend; a static non-Scrydon tool selected by an isolated Agent requires the source-pinned, proof-gated vendor microVM.
Extension manifests cannot contain function closures. Use tools.toolSelector for routing, direct block inputs for parameters, and perform any remaining coercion inside the tool's execute() function. The build and static-test commands warn when tools.config.tool or tools.config.params would be dropped.
Continuous speech-to-text sessions
An STT extension that declares streaming models must implement both
createStreamingSession(request) and openStreamingSession(request, context).
The build validates this pairing. streaming.protocol is an extension-owned
identifier; adding a provider does not require a recording-app protocol adapter.
Import the session types from
@scrydon/sdk-authoring/extensions/capabilities/stt-streaming:
import type {
SttSession,
SttSessionContext,
SttStreamingSessionRequest,
} from "@scrydon/sdk-authoring/extensions/capabilities/stt-streaming";
// Signature of runtime.openStreamingSession:
type OpenSpeechSession = (
request: SttStreamingSessionRequest,
context: SttSessionContext,
) => Promise<SttSession>;SttSession provides sendAudio(Uint8Array), finalize(), close(),
onTranscript(handler), onError(handler), and onClose(handler).
Audio is continuous mono 16 kHz PCM16. The extension owns provider authentication,
resampling, framing, credential renewal, and SDK dependencies. Keep SDKs compatible
with the sandbox's brokered fetch and WebSocket; native networking modules
cannot bypass these controls. Honor context.signal and close provider resources
when the session ends. context.auth, context.secrets, and
context.profileConfig use the canonical extension context, so authentication
is not restricted to API keys. finalize() must drain accepted audio before reporting
completion.
Emit { text, isFinal, startMs?, endMs?, confidence?, speakerId? }. Timestamps
are milliseconds from this session's audio origin. Omit unsupported timing or
attribution; do not invent it. A provider's speaker label identifies a speaker
only within the continuous session that produced it. It cannot identify an
organization member or establish identity across separate STT requests.
The platform returns a scrydon-stt-v1 connection grant to media clients. Provider
endpoints and credentials remain inside the extension runtime, including the
direct and proxied values returned by createStreamingSession.
Continuous sessions currently require reviewed extension code admitted to the worker execution backend. If organization policy requires a managed-process or microVM backend, streaming is refused before a connection ticket is issued; those backends currently support request/response execution. Batch transcription continues to use its normal supported execution tiers. Do not weaken organization isolation policy to make an unsupported streaming deployment appear available.
When upgrading, rebuild and reinstall streaming extension bundles with the updated
authoring SDK. Ticket-only bundles lack the required session hook. Deploy the
platform and media client together; the new client requires scrydon-stt-v1
and does not fall back to provider protocols in the recording app.
First-party OpenAI speech models
The OpenAI extension implements the same session contract as other extensions. Configure its OpenAI API-key connection and choose a model under the STT capability:
| Model | Input path | Timing and speakers |
|---|---|---|
gpt-live-transcribe | Native live session | Turn boundaries from server VAD; no word timestamps or provider speaker labels |
gpt-transcribe | File / utterance | Text and detected languages; no invented timestamps |
gpt-4o-transcribe, gpt-4o-mini-transcribe | File / utterance | Text; no word timestamps |
gpt-4o-transcribe-diarize | File / utterance | Timed segments with request-local speaker labels |
whisper-1 | File / utterance | Word or segment timestamps; English translation |
The existing default remains whisper-1. Live and file models are separate
selections; a live-only model cannot serve a final file-transcription request.
The extension uses the platform's brokered socket, converts 16 kHz PCM to 24 kHz
inside each session, and waits for accepted audio and pending final transcripts
before completing finalize(). It declares the provider's 60-minute session
ceiling for the existing rotation mechanism. Speaker tracking remains independent
of STT; batch speaker labels do not establish identity across requests.
File uploads preserve their audio container, including Recording Studio's FLAC
windows, and are limited to 25 MB. Diarization requests use diarized_json with
chunking_strategy=auto; unsupported prompts and word-timestamp options are omitted.
Model access still depends on the configured OpenAI account. Rebuild and reinstall
the OpenAI extension to expose the new models and live-session hook.
Provider references: file transcription, realtime transcription, and session lifecycle.