Lewati ke konten

describe.json Manifest

{/* AUTO-GENERATED by scripts/sync-reference-schemas.mjs - do not edit by hand. */}

Source schema: ../greentic-designer-sdk/crates/greentic-extension-sdk-contract/schemas/describe-v2.json

Schema id: https://store.greentic.cloud/schemas/describe-v2.json

describe.json is the manifest every Greentic extension ships. It is the source of truth for the extension’s identity, the capabilities it offers and requires, the WASM components it loads, and what it contributes to the Designer. gtdx validates it against this JSON Schema, and the publisher’s Ed25519 signature is written back into the same file.

This schema covers the 5 Designer extension kinds (DesignExtension, BundleExtension, DeployExtension, ProviderExtension, AddonExtension). Artifacts of kind wasix:mcp/router are validated against a separate manifest schema, ../greentic-designer-sdk/crates/greentic-extension-sdk-contract/schemas/describe-mcp-v1.json, which declares a different required set and a different secret-requirement field. Do not read this page as describing that kind.

The top-level object sets additionalProperties: false, so any field not listed below is rejected.

FieldRequiredTypeDescription
$schemanostringMust be exactly https://store.greentic.cloud/schemas/describe-v2.json. gtdx lint reports E_SCHEMA_HOST for any other value (including the legacy store.greentic.ai host) and for a missing key.
apiVersionyesgreentic.ai/v2Describe-contract discriminator, fixed at greentic.ai/v2. A designer older than 1.2.0 does not understand this contract and skips the extension at boot, so it never appears in /api/extensions and nothing in the logs points at the version.
kindyesDesignExtension, BundleExtension, DeployExtension, ProviderExtension, AddonExtensionWhich extension family this artifact belongs to; selects the on-disk install directory (design, bundle, deploy, provider, addon). wasix:mcp/router is deliberately absent from this enum: those artifacts are validated against describe-mcp-v1.json instead. AddonExtension is a component the platform provisions and reconciles as declarative infrastructure (a Qdrant, a Redis) rather than a flow-time or design-time extension; it declares itself through contributions.addons[] like any other kind, but its runtime component implements greentic:extension-addon@0.1.0’s addon-extension world instead of a design/bundle/deploy/provider one.
compatyescompatMinimum designer/runner versions plus the literal contract version the descriptor was authored against. Parsed eagerly, so an invalid descriptor fails at deserialize time rather than when an installer tries to match.
metadatayesmetadataIdentity and catalogue information for the extension. metadata.id and metadata.version together form the identity key (<id>@<version>) the store and installer address the artifact by.
enginenoengineDeprecated. gtdx lint rejects any describe that still carries this block (E_ENGINE_DEPRECATED); compat.min_designer_version / compat.min_runner_version are the sole source of version constraints. Still accepted here so pre-deprecation artifacts keep validating.
capabilitiesyescapabilitiesCapability contracts this extension offers to others and requires from its host. An id that appears in both lists is a self-cycle and is rejected by gtdx lint (E_CAP_CYCLE).
runtimeyesruntimeThe WASM components that back this extension, plus the sandbox limits and host permissions they run under.
executionnoobjectBundleExtension dispatch config (builtin vs wasm). Ignored for other kinds.
contributionsyesobjectWhat the extension adds to the designer. This schema lists contributions as required; the sibling describe-mcp-v1.json omits it entirely, because kind: wasix:mcp/router artifacts carry no contributions - a router’s tools are discovered at runtime via list-tools. The conditionality therefore lives in the two-schema split, not in a conditional inside v2.
localizationnoobjectTop-level translation table { default_locale, strings }, where strings maps a flat key (e.g. node.adaptive_card.label) to a per-locale string map. Designer reads this when a localized string does not carry inline locales.
signaturenosignatureDetached ed25519 signature over this describe. The signed payload is the RFC 8785 (JCS) canonicalization of this document with signature removed, so signing is idempotent and independent of key order or serde version. It does not cover the rest of the archive - that binding is manifestSha256.
manifestSha256nostringSHA-256 (lowercase hex) of the canonical manifest.json. Binds the whole-archive ledger into the signed describe (audit C2/H7). Optional only for backward compatibility during migration; production packs MUST set it. Pattern: ^[0-9a-f]{64}$.
requiredSecretsnoarray of secretRequirementCanonical list of credential secrets the operator must supply before this extension can function. This is the v2 spelling; kind: wasix:mcp/router artifacts instead emit snake_case secret_requirements, which only describe-mcp-v1.json accepts. Both schemas set additionalProperties: false, so each rejects the other’s field name.

Version floors the extension declares against the Designer, the runner, and the extension contract itself. Referenced as #/$defs/compat.

FieldRequiredTypeDescription
min_designer_versionyesstringSemver range for the oldest designer that can load this extension - the contract floor (>=1.2.0 for v2), NOT the version of the SDK that generated the artifact. Those are different axes: an extension built by SDK 1.3.x still loads on any designer that speaks v2, so pinning this to the generating SDK would declare a constraint far stricter than the extension actually has.
min_runner_versionyesstringSemver range for the oldest Greentic runner this extension supports, e.g. ^1.2.0. Parsed as a version requirement, not an exact version.
contract_versionyesstringExact semver version of the describe contract this artifact was authored against. This is the field that tracks the SDK, unlike min_designer_version, and it is parsed as a concrete version rather than a range. Pattern: ^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$.

Identity and store-listing fields. Referenced as #/$defs/metadata.

FieldRequiredTypeDescription
idyesstringReverse-DNS extension id, e.g. greentic.telco-x. gtdx lint enforces a stricter form than the pattern here: E_ID_PATTERN requires ^greentic\.[a-z0-9][a-z0-9-]*$. Pattern: ^[a-z][a-z0-9.-]*\.[a-z0-9.-]+$.
nameyesstringHuman-readable display name shown in the store and designer catalogue. Must be non-empty. Minimum length: 1.
versionyesstringExact semver version of this extension release, e.g. 1.3.0-research.2. Pattern: ^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$.
summaryyesOne-line summary used in catalogue listings. Deliberately untyped: it accepts either a plain string or a localized object { default, locales }.
descriptionnoLong-form description. Same shape as summary - a plain string or { default, locales }.
authoryesobjectPublisher of the extension. Only name is required.
licenseyesstringLicense identifier for the extension, e.g. MIT or Apache-2.0. Required, but neither this schema nor gtdx lint checks the value against the SPDX list.
homepagenostringFormat: uri.
repositorynostringFormat: uri.
keywordsnoarray of string
iconnostringPack-relative path to the extension icon, e.g. assets/icon.svg. gtdx new --icon and gtdx publish write this field after copying the file into assets/; accepted formats are svg, png, jpg, jpeg and webp, capped at 1 MiB to match the store-server icon limit.
screenshotsnoarray of string
FieldRequiredTypeDescription
nameyesstring
emailnostring
publicKeynostring

Optional engine pins. Referenced as #/$defs/engine.

FieldRequiredTypeDescription
greenticDesigneryesstringDeprecated together with the rest of the engine block; superseded by compat.min_designer_version. Semver range for the designer, e.g. >=1.2.0. gtdx doctor reads it as the designer bound on a v1 describe.
extRuntimeyesstringDeprecated together with the rest of the engine block. Semver range for the extension runtime, e.g. ^1.2.0. Not interchangeable with compat.min_runner_version, which bounds the Greentic runner - shipped artifacts set the two to different ranges.

Capabilities the extension offers to the platform and requires from it. Referenced as #/$defs/capabilities.

FieldRequiredTypeDescription
offerednoarray of capRefCapability contracts this extension provides to others, e.g. greentic:guardrail/topic. gtdx publish requires each version here to parse as an exact semver, and dropping an entry without a version bump is flagged by gtdx lint (W_DESCRIBE_DIFF_BREAKING).
requirednoarray of capRefCapability contracts this extension needs its host or another extension to provide. An id present in both required and offered is a self-cycle and is rejected by gtdx lint (E_CAP_CYCLE).

Each entry of capabilities.offered and capabilities.required is a capability reference.

FieldRequiredTypeDescription
idyesstringCapability id in <namespace>:<path> form, e.g. greentic:guardrail/topic. The namespace is the segment before the first colon. Pattern: ^[a-z][a-z0-9-]*:[a-z][a-z0-9/._-]*$.
versionyesstringVersion constraint for the capability, parsed as a semver requirement. Parsing fails closed - a malformed string is an error, never a silent match-everything. Entries under capabilities.offered are additionally required by gtdx publish to be an exact version such as 1.0.0.
deprecatednoDeprecation marker { since, replaced_by?, removal_in? }. Designer renders a warning chip in the palette; the runner refuses to install once the current contract version is past removal_in.

The WASM components the extension loads and the host permissions they may use. Referenced as #/$defs/runtime.

FieldRequiredTypeDescription
memoryLimitMBnointegerMemory ceiling for the extension’s components. Defaults to 64 when omitted. The [1, 1024] bound is enforced twice - by this schema and again by the Rust deserializer - so a document that skips schema validation still cannot carry 0 or a multi-gigabyte value. Minimum: 1. Maximum: 1024.
permissionsyesobjectHost permissions the extension requests. gtdx install prints the network, secrets and cross-extension requests and asks for confirmation before installing, unless the install was pre-approved (--yes / CI).
componentsyesobject of runtimeComponentThe WASM components the extension ships, keyed by component id - a kebab-case identifier limited to lowercase letters, digits, -, _ and .. At least one entry is required, and every runtime_ref under contributions.nodeTypes and contributions.tools must name a key that exists here (gtdx lint reports a dangling one as E_RUNTIME_REF). Minimum properties: 1.
FieldRequiredTypeDescription
networknoarray of stringURL patterns the extension may reach, e.g. https://api.example.com/*. gtdx publish requires https://, with one exception: plain http:// is accepted for loopback hosts (127.0.0.1, localhost, [::1]) only. This mirrors the extension runtime, which honours plain http for loopback hosts and drops non-loopback http patterns.
secretsnoarray of stringSecret keys the extension declares it needs to read. Listed in the gtdx install consent prompt.
callExtensionKindsnoarray of stringExtension kinds this extension may call into. Surfaced in the gtdx install consent prompt as a cross-extension request.
llmRolesnoarray of stringLLM roles (wire names, e.g. sorla_composer) this extension may request from the host greentic:extension-host/llm import.
oauthProvidersnoarray of stringOAuth provider ids (e.g. hubspot) this extension may request tokens for via the host greentic:oauth-broker/broker-v1 import. The host rejects get-token for any provider not listed here.
uinoobjectGrants that apply to browser-executed view code, not to the WASM guest. Kept separate from network on purpose: network authorises http.fetch from inside the guest, where the caller is the extension’s own logic, while these authorise requests a human clicking in a browser can trigger, whose responses land in browser-executed code.

Each key of runtime.components is a component id; each value has this shape.

FieldRequiredTypeDescription
oci_refnostringOCI reference to the component image, e.g. ghcr.io/greentic/my-ext:0.1.0. The preferred distribution channel; gtpack is the offline fallback.
gtpacknoobjectOffline fallback payload shipped inside the archive, shaped { file, sha256, pack_id, component_version }. The nested sha256 is validated as lowercase hex at parse time, so an uppercase digest passes this untyped schema yet fails on load.
sha256yesstringLowercase-hex SHA-256 of the component artifact; uppercase hex is rejected. gtdx lint --publish additionally rejects the all-zeros placeholder that scaffolds ship with (E_SHA256_ZERO). Pattern: ^[0-9a-f]{64}$.
worldyesstringWIT world the component exports, e.g. greentic:extension-design/guardrail@0.3.0.

What the extension adds to the Designer. Most contribution arrays are declared as untyped arrays in this schema, so their element shapes are not constrained here; gtdx lint checks the cross-field invariants that the schema cannot express.

FieldRequiredTypeDescription
nodeTypesnoarrayPalette entries contributed by a design extension. Each entry’s optional runtime_ref is what the designer’s flow compiler reads, rather than the pinned ref in flow_generator/catalog.baseline.yaml.
dwProvidersnoarrayDW Composer provider cards the extension offers. Mirrors the fields the designer’s /api/dw/providers serves, so an installed extension can publish a provider choice (e.g. an LLM backend) without the designer carrying a static catalogue entry for it.
toolsnoarrayDesign-time tools the extension exposes. A tool’s runtime_ref selects a single component to dispatch through; if absent the runtime selects the only component declared in runtime.components. gtdx lint requires each export to be a fully-qualified greentic:extension-design/<interface>.<member> reference (E_EXPORT_FORM), never a bare member name.
recipesnoarrayPackaging recipes contributed by a bundle extension. Each carries an id, a localized display_name, an optional description, and a config_schema.
knowledgenoarrayKnowledge-base artifacts shipped with the extension. Each entry is a { path } object whose path is relative to the gtxpack root.
promptsnoarrayMarkdown prompt fragments shipped with the extension. Each entry is a { path } object whose path is relative to the gtxpack root.
schemasnoarrayJSON Schema documents shipped with the extension. Each entry is a { path } object whose path is relative to the gtxpack root.
guardrailsnoarray of objectContent-moderation guardrails contributed by the extension.
viewsnoarray of objectUI pages the extension contributes to a host surface. The page’s assets ship in the pack under assets/views/<id>/; the host serves them and renders entry in a sandboxed iframe with an opaque origin. What the page may reach is declared once in runtime.permissions.ui, not per view.
addonsnoarray of objectManaged services this extension offers to an environment - Qdrant, Redis, Postgres. Catalogue metadata only: the platform provisions the workload, and the addon declares what it needs. Secrets never appear in desired_state_schema; they reach the addon through its runtime binding, because a secret in desired state can never be read back and so diffs forever.
connection_testnoobjectOptional self-test the extension declares so a consumer (designer, gtdx, store) can verify a live connection or credential by invoking one of the extension’s own tools. Snake-case on the wire, unlike the camelCase siblings in this block - that spelling matches how extensions and the designer already read contributions.connection_test.
FieldRequiredTypeDescription
exportyesstringFully-qualified WIT export implementing the guardrail, e.g. greentic:extension-design/guardrail.evaluate.
runtime_refnostringThe runtime component (by its key in runtime.components) that provides the guardrail. Absent means the runtime selects the sole declared component.
FieldRequiredTypeDescription
toolyesstringName of the contributed tool (matching a contributions.tools[].name) to invoke to exercise the connection.
argsnoobjectArguments to pass to the tool. Absent is treated as {}.

Each entry of the top-level requiredSecrets array. Referenced as #/$defs/secretRequirement.

FieldRequiredTypeDescription
keyyesstringNamespaced secret key, e.g. tavily/api_key.
requirednobooleanWhether the secret is mandatory for the extension to function.
descriptionnostringHuman-readable description of what this secret is used for.
formatnobytes, text, jsonExpected encoding of the secret value.
scopenoobjectScope hints: env, tenant, team.
schemanoobjectJSON Schema describing the secret value structure (used when format=json).
examplesnoarray of stringExample values or placeholder strings for operator UX.

The publisher’s detached signature, written back into describe.json in place by gtdx sign. Referenced as #/$defs/signature.

FieldRequiredTypeDescription
algorithmyesed25519Signature algorithm. ed25519 is the only algorithm the contract implements; any other value is rejected.
publicKeyyesstringBase64-encoded 32-byte ed25519 public key of the signer (44 characters). An ed25519: prefix is stripped if present. This is the key verification uses, and the one gtdx keygen tells authors to distribute here.
valueyesstringBase64-encoded 64-byte ed25519 signature over the JCS canonicalization of this document with signature removed. An ed25519: prefix is stripped if present.
keyIdnostringOptional label naming which publisher key signed, carried through from gtdx publish --key-id. Advisory only - verification uses publicKey and never reads this field.
Full schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://store.greentic.cloud/schemas/describe-v2.json",
"title": "Greentic Designer Extension describe.json v2",
"type": "object",
"required": [
"apiVersion",
"kind",
"compat",
"metadata",
"capabilities",
"runtime",
"contributions"
],
"additionalProperties": false,
"properties": {
"$schema": {
"description": "Must be exactly `https://store.greentic.cloud/schemas/describe-v2.json`. `gtdx lint` reports `E_SCHEMA_HOST` for any other value (including the legacy `store.greentic.ai` host) and for a missing key.",
"type": "string"
},
"apiVersion": {
"description": "Describe-contract discriminator, fixed at `greentic.ai/v2`. A designer older than 1.2.0 does not understand this contract and skips the extension at boot, so it never appears in `/api/extensions` and nothing in the logs points at the version.",
"const": "greentic.ai/v2"
},
"kind": {
"description": "Which extension family this artifact belongs to; selects the on-disk install directory (`design`, `bundle`, `deploy`, `provider`, `addon`). `wasix:mcp/router` is deliberately absent from this enum: those artifacts are validated against describe-mcp-v1.json instead. `AddonExtension` is a component the platform provisions and reconciles as declarative infrastructure (a Qdrant, a Redis) rather than a flow-time or design-time extension; it declares itself through `contributions.addons[]` like any other kind, but its runtime component implements `greentic:extension-addon@0.1.0`'s `addon-extension` world instead of a design/bundle/deploy/provider one.",
"enum": [
"DesignExtension",
"BundleExtension",
"DeployExtension",
"ProviderExtension",
"AddonExtension"
]
},
"compat": {
"description": "Minimum designer/runner versions plus the literal contract version the descriptor was authored against. Parsed eagerly, so an invalid descriptor fails at deserialize time rather than when an installer tries to match.",
"$ref": "#/$defs/compat"
},
"metadata": {
"description": "Identity and catalogue information for the extension. `metadata.id` and `metadata.version` together form the identity key (`<id>@<version>`) the store and installer address the artifact by.",
"$ref": "#/$defs/metadata"
},
"engine": {
"description": "Deprecated. `gtdx lint` rejects any describe that still carries this block (`E_ENGINE_DEPRECATED`); `compat.min_designer_version` / `compat.min_runner_version` are the sole source of version constraints. Still accepted here so pre-deprecation artifacts keep validating.",
"$ref": "#/$defs/engine"
},
"capabilities": {
"description": "Capability contracts this extension offers to others and requires from its host. An id that appears in both lists is a self-cycle and is rejected by `gtdx lint` (`E_CAP_CYCLE`).",
"$ref": "#/$defs/capabilities"
},
"runtime": {
"description": "The WASM components that back this extension, plus the sandbox limits and host permissions they run under.",
"$ref": "#/$defs/runtime"
},
"execution": {
"type": "object",
"description": "BundleExtension dispatch config (builtin vs wasm). Ignored for other kinds."
},
"contributions": {
"description": "What the extension adds to the designer. This schema lists `contributions` as required; the sibling describe-mcp-v1.json omits it entirely, because `kind: wasix:mcp/router` artifacts carry no contributions - a router's tools are discovered at runtime via `list-tools`. The conditionality therefore lives in the two-schema split, not in a conditional inside v2.",
"type": "object",
"additionalProperties": false,
"properties": {
"nodeTypes": {
"description": "Palette entries contributed by a design extension. Each entry's optional `runtime_ref` is what the designer's flow compiler reads, rather than the pinned ref in `flow_generator/catalog.baseline.yaml`.",
"type": "array",
"items": {}
},
"dwProviders": {
"description": "DW Composer provider cards the extension offers. Mirrors the fields the designer's `/api/dw/providers` serves, so an installed extension can publish a provider choice (e.g. an LLM backend) without the designer carrying a static catalogue entry for it.",
"type": "array",
"items": {}
},
"tools": {
"description": "Design-time tools the extension exposes. A tool's `runtime_ref` selects a single component to dispatch through; if absent the runtime selects the only component declared in `runtime.components`. `gtdx lint` requires each `export` to be a fully-qualified `greentic:extension-design/<interface>.<member>` reference (`E_EXPORT_FORM`), never a bare member name.",
"type": "array",
"items": {}
},
"recipes": {
"description": "Packaging recipes contributed by a bundle extension. Each carries an `id`, a localized `display_name`, an optional `description`, and a `config_schema`.",
"type": "array",
"items": {}
},
"knowledge": {
"description": "Knowledge-base artifacts shipped with the extension. Each entry is a `{ path }` object whose `path` is relative to the gtxpack root.",
"type": "array",
"items": {}
},
"prompts": {
"description": "Markdown prompt fragments shipped with the extension. Each entry is a `{ path }` object whose `path` is relative to the gtxpack root.",
"type": "array",
"items": {}
},
"schemas": {
"description": "JSON Schema documents shipped with the extension. Each entry is a `{ path }` object whose `path` is relative to the gtxpack root.",
"type": "array",
"items": {}
},
"guardrails": {
"description": "Content-moderation guardrails contributed by the extension.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"export"
],
"properties": {
"export": {
"description": "Fully-qualified WIT export implementing the guardrail, e.g. `greentic:extension-design/guardrail.evaluate`.",
"type": "string"
},
"runtime_ref": {
"description": "The runtime component (by its key in `runtime.components`) that provides the guardrail. Absent means the runtime selects the sole declared component.",
"type": "string"
}
}
}
},
"views": {
"description": "UI pages the extension contributes to a host surface. The page's assets ship in the pack under `assets/views/<id>/`; the host serves them and renders `entry` in a sandboxed iframe with an opaque origin. What the page may reach is declared once in `runtime.permissions.ui`, not per view.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"surface",
"title_key",
"title_fallback",
"entry",
"placement"
],
"properties": {
"id": {
"description": "Unique within the extension. The host namespaces it as `<extension_id>/<id>`.",
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"surface": {
"description": "Which host application the view targets. A view that belongs in both declares two entries, because placement differs per surface.",
"enum": [
"designer",
"admin"
]
},
"title_key": {
"description": "Key resolved against the top-level `localization` block.",
"type": "string"
},
"title_fallback": {
"description": "Literal shown when `title_key` has no entry for the active locale.",
"type": "string"
},
"icon": {
"description": "Host-resolved icon name.",
"type": "string"
},
"entry": {
"description": "Entry HTML relative to `assets/views/<id>/` inside the pack. `gtdx lint` reports a missing file as `E_VIEW_ENTRY_MISSING` and a path that escapes the view directory as `E_VIEW_ENTRY_PATH`.",
"type": "string"
},
"placement": {
"description": "The author's suggested placement. Every configuration layer may override it. `slot` and `path` are free strings rather than an enum because the hosts' tab sets change with the product while a published describe is signed and immutable; `gtdx lint` reports an unknown slot as the warning `W_VIEW_SLOT_UNKNOWN`, and a host that cannot resolve a placement mounts the view under an \"Extensions\" section with a diagnostic rather than dropping it.",
"type": "object",
"additionalProperties": false,
"required": [
"slot"
],
"properties": {
"slot": {
"description": "Host-defined mount point naming where the view appears, e.g. `designer.sidebar`, `admin.sidebar`, `admin.tenantDetail`. Free string rather than an enum because the hosts' navigation changes with the product while a published describe is signed and immutable; `gtdx lint` warns on an unknown slot rather than erroring, and a host that cannot resolve a slot mounts the view under an \"Extensions\" section with a diagnostic rather than dropping it.",
"type": "string"
},
"path": {
"description": "The section/group path beneath the slot, e.g. `[\"Governance\"]` or `[\"access\", \"teams\"]`. Empty or absent means the top level of the slot.",
"type": "array",
"items": {
"type": "string"
}
},
"order": {
"description": "Sort hint among siblings under the same parent. Hosts break ties by extension id then view id, so ordering stays total and stable even when two extensions pick the same number.",
"type": "integer"
}
}
},
"min_visibility": {
"description": "Floor on who may see the view. Only a floor: the operative gate is the tenant and team configuration the host holds. `tenant_admin` covers the Admin `partnership` tier as well.",
"enum": [
"member",
"tenant_admin",
"platform_admin"
]
},
"tools": {
"description": "Names of this extension's own contributed tools the view may invoke through the host bridge. Every name must appear in `contributions.tools[].name` — the Rust deserializer rejects a dangling one.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"addons": {
"description": "Managed services this extension offers to an environment - Qdrant, Redis, Postgres. Catalogue metadata only: the platform provisions the workload, and the addon declares what it needs. Secrets never appear in `desired_state_schema`; they reach the addon through its runtime binding, because a secret in desired state can never be read back and so diffs forever.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"family",
"display_name",
"description",
"config_schema",
"desired_state_schema"
],
"properties": {
"id": {
"description": "Unique within the extension. The platform namespaces it as `<extension_id>/<id>`.",
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"family": {
"description": "What kind of thing this is - `vector-db`, `cache`, `sql`. A catalogue grouping label a flow author can browse or filter by - not a substitutability guarantee: two addons sharing a family are not promised compatible outputs, so this field alone is not a basis for automatically swapping one for the other. An open string on purpose: describe.json is signed and immutable once published, so a closed enum in it rots. `gtdx lint` warns on an unfamiliar family instead.",
"type": "string"
},
"display_name": {
"type": "string"
},
"description": {
"type": "string"
},
"icon": {
"description": "Host-resolved icon name.",
"type": "string"
},
"config_schema": {
"description": "JSON Schema (Draft 2020-12) for the knobs a user sets per environment - size, replicas, version. Rendered as a form by the Designer. Stringly-encoded because it is a payload passed to a renderer, not host control data. Must parse as JSON; the Rust deserializer rejects it otherwise.",
"type": "string"
},
"desired_state_schema": {
"description": "JSON Schema for the day-2 state the addon reconciles - Qdrant collections, Redis ACL users. Secrets do not belong here: a password in desired state can never be read back by `observe`, so it diffs forever and no plan is ever clean. `gtdx lint` reports a secret-looking property as an error.",
"type": "string"
},
"outputs": {
"description": "Values the addon publishes once provisioned, referenced from another resource as `${resources.<id>.outputs.<name>}`.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"type"
],
"properties": {
"name": {
"description": "Referenced as `${resources.<id>.outputs.<name>}`. `gtdx lint` constrains this to characters that survive becoming an environment variable, because that is what the platform does with it.",
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"type": {
"description": "Scalar only. An output is interpolated into another resource's configuration, where a structured value has no meaningful rendering; an addon wanting structure exposes several outputs.",
"enum": [
"text",
"number",
"boolean"
]
},
"sensitive": {
"description": "A sensitive output never becomes a literal value - the platform resolves it to a secret reference, so it never passes through a plan document, a plan UI, or a support bundle. Getting this wrong is how a password ends up in a log.",
"type": "boolean"
},
"description": {
"type": "string"
}
}
}
},
"supports_backup": {
"description": "Whether the addon can snapshot before a destructive change. The platform offers to back up on the strength of this flag, so declare true only when a snapshot genuinely happens.",
"type": "boolean"
},
"schema_version": {
"description": "Version of this addon's `desired_state_schema`, not of the addon. It lets one extension migrate instances from a v1 shape to a v2 shape rather than breaking them. Defaults to 1.",
"type": "integer",
"minimum": 1
}
}
}
},
"connection_test": {
"description": "Optional self-test the extension declares so a consumer (designer, gtdx, store) can verify a live connection or credential by invoking one of the extension's own tools. Snake-case on the wire, unlike the camelCase siblings in this block - that spelling matches how extensions and the designer already read `contributions.connection_test`.",
"type": "object",
"additionalProperties": false,
"required": [
"tool"
],
"properties": {
"tool": {
"description": "Name of the contributed tool (matching a `contributions.tools[].name`) to invoke to exercise the connection.",
"type": "string"
},
"args": {
"description": "Arguments to pass to the tool. Absent is treated as `{}`.",
"type": "object"
}
}
}
}
},
"localization": {
"description": "Top-level translation table `{ default_locale, strings }`, where `strings` maps a flat key (e.g. `node.adaptive_card.label`) to a per-locale string map. Designer reads this when a localized string does not carry inline locales.",
"type": "object"
},
"signature": {
"description": "Detached ed25519 signature over this describe. The signed payload is the RFC 8785 (JCS) canonicalization of this document with `signature` removed, so signing is idempotent and independent of key order or serde version. It does not cover the rest of the archive - that binding is `manifestSha256`.",
"$ref": "#/$defs/signature"
},
"manifestSha256": {
"description": "SHA-256 (lowercase hex) of the canonical `manifest.json`. Binds the whole-archive ledger into the signed describe (audit C2/H7). Optional only for backward compatibility during migration; production packs MUST set it.",
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"requiredSecrets": {
"type": "array",
"description": "Canonical list of credential secrets the operator must supply before this extension can function. This is the v2 spelling; `kind: wasix:mcp/router` artifacts instead emit snake_case `secret_requirements`, which only describe-mcp-v1.json accepts. Both schemas set `additionalProperties: false`, so each rejects the other's field name.",
"items": {
"$ref": "#/$defs/secretRequirement"
}
}
},
"$defs": {
"secretRequirement": {
"type": "object",
"required": [
"key"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
"description": "Namespaced secret key, e.g. tavily/api_key."
},
"required": {
"type": "boolean",
"description": "Whether the secret is mandatory for the extension to function."
},
"description": {
"type": "string",
"description": "Human-readable description of what this secret is used for."
},
"format": {
"type": "string",
"enum": [
"bytes",
"text",
"json"
],
"description": "Expected encoding of the secret value."
},
"scope": {
"type": "object",
"description": "Scope hints: env, tenant, team."
},
"schema": {
"type": "object",
"description": "JSON Schema describing the secret value structure (used when format=json)."
},
"examples": {
"type": "array",
"items": {
"type": "string"
},
"description": "Example values or placeholder strings for operator UX."
}
}
},
"compat": {
"type": "object",
"required": [
"min_designer_version",
"min_runner_version",
"contract_version"
],
"properties": {
"min_designer_version": {
"description": "Semver range for the oldest designer that can load this extension - the contract floor (`>=1.2.0` for v2), NOT the version of the SDK that generated the artifact. Those are different axes: an extension built by SDK 1.3.x still loads on any designer that speaks v2, so pinning this to the generating SDK would declare a constraint far stricter than the extension actually has.",
"type": "string"
},
"min_runner_version": {
"description": "Semver range for the oldest Greentic runner this extension supports, e.g. `^1.2.0`. Parsed as a version requirement, not an exact version.",
"type": "string"
},
"contract_version": {
"description": "Exact semver version of the describe contract this artifact was authored against. This is the field that tracks the SDK, unlike `min_designer_version`, and it is parsed as a concrete version rather than a range.",
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
}
}
},
"metadata": {
"type": "object",
"required": [
"id",
"name",
"version",
"summary",
"author",
"license"
],
"properties": {
"id": {
"description": "Reverse-DNS extension id, e.g. `greentic.telco-x`. `gtdx lint` enforces a stricter form than the pattern here: `E_ID_PATTERN` requires `^greentic\\.[a-z0-9][a-z0-9-]*$`.",
"type": "string",
"pattern": "^[a-z][a-z0-9.-]*\\.[a-z0-9.-]+$"
},
"name": {
"description": "Human-readable display name shown in the store and designer catalogue. Must be non-empty.",
"type": "string",
"minLength": 1
},
"version": {
"description": "Exact semver version of this extension release, e.g. `1.3.0-research.2`.",
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
},
"summary": {
"description": "One-line summary used in catalogue listings. Deliberately untyped: it accepts either a plain string or a localized object `{ default, locales }`."
},
"description": {
"description": "Long-form description. Same shape as `summary` - a plain string or `{ default, locales }`."
},
"author": {
"description": "Publisher of the extension. Only `name` is required.",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"publicKey": {
"type": "string"
}
}
},
"license": {
"description": "License identifier for the extension, e.g. `MIT` or `Apache-2.0`. Required, but neither this schema nor `gtdx lint` checks the value against the SPDX list.",
"type": "string"
},
"homepage": {
"type": "string",
"format": "uri"
},
"repository": {
"type": "string",
"format": "uri"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
}
},
"icon": {
"description": "Pack-relative path to the extension icon, e.g. `assets/icon.svg`. `gtdx new --icon` and `gtdx publish` write this field after copying the file into `assets/`; accepted formats are svg, png, jpg, jpeg and webp, capped at 1 MiB to match the store-server icon limit.",
"type": "string"
},
"screenshots": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"engine": {
"type": "object",
"required": [
"greenticDesigner",
"extRuntime"
],
"properties": {
"greenticDesigner": {
"description": "Deprecated together with the rest of the `engine` block; superseded by `compat.min_designer_version`. Semver range for the designer, e.g. `>=1.2.0`. `gtdx doctor` reads it as the designer bound on a v1 describe.",
"type": "string"
},
"extRuntime": {
"description": "Deprecated together with the rest of the `engine` block. Semver range for the extension runtime, e.g. `^1.2.0`. Not interchangeable with `compat.min_runner_version`, which bounds the Greentic runner - shipped artifacts set the two to different ranges.",
"type": "string"
}
}
},
"capabilities": {
"type": "object",
"properties": {
"offered": {
"description": "Capability contracts this extension provides to others, e.g. `greentic:guardrail/topic`. `gtdx publish` requires each `version` here to parse as an exact semver, and dropping an entry without a version bump is flagged by `gtdx lint` (`W_DESCRIBE_DIFF_BREAKING`).",
"type": "array",
"items": {
"$ref": "#/$defs/capRef"
}
},
"required": {
"description": "Capability contracts this extension needs its host or another extension to provide. An id present in both `required` and `offered` is a self-cycle and is rejected by `gtdx lint` (`E_CAP_CYCLE`).",
"type": "array",
"items": {
"$ref": "#/$defs/capRef"
}
}
}
},
"capRef": {
"description": "A reference to a capability contract: an id plus the version constraint the referrer expects.",
"type": "object",
"required": [
"id",
"version"
],
"properties": {
"id": {
"description": "Capability id in `<namespace>:<path>` form, e.g. `greentic:guardrail/topic`. The namespace is the segment before the first colon.",
"type": "string",
"pattern": "^[a-z][a-z0-9-]*:[a-z][a-z0-9/._-]*$"
},
"version": {
"description": "Version constraint for the capability, parsed as a semver requirement. Parsing fails closed - a malformed string is an error, never a silent match-everything. Entries under `capabilities.offered` are additionally required by `gtdx publish` to be an exact version such as `1.0.0`.",
"type": "string"
},
"deprecated": {
"description": "Deprecation marker `{ since, replaced_by?, removal_in? }`. Designer renders a warning chip in the palette; the runner refuses to install once the current contract version is past `removal_in`."
}
}
},
"runtime": {
"type": "object",
"required": [
"permissions",
"components"
],
"properties": {
"memoryLimitMB": {
"description": "Memory ceiling for the extension's components. Defaults to 64 when omitted. The `[1, 1024]` bound is enforced twice - by this schema and again by the Rust deserializer - so a document that skips schema validation still cannot carry 0 or a multi-gigabyte value.",
"type": "integer",
"minimum": 1,
"maximum": 1024
},
"permissions": {
"description": "Host permissions the extension requests. `gtdx install` prints the network, secrets and cross-extension requests and asks for confirmation before installing, unless the install was pre-approved (`--yes` / CI).",
"type": "object",
"additionalProperties": false,
"properties": {
"network": {
"description": "URL patterns the extension may reach, e.g. `https://api.example.com/*`. `gtdx publish` requires `https://`, with one exception: plain `http://` is accepted for loopback hosts (`127.0.0.1`, `localhost`, `[::1]`) only. This mirrors the extension runtime, which honours plain http for loopback hosts and drops non-loopback http patterns.",
"type": "array",
"items": {
"type": "string"
}
},
"secrets": {
"description": "Secret keys the extension declares it needs to read. Listed in the `gtdx install` consent prompt.",
"type": "array",
"items": {
"type": "string"
}
},
"callExtensionKinds": {
"description": "Extension kinds this extension may call into. Surfaced in the `gtdx install` consent prompt as a cross-extension request.",
"type": "array",
"items": {
"type": "string"
}
},
"llmRoles": {
"type": "array",
"items": {
"type": "string"
},
"description": "LLM roles (wire names, e.g. sorla_composer) this extension may request from the host greentic:extension-host/llm import."
},
"oauthProviders": {
"description": "OAuth provider ids (e.g. `hubspot`) this extension may request tokens for via the host `greentic:oauth-broker/broker-v1` import. The host rejects `get-token` for any provider not listed here.",
"type": "array",
"items": {
"type": "string"
}
},
"ui": {
"description": "Grants that apply to browser-executed view code, not to the WASM guest. Kept separate from `network` on purpose: `network` authorises `http.fetch` from inside the guest, where the caller is the extension's own logic, while these authorise requests a human clicking in a browser can trigger, whose responses land in browser-executed code.",
"type": "object",
"additionalProperties": false,
"properties": {
"fetchHosts": {
"description": "Hosts a view may reach through the host's server-side proxy. The view never issues these itself: an iframe without `allow-same-origin` sends `Origin: null`, which most third-party APIs reject at CORS. Same address rules as `network` - https only, loopback and link-local rejected.",
"type": "array",
"items": {
"type": "string"
}
},
"platformApi": {
"description": "Platform REST endpoints a view may call through the bridge. The host intersects this with the calling user's own RBAC, so it can only narrow what that user could already do by hand - never widen it.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"method",
"path_pattern"
],
"properties": {
"method": {
"description": "HTTP method of the grant. Constrained here rather than by a Rust enum so a describe naming a method the current SDK does not know still round-trips instead of failing the whole parse.",
"enum": [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE"
]
},
"path_pattern": {
"description": "The platform endpoint path this grant covers, e.g. `/api/flows` or `/api/admin/tenants/*`. The host intersects the grant with the calling user's own RBAC, so it can only narrow what that user could already do by hand, never widen it.",
"type": "string"
}
}
}
}
}
}
}
},
"components": {
"description": "The WASM components the extension ships, keyed by component id - a kebab-case identifier limited to lowercase letters, digits, `-`, `_` and `.`. At least one entry is required, and every `runtime_ref` under `contributions.nodeTypes` and `contributions.tools` must name a key that exists here (`gtdx lint` reports a dangling one as `E_RUNTIME_REF`).",
"type": "object",
"minProperties": 1,
"additionalProperties": {
"$ref": "#/$defs/runtimeComponent"
}
}
}
},
"runtimeComponent": {
"description": "One entry in `runtime.components`. At least one of `oci_ref` or `gtpack` must be present, and both may be. This schema does not enforce that; the Rust deserializer does, so a document that passes validation here can still be rejected on load.",
"type": "object",
"required": [
"sha256",
"world"
],
"properties": {
"oci_ref": {
"description": "OCI reference to the component image, e.g. `ghcr.io/greentic/my-ext:0.1.0`. The preferred distribution channel; `gtpack` is the offline fallback.",
"type": "string"
},
"gtpack": {
"description": "Offline fallback payload shipped inside the archive, shaped `{ file, sha256, pack_id, component_version }`. The nested `sha256` is validated as lowercase hex at parse time, so an uppercase digest passes this untyped schema yet fails on load.",
"type": "object"
},
"sha256": {
"description": "Lowercase-hex SHA-256 of the component artifact; uppercase hex is rejected. `gtdx lint --publish` additionally rejects the all-zeros placeholder that scaffolds ship with (`E_SHA256_ZERO`).",
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"world": {
"description": "WIT world the component exports, e.g. `greentic:extension-design/guardrail@0.3.0`.",
"type": "string"
}
}
},
"signature": {
"type": "object",
"required": [
"algorithm",
"publicKey",
"value"
],
"properties": {
"algorithm": {
"description": "Signature algorithm. ed25519 is the only algorithm the contract implements; any other value is rejected.",
"const": "ed25519"
},
"publicKey": {
"description": "Base64-encoded 32-byte ed25519 public key of the signer (44 characters). An `ed25519:` prefix is stripped if present. This is the key verification uses, and the one `gtdx keygen` tells authors to distribute here.",
"type": "string"
},
"value": {
"description": "Base64-encoded 64-byte ed25519 signature over the JCS canonicalization of this document with `signature` removed. An `ed25519:` prefix is stripped if present.",
"type": "string"
},
"keyId": {
"description": "Optional label naming which publisher key signed, carried through from `gtdx publish --key-id`. Advisory only - verification uses `publicKey` and never reads this field.",
"type": "string"
}
}
}
}
}
  • gtdx validate checks an extension directory’s describe.json against this JSON Schema.
  • gtdx lint checks cross-field invariants beyond the schema; gtdx lint --publish adds publish-only rules.
  • gtdx sign signs describe.json in place and fills the signature block.
  • gtdx publish uploads the signed .gtxpack built around this manifest.