diff --git a/docs/.custom_wordlist.txt b/docs/.custom_wordlist.txt
index bcb2d6c68..8e67b7b67 100644
--- a/docs/.custom_wordlist.txt
+++ b/docs/.custom_wordlist.txt
@@ -99,6 +99,7 @@ linters
localhost
l ookups
lookups
+loopback
lspci
lxc
lxd
diff --git a/docs/coverage.md b/docs/coverage.md
index 9b3e731ea..d2542a5b5 100644
--- a/docs/coverage.md
+++ b/docs/coverage.md
@@ -85,7 +85,7 @@
concept |
[part-2-work-with-interfaces.rst] [part-4-craft-sdks.rst] |
|
- [concepts.rst] [concepts.rst] |
+ [concepts.rst] [concepts.rst] |
[workshops.rst] |
SK000 SK015 SK020 SK026 |
@@ -93,8 +93,8 @@
| interface |
concept |
|
- |
- [concepts.rst] [concepts.rst] [concepts.rst] |
+ [design-interface-layout.rst] |
+ [concepts.rst] [concepts.rst] [plugs-and-slots.rst] [concepts.rst] |
|
SK020 |
@@ -102,8 +102,8 @@
| interface |
concept |
[part-4-craft-sdks.rst] |
- [run-jetbrains-gateway.rst] [resolve-plug-conflicts.rst] |
- [concepts.rst] |
+ [declare-plugs-slots.rst] [run-jetbrains-gateway.rst] [resolve-plug-conflicts.rst] |
+ [plugs-and-slots.rst] [concepts.rst] |
[sdks.rst] |
SK020 |
@@ -111,8 +111,8 @@
| interface |
concept |
|
- [run-jetbrains-gateway.rst] |
- [concepts.rst] |
+ [declare-plugs-slots.rst] [run-jetbrains-gateway.rst] |
+ [plugs-and-slots.rst] [concepts.rst] |
[sdks.rst] |
SK020 |
@@ -148,7 +148,7 @@
| concept |
[part-2-work-with-interfaces.rst] |
[run-jetbrains-gateway.rst] |
- [concepts.rst] [concepts.rst] |
+ [concepts.rst] [concepts.rst] |
|
SK028 SK039 |
@@ -166,7 +166,7 @@
| concept |
[part-3-sketch-sdks.rst] |
[use-multiple-workshops.rst] |
- [concepts.rst] [concepts.rst] [multi-workshop-patterns.rst] |
+ [concepts.rst] [concepts.rst] [multi-workshop-patterns.rst] |
|
|
@@ -228,7 +228,7 @@
| sdkcraft (CLI) |
|
[part-1-get-started.rst] [part-4-craft-sdks.rst] [part-4-craft-sdks.rst] |
- [build-an-sdk.rst] [publish-an-sdk.rst] |
+ [build-an-sdk.rst] [declare-plugs-slots.rst] [publish-an-sdk.rst] |
[concepts.rst] [concepts.rst] [sdk-vs-dockerfile.rst] [sdkcraft-cli.rst] |
[index.rst] [sdkcraft.rst] [sdkcraft-definition.rst] [sdks.rst] |
SK026 |
@@ -256,7 +256,7 @@
concept |
[part-1-get-started.rst] [part-3-sketch-sdks.rst] [part-4-craft-sdks.rst] |
[build-an-sdk.rst] [publish-an-sdk.rst] [run-github-actions-locally.rst] [use-workshops-with-ai-agents.rst] [index.rst] |
- [index.rst] [concepts.rst] [best-practices.rst] [concepts.rst] [concepts.rst] [index.rst] [parts.rst] [sdk-vs-dockerfile.rst] [sdk-vs-dockerfile.rst] [concepts.rst] |
+ [index.rst] [concepts.rst] [best-practices.rst] [concepts.rst] [concepts.rst] [index.rst] [parts.rst] [sdk-vs-dockerfile.rst] [sdk-vs-dockerfile.rst] [concepts.rst] |
[ai-agents.rst] [index.rst] [sdkcraft-definition.rst] [index.rst] [index.rst] [sdks.rst] [workshops.rst] |
SK000 |
@@ -356,7 +356,7 @@
|
|
|
- [sdk-definition.rst] [sdkcraft-definition.rst] |
+ [sdk-definition.rst] [sdkcraft-definition.rst] |
|
| SDK state |
@@ -400,7 +400,7 @@
concept |
[part-3-sketch-sdks.rst] |
|
- [concepts.rst] [concepts.rst] |
+ [concepts.rst] [concepts.rst] |
|
SK036 |
@@ -652,8 +652,8 @@
| command |
[part-1-get-started.rst] |
|
- [concepts.rst] |
- [workshop-actions.rst] [workshop-definition.rst] |
+ [concepts.rst] |
+ [workshop-actions.rst] [workshop-definition.rst] |
SK046 |
| workshop changes |
@@ -670,7 +670,7 @@
command |
[part-2-work-with-interfaces.rst] |
[add-mounts.rst] |
- [camera-interface.rst] [custom-device-interface.rst] [desktop-interface.rst] [ssh-interface.rst] [tunnel-interface.rst] |
+ [camera-interface.rst] [custom-device-interface.rst] [desktop-interface.rst] [ssh-interface.rst] [tunnel-interface.rst] |
[workshop-connect.rst] |
SK028 |
@@ -679,7 +679,7 @@
| command |
[part-2-work-with-interfaces.rst] |
|
- [camera-interface.rst] [concepts.rst] [custom-device-interface.rst] [desktop-interface.rst] [gpu-interface.rst] [mount-interface.rst] [ssh-interface.rst] [tunnel-interface.rst] |
+ [camera-interface.rst] [custom-device-interface.rst] [desktop-interface.rst] [gpu-interface.rst] [mount-interface.rst] [ssh-interface.rst] [tunnel-interface.rst] |
[workshop-connections.rst] |
SK028 |
@@ -688,7 +688,7 @@
| command |
[part-2-work-with-interfaces.rst] |
[add-mounts.rst] |
- [camera-interface.rst] [custom-device-interface.rst] [desktop-interface.rst] [mount-interface.rst] [ssh-interface.rst] [tunnel-interface.rst] |
+ [camera-interface.rst] [custom-device-interface.rst] [desktop-interface.rst] [mount-interface.rst] [ssh-interface.rst] [tunnel-interface.rst] |
[workshop-disconnect.rst] |
SK028 |
@@ -706,7 +706,7 @@
| command |
[part-1-get-started.rst] |
[add-mounts.rst] |
- [mount-interface.rst] [tunnel-interface.rst] |
+ [mount-interface.rst] [tunnel-interface.rst] |
[workshop-info.rst] |
SK014 SK027 |
@@ -715,7 +715,7 @@
| command |
[part-1-get-started.rst] |
[move-projects.rst] [use-multiple-workshops.rst] [run-github-actions-locally.rst] [run-workshops-in-github-actions.rst] [use-git.rst] [debug-issues.rst] |
- [concepts.rst] [sdk-vs-dockerfile.rst] [concepts.rst] |
+ [concepts.rst] [sdk-vs-dockerfile.rst] [concepts.rst] |
[workshop-launch.rst] [workshops.rst] |
SK001 SK003 SK008 SK020 |
@@ -742,7 +742,7 @@
| command |
[part-1-get-started.rst] |
[add-mounts.rst] [run-github-actions-locally.rst] [debug-issues.rst] |
- [concepts.rst] [mount-interface.rst] [concepts.rst] |
+ [concepts.rst] [mount-interface.rst] [concepts.rst] |
[workshop-refresh.rst] [workshops.rst] |
SK012 SK020 |
@@ -876,7 +876,7 @@
| workshop (container) |
file |
[part-1-get-started.rst] |
- [use-multiple-workshops.rst] [use-workshops-with-ai-agents.rst] |
+ [design-interface-layout.rst] [use-multiple-workshops.rst] [use-workshops-with-ai-agents.rst] |
[index.rst] [concepts.rst] [concepts.rst] [multi-workshop-patterns.rst] [projects.rst] |
[index.rst] [workshop-definition.rst] |
SK001 SK006 SK008 |
@@ -896,7 +896,7 @@
|
|
|
- [workshop-definition.rst] |
+ [workshop-definition.rst] |
SK001 |
| workshop state management |
diff --git a/docs/explanation/interfaces/concepts.rst b/docs/explanation/interfaces/concepts.rst
index ed8559e57..beb8ec079 100644
--- a/docs/explanation/interfaces/concepts.rst
+++ b/docs/explanation/interfaces/concepts.rst
@@ -43,44 +43,16 @@ Currently, |ws_markup| and |sdk_markup| support the following:
- :ref:`GPU interface ` (auto-connected)
- :ref:`Mount interface ` (auto-connected)
- :ref:`SSH interface ` (manually connected)
+- :ref:`Tunnel interface ` (conditionally auto-connected)
-.. _exp_plugs_slots:
-
Plugs and slots
---------------
-To make use of these interfaces,
-SDKs and :ref:`workshops ` define *slots*.
-For example, a :ref:`mount interface ` slot
-creates a source directory to be mounted inside the workshop via a plug.
-
-Further, SDKs and :ref:`workshops ` define *plugs*
-to connect to a slot of a certain interface type.
-For example, a :ref:`mount interface ` plug
-mounts the slot to a target directory inside the workshop.
-
-You can think of the plug as the recipient of the resources exposed by the slot;
-note that a slot can handle connections with multiple plugs.
-
-Connections can be established:
-
-- Automatically:
- By running :command:`workshop launch`, :command:`workshop refresh`,
- or :command:`workshop start`.
-
-- Manually:
- By running :command:`workshop connect` after the workshop has started,
- or by listing connections in the
- :ref:`workshop definition `
- and running :command:`workshop refresh`.
-
-
-All connections are subject to validation.
-Also, automatic connections require plugs and slots to have matching details
-and aren't allowed for some interfaces, such as :samp:`ssh-agent`.
-Finally, the order of automatic connections is not guaranteed,
-so you should not rely on it.
+Interfaces become useful when SDKs declare *plugs* to consume them
+and *slots* to provide them.
+See :ref:`exp_plugs_slots` for the full mechanics,
+including the wiring you can express in the workshop definition.
.. _exp_interfaces_validation:
@@ -147,39 +119,6 @@ when a workshop is launched, refreshed, or restored,
see :ref:`exp_workshop_connection_lifecycle`.
-.. _exp_plug_bindings:
-
-Plug bindings
--------------
-
-SDKs usually access host resources via :ref:`interface plugs `.
-When multiple SDKs try to use the same resource in conflicting ways,
-the workshop won't launch and shows an error.
-
-To fix this issue, you can bind one plug to another of the same interface type.
-This makes both plugs point to the same resource without conflicts.
-Any action performed on one plug (like mounting or remounting)
-thus automatically applies to *all* bound plugs.
-
-When you run :command:`workshop connections`,
-a bound plug will have :samp:`bind` listed under :samp:`Notes`,
-along with the line number of the target plug:
-
-.. @artefact workshop connections
-
-.. code-block:: console
-
- $ workshop connections digits
-
- INTERFACE PLUG SLOT NOTES
- mount digits/torchaudio:hub digits/system:mount bind.1
- mount digits/torchvision:hub digits/system:mount bind.1
-
-
-Here, both plugs are listed as :samp:`bind.1`,
-pointing to :samp:`torchaudio:hub` in the *first* line.
-
-
.. _exp_interfaces_cli_operations:
Related CLI operations
diff --git a/docs/explanation/interfaces/index.rst b/docs/explanation/interfaces/index.rst
index ef1556dbf..189b9f01c 100644
--- a/docs/explanation/interfaces/index.rst
+++ b/docs/explanation/interfaces/index.rst
@@ -21,6 +21,7 @@ to connect SDKs to host resources and to each other:
:maxdepth: 1
concepts
+ plugs-and-slots
Hardware interfaces
diff --git a/docs/explanation/interfaces/plugs-and-slots.rst b/docs/explanation/interfaces/plugs-and-slots.rst
new file mode 100644
index 000000000..e13ee3b72
--- /dev/null
+++ b/docs/explanation/interfaces/plugs-and-slots.rst
@@ -0,0 +1,292 @@
+.. _exp_plugs_slots:
+
+.. meta::
+ :description: Plugs and slots are the mechanism through which SDKs in a
+ workshop expose and consume capabilities, forming the
+ capability topology that connects providers to consumers.
+
+Plugs and slots
+===============
+
+.. @artefact interface plug
+.. @artefact interface slot
+.. @artefact interface connection
+
+A workshop is a graph of capabilities.
+Each SDK can act as a provider, a consumer, or both,
+and the wiring between them
+is what lets a workshop deliver a coherent environment
+out of independently published parts.
+
+In |ws_markup|, that wiring uses two named endpoints:
+*plugs* and *slots*.
+Both reference an :ref:`interface ` type;
+slots provide a capability of that type,
+and plugs consume one.
+The workshop connects matching pairs at launch
+and lets you adjust the topology
+in the :ref:`workshop definition `
+when the defaults are not what you want.
+
+
+Slots provide capabilities
+--------------------------
+
+A slot exposes a capability that other SDKs can consume;
+a single slot can serve connections from multiple plugs at once.
+What a slot exposes depends on its interface:
+
+- A :ref:`mount interface ` slot
+ exposes a directory.
+
+- A :ref:`tunnel interface ` slot
+ exposes a network endpoint.
+
+- A :ref:`GPU interface ` slot
+ exposes a GPU device.
+
+- A :ref:`camera interface `,
+ :ref:`custom device interface `,
+ :ref:`desktop interface `,
+ or :ref:`SSH interface ` slot
+ exposes the corresponding host facility.
+
+
+Some capabilities are inherently host-rooted,
+like a camera device or a host-side directory.
+Only the :ref:`system SDK `
+can expose host-rooted slots,
+which is why every workshop has one installed by default.
+Regular SDKs can still publish slots
+that expose directories or endpoints from inside the workshop;
+a mount slot in a regular SDK,
+for example,
+points at a path within the SDK or the :samp:`workshop` user's home,
+not the host filesystem.
+
+
+Plugs consume capabilities
+--------------------------
+
+A plug is the consumer end.
+It is named within the SDK that declares it,
+references an interface type,
+and carries any attributes the consumer needs to apply
+once a slot is connected.
+For instance, with a mount plug,
+the central attribute is the target path inside the workshop
+where the slot's directory should appear.
+
+A plug stays declared even if no slot is connected to it,
+which means an SDK can ship optional plugs
+that only activate when a corresponding provider is also installed.
+
+
+.. _exp_interface_auto_connection:
+
+Auto-connection
+---------------
+
+When a workshop launches or refreshes,
+|ws_markup| tries to connect each plug
+to a slot of the same interface type.
+The attempt succeeds when the interface policy
+permits the plug to connect to a candidate slot.
+The policy is what gates auto-connection,
+not the number of candidate slots in the workshop.
+
+Auto-connection behavior varies by interface:
+
+- The mount interface auto-connects to slots provided by the system SDK.
+ Regular-SDK mount slots are not auto-connected by default;
+ wire them by listing the pair in the workshop's
+ :ref:`top-level connections `.
+
+- The GPU interface auto-connects.
+
+- The tunnel interface auto-connects only from host to workshop,
+ between a plug and a slot of the same name,
+ and only when the plug's endpoint
+ is a loopback address or a Unix domain socket.
+ See :ref:`exp_tunnel_connection` for the full policy.
+
+- The camera, custom device, desktop, and SSH interfaces
+ do not auto-connect.
+ They have to be connected manually with :command:`workshop connect`.
+
+
+When more than one slot is policy-eligible for the same plug,
+|ws_markup| attempts a connection for each of them
+in an order it does not guarantee.
+The right way to express a specific topology
+is to write it down in the workshop definition's :samp:`connections:` list
+instead of leaving it to auto-connection.
+
+
+Wiring mechanisms
+-----------------
+
+The :ref:`workshop definition `
+gives you two distinct YAML mechanisms for shaping the topology:
+
+- An inline :ref:`plug binding `,
+ written as :samp:`bind:` inside a plug entry,
+ delegates one plug to another plug.
+ Both plugs then point at the same target,
+ which is how same-interface conflicts are resolved.
+
+- A top-level :samp:`connections:` list,
+ written at workshop scope,
+ pairs a specific plug with a specific slot.
+ Use it to override the default auto-connect target,
+ for example to wire a mount plug to a regular SDK's slot
+ rather than the system SDK's.
+ The pair still has to satisfy the interface's auto-connection policy,
+ so interfaces that block auto-connection outright
+ (such as :samp:`ssh-agent`)
+ cannot be wired this way
+ and must be connected with :command:`workshop connect`.
+
+
+The two mechanisms are mutually exclusive for a given plug:
+if it's bound to another plug,
+it cannot also appear in a top-level :samp:`connections:` entry.
+
+|ws_markup| also exposes the runtime-only
+:command:`workshop connect` and :command:`workshop disconnect` commands,
+which change the wiring of a running workshop.
+
+
+.. _exp_plug_bindings:
+
+Inline plug bindings
+~~~~~~~~~~~~~~~~~~~~
+
+A plug binding lets one plug stand in for another:
+
+.. code-block:: yaml
+ :caption: workshop.yaml
+
+ sdks:
+ - name: consumer-sdk
+ plugs:
+ tools:
+ bind: provider-sdk:tools
+
+
+Both plugs then point to the same resource,
+and any action performed on one,
+such as connecting or remounting,
+applies to all bound plugs.
+
+Bindings are the right tool when two plugs of the same interface
+would otherwise conflict over the same target,
+typically because two SDKs each declare a plug
+with overlapping attributes.
+
+A bound plug only carries the binding;
+it cannot also define plug attributes of its own.
+The attributes come from the plug it is bound to.
+
+When you run :command:`workshop connections`,
+both the bound plug and its target
+carry a :samp:`bind.` note in the :samp:`NOTES` column,
+where :samp:`` is the row number of the target plug
+in the same output.
+
+
+Top-level connections
+~~~~~~~~~~~~~~~~~~~~~
+
+The top-level :samp:`connections:` list
+pairs a plug with a slot directly:
+
+.. code-block:: yaml
+ :caption: workshop.yaml
+
+ connections:
+ - plug: consumer-sdk:tools
+ slot: provider-sdk:bin
+
+
+Each entry uses the :samp:`:` form
+on both sides.
+Once the workshop is launched or refreshed,
+that pairing is the one |ws_markup| applies,
+regardless of what other slots could have matched.
+
+This is the mechanism to use
+when the workshop has more than one provider for an interface
+and you want to be specific about which one a consumer reads from.
+
+
+Example: two SDKs sharing a mount
+---------------------------------
+
+Consider a workshop that installs two SDKs:
+
+- :samp:`provider-sdk` ships a mount slot named :samp:`bin`
+ that exposes a directory inside its own filesystem.
+
+- :samp:`consumer-sdk` declares a mount plug named :samp:`tools`
+ whose target is a path under its workshop user's home.
+
+
+Auto-connection alone does not wire :samp:`consumer-sdk:tools`
+to :samp:`provider-sdk:bin`:
+the mount interface auto-connects to system SDK slots by default,
+so :samp:`consumer-sdk:tools` lands on :samp:`system:mount`
+and :samp:`provider-sdk:bin` stays listed but unconnected.
+You name the pairing explicitly
+with a top-level :samp:`connections:` entry:
+
+.. code-block:: yaml
+ :caption: workshop.yaml
+
+ sdks:
+ - name: provider-sdk
+ - name: consumer-sdk
+
+ connections:
+ - plug: consumer-sdk:tools
+ slot: provider-sdk:bin
+
+
+After :command:`workshop launch` or :command:`workshop refresh`,
+:command:`workshop connections` shows the chosen pairing
+and you can verify that :samp:`consumer-sdk` reads from :samp:`provider-sdk`
+rather than the system SDK's default mount slot.
+
+
+See also
+--------
+
+Explanation:
+
+- :ref:`exp_camera_interface`
+- :ref:`exp_custom_device_interface`
+- :ref:`exp_desktop_interface`
+- :ref:`exp_gpu_interface`
+- :ref:`exp_interface_concepts`
+- :ref:`exp_mount_interface`
+- :ref:`exp_sdks`
+- :ref:`exp_ssh_interface`
+- :ref:`exp_tunnel_interface`
+- :ref:`exp_workshop`
+
+
+How-to guides:
+
+- :ref:`how_declare_plugs_slots`
+- :ref:`how_design_interface_layout`
+- :ref:`how_resolve_plug_conflicts`
+
+
+Reference:
+
+- :ref:`ref_sdk_internals`
+- :ref:`ref_sdk_plugs_slots`
+- :ref:`ref_workshop_connect`
+- :ref:`ref_workshop_connections`
+- :ref:`ref_workshop_definition`
+- :ref:`ref_workshop_disconnect`
diff --git a/docs/explanation/interfaces/tunnel-interface.rst b/docs/explanation/interfaces/tunnel-interface.rst
index a30ce54eb..f083bbd95 100644
--- a/docs/explanation/interfaces/tunnel-interface.rst
+++ b/docs/explanation/interfaces/tunnel-interface.rst
@@ -75,7 +75,9 @@ provided that:
- The slot is declared in a regular SDK
-- The plug listens on :samp:`localhost` or a Unix domain socket
+- The plug listens on a loopback address
+ (for example, :samp:`localhost`, :samp:`127.0.0.1`, or :samp:`::1`)
+ or a Unix domain socket
- The plug can be matched to the slot by its name,
or via a :samp:`connections` entry in the :ref:`definition `,
diff --git a/docs/how-to/customize-workshops/design-interface-layout.rst b/docs/how-to/customize-workshops/design-interface-layout.rst
new file mode 100644
index 000000000..7b97736b6
--- /dev/null
+++ b/docs/how-to/customize-workshops/design-interface-layout.rst
@@ -0,0 +1,222 @@
+.. _how_design_interface_layout:
+
+.. meta::
+ :description: Shape a workshop's interface topology with explicit connection
+ entries in the workshop definition: survey what auto-connects,
+ wire a consumer to a specific provider, graft a missing plug,
+ or rewire a running workshop with workshop connect.
+
+How to design the interface layout of a workshop
+================================================
+
+.. @tests in tests/docs-how-to/design-interface-layout/task.yaml
+
+.. @artefact interface connection
+.. @artefact workshop definition
+
+You can shape the topology of a workshop
+by writing explicit plug-to-slot connections in the workshop definition.
+Use explicit connections
+when several SDKs in the workshop expose or consume the same interface
+and you want to be specific about which provider satisfies which consumer,
+when auto-connection lands a plug on a slot you did not intend,
+or when a consumer SDK ships no plug for a capability you want it to use.
+You need a workshop definition under :file:`.workshop/`
+that lists at least two SDKs, one with a slot and one with a matching plug.
+
+This is a different problem from same-interface plug conflicts,
+where two plugs would compete over the same target.
+For that case, see :ref:`how_resolve_plug_conflicts`,
+which uses an inline :samp:`bind:` attribute to delegate one plug to another.
+
+
+Survey the plugs and slots in scope
+-----------------------------------
+
+Launch the workshop once to see what |ws_markup| connects on its own.
+The examples here use three in-project SDKs
+that live under :file:`.workshop/` next to the definition:
+:samp:`provider-a` and :samp:`provider-b`
+each expose a mount slot named :samp:`data`,
+and :samp:`consumer` declares a mount plug named :samp:`feed`.
+
+.. code-block:: console
+
+ $ workshop launch dev
+ $ workshop connections dev
+
+ INTERFACE PLUG SLOT NOTES
+ mount - dev/provider-a:data -
+ mount - dev/provider-b:data -
+ mount dev/consumer:feed dev/system:mount -
+
+
+The output lists every plug and slot in the workshop,
+the slot each plug is connected to (if any),
+and any notes on the connection.
+:samp:`consumer:feed` landed on the system SDK's default mount slot,
+not on either regular provider,
+because mount plugs auto-connect to system SDK slots by default.
+The :samp:`provider-a:data` and :samp:`provider-b:data` slots
+stay listed but unconnected:
+regular SDK mount slots are not reached by auto-connection by default,
+even when a matching plug is in scope.
+The result is a working workshop, but probably not the one you intended.
+A regular SDK slot is wired
+either by a top-level :samp:`connections:` entry in the definition
+or manually with :command:`workshop connect`.
+
+
+Wire a consumer to a specific provider
+--------------------------------------
+
+Add a top-level :samp:`connections:` list to the workshop definition,
+pairing the plug with the slot you want it to use:
+
+.. code-block:: yaml
+ :caption: .workshop/dev.yaml
+ :emphasize-lines: 8-10
+
+ name: dev
+ base: ubuntu@22.04
+ sdks:
+ - name: project-provider-a
+ - name: project-provider-b
+ - name: project-consumer
+
+ connections:
+ - plug: consumer:feed
+ slot: provider-b:data
+
+
+Each entry uses the :samp:`:` form on both sides.
+In-project SDKs take the :samp:`project-` prefix
+in the :samp:`sdks:` list only;
+:samp:`connections:` entries and CLI output use the bare name.
+After :command:`workshop refresh`,
+|ws_markup| applies the listed pairing
+regardless of what other slots could have matched it:
+
+.. code-block:: console
+
+ $ workshop refresh dev
+ $ workshop connections dev
+
+ INTERFACE PLUG SLOT NOTES
+ mount - dev/provider-a:data -
+ mount dev/consumer:feed dev/provider-b:data -
+
+
+This decision is persistent;
+re-launching the workshop or recreating it
+applies the same pairing every time.
+To inspect the resolved mount details, run :command:`workshop info dev`,
+which lists each connected mount plug
+along with the source path the slot exposed
+and the target path inside the workshop.
+
+
+Graft a missing plug onto a consumer SDK
+----------------------------------------
+
+Suppose a consumer SDK ships no plug for the capability you want it to use.
+The workshop definition can add one:
+declare the plug under the SDK's entry in :samp:`sdks`,
+then connect it as before.
+This example pairs :samp:`provider-sdk`,
+which exposes a mount slot named :samp:`bin`,
+with :samp:`consumer-sdk`, which ships no matching plug:
+
+.. code-block:: yaml
+ :caption: .workshop/dev.yaml
+ :emphasize-lines: 5-9, 11-13
+
+ name: dev
+ base: ubuntu@22.04
+ sdks:
+ - name: project-provider-sdk
+ - name: project-consumer-sdk
+ plugs:
+ tools:
+ interface: mount
+ workshop-target: /home/workshop/.local/share/tools
+
+ connections:
+ - plug: consumer-sdk:tools
+ slot: provider-sdk:bin
+
+
+This grafts a new plug onto :samp:`consumer-sdk`
+without modifying the SDK itself.
+The publisher does not need to ship every plug
+their users might want;
+the workshop author can add the missing piece locally.
+
+
+Rewire a running workshop with workshop connect
+-----------------------------------------------
+
+For a one-off change, :command:`workshop connect` rewires a running workshop
+without editing the workshop definition.
+Pass the plug and the target slot explicitly:
+
+.. code-block:: console
+
+ $ workshop disconnect dev/consumer:feed
+ $ workshop connect dev/consumer:feed dev/provider-a:data
+ $ workshop connections dev
+
+ INTERFACE PLUG SLOT NOTES
+ mount - dev/provider-b:data -
+ mount dev/consumer:feed dev/provider-a:data manual
+
+
+The :samp:`manual` note in the :samp:`NOTES` column flags that the connection
+came from a CLI invocation rather than the workshop definition
+or the auto-connection mechanism.
+
+The workshop definition on disk is unchanged,
+and the runtime marks are not reconciled with it:
+the next :command:`workshop refresh` that applies updates
+drops connections made with :command:`workshop connect`,
+while plugs disconnected with :command:`workshop disconnect`
+stay disconnected,
+unless the disconnection was made with :option:`!--forget`.
+In the example above,
+a refresh thus leaves :samp:`consumer:feed` unconnected:
+the manual connection to :samp:`provider-a:data` is dropped,
+and the definition's pairing with :samp:`provider-b:data` is not restored
+because the plug was manually disconnected from it.
+Running :command:`workshop remove` discards all runtime marks,
+so a subsequent :command:`workshop launch` starts from the definition.
+
+For a topology that survives refreshes
+and travels with the project,
+edit the workshop definition instead.
+
+
+See also
+--------
+
+Explanation:
+
+- :ref:`exp_in_project_sdk`
+- :ref:`exp_interface_concepts`
+- :ref:`exp_plug_bindings`
+- :ref:`exp_plugs_slots`
+
+
+How-to guides:
+
+- :ref:`how_declare_plugs_slots`
+- :ref:`how_resolve_plug_conflicts`
+
+
+Reference:
+
+- :ref:`ref_workshop_connect`
+- :ref:`ref_workshop_connections`
+- :ref:`ref_workshop_definition`
+- :ref:`ref_workshop_disconnect`
+- :ref:`ref_workshop_info`
+- :ref:`ref_workshop_refresh`
diff --git a/docs/how-to/customize-workshops/index.rst b/docs/how-to/customize-workshops/index.rst
index 6ef4f43d0..75c0468da 100644
--- a/docs/how-to/customize-workshops/index.rst
+++ b/docs/how-to/customize-workshops/index.rst
@@ -15,6 +15,7 @@ workshops in parallel:
Add actions to workshops
Add mounts
+ Design the interface layout
Forward ports
Move projects around
Use multiple workshops
diff --git a/docs/how-to/develop-sdks/declare-plugs-slots.rst b/docs/how-to/develop-sdks/declare-plugs-slots.rst
new file mode 100644
index 000000000..5fe4bbd1b
--- /dev/null
+++ b/docs/how-to/develop-sdks/declare-plugs-slots.rst
@@ -0,0 +1,171 @@
+.. _how_declare_plugs_slots:
+
+.. meta::
+ :description: Step-by-step guide on declaring mount and tunnel plugs and
+ slots in an SDK definition so that an SDK can consume and
+ expose capabilities to other SDKs in a workshop.
+
+How to declare plugs and slots
+==============================
+
+.. @tests in tests/docs-how-to/declare-plugs-slots/task.yaml
+
+.. @artefact interface plug
+.. @artefact interface slot
+.. @artefact sdkcraft (CLI)
+
+This guide shows how to declare plugs and slots
+in an SDK definition,
+so that an SDK can consume capabilities from other SDKs
+or expose its own to them.
+The examples cover the :samp:`mount` and :samp:`tunnel` interfaces;
+plugs and slots for the other supported interfaces
+follow the same shape.
+
+
+Prerequisites
+-------------
+
+You need a working |sdk_markup| installation
+and an :file:`sdkcraft.yaml` you can edit.
+If you don't have one yet,
+:ref:`tut_craft_sdks` walks through scaffolding an SDK with
+:command:`sdkcraft init`.
+The declarations below go under top-level
+:samp:`plugs:` and :samp:`slots:` keys in :file:`sdkcraft.yaml`.
+
+
+Declare a mount plug
+--------------------
+
+A mount plug consumes a directory
+that becomes available at a path inside the workshop.
+The required attribute is :samp:`workshop-target`,
+which must be an absolute path
+and may use :envvar:`$SDK` to refer to the SDK installation directory:
+
+.. code-block:: yaml
+ :caption: sdkcraft.yaml
+ :emphasize-lines: 3-5
+
+ # ...
+
+ plugs:
+ cache:
+ interface: mount
+ workshop-target: /home/workshop/.cache/cachekit
+
+
+When a workshop installs the SDK,
+|ws_markup| connects this plug
+to a matching slot,
+either auto-connecting it to the workshop's :ref:`system SDK `
+or to another SDK's slot
+when the workshop definition wires that pairing explicitly.
+The :samp:`mode`, :samp:`uid`, :samp:`gid`,
+and :samp:`read-only` attributes are optional.
+
+
+Declare a mount slot
+--------------------
+
+A mount slot exposes a directory the SDK provides
+so that other SDKs can plug into it.
+The required attribute is :samp:`workshop-source`,
+which must be an absolute path inside the workshop
+and may use :envvar:`$SDK`:
+
+.. code-block:: yaml
+ :caption: sdkcraft.yaml
+ :emphasize-lines: 3-5
+
+ # ...
+
+ slots:
+ shared:
+ interface: mount
+ workshop-source: /home/workshop/cachekit-share
+
+
+This is for cross-SDK sharing within the workshop.
+Exposing a directory from the host
+is the responsibility of the
+:ref:`system SDK `;
+a regular SDK cannot declare a host-rooted mount slot.
+
+
+Declare a tunnel slot
+---------------------
+
+A tunnel slot exposes a network endpoint
+inside the workshop:
+
+.. code-block:: yaml
+ :caption: sdkcraft.yaml
+ :emphasize-lines: 3-5
+
+ # ...
+
+ slots:
+ api:
+ interface: tunnel
+ endpoint: 127.0.0.1:8080
+
+
+A tunnel slot is auto-connected only when the host side
+declares a tunnel plug that matches the slot by name
+or through a :samp:`connections:` entry in the workshop definition,
+and only when that plug's endpoint
+is a loopback address or a Unix domain socket.
+Other pairings have to be connected manually
+with :command:`workshop connect`.
+The endpoint syntax accepts shorthand forms,
+including bare port numbers and unix socket paths.
+See :ref:`ref_tunnel_interface` for the full grammar.
+
+
+Verify the declarations
+-----------------------
+
+After packing the SDK with :command:`sdkcraft pack`,
+confirm that the declared plugs and slots
+ended up in the SDK metadata by extracting :file:`meta/sdk.yaml`
+from the artifact:
+
+.. code-block:: console
+
+ $ tar xOf __.sdk meta/sdk.yaml
+
+
+The output should include the :samp:`plugs:` and :samp:`slots:` blocks
+exactly as they were declared.
+
+
+See also
+--------
+
+Explanation:
+
+- :ref:`exp_mount_interface`
+- :ref:`exp_plugs_slots`
+- :ref:`exp_sdks`
+- :ref:`exp_tunnel_interface`
+- :ref:`exp_workshop_definition_connections`
+
+
+How-to guides:
+
+- :ref:`how_design_interface_layout`
+- :ref:`how_resolve_plug_conflicts`
+
+
+Reference:
+
+- :ref:`ref_sdk_definition`
+- :ref:`ref_sdk_plugs_slots`
+- :ref:`ref_tunnel_interface`
+
+
+Tutorial:
+
+- :ref:`tut_craft_sdks`
diff --git a/docs/how-to/develop-sdks/index.rst b/docs/how-to/develop-sdks/index.rst
index f230dcd4b..b3d5d7c84 100644
--- a/docs/how-to/develop-sdks/index.rst
+++ b/docs/how-to/develop-sdks/index.rst
@@ -17,4 +17,5 @@ and publishing the result to the SDK Store.
:maxdepth: 1
Build an SDK
+ Declare plugs and slots
Publish an SDK
diff --git a/tests/docs-how-to/declare-plugs-slots/sdkcraft.yaml b/tests/docs-how-to/declare-plugs-slots/sdkcraft.yaml
new file mode 100644
index 000000000..983b9fd09
--- /dev/null
+++ b/tests/docs-how-to/declare-plugs-slots/sdkcraft.yaml
@@ -0,0 +1,26 @@
+name: cachekit
+version: '0.1'
+summary: Synthesized SDK exercising plug and slot declaration
+description: |
+ cachekit is a synthesized example for the declare-plugs-slots how-to.
+license: GPL-3.0
+platforms:
+ ubuntu@22.04:amd64:
+ ubuntu@24.04:amd64:
+
+parts:
+ my-part:
+ plugin: nil
+
+plugs:
+ cache:
+ interface: mount
+ workshop-target: /home/workshop/.cache/cachekit
+
+slots:
+ shared:
+ interface: mount
+ workshop-source: /home/workshop/cachekit-share
+ api:
+ interface: tunnel
+ endpoint: 127.0.0.1:8080
diff --git a/tests/docs-how-to/declare-plugs-slots/task.yaml b/tests/docs-how-to/declare-plugs-slots/task.yaml
new file mode 100644
index 000000000..bedda1184
--- /dev/null
+++ b/tests/docs-how-to/declare-plugs-slots/task.yaml
@@ -0,0 +1,32 @@
+summary: Test 'How to declare plugs and slots' to ensure it's operational
+prepare: |
+ . "$TESTSLIB"/utils.sh
+ install_sdkcraft
+restore: |
+ rm -rf cachekit /tmp/packed-meta.yaml
+execute: |
+ . "$TESTSLIB"/utils.sh
+
+ # Scaffold an SDK named cachekit, then replace the generated sdkcraft.yaml
+ # with the one the how-to teaches.
+ mkdir cachekit/ && cd cachekit/
+ chown ubuntu:ubuntu .
+ run_sdkcraft init
+ cp ../sdkcraft.yaml ./sdkcraft.yaml
+ chown ubuntu:ubuntu ./sdkcraft.yaml
+
+ run_sdkcraft pack
+
+ # The packed artifact is named __.sdk; the build host is amd64.
+ ARTIFACT=$(ls cachekit_amd64_*.sdk | head -1)
+ test -n "$ARTIFACT"
+
+ # Confirm the embedded meta/sdk.yaml carries the declared plugs and slots
+ # exactly as written in sdkcraft.yaml.
+ tar xOf "$ARTIFACT" meta/sdk.yaml | tee /tmp/packed-meta.yaml
+
+ MATCH "interface: mount" /home/workshop/provider-a-data/marker
+chown 1000:1000 /home/workshop/provider-a-data/marker
diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml b/tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml
new file mode 100644
index 000000000..df7e24b98
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml
@@ -0,0 +1,13 @@
+name: provider-a
+base: ubuntu@22.04
+summary: First synthesized provider for the ambiguity test
+description: |
+ Exposes a mount slot named "data". Combined with provider-b in the same
+ workshop, this creates ambiguous autowiring for the consumer's "feed" plug.
+
+sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00"
+
+slots:
+ data:
+ interface: mount
+ workshop-source: /home/workshop/provider-a-data
diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base
new file mode 100755
index 000000000..8726619a4
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base
@@ -0,0 +1,4 @@
+#!/bin/bash
+install -d -m 0755 -o 1000 -g 1000 /home/workshop/provider-b-data
+echo "from-b" >/home/workshop/provider-b-data/marker
+chown 1000:1000 /home/workshop/provider-b-data/marker
diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml
new file mode 100644
index 000000000..34ce49271
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml
@@ -0,0 +1,13 @@
+name: provider-b
+base: ubuntu@22.04
+summary: Second synthesized provider for the ambiguity test
+description: |
+ Exposes a mount slot named "data" with the same interface as provider-a,
+ creating the ambiguous autowiring scenario the how-to teaches you to resolve.
+
+sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00"
+
+slots:
+ data:
+ interface: mount
+ workshop-source: /home/workshop/provider-b-data
diff --git a/tests/docs-how-to/design-interface-layout/dev.connected.yaml b/tests/docs-how-to/design-interface-layout/dev.connected.yaml
new file mode 100644
index 000000000..29f533db8
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/dev.connected.yaml
@@ -0,0 +1,10 @@
+name: dev
+base: ubuntu@22.04
+sdks:
+ - name: project-provider-a
+ - name: project-provider-b
+ - name: project-consumer
+
+connections:
+ - plug: consumer:feed
+ slot: provider-b:data
diff --git a/tests/docs-how-to/design-interface-layout/dev.reordered.yaml b/tests/docs-how-to/design-interface-layout/dev.reordered.yaml
new file mode 100644
index 000000000..cf484ab95
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/dev.reordered.yaml
@@ -0,0 +1,10 @@
+name: dev
+base: ubuntu@22.04
+sdks:
+ - name: project-provider-b
+ - name: project-provider-a
+ - name: project-consumer
+
+connections:
+ - plug: consumer:feed
+ slot: provider-b:data
diff --git a/tests/docs-how-to/design-interface-layout/dev.yaml b/tests/docs-how-to/design-interface-layout/dev.yaml
new file mode 100644
index 000000000..f2ee27c70
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/dev.yaml
@@ -0,0 +1,6 @@
+name: dev
+base: ubuntu@22.04
+sdks:
+ - name: project-provider-a
+ - name: project-provider-b
+ - name: project-consumer
diff --git a/tests/docs-how-to/design-interface-layout/task.yaml b/tests/docs-how-to/design-interface-layout/task.yaml
new file mode 100644
index 000000000..d4fe3c26d
--- /dev/null
+++ b/tests/docs-how-to/design-interface-layout/task.yaml
@@ -0,0 +1,37 @@
+summary: Test 'How to design the interface layout of a workshop' to ensure it's operational
+restore: |
+ . "$TESTSLIB"/utils.sh
+ workshop_exec remove dev graft || true
+execute: |
+ . "$TESTSLIB"/utils.sh
+ chown -R ubuntu:ubuntu .
+
+ echo "Stage 1: launch without connections; mount plugs do not auto-connect to regular SDK slots, so consumer:feed falls back to system:mount."
+ cp dev.yaml .workshop/dev.yaml
+ workshop_exec launch dev
+ workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/system:mount'
+
+ echo "Stage 2: add a connections entry pointing the plug at provider-b; refresh; verify the chosen slot connects and the marker is from-b."
+ cp dev.connected.yaml .workshop/dev.yaml
+ workshop_exec refresh dev
+ workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/provider-b:data'
+ workshop_exec exec dev -- cat /home/workshop/feed/marker | MATCH '^from-b$'
+
+ echo "Stage 3: disconnect then connect to provider-a at runtime; the runtime change is visible (manual) but the on-disk definition is unchanged."
+ EXPECTED_YAML_HASH=$(sha256sum .workshop/dev.yaml | cut -d' ' -f1)
+ workshop_exec disconnect dev/consumer:feed
+ workshop_exec connect dev/consumer:feed dev/provider-a:data
+ workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/provider-a:data.+manual'
+ workshop_exec exec dev -- cat /home/workshop/feed/marker | MATCH '^from-a$'
+ ACTUAL_YAML_HASH=$(sha256sum .workshop/dev.yaml | cut -d' ' -f1)
+ test "$EXPECTED_YAML_HASH" = "$ACTUAL_YAML_HASH"
+
+ echo "Stage 4: a refresh that applies updates drops the manual connection and keeps the manually disconnected pairing disconnected, so the plug ends up unconnected."
+ cp dev.reordered.yaml .workshop/dev.yaml
+ workshop_exec refresh dev
+ workshop_exec connections dev | MATCH '^mount +dev/consumer:feed +- +-'
+
+ echo "Stage 5: graft a plug onto an SDK that ships none; launch the graft workshop; the grafted plug connects to provider-a and reads from-a."
+ workshop_exec launch graft
+ workshop_exec connections graft | MATCH '^mount.+graft/plainconsumer:feed.+graft/provider-a:data'
+ workshop_exec exec graft -- cat /home/workshop/feed/marker | MATCH '^from-a$'