UWS 1.6 Ansible Module Source Design (historical)
Superseded. This records the UWS 1.6 design. UWS 1.7 removed the
ansible-modulesource type: the managed host does not expose a collection module as a pre-existing named operation; the control node supplies and runs that implementation, so the argspec is a client-side library manifest. UWS 1.7 defines no replacement Ansible operation profile.versions/ansible.1.0.{json,md}remains only as historical UWS 1.6 material; the retired supplement and Go helpers are available from repository history as described in the project README. The document below is kept as the design record of the 1.6 decision; see the withdrawal record infuture-source-profiles.mdfor what the admission criteria missed.Design record for the UWS 1.6 adoption of
ansible-moduleas a first-classsourceDescriptions[].type, with the heavier sub-spec published separately asversions/ansible.1.0.{json,md}— the same two-layer packaging used byruntime.1.0andbrowser.1.5. The companion roadmap entry lives infuture-source-profiles.md("Adopted in UWS 1.6: Ansible Module Source Profiles"). Sub-spec versions are numbered by their own streams.
1. Core Philosophy: Module Argspecs Are Published Operation Contracts
Ansible collections publish machine-readable contracts for every module:
ansible-doc --json emits the documented parameter specification (name, type,
required, choices, defaults) and return-value documentation for each module.
That is a provider-published operation contract — exactly the artifact class
UWS already binds to for OpenAPI, Google Discovery, AWS Smithy, AsyncAPI,
GraphQL, OpenRPC, gRPC/protobuf, and OData.
UWS 1.6 therefore enforces the same boundary as every earlier source family:
- The collection owns the module contract — parameter schemas, return shapes, module semantics, idempotent convergence behavior.
- UWS owns the workflow overlay — which modules run, in what order, with which argument values, gated by which conditions, feeding which outputs.
- Converters own playbook lowering — Ansible directives such as
when,loop,register,notify,changed_when,failed_when, anduntil/retriesare represented only when source-aware tooling can lower them into existing UWS objects (sequence,switch,forEach,outputs,successCriteria, andonFailure: retry). - Execution is out of scope for UWS 1.6 — connection plugins,
becomeprivilege escalation, forks/serial strategy, check mode, vault decryption, module invocation, inventory connections, and the Python execution environment are not standardized by this source profile.
A UWS document never restates a module's parameter schema, just as it never restates an OpenAPI operation's request schema. It names the module and binds values.
Two properties make Ansible modules an unusually good fit for UWS:
- Module invocations are JSON-shaped on both sides. Arguments are a JSON
object; results are a JSON object (with conventional keys such as
changed,failed,msg,rc). They map directly ontorequest.bodyand$response.body.*with no impedance mismatch. - Playbooks are ordered task lists with convergent leaves. That is
imperative control flow over idempotent operations — structurally closer to
the UWS execution model than any prior source family.
when,loop,dependsOn, andregister→outputsmap one-to-one.
The ansible-module source type does not standardize those playbook lowering
rules. It gives tooling a portable module leaf binding. A converter may emit
the same orchestration shape for UWS 1.5 using extension-owned module calls or
for UWS 1.6 using first-class ansible-module source bindings.
2. Document Structure: ansible-module
UWS 1.6 introduces ansible-module as a first-class sourceDescriptions[].type.
The referenced source document is a collection argspec document (the
ansible-doc --json output shape, or the sub-spec's normalized equivalent).
sourceDescriptions:
- name: builtin
type: ansible-module
url: ./ansible/ansible-builtin.argspec.json
operations:
- operationId: install_nginx
sourceDescription: builtin
sourceOperationId: ansible.builtin.apt # FQCN selects the module
request:
body:
name: nginx
state: present
outputs:
changed: $response.body.changed
Selector rules follow the generic pattern introduced in UWS 1.2 and reused by every family since:
sourceOperationIdis the module's fully qualified collection name (FQCN), e.g.ansible.builtin.apt,community.postgresql.postgresql_db.sourceOperationRef, when used, MUST be a JSON Pointer fragment; the preferred target is#/modules/<fqcn>.- Legacy
openapiOperationId/openapiOperationRefselectors are forbidden onansible-modulesources (consistent with all non-openapi types since 1.2). - UWS core validates only the type name and selector shape. Whether an FQCN
resolves to a real module, and whether
request.bodysatisfies the module's argspec, is the job of source-aware conversion and review tooling — the same deferral that kept GraphQL/OData/protobuf support thin in 1.4.
3. Worked Example: A Playbook Three Ways
3.1 The original playbook
- hosts: webservers
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
3.2 Valid today (UWS 1.5, degenerate lowering)
A single-target playbook lowers to valid UWS 1.5 right now through the
uws.runtime.1.0 extension profile, whose type identifiers (ssh, cmd,
fileio, scp, sftp) already cover Ansible's transport verbs:
uws: "1.5.0"
info:
title: Nginx Setup (degenerate lowering)
version: 1.0.0
operations:
- operationId: install_nginx
x-uws-operation-profile: uws.runtime.1.0
x-uws-runtime:
type: ssh
command: "apt-get install -y nginx"
successCriteria:
- condition: $response.statusCode == 0
- operationId: deploy_config
x-uws-operation-profile: uws.runtime.1.0
x-uws-runtime:
type: scp
command: "nginx.conf /etc/nginx/nginx.conf"
dependsOn: [install_nginx]
- operationId: restart_nginx
x-uws-operation-profile: uws.runtime.1.0
x-uws-runtime:
type: ssh
command: "systemctl restart nginx"
dependsOn: [deploy_config]
workflows:
- workflowId: main
type: sequence
steps:
- stepId: install
operationRef: install_nginx
- stepId: deploy
operationRef: deploy_config
- stepId: restart
operationRef: restart_nginx
This works, but it is a degenerate lowering: it shells out to apt-get
instead of calling the convergent ansible.builtin.apt module, it always
restarts instead of restarting only on change, and the template is pre-rendered
out of band. It demonstrates the floor, not the target.
3.2.1 Retired UWS 1.5 compatibility experiment
For historical context, tooling briefly used an extension-owned leaf operation to preserve Ansible module identity without the UWS 1.6 source type. That supplement is retired and the example below is not supported by UWS 1.7:
uws: "1.5.0"
info:
title: Nginx Setup (compatibility lowering)
version: 1.0.0
operations:
- operationId: install_nginx
x-uws-operation-profile: uws.ansible-module-call.1.0
x-uws-ansible-module:
module: ansible.builtin.apt
argspec:
sourceId: builtin
url: ./ansible/ansible-builtin.argspec.json
collection: ansible.builtin
request:
body:
name: nginx
state: present
outputs:
changed: $response.body.changed
successCriteria:
- condition: $response.body.failed != true
This keeps Ansible module execution runtime-owned while preserving the same UWS orchestration shape used by the 1.6 source-bound form.
3.3 UWS 1.6 form
uws: "1.6.0"
info:
title: Nginx Setup
version: 1.0.0
sourceDescriptions:
- name: builtin
type: ansible-module
url: ./ansible/ansible-builtin.argspec.json
operations:
- operationId: install_nginx
sourceDescription: builtin
sourceOperationId: ansible.builtin.apt
request:
body:
name: nginx
state: present
outputs:
changed: $response.body.changed
- operationId: deploy_config
sourceDescription: builtin
sourceOperationId: ansible.builtin.template
dependsOn: [install_nginx]
request:
body:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
outputs:
changed: $response.body.changed
- operationId: restart_nginx
sourceDescription: builtin
sourceOperationId: ansible.builtin.service
dependsOn: [deploy_config]
request:
body:
name: nginx
state: restarted
workflows:
- workflowId: main
type: sequence
steps:
- stepId: install
operationRef: install_nginx
- stepId: deploy
operationRef: deploy_config
- stepId: restart
operationRef: restart_nginx
when: $steps.deploy.outputs.changed == true
The handler becomes an ordinary step gated on the notifying task's changed
output. Where several tasks notify the same handler, the ansible.1.0 sub-spec
defines the lowering rule: a switch step gates each case on one notifier's
changed output, and each case references the same handler operation. Because
switch executes at most one matching case, this preserves Ansible's
run-once-at-end semantics without adding logical OR to UWS core. The Jinja2
template (nginx.conf.j2) stays a file the template module consumes; UWS
never parses it.
4. Inventory and Execution Targets
UWS has no host axis; an Ansible play's execution matrix is tasks × hosts. The adopted design takes the two-stage path browser profiles took:
Stage 1 — no core change. Inventory is input data. A loop construct with
items: $inputs.hosts fans tasks out per host; connection details and
credentials stay runtime-private (resolved per host by the bound runtime, in
the same way credential bindings already resolve). This works today but treats
hosts as untyped strings.
Stage 2 — inventory.1.0 sub-spec. Publish
versions/inventory.1.0.{json,md} alongside runtime.1.0 / browser.1.5: a
typed inventory document (hosts, groups, group/host variables — never
secrets, connection passwords, or vault material) plus a step/operation-level
execution-target extension (x-uws-target: webservers). Graduation of the
target field into core happens only if multiple runtimes need interoperable
inventory interchange — the same graduation bar every other profile met.
5. Convergence Results and Handlers
Ansible's result model is three-state (ok / changed / failed, plus
skipped), and handler semantics depend on changed. UWS core stays binary
(successCriteria); the ansible.1.0 sub-spec layers the convention on top:
- Module operations SHOULD expose
changed: $response.body.changedinoutputs. Module results are JSON;changedis already there. failedmaps to UWS failure viasuccessCriteria: [{condition: $response.body.failed != true}]or the runtime's module-failure signaling.skippedis represented structurally: a step whosewhengate is false does not execute — same semantics, no new state needed.- Handlers lower to
when-gated steps (single notifier) or switch-gated steps (multiple notifiers), as in §3.3.
A possible future core convenience — an onChanged action list parallel to
onSuccess/onFailure — is explicitly not part of UWS 1.6. The
convention covers the need without widening core.
5.1 Converter-Owned Playbook Lowering
Playbook directives are not part of the ansible-module source profile. A
static converter can still represent a useful subset through existing UWS core
objects:
| Ansible construct | UWS representation |
|---|---|
| Ordered tasks | workflow.type: sequence with operation steps. |
when simple comparison |
Step when expression. |
when with AND |
Nested switch guards or equivalent sequential guards. |
when with OR |
switch.cases, one case per disjunct, when a stable output identity is not required. |
loop / with_items |
Step forEach; $item and $index stay expression-scope values. |
register field reads |
Producer operation outputs plus consumer $steps.<step>.outputs.<field> references. |
changed_when |
Override the operation's changed output expression when statically expressible. |
failed_when |
Override or extend successCriteria when the condition can be represented as UWS comparisons. |
until + retries + delay |
Additional successCriteria plus onFailure retry metadata. |
notify / handlers |
Changed-output gates or switch cases, as described above. |
Constructs that cannot be represented without changing UWS core remain
converter diagnostics or runtime-owned behavior. In particular,
ignore_errors requires continue-on-failure semantics that UWS 1.6 does not
define; a future UWS version may add a narrow primitive for that behavior, but
this source profile deliberately does not.
6. Safety & Integrity Controls
- No secrets in the wire format. Vault-encrypted values, connection passwords, and become passwords never appear in a UWS document or inventory document. Credential bindings stay symbolic; the runtime resolves them. This is the existing UWS posture applied unchanged.
- Side-effect declaration. Module operations that mutate hosts (
state: absent,service: restarted, user/file deletion) SHOULD carry the same side-effect/confirmation metadata conventions used elsewhere in UWS packages; check-mode execution (dry run) is a runtime feature, not a wire-format field. - Fail closed on contract drift. If an FQCN does not resolve in the
referenced argspec document, or
request.bodyviolates the argspec, source-aware tooling rejects the document before execution rather than guessing — consistent with every other source family.
7. Non-Goals
UWS 1.6 deliberately does NOT standardize:
- Jinja2. Ansible's templating language never enters the UWS expression
grammar. Authoring tooling lowers templates: static values render inline,
dynamic references become UWS expressions, complex renders stay inside module
arguments (e.g. the
templatemodule'ssrcfile) where the module owns evaluation. Precedent:browser.1.5keeps{{param}}templating sub-spec-owned. - Connection and execution policy. Connection plugins,
become, forks/serial/strategy, async/poll, and check mode are runtime-owned, exactly as HTTP transport and retries are for API sources. - Continue-on-error semantics. Ansible
ignore_errorschanges sequence failure propagation. UWS 1.6 has no portable continue-on-failure primitive, so converters must either fail closed or keep that behavior runtime-owned until a future core or profile field is defined. - Roles and collection packaging. Galaxy packaging, role directory layout, and dependency resolution stay in the Ansible ecosystem. UWS binds to module contracts, not to distribution mechanics.
- Block recovery flow (
rescue/always). UWS 1.6 does not standardize Ansible block failure recovery. Staticblockmay be represented only as ordinary structural grouping when its child tasks can be lowered with existing UWS constructs.rescueandalwaysare Ansible runtime error handling behavior and remain out of scope for theansible-modulesource profile. - The Ansible execution engine. UWS does not re-specify task executor
internals, callback plugins, strategy plugins, inventory connections, or
module invocation. The
ansible-modulesource profile is inert conversion and review metadata, not an execution contract.
8. Graduation Criteria Check
Against the source-family baseline future-source-profiles.md applies
(the UWS 1.4 standard):
| Criterion | Status |
|---|---|
| Clear operation selector model | FQCN as sourceOperationId; #/modules/<fqcn> as sourceOperationRef. |
| Source-owned request/response metadata | Collection argspecs own parameter schemas and return docs; UWS never restates them. |
| Parser/index evidence or bounded tooling path | ansible-doc --json output is stable, machine-readable, and already shipped by every collection; an apitools-style argspec parser is a bounded effort. |
| Static conversion path that preserves source semantics | Playbook-to-UWS conversion binds module operations to inert argspec metadata instead of flattening them into shell strings; execution behavior remains outside UWS 1.6. |
| No secrets / credentials in the wire format | §6; vault and connection material stay runtime-private. |
| Multi-tool review need | Playbook-style automation is reviewed across many tools (ansible-core, AWX/Controller, custom converters); a neutral argspec binding lets UWS-authoring tools produce consistent review metadata without standardizing execution. |
The adopted implementation follows the established per-release recipe: schema
enum + selector-description enrichment, Go constant + validation switch +
supports16 version gate, parametrized binding tests, and the
conformance/convert round-trip extensions.