Delivering & Managing Custom Extensions
Deliver custom extensions through extensions (Git/OCI sources), and manage installed extensions
Manual tarball upload has been removed. Custom extensions are now delivered exclusively
through extensions — a Git or OCI source your CI/CD publishes to, installed from your org's extension
catalog. Publish your .archive.tar.gz inside an extension instead of uploading it directly. See
Extension sources and
Packaging a custom extension in an extension.
Custom Extension Scopes
Extension identities reserve three scopes. Organization uploads are the currently supported custom-extension path:
| Scope | Origin | Visibility | How it's delivered |
|---|---|---|---|
First-party (org:scrydon:) | Baked into the Docker image | All organizations | Shipped with Scrydon releases |
Platform (org:platform:) | Reserved for a platform-scoped extension | All organizations | Not currently enabled; requires dedicated platform-owned artifact storage |
Organization (org:{orgId}:) | Org extension source | Single organization | Org admin installs from the org's extension catalog |
Every organization-installed tool has a platform microVM floor. Only exact reviewed code shipped by Scrydon may use the approved Worker Thread tool backend; a matching vendor name never grants uploaded code that trust. A custom tool is selectable by an isolated Agent only while the exact Runtime Plane backend is qualified and attested.
Delivering via an extension source
Publish your built .archive.tar.gz as an extension content entry inside an extension, to a Git
or OCI source your CI/CD controls. See
Packaging a custom extension in an extension.
Go to Settings > Platform > Extensions → the Sources tab (direct URL:
/settings/platform/extensions#sources) and register your extension source. Sources sync extensions
into your org's extension catalog. See Extension sources.
Synced custom extensions appear in Add extension as rows that name their extension (extension name and version) with a Connect action when the first step after install is a credential, and Install when it is not — either one installs and activates it immediately (your explicit install is the review). A cosign-signed source is also trusted for non-interactive installs; unsigned automated syncs land as Pending Review.
When a new artifact is published, the card shows an Update badge; clicking it takes you to the extension's sheet (Settings → Platform → Extensions → Installed → open the extension → Version & security), where you review workflow impact and apply the update. See Extension version management.
What Happens on Install
When an extension carrying an extension entry is installed, the platform runs the same archive
pipeline it always has:
Extracts the .tar.gz with security checks:
- No symlinks allowed
- No absolute paths
- No path traversal (
../) - No device, FIFO, duplicate-path, or unsupported archive entries
- A bounded entry count and bounded decompression output
- Only allowed file extensions (
.js,.js.map,.json,.svg,.png,.jpg) - A required
dist/index.jsentry point - Maximum extracted size: 100 MB
Validates manifest.json against the ManifestSchema (Zod):
manifestVersionmust be1, the sole supported contract; any other version is rejected- Product workflow UI belongs under
workflow; resources underresources; app consent undersystemMode.permissions - Vendor ID format:
/^[a-z][a-z0-9-]*$/ - Tool IDs reference valid vendor prefixes
- Auth credential types are valid
- No duplicate product IDs
- Required fields are present
Writes the artifact to blob storage:
00_organization/extension-archives/{extensionId}/{version}/archive.tar.gzRegisters the extension with the vendor ID, version, manifest JSON, storage key, SHA-256
hash, and scope in the org's installed-extension record (extension_code_release).
There is no compatibility upgrade during install. Rebuild any archive built against an older
contract with the current authoring SDK before publishing it. Do not hand-edit manifest.json inside the archive.
Managing Installed Custom Extensions
Settings > Platform > Extensions > Installed lists every extension your organization has explicitly installed — through Add extension or an extension-catalog install — plus the built-in Scrydon vendor (shipped with every organization, cannot be removed) and your registered MCP servers, one row each. Catalog vendors nobody has added are not rows; they live in the Add-extension flow. A row reads as a sentence: the vendor's mark and name, its publisher (Scrydon, a vendor, or your org), one line of what it brings in words, a single line of state, and its version — an available update shows in the version column as 1.3.1 → 1.4.2 so it never displaces a state that needs a human. Enabling happens inside the sheet, where its org-wide scope is spelled out. Rows that need attention sort to the top, and the Filter button narrows by what a row includes (AI models, agent tools, content, MCP servers), state, and publisher.
States
| State | Meaning |
|---|---|
| Available | Ready to use — credentials in place, products available in workflows. Content extensions append adoption: · on in 3 of 5 workspaces, or · not on in any workspace yet when no workspace has turned anything on |
| Needs credentials · step 2 of 3 | Installed, but an org-owned setup step is unfinished. The state names the next step and, in grey, where it sits in the count — the same count the sheet's Setup checklist shows. There is no separate "in progress" state |
| Credential failing | The last health check against the vendor failed — reconfigure or reconnect |
| Source sync failing | The extension source that delivers this extension cannot sync — see the Sources tab |
| Deprecated until … | Scheduled for removal on that date; workflows still run until then |
| Not enabled | Every product is switched off — the extension stays listed so you can switch it back on |
| Disabled | Disabled by lifecycle policy |
| Awaiting approval / Unreachable | MCP servers only: pending review, or the last discovery could not reach the server. On a content extension whose next step is an MCP approval, the state reads Awaiting approval · step 2 of 2 |
The extension sheet
Click a row to open the extension's sheet. Everything about it lives there, in tabs:
-
Setup — while something is still owed, your org-owned steps as a checklist ("Microsoft account connected — OAuth · 12 products") with a progress meter, and the connection sections (console instructions, the OAuth redirect URL, organization-wide access) laid out flat beneath it. Once nothing is owed the checklist goes and the tab reads as the connection overview — the state it would have reported is already on the extension's row in the list. An extension that needs no account at all keeps a single line saying so.
-
Includes — what the extension gives you: AI models first (with the model list and the allowlist policy editor behind each capability), then agent tools grouped by product, each with its Enabled · org-wide switch — Not enabled when it is off. Turn on is the workspace word and never appears here. Every product gets a row, however many there are; the section header states the scale ("142 tools across 18 products") before the list starts.
-
Connections — headed Connections · N, one row per connection with five columns: Connection, Environment, Who can use it, Used by and Status (Ready once its credential is supplied and its address admitted). The grey line under a connection's name says how it signs in (the account it acts as, or the kind of credential it holds), which products it serves when that is not all of them (Outlook only, 2 of 5 products), and when it was last used. When you add or edit a connection, Products this connection serves is All products or Only selected. Narrowing it first lists the data sources fixed to the products it stops serving: they stop and ask for another connection, and their tables and rows are kept. Save & test and Test connection show their checks above the table, not in a dialog, so you can go straight on to the new connection's Who can use it.
Each connection's Who can use it column says who may use it — Everyone (everyone in the organization), the teams by name (a count once there are more than two), how many people, or Admins only. Select it to add a person or team, Remove one (one click), or Allow everyone in the organization: they can pick the connection in Analytics when adding a data source. Each team says how many people it has (team · 6). When someone asks for access from Analytics, the notification opens these connections with that person offered under Who can use it of every connection serving the environment they asked about — the popover opens on its own when only one does — and Share gives them use. Nothing is shared until you select Share. Using a connection never shows its credential. Organization admins can always use every connection, and only they grant or remove access, and create, edit, test, rotate, change the products of or delete a connection. A new connection starts usable by admins only; connections that existed before per-connection access stay usable by everyone in your organization until you remove that access. Workspaces… in the connection's row menu sets which workspaces can use it; workspace access applies to every product of the connection. Every change is recorded in the audit log under the person who made it.
-
Access — which workspaces may use the vendor. Granted workspaces are listed 50 a page, with the total above the list; Start fresh revokes every grant, not only the page on screen.
-
Usage — workflows and knowledge bases that reference it. For a data-source extension, the data sources added from it, each with the workspace and environment that runs it and when it was added; an extra instance is named by its source and the name it was added under.
-
Version & security — present for every extension, and read top to bottom. Provenance first: who published it, whether its code runs in the platform process or in an isolated microVM (a trust shield marks third-party code; a Scrydon-published extension carries none), and May reach — the destinations its tools declare, and whether your organization's egress policy treats that declaration as a grant. Then Updates: the installed version and the one available, what changed, the update policy, and which workflows use it. An extension that ships with the platform has no separate version to check or policy to set, so that half reads as a single line and the provenance half is the tab.
The ··· menu in the sheet's header holds Enable / Disable for the organization and the
lifecycle actions — Review new version, Deprecate / Cancel deprecation, Remove.
The sheet is deep-linkable: /settings/platform/extensions?extension=<slug> opens it directly
(older ?vendor= links still resolve).
Deprecate offers Apply replacements now when another enabled extension provides the same capability. For a language model, every model picker in your organization's workflows that names one of the deprecated extension's models — in standard and automation workflows alike — is pointed at the replacement model you pick, and the capability default moves with it. Only a field where you pick a language model from the list is rewritten: a model typed as text (the Guardrails hallucination check, the Web block's parsed output) keeps its value, and so does a field that picks an image or embedding model, so check those by hand. Knowledge bases keep their embedding model: changing it means re-indexing, so do that from the knowledge base. Each product's workflows are rewritten in one step, so a failure leaves that product's workflows as they were; the defaults move only once every product succeeded, and the dialog asks you to retry the rest.
The Connections tab
Connections, beside Installed, lists every connection in your organization across extensions — extension, name, environment, who can use it, when it was last used and who turned it on — one page at a time. Who can use it opens the same popover as the extension sheet, so access is changed here too; open the extension's sheet to change the connection itself.
The Models & defaults tab
Settings > Platform > Extensions > Models & defaults is the inventory of AI capabilities (LLM, embeddings, speech to text, text to speech, image, video, OCR, moderation, web search) and where the organization's defaults are set: one row per capability, always all of them, with the vendor that serves it and the model it currently resolves to, tagged automatic (the platform ranks what is installed) or pinned. The model name never repeats the vendor beside it. Change opens the picker in place: Select automatically, or a specific provider/model, saved per row. A capability whose vendor is present but resolves no model reads No model resolved rather than a dash. A capability nobody serves offers Add a vendor (the Add-extension flow). A note beside the row names what is worth knowing: a vendor that is installed but has every product switched off (with an Enable shortcut), or a vendor that is being retired and by when.
Every organization starts with Scrydon Local embeddings enabled. It runs the
384-dimensional all-MiniLM-L6-v2 model inside API Platform without provider
credentials or an outbound model call. Automatic selection uses it when no other
enabled embedding provider is available. You can pin it explicitly or disable its
connection in the Scrydon extension settings.
The former Platform Defaults page is this tab; its address
(/settings/platform/defaults) redirects here. Mute the sidebar badge in the tab's footer
acknowledges the current gaps organization-wide so the badge on the Extensions menu item
stays quiet until a new gap appears — the inventory itself is never hidden.
Turning web search off
The Web search row carries a Disable web search switch. It is the one capability an organization can turn off outright, for deployments where reaching the public web is a compliance question rather than a configuration one. The switch saves on flip — there is no Save button, because the opt-out is its own organization policy rather than part of the row's provider selection — and it renders even when no web-search vendor is installed, so an organization can set and evidence an explicit "off" before it has bought anything.
While it is on, the row reads off and shows no provider picker, and the platform refuses web search at both seams:
- the web-search capability (
action.search.web) — used by chat, the Search block, the notebook SDK and agent tool calls — refuses; - a vendor tool belonging to a toolkit that serves web search (for example Google's
searchtool) refuses too, even when an agent names it directly.
The refusal is deliberately the same one a caller sees when nothing is installed
(CAPABILITY_NOT_CONFIGURED), so features degrade instead of erroring. Logs and audit
records carry policyCode: CAPABILITY_DISABLED_BY_ORG_POLICY so a compliance decision is
never mistaken for a broken setup. Turning the switch back off returns the row to
Select automatically; it does not restore a previously pinned provider.
Any organization admin or owner can set it; a member's attempt is refused. It takes effect immediately at the platform — it is not cached behind a refresh window.
Cortex stops offering its web-search tool to the model while web search is disabled, on the main chat turn and for research sub-agents alike, so the assistant answers from the conversation, knowledge bases and other tools instead of planning around a search it cannot run. Cortex re-checks within about a minute of a change; in that window the platform still refuses the call, and the assistant is told web search is disabled for the organization.
Both the decision and its effect are recorded in the Audit Log as governance events:
| Event | When | What it records |
|---|---|---|
capability.policy.updated | An admin turns web search off or back on | Who made the change, the previous and new policy, and where web search is off (organization default and/or named environments) |
capability.policy.refused | A web-search call or web-search vendor tool is refused | The capability, the refused operation, the organization, workspace and environment, and the principal the call ran as |
A readiness check (for example Cortex asking whether to offer the tool) is not a refusal and writes no event.
This is a governance opt-out for the web-search capability, not an air gap. It does not cover a model vendor's own server-side retrieval, the webpage-fetch policy, MCP servers reached over HTTPS, or data-source polling. Network-level isolation is enforced by the egress policy, not by this switch.
Enable / Disable
Enable / Disable for the organization in the sheet's ··· menu enables or disables every product of the vendor at once; the switch on a product row in the sheet's Includes tab toggles that product alone, org-wide. Disabling removes the products from the workflow editor but does not delete the stored files or credentials.
Versioning
Publish a new extension version to update the extension. The platform stores each version by
{extensionId}/{version}/, so multiple versions can coexist; the live version is the semver-max
active one (or the pinned version if your org pinned one — see
Extension version management).
Uninstalling or superseding a custom extension will break any workflows that reference its tools. Verify no workflows depend on it first.
Catalog and Execution Sequence
When the platform starts:
- Scan first-party archives — parse
manifest.jsonfrom/app/archives/(Docker image) - Scan platform archives — parse manifests from platform storage
- Scan org archives — parse manifests from each organization's storage
- Populate Manifest Catalog — metadata only, no vendor code loaded
- Publish dynamic readiness — the Runtime Plane reports whether the exact digest-pinned vendor profile has current candidate and deployment proof
- Admit on demand — the compiler exposes a custom Agent tool only when its verified SHA-256 source and exact backend pass the security floor
- Execute once — the trusted broker revalidates tenant, environment, connection, source, policy, and binding revisions, then starts a fresh
vendor_archivemicroVM
Custom extension code is never loaded on the Agentic/API host at boot. The platform reads
manifest metadata only. At execution, the active archive bytes are re-hashed, transferred through
a one-use sealed grant, unpacked inside the vendor guest, and deleted with that single-use VM.
The Agent's own agent_turn VM never receives the archive or reusable credentials.
Agent tool readiness
For an isolated Agent, a non-Scrydon tool is available only when all of these are true:
- the active archive has a verified SHA-256 identity and still matches the compiled source revision;
- the authenticated Runtime Plane supports the
vendor_archivecontract; - runner, broker, and enforcer images are digest-pinned;
- a schedulable Kata/KVM substrate is observed;
- signed exact-candidate and deployment proofs match the configured images, profile, and evidence digests;
- actual-request egress, outbound DLP, and audit are ready; and
- the encrypted one-use grant store is ready.
Any missing condition produces a typed remediation error. The platform never degrades an organization-installed Agent tool to a Worker Thread.
Organization Policies
Organization policies (configured by admins) govern custom extension behavior:
Execution Policy
{
"maxConcurrentExecutions": 10,
"maxTimeoutSeconds": 30,
"allowedCapabilities": ["llm", "stt", "tools"]
}Governance Policy
{
"allowedVendorScopes": ["scrydon", "platform"],
"blockedVendorIds": []
}blockedVendorIds— vendor IDs blocked from installing (enforced on extension install)allowedCapabilities— which capability types are permitted
The legacy allowOrgUploads / requireApprovalForUploads fields governed the removed manual
upload flow and no longer have any effect. Whether an extension-delivered extension activates on
install is governed by source signing and the explicit-install policy (see the delivery steps
above), not these fields.
Package Format
Every custom extension is packaged as a .tar.gz archive with this structure:
{extensionId}-{version}.archive.tar.gz
├── manifest.json (vendor metadata, JSON Schemas, UI definitions)
├── dist/
│ └── index.js (compiled, minified ESM; all dependencies inlined)
├── meta/ (automatically generated)
│ ├── sbom.cdx.json (CycloneDX 1.6 SBOM)
│ └── metafile.json (esbuild dependency graph)
└── assets/ (optional)
└── icon.svg, icon.png (vendor/product icons)SBOM Generation
Every custom extension artifact automatically includes a CycloneDX 1.6 Software Bill of Materials (SBOM) at meta/sbom.cdx.json. The SBOM is generated during sdk-authoring extension build with no additional tools or configuration required.
What's in the SBOM
The SBOM lists every NPM package that esbuild archived into dist/index.js:
- Package name and version — exactly what's in the archive
- License — SPDX license identifier from
package.json - Package URL (purl) — machine-readable identifier (
pkg:npm/zod@3.24.0) - Evidence — file paths proving the package is actually archived (tree-shaken code is excluded)
The SBOM only includes packages that esbuild actually archived. If a dependency was tree-shaken away, it won't appear — making the SBOM an accurate reflection of what ships in your archive.
How it's generated
During sdk-authoring extension build, the CLI:
- Runs esbuild with
metafile: trueto capture all input files - Extracts unique NPM packages from
node_modules/paths in the metafile - Reads each package's
package.jsonfor name, version, license, and description - Serializes everything to CycloneDX 1.6 JSON
- Writes the result to
meta/sbom.cdx.jsonalongside the esbuild metafile
No external tools or dependencies are required — the generation is built into the CLI.
How admins use it
After installing a custom extension, the Version & security tab of the extension's sheet shows the SBOM component count and lists all auto-detected packages alongside declared dependencies. See Custom Extension Review Before Activation for the full review workflow.
Compliance
The CycloneDX format is widely supported by compliance tools. For environments that require SPDX, convert using:
cyclonedx-cli convert --input-file meta/sbom.cdx.json --output-file sbom.spdx.json --output-format spdxjsonThe raw esbuild dependency graph is also available at meta/metafile.json for debugging. You can visualize it at esbuild.github.io/analyze.
Custom Extension Review Before Activation
After a custom extension is installed, administrators can review its contents before enabling it for the organization. The extension's sheet provides a structured review experience across its tabs.
Includes
Shows all products, tools, triggers, and runtime capabilities the custom extension provides, each with its Enabled · org-wide switch. Intelligence capabilities (LLM, STT, TTS, Embedding) display model counts and support allowlist-mode policy configuration. See Capabilities for details.
Version & security — dependencies
The dependency facts sit in Version & security, in two complementary views of the custom extension's dependency tree:
- Declared dependencies — Hand-authored entries from
defineToolkit({ dependencies: [...] })that include a human-readablereasonfor each dependency. - Auto-detected dependencies (SBOM) — The full set of NPM packages that esbuild archived into
dist/index.js, extracted frommeta/sbom.cdx.json. Each entry shows the package name, version, license, and a machine-readable package URL (purl).
The section shows the total SBOM component count so administrators can quickly gauge the custom extension's dependency footprint. Organization dependency policies (risk levels, blocklists, post-install script blocking) are evaluated against both declared and auto-detected dependencies.
If a dependency is blocked by the organization's policy, it is flagged in Version & security with the reason (e.g., "Risk 'high' exceeds max 'medium'" or "Has post-install scripts"). Blocked dependencies prevent the custom extension from being activated until the policy is adjusted or the .archive.tar.gz is rebuilt without the offending package.
Setup
Shows the vendor's authentication configuration (OAuth, API Key, Bot Token, or None) and allows administrators to configure credentials before enabling the vendor's capabilities.
Storage Layout
First-party (baked into Docker image):
/app/archives/{extensionId}/dist/index.js
Platform uploads:
{platformPrefix}/extension-archives/{extensionId}/{version}/archive.tar.gz
Org uploads:
{storageKeyPrefix}/00_organization/extension-archives/{extensionId}/{version}/archive.tar.gzTroubleshooting
Install fails with "Invalid manifest"
Run the CLI validator locally before publishing the extension to see detailed errors:
bunx @scrydon/sdk-authoring extension test --level staticCommon issues:
- Vendor ID contains uppercase or special characters
- Tool ID doesn't start with the vendor ID prefix
- Missing required fields (
id,name,version,icon,auth) - Zod schema uses unsupported types (the extractor converts Zod → JSON Schema)
Custom extension status shows "Failed"
Check the error message in the extension's sheet (and server logs). Common causes:
dist/index.jshas a syntax error- Default export is not a
defineExtension()result - Missing
export defaultstatement - Runtime import fails (ensure all deps are archived — esbuild should inline them)
Tool doesn't appear in the workflow editor
- Verify the extension that carries the extension is installed (the extension's row on Settings > Platform > Extensions > Installed) and its version is active
- Verify the product is enabled for your organization in Settings > Platform > Extensions
- Check the block's
category— it must be"tools","blocks", or"triggers"to appear in the correct palette section
Build fails with block_tools_not_manifest_safe
An installed workflow block only has its JSON-safe manifest definition, so the build refuses a block the platform would run wrongly (static check S19):
tools.config.toolortools.config.paramsis a function. A function cannot be written tomanifest.json, so the platform never ran it. Route withtools.toolSelector, send a field under another input name withcanonicalParamId, and declare typed toolinputschemas — the platform converts field text to numbers, booleans, lists and objects itself.tools.accesslists several tools and there is notools.toolSelector. Every run would call the first tool, whatever operation was picked. Map each option of the operation field to its tool.