Extensions
This page describes how Flow Steward loads, validates, renders, and invokes extensions. For an authoring walkthrough, start with Build An Extension.
Core does not bundle any extension. The authoring guide includes a small
provider-free example, and the
Example Showcase extension
(flowsteward.example-showcase, in the Extension Catalog and at
github.com/Flow-Steward/ext-example-showcase)
demonstrates every feature on this page in one bundle. The
Extension Authoring Skill helps coding
agents follow these contracts and report actual validation evidence.
Runtime Boundary
Every release declares runtime.contract_version: extension_host_v2 in
extension.yaml. Validation, install and invocation refuse a missing or
different value; there is no fallback to an older contract. Every request
envelope carries that version and the four execution-scope keys group_id,
for_each_id, step_id and step_execution_id.
Publisher identity and catalog verification carry no privilege tier. Host access
comes from supported scopes and extension-owned runtime_grants, validated
against the owning namespace. runtime_target belongs in .fs-package.yaml,
not extension.yaml; the archive validator checks it before publishing files.
Successful subprocesses exit 0 with ok: true. Domain failures exit 2 with
ok: false, nonempty error_code and error, and a nonempty errors list
whose first code/message exactly match those top-level fields. Other failures
can trip the runtime circuit breaker. See the authoring guide's
response envelope.
Flow Steward treats local extensions as out-of-process provider bundles:
- Core code discovers and validates extension contracts.
- Core code renders operator UI from the compiled UI manifest.
- Core code invokes extension commands as subprocesses.
- Core code never imports provider runtime modules into the server process.
- Provider code receives a JSON request on stdin and returns a JSON response on stdout.
This boundary keeps provider dependencies, network calls, and provider-specific behavior outside the main server process.
Main Runtime Modules
The extension platform is split across these core responsibilities:
| Area | Responsibility |
|---|---|
| Runtime manifest compile | Finds extension.yaml, rejects forbidden files, resolves refs, compiles UI, actions, queries, schemas, callbacks, and workflow triggers. |
| Runtime manifest validation | Validates kind, features, scopes, ids, refs, UI component shape, action/query bindings, callbacks, dependencies, and lifecycle readiness. |
| Descriptor validation | Validates marketplace/private import descriptors, distribution settings, runtime compatibility, artifacts, remote HTTPS endpoints, and optional Contract v2 projections. |
| Contract v2 validation | Validates builder-facing operations, connection types, step UI, resource queries, event bindings, and artifact policies. |
| Invocation | Builds query/action envelopes, starts the subprocess, limits environment and output, applies timeouts, and normalizes responses. |
| UI rendering | Renders extension pages and components from the compiled manifest and calls query/action APIs. |
| API layer | Exposes install, scan, preview, health, query, action, callback, descriptor, and lifecycle endpoints. |
Key implementation areas in the Flow Steward source tree:
core/application/extensions/
extension_descriptor.py
extension_contract_v2.py
extension_projection_v2.py
lifecycle.py
core/infrastructure/extension_runtime/
extension_runtime_manifest.py
extension_runtime_ui_manifest.py
extension_runtime_validation.py
extension_runtime_feature_validation.py
extension_runtime_compile.py
extension_runtime_invocation.py
extension_runtime_entrypoints.py
extension_runtime_platform.py
boundary.py
python_dependencies.py
extension_runtime_helpers.py
core/infrastructure/extension_sdk/public/
flowsteward_extension_sdk/
assets/frontend/src/pages/extensions/
extension-manifest-renderer.tsx
extension-manifest-utils.ts
extension-manifest-repeater-field.tsx
extension-detail-panel.tsx
api.ts
Compile Pipeline
The compiler produces the compiled runtime manifest that the UI, preview, and runtime invocation paths consume.
Compile flow:
- Resolve the bundle root and locate
extension.yamlorextension.yml.icon.pngnext to it is checked separately byvalidate,bundleand install. - Reject YAML-only violations such as
extension.json,manifest.json,schemas/*.schema.json, or JSON files underui/actions,ui/queries,ui/pages, andui/components. - Parse the manifest and normalize identity, kind, version, display fields, features, scopes, entrypoint, and healthcheck.
- Resolve schema refs. Refs must be relative to the bundle and cannot escape the bundle root.
- Resolve
ui/ui_manifest.yaml, all page refs, and all component refs. - Detect duplicate page ids and component ids.
- Resolve
ui/actions/actions.yamlandui/queries/queries.yaml. - Detect duplicate action and query ids.
- Normalize and validate
python_requirements. - Validate bundled wheels when Python requirements are declared.
- Validate component references to action and query ids.
- Validate callbacks, dependencies, event subscriptions, workflow triggers, and kind-specific feature rules.
- Validate the runtime boundary for private
core.*imports andsys.pathmutation. - Check
runtime.contract_versionagainst the host contract (extension_host_v2), the optionalegressdeclaration andlocales/. - Return a lifecycle-aware result with compile errors, validation errors, warnings, preview details, and enablement readiness.
Compiled manifest fields are intentionally restricted. The compiler only keeps
known fields such as identity, kind, version, features, scopes, schemas, UI,
actions, queries, callbacks, event subscriptions, supported events,
dependencies, python_requirements, workflow triggers, entrypoint,
healthcheck, and validation metadata.
Validation Issues
Validation issues are structured objects:
{
"severity": "error",
"code": "unknown_query_reference",
"message": "Component references an unknown query.",
"file_path": "ui/components/provider_health.yaml",
"field_path": "query_id",
"hint": "Declare the query in ui/queries/queries.yaml."
}
Use compiler issue codes when inspecting structured compiler results rather than
string-matching full messages. These are not the public CLI envelope's error
codes: extensions validate and extensions test map failed validation to
process exit 3 and validation_failed. JSON command output does not make every
compiler code available as .error.code; internal tooling exit values also
differ from public process statuses.
Common validation failures:
| Emitted code or prefix | Meaning |
|---|---|
extension.yaml_only_violation | A forbidden JSON authoring contract exists. |
extension_folder_mismatch | Folder name does not match normalized extension_id. |
ref_resolution_error | A referenced file cannot be resolved. |
unknown_query_reference | A UI binding references an undeclared query. |
workflow_visibility_required | An action lacks an explicit boolean visibility flag. |
workflow_action_requires_contract_v2 | A visible action lacks its required Contract V2 declaration. |
workflow_action_operation_mismatch | A visible action does not match its visible V2 operation. |
task_ingress_feature_required | A task provider lacks its required feature. |
invalid_notification_provider_delivery_contract | An active notification provider lacks its reserved delivery contract. |
invalid_section_type | A Contract V2 section has an invalid type; no automatic extension.contract_v2_ prefix is added. |
invalid_python_requirement* | Malformed python_requirements entries. |
python_requirement_* | Missing hashes, undeclared wheels, hash mismatch, or incomplete dependency closure. |
extension_private_core_import | Runtime code imports private core.* modules. |
extension_sys_path_manipulation | Runtime code mutates sys.path instead of using SDK/dependencies. |
missing_host_contract, unsupported_host_contract | runtime.contract_version is absent or not extension_host_v2. |
extension_icon_missing, extension_icon_invalid | icon.png is absent, not a square PNG of 32–512 px, or over 100 KB. |
egress_undeclared (warning) | No egress block; declare the hosts or egress: {}. |
missing_python_requirement_wheel, undeclared_python_requirement_wheel, python_requirement_wheel_hash_missing | A requirement has no bundled wheel with a listed hash, a wheel is not declared, or its hash is not listed. |
Runtime Manifest V2
Runtime manifests use manifest_version: 2. Supported kinds are:
task_provider
notification_provider
tool_provider
Kind-specific rules:
| Kind | Required capability |
|---|---|
task_provider | task_ingress |
notification_provider | notify |
tool_provider | tool |
Capability flags do not replace operation contracts. See the kind-specific operation tables for each task-ingress action/query, required result fields and error catalog, and the active notification provider's reserved deliver_notification action/V2 operation/effect. The placeholder delivery exemption does not bypass kind/feature validation.
The ingress_event feature is valid only on notification providers. It requires an ingress_event action declaring result.ingress_type and an authored event_subscriptions selector for external.comment.received. notification.interaction_received is recommended, not mandatory. Compilation derives supported_events; authors must not write that projection directly.
Python Dependencies
Extension subprocess dependencies are declared in the runtime manifest and can also appear in the distribution descriptor:
python_requirements:
- name: attrs
version: "25.3.0"
hashes:
- sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
The validator normalizes and enforces:
| Rule | Enforcement |
|---|---|
| List shape | python_requirements must be a list of objects. |
| Package name | PEP 503-normalized lowercase name. |
| Version | Exact PEP 440 version, not a range or wildcard. |
| Hashes | Non-empty list of lowercase sha256: hashes. |
| Closure | Wheel metadata must not reference undeclared or incompatible transitive dependencies. |
| Wheels only | Bundled artifacts under wheels/ must be .whl files. |
| Offline default | With FS_EXTENSION_DEPS_FETCH=off, every declared requirement must have a matching bundled wheel and hash. |
extensions lock-deps resolves requirements.in with binary wheels for each
target platform (default manylinux2014_x86_64,manylinux2014_aarch64) and
writes the full closure with every wheel hash into extension.yaml.
extensions bundle --with-wheels downloads each locked requirement for each
platform into wheels/ with --require-hashes and stops when a platform has no
wheel whose hash is locked. The tooling interpreter (Python 3.12 in the
Development tool runtime) sets the Python version and ABI; the server runtime is
CPython 3.12 on Debian (glibc 2.36), amd64 or arm64.
What each stage enforces about architectures:
| Stage | Checks |
|---|---|
lock-deps, bundle --with-wheels | Every requirement resolves/downloads for every platform passed (both by default). --platforms active checks only the tool runtime's own architecture. |
extensions validate | Files in wheels/ are declared wheels with listed hashes, the closure is complete, and each requirement has at least one bundled wheel. It does not check that both x86_64 and aarch64 wheels are present. |
| Install and update | A wheel compatible with the server's own Python and architecture exists for every requirement; otherwise No compatible wheel for python requirement …. |
Archives are limited to 50 MiB zipped and 150 MiB unpacked (packager and
installer). bundle --runtime-target linux-amd64|linux-arm64 writes a
per-architecture archive that keeps only that architecture's wheels and records
the target in .fs-package.yaml; a server refuses an archive for the other
target. Native executables under bin/<runtime-target>/ always need these two
archives. Step-by-step instructions, including vendoring with plain
pip download, are in
Python Dependencies And Wheel Distribution.
During install or update, Flow Steward materializes selected compatible wheels into:
.fs-python/<extension-version>/site/
The host Python environment is not modified. Obsolete dependency directories are removed after versioned updates.
Runtime dependency settings:
| Setting | Default | Description |
|---|---|---|
FS_EXTENSION_DEPS_FETCH | off | off requires bundled wheels. allowlist permits install-time wheel fetches from the default index when hashes match. |
FS_EXTENSION_DEPS_SITE_MAX_BYTES | 500 MiB | Maximum size for the materialized site directory, clamped between 1 MiB and 10 GiB. |
Descriptor Model
extension-descriptor.yaml is the distribution and import descriptor. It is
validated separately from the runtime manifest.
Root descriptor fields:
| Field | Description |
|---|---|
schema_version | Descriptor schema version, for example extension_descriptor_v1. |
extension_id | Stable id. Must match the runtime manifest. |
release_version | Release version of the distributable artifact. |
extension_type | Listing/import classification. |
identity | Display name, description, tags. |
vendor | Vendor name and support URL. |
listing | Marketplace visibility, category, and extra listing metadata. |
distribution | Channel, execution modes, self-hosting support, and import sources. |
runtime | Runtime contract version, compatibility, auth, local or remote settings, and optional Contract v2. |
python_requirements | Optional dependency lock validated with the same rules as the runtime manifest. |
dependencies | External dependency metadata. |
artifacts | Workflows, agents, skills, tools, contracts, guardrails, and nested extension artifacts. |
provenance | Origin and supply-chain metadata. |
commerce | Billing and plan metadata. |
Descriptor models are strict. Root and most nested blocks reject extra fields unless the block is intentionally metadata-like, such as listing or commerce.
Distribution channels:
marketplace_managed
private_byo
Execution modes:
local
remote_https
When remote_https is declared, runtime.remote must include an HTTPS
base_url and endpoint paths for handshake, health, query, action, and event
emission. Endpoint values must be absolute paths and must not contain path
traversal.
Contract V2
Extension Contract v2 is the builder-facing contract layer. It is used to build registry projections and workflow builder surfaces.
Sections:
| Section | Validates |
|---|---|
connection_types | Connection ids, auth types, OAuth2 details, token placement, and secret names. |
dataset_connectors | Dataset connector ids, runtime kind, and data contract refs. |
operation_manifest | Operation ids, kinds, inputs, outputs, connection refs, batching, async support, and failure behavior. |
step_ui_manifest | Builder UI fields and their operation/query references. |
resource_query_manifest | Resource picker query ids, cursor modes, filters, and result shapes. |
event_bindings | Trigger ids, operation refs, and canonical event names. |
artifact_policies | Operation artifact inputs, outputs, retention, and naming policy. |
Allowed operation kinds:
source, action, bulk_action, report, human_channel
Allowed connection auth types:
none, api_key, oauth2, static_secret
OAuth2 connections require an authorization-code grant, PKCE marked as required, HTTP(S) authorization and token endpoints, at least one required scope, platform secret client credentials, token placement in secrets, and access token secret names.
Contract refs can be inline objects, local refs, URL refs that stay on the same origin and path scope, or snapshot refs. Local refs cannot escape the bundle.
UI Manifest Runtime
The frontend renders extension UI from the compiled manifest. The primary
renderer lives in the extension manifest renderer and utility files under
assets/frontend/src/pages/extensions.
Rendered page flow:
- The extension detail page loads the compiled manifest or preview.
- The manifest renderer lists pages from
ui_manifest.pages. - Each page renders its components.
- Query-bound components call extension query endpoints.
- Action-bound components call extension action endpoints.
- Successful actions can request component refresh through the
reloadfield.
Component ids are important because they are used for query context, action context, and targeted reload.
Component Validation
The validator recognizes these component types:
section, columns, stack, action_bar, form, fieldset, repeater, key_value,
json_editor, secret_field, grid, stats_row, key_value_card, markdown,
health_panel, log_list, empty_state, alert, confirm_action, drawer_detail,
status_badge, filter_bar, callback_setup
Form widgets:
text, textarea, url, email, number, integer, boolean, select, radio_group,
checkbox_group, tag_picker, secret, json, key_value, repeater, event_selector,
project_selector, workflow_selector, provider_selector
Grid column types:
text, datetime, date, number, badge, boolean, json_summary, link, id
The frontend implements the common extension components directly, including forms, grids, stats rows, health panels, callback setup, markdown, log lists, and repeater form fields. Other validated component types are available as contract vocabulary and may need frontend implementation in the target version.
Query And Action Invocation
The platform has separate invocation paths for queries and actions.
Query endpoint:
POST /api/extensions/{extension_id}/query/{query_id}
Action endpoint:
POST /api/extensions/{extension_id}/action/{action_id}
The frontend action payload includes:
| Field | Description |
|---|---|
component_id | Component that initiated the action. |
page_id | Current extension page id. |
context | Page, project, workflow, or component context. |
target | Row, connection, callback, or selected resource target. |
values | Current UI values. |
data | Additional component data. |
The backend normalizes this into the runtime action.input envelope before
launching the extension subprocess.
Public Extension SDK
The public SDK is exported on the subprocess PYTHONPATH as
flowsteward_extension_sdk. Extension runtime code may import this namespace,
but must not import private core.* modules.
Public SDK surface:
The package is flowsteward-extension-sdk 0.4.0.
| Module area | Exports |
|---|---|
| Runtime context | RuntimeContext, RuntimeResources, runtime_context_from_payload, HOST_CONTRACT_VERSION, EXECUTION_SCOPE_FIELDS |
| Safe HTTP | assert_safe_remote_http_url, assert_safe_remote_https_url, resolve_pinned_ips, build_pinned_opener, open_pinned_url, parse_retry_after, Request, PinnedPeerError |
| Artifact and dataset access | read_artifact_bytes, write_artifact_bytes, stream_artifact_bytes, write_artifact_stream, artifact_to_local_path, read_dataset_bytes, write_dataset_bytes, stream_dataset_bytes, write_dataset_stream, dataset_to_local_path, find_artifact_descriptor, find_dataset_descriptor, artifact_descriptors_from_payload, dataset_descriptors_from_payload, ArtifactAccessError |
| Artifact/dataset plane | ArtifactDatasetPlane, ArtifactHandle, DatasetHandle, artifact_plane_from_payload, create_storage_adapter, LocalFilesystemStorageAdapter, InMemoryObjectStorage, S3ObjectStorageAdapter |
| Progress | report_progress (message and done/total on the running step in Job Details) |
| Host mutations and migrations | host_mutation, MigrationEnvelope, migration_result |
| Remote runtimes | handshake_result |
| Testing | ExtensionWorkflowHarness, resolve_integration_contract, IntegrationManifest, IntegrationOperation |
Example:
from flowsteward_extension_sdk import open_pinned_url, runtime_context_from_payload
def provider_health(request_payload: dict) -> dict:
ctx = runtime_context_from_payload(request_payload)
return {
"ok": True,
"result": {
"project_id": ctx.project_id,
"safe_http_available": callable(open_pinned_url),
},
}
The runtime boundary validator scans extension Python files, entrypoint inline
commands, and health inline commands. It rejects private core.* imports,
dynamic private core imports, and sys.path manipulation. The current
grandfathered exception set is empty. Use the SDK and python_requirements
for every extension.
Subprocess Isolation
Local extension subprocesses receive a limited environment. Host database DSNs, encryption keys, and unrelated process environment values are not passed to the extension.
Allowed inherited environment names include common runtime variables such as:
PATH, HOME, USER, LANG, LC_ALL, LC_CTYPE, TMPDIR, TMP, TEMP,
PYTHONDONTWRITEBYTECODE, PYTHONUNBUFFERED, NODE_PATH, NODE_ENV,
FS_ALLOW_PRIVATE_REMOTE_URLS
PYTHONPATH is not inherited. The platform constructs it for each invocation:
<extension bundle>/.fs-python/<version>/site:<public extension SDK path>
The dependency site directory is present only when python_requirements were
materialized for that extension version. The SDK path is always appended.
Subprocess controls:
| Control | Default or behavior |
|---|---|
| Timeout | Uses operation timeout or defaults to 30 seconds. |
| Output cap | Controlled by FS_EXTENSION_OUTPUT_MAX_BYTES; default is 5 MiB, with safety min and max bounds. |
| Circuit breaker | Tracks failures per extension and quarantines repeatedly failing providers. |
| Concurrency | Per-extension concurrency is capped. |
| I/O | One JSON request on stdin and one JSON response on stdout. |
If the subprocess exceeds output limits, times out, exits with invalid JSON, or omits the response envelope, the platform returns a normalized extension error.
Response Normalization
The canonical response envelope is:
{
"ok": true,
"result": {},
"commands": [],
"events": [],
"reload": [],
"errors": []
}
Normalization rules:
- Missing
okis treated as an invalid response. resultis normalized to an object when possible.commands,events,reload, anderrorsare normalized to lists.- Response events are checked against
supported_eventswhen the extension declares supported event types. - Mutating action responses are checked against the declared command type.
Known command types:
start_workflow, pause_job, resume_job, cancel_job, retry_step,
request_approval, resolve_approval, enqueue_followup_job,
invoke_extension_action, update_external_status
Event Model
There are three event layers developers should keep separate:
| Layer | Examples | Purpose |
|---|---|---|
| Workflow reaction events | job.completed, job.step_failed, job.group_completed, job.approval_waiting | Built-in workflow lifecycle signals. |
| Contract v2 canonical events | notification.requested, approval.received, interaction.received | Builder-facing event vocabulary. |
| Provider external events | external.comment.received, external.task.created | Extension-owned provider events. |
Runtime event envelopes can carry event id, type, correlation id, occurrence time, account id, severity, summary, payload, project/workflow/job/group/For each/step scope, source, aggregate id, actor, URLs, and metadata.
Emitted events must match the compiler-derived supported_events projection.
Authors declare delivery selectors through event_subscriptions, not by adding
supported_events to extension.yaml.
Callback Runtime
Supported auth modes are none, shared_secret_header, and hmac_sha256.
The HMAC option has a fixed timestamp/delivery-ID/raw-body contract, bounded
freshness, and replay detection; see the
authoring callback contract.
OAuth authorization callbacks follow their separate host-owned flow.
Callbacks bridge provider webhooks into extension actions.
Manifest callback declaration:
callbacks:
- callback_id: provider_events
purpose: Receives provider interaction events.
methods: [POST]
action_id: ingress_event
connection_scoped: true
auth:
type: shared_secret_header
header: X-Provider-Signature
Callback management endpoints:
GET /api/extensions/{extension_id}/callbacks
POST /api/extensions/{extension_id}/callbacks/{callback_id}
POST /api/extensions/{extension_id}/callbacks/{registration_id}/rotate
POST /api/extensions/{extension_id}/callbacks/{registration_id}/status
Inbound callback delivery is ultimately routed to the declared action_id.
Use a callback_setup component to expose setup and status in the extension UI.
API Surface
Main extension endpoints:
GET /api/extensions/capabilities/archive-lifecycle
POST /api/extensions/install
POST /api/extensions/scan
GET /api/extensions/{extension_id}/preview
GET /api/extensions/{extension_id}/compiled-manifest
GET /api/extensions/{extension_id}/health
POST /api/extensions/{extension_id}/query/{query_id}
POST /api/extensions/{extension_id}/action/{action_id}
POST /api/extensions/{extension_id}/invoke
POST /api/extensions/{extension_id}/enable
POST /api/extensions/{extension_id}/disable
GET /api/extensions/{extension_id}/uninstall-plan
POST /api/extensions/{extension_id}/uninstall {"revision":"<plan-revision>"}
Descriptor and import endpoints:
POST /api/extensions/descriptors/validate
POST /api/extensions/descriptors/import/file
POST /api/extensions/descriptors/import/url
These descriptor routes accept JSON, not an extension manifest or archive upload. Validation accepts source_type (file, url, builtin, or payload). File/URL/payload modes use the corresponding descriptor_path, descriptor_url, or descriptor_payload; builtin mode uses a descriptor payload or path with optional source_root, not a promise of a bundled showcase. File import takes a server-visible descriptor_path; URL import takes descriptor_url. Import requests may include install_after_import, runtime_mode and remote_base_url under the supported request schema. All remain subject to caller authorization and source policy.
Archive installation is POST /api/extensions/install with multipart/form-data, required file, and optional approved_browser_authority_impact_hash for a reviewed authority impact. The same request updates an installed extension when the archive has a higher version; the same version or a lower one is refused. The UI's Install extension from file uses this endpoint. On Compact the application forwards the archive to its installer service, which owns the extensions folder, so the UI and this API are the install paths there; GET /api/extensions/capabilities/archive-lifecycle reports whether archive install is available. Manifest compilation and ZIP bundling are authoring tooling operations, not bare /api/extensions/validate, /build, or /import endpoints. Descriptor validation does not replace compiler validation.
Marketplace and installation endpoints:
GET /api/extensions
GET /api/extensions/catalog
GET /api/extensions/catalog/availability
POST /api/extensions/catalog/install
GET /api/extensions/marketplace
GET /api/extensions/{extension_id}
GET /api/extensions/installs/{install_id}
POST /api/extensions/releases/{release_id}/install
POST /api/extensions/installs/{install_id}/handshake
POST /api/extensions/{extension_id}/install
POST /api/extensions/runtime/token
POST /api/extensions/generated-jobs
CLI Reference
Authoring commands:
./flow-steward extensions create <extension_id>
./flow-steward extensions validate <extension_id>
./flow-steward extensions test <extension_id>
./flow-steward extensions lock-deps <extension_id>
./flow-steward extensions bundle <extension_id> --with-wheels
Operator commands (Development and Scalable; on Compact use the UI or the install API):
./flow-steward extensions list
./flow-steward extensions show <extension_id>
./flow-steward extensions install --archive <path.zip>
./flow-steward extensions enable <extension_id> --extension-version <version>
./flow-steward extensions health <extension_id>
./flow-steward extensions disable <extension_id> --extension-version <version>
./flow-steward extensions uninstall <extension_id>
extensions install --archive also updates an installed extension to a higher
version.
For noninteractive authoring, explicitly confirm local changes with --confirm on create, lock-deps and bundle. The reviewed native CLI returns review exit 10 for unconfirmed changes and 0 for success; installed launcher dispatch can have different confirmation/error boundaries. Record the actual context and status rather than treating any nonzero code as success.
Use the explicit validate → test → bundle sequence, running each command separately and stopping after any nonzero exit. Bundling is packaging, not full compiler validation: an invalid manifest can still be packaged. Validate the unpacked final archive as well when verifying a published deliverable. validate --marketplace checks manifest vendor/listing/scopes/machine-auth/account-scope metadata, not descriptor-schema validity; ordinary validation also checks some descriptor-referenced artifact modules.
Default testing requires requirements-dev.txt, validates the manifest and runs smoke/local pytest targets in an environment inheriting the tooling interpreter's packages. Development dependency installation is offline (--no-index, dev-wheels/wheels); missing requirements or unavailable dependencies fail. The generated scaffold has a comment-only requirements-dev.txt and one pytest file, and needs neither wheel directory. Missing pytest targets fail by default; --allow-no-pytest permits their absence explicitly, and --skip-pytest skips them. Neither proves regression coverage. Internal test tooling returns 1 for failed checks and 2 for a missing bundle or unavailable dedicated test runtime; the public CLI wraps those failures as exit 3 with validation_failed. extensions test has no --confirm. Do not substitute private tooling modules or a host interpreter for the supported Development launcher.
Useful options:
./flow-steward extensions validate <extension_id> --json
./flow-steward extensions validate <extension_id> --health
./flow-steward extensions validate <extension_id> --marketplace
./flow-steward extensions test <extension_id> --skip-pytest
./flow-steward extensions lock-deps <extension_id> --input requirements.in
./flow-steward extensions lock-deps <extension_id> --platforms active
./flow-steward extensions bundle <extension_id> --extension-version 0.2.0
./flow-steward extensions bundle <extension_id> --with-wheels
./flow-steward extensions bundle <extension_id> --with-wheels --platforms manylinux2014_x86_64,manylinux2014_aarch64
./flow-steward extensions bundle <extension_id> --runtime-target linux-arm64
For a bundle that actually includes a distribution descriptor, descriptor validation can also be called directly from Python in the Development toolchain:
python - <<'PY'
from core.application.extensions.lifecycle import validate_extension_descriptor_file
result = validate_extension_descriptor_file(
"extensions/com_acme_crm_sync/extension-descriptor.yaml"
)
print(result["valid"])
print(result.get("extension_contract_v2", {}))
PY
Scripts Versus Tests
Smoke scripts and pytest tests are both first-class extension checks.
| Check | Runs how | Best for |
|---|---|---|
scripts/smoke_check.py | Direct Python execution, also called by extensions test. | Fast proof that health, representative queries, and representative actions work. |
tests/test_*.py | Pytest, also called by extensions test. | Regression coverage for compiler output, descriptor validation, Contract v2, ingress events, OAuth secrets, external request safety, and response shapes. |
Smoke scripts should be short and close to the package. Tests should be specific, assert on stable contract fields, and cover behavior that could regress during platform or extension changes.
Debugging Checklist
When an extension does not compile or render correctly:
- Confirm the folder name matches the normalized
extension_id,icon.pngis present andruntime.contract_versionisextension_host_v2. - Run validation with
--jsonand inspect issue codes. - Check for forbidden JSON authoring files.
- Confirm every ref is relative, YAML, inside the bundle, and present.
- Confirm every UI query or action binding points to a declared operation id.
- Confirm form action bindings are under
data. - Confirm mutating actions declare
command_type. - Confirm runtime files import
flowsteward_extension_sdk, not privatecore.*modules. - Confirm runtime files do not mutate
sys.path. - If
python_requirementsis present, confirmwheels/contains matching hashes for every requirement, with an_x86_64and an_aarch64wheel (or oneanywheel) for each. - Confirm response envelopes include
ok. - Confirm authoring uses
event_subscriptionsand emitted events match the compiledsupported_eventsprojection. - Run the smoke script directly, then run pytest for the extension.
Platform Design Rules
- Keep provider-specific integrations in the extension runtime.
- Use declared commands for host-side mutations.
- Keep platform mutations auditable through action manifests and response commands.
- Keep credentials in secrets, not config, manifests, logs, or UI responses.
- Keep platform primitives in
flowsteward_extension_sdk, not private core imports. - Keep third-party runtime libraries in
python_requirements, not manualsys.pathmanipulation or runtimepip install. - Treat action, query, trigger, connection type, operation, resource query, and artifact ids as stable public contracts.
- Add validation or pytest coverage before broadening the extension contract.