diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml
index c3ed6d29..72bd2d89 100644
--- a/.github/upstream-projects.yaml
+++ b/.github/upstream-projects.yaml
@@ -44,7 +44,7 @@ projects:
- id: toolhive
repo: stacklok/toolhive
- version: v0.42.0
+ version: v0.42.1
# toolhive is a monorepo covering the CLI, the Kubernetes
# operator, and the vMCP gateway. It also introduces cross-
# cutting features that land in concepts/, integrations/,
diff --git a/docs/toolhive/guides-cli/ai-plugins.mdx b/docs/toolhive/guides-cli/ai-plugins.mdx
index e803c7d4..123d61c2 100644
--- a/docs/toolhive/guides-cli/ai-plugins.mdx
+++ b/docs/toolhive/guides-cli/ai-plugins.mdx
@@ -340,3 +340,19 @@ docker login
Then retry the push.
+
+
+Plugin install or upgrade times out on a slow registry pull
+
+`thv ai-plugin` commands that pull OCI artifacts (`install`, `sync`, `upgrade`)
+use a client timeout of 10 minutes by default. If pulls are taking longer than
+that on a slow network or a very large plugin, raise the limit with the
+`TOOLHIVE_API_TIMEOUT` environment variable:
+
+```bash
+TOOLHIVE_API_TIMEOUT=30m thv ai-plugin install
+```
+
+The value is a Go duration such as `5m`, `30m`, or `2h`.
+
+
diff --git a/docs/toolhive/guides-cli/skills-management.mdx b/docs/toolhive/guides-cli/skills-management.mdx
index 9c0a5752..dbd976d3 100644
--- a/docs/toolhive/guides-cli/skills-management.mdx
+++ b/docs/toolhive/guides-cli/skills-management.mdx
@@ -458,3 +458,19 @@ docker login
Then retry the push command.
+
+
+Skill install or upgrade times out on a slow registry pull
+
+`thv skill` commands that pull OCI artifacts (`install`, `sync`, `upgrade`) use
+a client timeout of 10 minutes by default. If pulls are taking longer than that
+on a slow network or a very large skill, raise the limit with the
+`TOOLHIVE_API_TIMEOUT` environment variable:
+
+```bash
+TOOLHIVE_API_TIMEOUT=30m thv skill install
+```
+
+The value is a Go duration such as `5m`, `30m`, or `2h`.
+
+
diff --git a/docs/toolhive/guides-vmcp/composite-tools.mdx b/docs/toolhive/guides-vmcp/composite-tools.mdx
index 8d153c66..f0517e27 100644
--- a/docs/toolhive/guides-vmcp/composite-tools.mdx
+++ b/docs/toolhive/guides-vmcp/composite-tools.mdx
@@ -642,6 +642,69 @@ steps[conditional_step].defaultResults[value] is required: step "conditional_ste
may be skipped and field "value" is referenced by step use_result
```
+### Tool annotations
+
+A composite tool can carry the same MCP tool annotations (`title`,
+`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) that
+individual backend tools do. Clients use these hints to decide how to present
+the composite tool, for example to prompt before running a destructive one.
+
+When the workflow contains at least one tool step and you don't declare
+annotations, vMCP derives a conservative floor from the annotations of the step
+tools. This floor is fail-closed: any step whose annotations are unset or
+unknown taints the derived values pessimistically (`readOnlyHint: false`,
+`destructiveHint: true`, `openWorldHint: true`). `idempotentHint` is never
+derived. A workflow with no tool steps (for example, one containing only an
+elicitation) carries no floor.
+
+Declare annotations explicitly to override the floor:
+
+```yaml title="VirtualMCPCompositeToolDefinition resource"
+apiVersion: toolhive.stacklok.dev/v1beta1
+kind: VirtualMCPCompositeToolDefinition
+metadata:
+ name: gather-pr-context
+spec:
+ name: gather_pr_context
+ description: Gather PR metadata, diff, and changed files in a single call
+ # highlight-start
+ annotations:
+ title: Gather PR context
+ readOnlyHint: true
+ idempotentHint: true
+ # highlight-end
+ parameters:
+ # ...
+ steps:
+ # ...
+```
+
+Explicit annotations are merged over the derived floor, but a safety-floor
+guardrail drops the composite tool at advertise time if an explicit hint would
+make it look safer than its steps allow. Concretely, vMCP drops the composite
+tool with a warning naming the offending step tools when:
+
+- `readOnlyHint: true` is declared, but at least one step tool is not read-only
+- `destructiveHint: false` is declared, but at least one step tool is
+ destructive
+- `openWorldHint: false` is declared, but at least one step tool touches an open
+ world
+
+The check runs at advertise time when `tools/list` is served. Neither CRD
+admission nor `thv vmcp validate` catches a contradictory annotation, so it
+surfaces only once the backend tools are aggregated.
+
+:::note
+
+Most backend tools declare no annotations, so composite tools with a derived
+floor typically advertise `destructiveHint: true` and `openWorldHint: true`.
+This matches the MCP specification's defaults for absent annotations, so clients
+that key off explicit hints may prompt for confirmation on these composite
+tools. Declare `annotations` explicitly to override the floor when the derived
+values are stricter than the workflow warrants.
+
+:::
+
### Structured output
By default, a composite tool returns the raw output of the most recently
diff --git a/docs/toolhive/guides-vmcp/local-cli.mdx b/docs/toolhive/guides-vmcp/local-cli.mdx
index 6d952e13..bf0ffc78 100644
--- a/docs/toolhive/guides-vmcp/local-cli.mdx
+++ b/docs/toolhive/guides-vmcp/local-cli.mdx
@@ -197,6 +197,12 @@ To hide all backend tools globally (or per workload) and expose only
are removed from `tools/list` but remain routable internally. See
[Excluding all tools](./tool-aggregation.mdx#excluding-all-tools) for examples.
+To enumerate the exposed backends deliberately rather than inheriting from group
+membership, set `aggregation.defaultToolVisibility: deny` and list the allowed
+backends under `aggregation.tools`. See
+[Change the default visibility](./tool-aggregation.mdx#change-the-default-visibility)
+for the trade-offs.
+
For the full filter and override reference, see
[Tool aggregation](./tool-aggregation.mdx).
diff --git a/docs/toolhive/guides-vmcp/tool-aggregation.mdx b/docs/toolhive/guides-vmcp/tool-aggregation.mdx
index e40d3baa..5575282c 100644
--- a/docs/toolhive/guides-vmcp/tool-aggregation.mdx
+++ b/docs/toolhive/guides-vmcp/tool-aggregation.mdx
@@ -137,8 +137,9 @@ To hide every tool from `tools/list` — globally or per workload — use
through [composite tools](./composite-tools.mdx) workflows rather than raw
backend tools.
-Hidden tools are removed from `tools/list` responses but remain in the internal
-routing table, so composite tools can still call them.
+Hidden tools are removed from `tools/list` responses and clients cannot invoke
+them directly through `tools/call`. They remain in the internal routing table so
+composite tools can call them from workflow steps.
### Hide all backend tools globally
@@ -173,6 +174,52 @@ prevent clients from calling raw backend tools directly, then define
[composite tools](./composite-tools.mdx) that orchestrate the hidden tools
internally.
+## Change the default visibility
+
+By default, every backend in the group has its tools advertised even when it has
+no matching entry under `aggregation.tools`. Set
+`aggregation.defaultToolVisibility: deny` to invert this: only backends
+explicitly listed in `aggregation.tools` contribute tools to `tools/list`.
+
+```yaml title="VirtualMCPServer resource"
+spec:
+ config:
+ aggregation:
+ defaultToolVisibility: deny # unlisted backends contribute no tools
+ tools:
+ - workload: github
+ filter: ['create_issue', 'get_issue']
+ - workload: jira
+ filter: ['create_issue']
+```
+
+With this configuration, tools from `github` and `jira` are advertised according
+to their entries, and any other backend joining the group later contributes
+nothing to `tools/list` until you add an entry for it.
+
+A backend that _does_ have a `tools` entry is unaffected by
+`defaultToolVisibility`: its entry opts it in, and `excludeAll` or `filter` on
+that entry decide the rest. `defaultToolVisibility` gates tools only. Resources,
+resource templates, and prompts from unlisted backends are still advertised.
+Composite tools can still call any backend tool from workflow steps, since
+hidden tools remain in the internal routing table.
+
+**When to use:** When the set of exposed tools must be enumerated deliberately,
+rather than inherited from group membership.
+
+:::warning
+
+`defaultToolVisibility` requires the ToolHive v0.42.1 CRDs. On a cluster running
+a newer operator against older CRDs, the field is silently pruned at admission
+and aggregation falls back to `allow`. Verify the value is preserved with:
+
+```bash
+kubectl get virtualmcpserver \
+ -o jsonpath='{.spec.config.aggregation.defaultToolVisibility}'
+```
+
+:::
+
## Tool overrides
Use overrides to customize tool names and descriptions without modifying backend
diff --git a/docs/toolhive/reference/cli/thv_skill_upgrade.md b/docs/toolhive/reference/cli/thv_skill_upgrade.md
index 42118fbc..51059c3b 100644
--- a/docs/toolhive/reference/cli/thv_skill_upgrade.md
+++ b/docs/toolhive/reference/cli/thv_skill_upgrade.md
@@ -24,8 +24,9 @@ Skills pinned to an immutable reference (an OCI digest or a full git commit
hash) are reported not-upgradable — there is nothing newer to resolve to.
Use --preview to see what would change without persisting anything (OCI
sources are still fetched into the local artifact store to compare digests),
-and --allow-ref-change to permit the resolved reference itself changing
-(e.g. a registry entry repointed at a different repository).
+and --allow-ref-change to permit the artifact moving to a different
+repository (a version bump within the same repository is not a change
+this guard blocks).
--fail-on-changes evaluates the same plan and never installs: it is a CI
freshness gate.
@@ -40,7 +41,7 @@ thv skill upgrade [skill-name...] [flags]
### Options
```
- --allow-ref-change Permit the resolved reference itself to change during upgrade
+ --allow-ref-change Permit the artifact to move to a different repository during upgrade
--allow-signer-change Permit upgrading to an artifact signed by a different identity; the new identity replaces the recorded one
--clients string Comma-separated target client apps (e.g. claude-code,opencode), or "all" for every available client
--fail-on-changes Report what would change without installing anything; a CI freshness gate
diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml
index 0969382e..5a8d3ad9 100644
--- a/static/api-specs/toolhive-api.yaml
+++ b/static/api-specs/toolhive-api.yaml
@@ -667,6 +667,28 @@ components:
$ref: '#/components/schemas/storage.RunConfig'
token_lifespans:
$ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.TokenLifespanRunConfig'
+ trusted_issuers:
+ description: |-
+ TrustedIssuers lists external OIDC issuers whose tokens are accepted as
+ subject tokens during RFC 8693 token exchange. Empty (the default) means
+ only self-issued subject tokens are accepted.
+
+ Prerequisite: the token-exchange grant requires a confidential client,
+ and no supported deployment path provisions one today (DCR and CIMD
+ clients are both public-only, and there is no client-seeding field on
+ this RunConfig), so this grant is not yet usable end to end for
+ self-issued or external subject tokens alike. Tracked in
+ https://github.com/stacklok/toolhive/issues/6082.
+
+ See tokenexchange.TrustedIssuer for the per-issuer field reference, and
+ docs/arch/17-token-exchange-delegation.md for the trust model, consent
+ signals, and operator-facing constraints (audience/scope bounding,
+ subject namespace qualification, required client binding) that aren't
+ visible from the config shape alone.
+ items:
+ $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver_server_tokenexchange.TrustedIssuer'
+ type: array
+ uniqueItems: false
upstreams:
description: |-
Upstreams configures connections to upstream Identity Providers.
@@ -830,6 +852,75 @@ components:
If not specified, defaults to GET.
type: string
type: object
+ github_com_stacklok_toolhive_pkg_authserver_server_tokenexchange.TrustedIssuer:
+ properties:
+ actor_claim:
+ description: |-
+ ActorClaim names the claim identifying the client that requested the
+ subject token from THIS EXTERNAL ISSUER (used by AllowedActors below).
+ Values are in the external issuer's namespace, NOT ToolHive client
+ IDs. Defaults to "azp"; use "appid" for Microsoft Entra v1, "cid" for
+ Okta. The special value "client_id" reads ValidatedClaims.ClientID
+ instead of Extra (assignClaim routes it to that field) — it is still
+ the external token's client_id claim, not a ToolHive one.
+ type: string
+ allow_private_ips:
+ description: |-
+ AllowPrivateIPs permits OIDC discovery and JWKS fetches for THIS
+ issuer to resolve to a private or loopback address. Use only when the
+ issuer is hosted inside the same cluster and has no public endpoint.
+ type: boolean
+ allowed_actors:
+ description: |-
+ AllowedActors is the allowlist of ActorClaim values authorized to
+ exchange a subject token from this issuer when it carries no
+ "may_act" claim; empty means only may_act-bearing tokens are
+ accepted. By itself names no ToolHive client — see
+ AllowedDelegateClients and docs/arch/17-token-exchange-delegation.md
+ ("Accepted limitations" #1).
+ items:
+ type: string
+ type: array
+ uniqueItems: false
+ allowed_delegate_clients:
+ description: |-
+ AllowedDelegateClients restricts which ToolHive client IDs may
+ exchange a subject token from this issuer, for BOTH consent paths.
+ Required (validateTrustedIssuer rejects empty/absent); "*" permits
+ any confidential client holding the grant. See
+ docs/arch/17-token-exchange-delegation.md ("Accepted limitations" #1).
+ items:
+ type: string
+ type: array
+ uniqueItems: false
+ expected_audience:
+ description: |-
+ ExpectedAudience is the expected "aud" claim value that must appear
+ in the token's audience list (a resource/API identifier, not a
+ client ID — required, but not enforced; see looksLikeResourceIdentifier).
+ See docs/arch/17-token-exchange-delegation.md ("ID/access-token
+ discrimination") for why and its limits.
+ type: string
+ insecure_allow_http:
+ description: |-
+ InsecureAllowHTTP permits plain-HTTP OIDC discovery and JWKS fetches
+ for THIS issuer only. Development and testing only — never set in
+ production. Does not relax the private-IP guard; see AllowPrivateIPs.
+ Deliberately per-issuer: this server's own InsecureAllowHTTP must not
+ silently permit plaintext discovery for every trusted external issuer
+ too — a network attacker who can intercept that traffic could
+ substitute a JWKS and forge subject tokens for that issuer's
+ namespace.
+ type: boolean
+ issuer_url:
+ description: IssuerURL is the expected "iss" claim value (exact match).
+ type: string
+ jwks_url:
+ description: |-
+ JWKSURL is the URL to fetch the issuer's JSON Web Key Set from.
+ If empty, it is resolved via OIDC discovery at {IssuerURL}/.well-known/openid-configuration.
+ type: string
+ type: object
github_com_stacklok_toolhive_pkg_authz.Config:
description: |-
DEPRECATED: Middleware configuration.
diff --git a/static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json b/static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json
index 5658182e..aa5f8afe 100644
--- a/static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json
+++ b/static/api-specs/toolhive-crds/mcpoidcconfigs.schema.json
@@ -70,7 +70,7 @@
},
"insecureAllowHTTP": {
"default": false,
- "description": "InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing.\nWARNING: This is insecure and should NEVER be used in production.",
+ "description": "InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuer and JWKS URLs for development/testing.\nWARNING: This is insecure and should NEVER be used in production.",
"type": "boolean"
},
"introspectionUrl": {
diff --git a/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json b/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json
index c2320024..3d3d389e 100644
--- a/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json
+++ b/static/api-specs/toolhive-crds/virtualmcpcompositetooldefinitions.schema.json
@@ -15,6 +15,32 @@
"spec": {
"description": "VirtualMCPCompositeToolDefinitionSpec defines the desired state of VirtualMCPCompositeToolDefinition.\nThis embeds the CompositeToolConfig from pkg/vmcp/config to share the configuration model\nbetween CLI and operator usage.",
"properties": {
+ "annotations": {
+ "description": "Annotations declares MCP tool annotations for the composite tool.\n\nAnnotation derivation runs at ADVERTISE TIME (when tools/list is served\nand the backend tools are aggregated), not at CRD admission — thv vmcp\nvalidate does NOT check annotation contradictions. The derived floor is\nfail-closed: when the workflow has one or more tool steps the floor is\nalways non-nil, and any step whose annotations are nil/unknown taints the\nfloor conservatively (readOnly=false, destructive=true, openWorld=true).\nA workflow with no tool steps (e.g. only elicitation) has no floor.\n\nWhen nil, annotations are derived from the annotations of the backend tools\nreferenced by the workflow's steps (e.g. readOnlyHint is true only when every\nstep tool is read-only). When set, the values are an explicit author\ndeclaration merged over the derived floor — subject to a safety-floor\nguardrail that drops the composite tool (with a warning naming the offending\nstep tools) if an explicit hint would make the tool look safer than its\nsteps allow.",
+ "properties": {
+ "destructiveHint": {
+ "description": "DestructiveHint sets the destructive hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "idempotentHint": {
+ "description": "IdempotentHint sets the idempotent hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "openWorldHint": {
+ "description": "OpenWorldHint sets the open-world hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "readOnlyHint": {
+ "description": "ReadOnlyHint sets the read-only hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "Title sets the human-readable title annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"description": {
"description": "Description describes what the workflow does.",
"type": "string"
diff --git a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json
index ed9b19a3..656db8fb 100644
--- a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json
+++ b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json
@@ -798,6 +798,14 @@
},
"type": "object"
},
+ "defaultToolVisibility": {
+ "description": "DefaultToolVisibility controls whether a backend with NO entry in Tools has its\ntools advertised to MCP clients.\n - allow (default): every tool from an unlisted backend is advertised, so\n adding a workload to the group exposes it without further configuration.\n - deny: an unlisted backend contributes no tools, so only backends named in\n Tools are advertised. Use this when the set of exposed tools must be\n enumerated deliberately rather than inherited from group membership.\n\nA backend that DOES have a Tools entry is unaffected by this setting: the\nentry opts it in, and ExcludeAll/Filter on that entry decide the rest. Like\nevery other visibility setting here, this controls advertising only — hidden\ntools remain in the routing table for composite tools (see the type doc).\n\nThis gates TOOLS only. An unlisted backend's resources, resource templates,\nand prompts are still advertised under deny; mergeResources/mergePrompts have\nno equivalent check.",
+ "enum": [
+ "allow",
+ "deny"
+ ],
+ "type": "string"
+ },
"excludeAllTools": {
"description": "ExcludeAllTools hides all backend tools from MCP clients when true.\nHidden tools are NOT advertised in tools/list responses, but they ARE\navailable in the routing table for composite tools to use.\nThis enables the use case where you want to hide raw backend tools from\ndirect client access while exposing curated composite tool workflows.",
"type": "boolean"
@@ -826,23 +834,23 @@
"description": "Annotations overrides specific tool annotation fields.\nOnly specified fields are overridden; others pass through from the backend.",
"properties": {
"destructiveHint": {
- "description": "DestructiveHint overrides the destructive hint annotation.",
+ "description": "DestructiveHint sets the destructive hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
"type": "boolean"
},
"idempotentHint": {
- "description": "IdempotentHint overrides the idempotent hint annotation.",
+ "description": "IdempotentHint sets the idempotent hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
"type": "boolean"
},
"openWorldHint": {
- "description": "OpenWorldHint overrides the open-world hint annotation.",
+ "description": "OpenWorldHint sets the open-world hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
"type": "boolean"
},
"readOnlyHint": {
- "description": "ReadOnlyHint overrides the read-only hint annotation.",
+ "description": "ReadOnlyHint sets the read-only hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
"type": "boolean"
},
"title": {
- "description": "Title overrides the human-readable title annotation.",
+ "description": "Title sets the human-readable title annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
"type": "string"
}
},
@@ -1051,6 +1059,32 @@
"items": {
"description": "CompositeToolConfig defines a composite tool workflow.\nThis matches the YAML structure from the proposal (lines 173-255).",
"properties": {
+ "annotations": {
+ "description": "Annotations declares MCP tool annotations for the composite tool.\n\nAnnotation derivation runs at ADVERTISE TIME (when tools/list is served\nand the backend tools are aggregated), not at CRD admission — thv vmcp\nvalidate does NOT check annotation contradictions. The derived floor is\nfail-closed: when the workflow has one or more tool steps the floor is\nalways non-nil, and any step whose annotations are nil/unknown taints the\nfloor conservatively (readOnly=false, destructive=true, openWorld=true).\nA workflow with no tool steps (e.g. only elicitation) has no floor.\n\nWhen nil, annotations are derived from the annotations of the backend tools\nreferenced by the workflow's steps (e.g. readOnlyHint is true only when every\nstep tool is read-only). When set, the values are an explicit author\ndeclaration merged over the derived floor — subject to a safety-floor\nguardrail that drops the composite tool (with a warning naming the offending\nstep tools) if an explicit hint would make the tool look safer than its\nsteps allow.",
+ "properties": {
+ "destructiveHint": {
+ "description": "DestructiveHint sets the destructive hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "idempotentHint": {
+ "description": "IdempotentHint sets the idempotent hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "openWorldHint": {
+ "description": "OpenWorldHint sets the open-world hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "readOnlyHint": {
+ "description": "ReadOnlyHint sets the read-only hint annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "Title sets the human-readable title annotation.\nFor tool overrides this replaces the backend value; for composite tools\nit is an explicit declaration merged over the derived safety floor.",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"description": {
"description": "Description describes what the workflow does.",
"type": "string"