{% set rfcid = “RFC-0235” %} {% include “docs/contribute/governance/rfcs/_common/_rfc_header.md” %}
{# Fuchsia RFCs use templates to display various fields from _rfcs.yaml. View the #} {# fully rendered RFCs at https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs #}
This RFC proposes runtime and declarative APIs for creating and routing bundles of capabilities, called dictionaries.
Today, the component framework only supports “point to point” routing for capabilities it defines: in order to route a capability C from component A to component B, a route segment must exist in every intermediate component to route C between the adjacent components.
A multitude of use cases that would strongly benefit from the ability to route a bundle of capabilities as a single logical unit. Without this feature, these customers have to resort to workarounds that are costly, inflexible, and brittle. Here is a sampling of these use cases:
LogSink
, InspectSink
, and trace provider are consumed by almost every component and it would simplify the routing topology significantly if we could route them as a bundle.fuchsia.debugdata.Publisher
are in a similar category as diagnostics capabilities, except they are only enabled on certain builds. Currently we don't have a good way for builds to configure the addition of a capability that needs to span the entire topology. This is the reason profilers are only enabled in test realms today.session_manager
: All the capabilities routed from core
to session_manager
need to be re-routed by session_manager.cml
. This is a large set of capabilities so it's hard to maintain, and it means some non-platform capabilities leak into session_manager.cml
.chromium
: Chromium cml files contain a lot of duplication and would be greatly simplified if capabilities could be grouped and routed under a single name. https://fxbug.dev/42072339)The following are also motivating use cases for capability bundling, but they have some special considerations that will require followup design work beyond the proposal in this RFC.
bootstrap
realm because all driver components live there, but it doesn’t make sense to explicitly name these components in the platform topology. We could deal with this if cml had a way to bundle these services together.driver_test_realm
. These tests wish to route different services from the driver to the test. In these tests the driver test realm component is sitting between the drivers and the test, and we would like to be able to reuse the driver test realm in these tests without modifying it.Finally, there are already several existing component framework APIs that involve grouping capabilities. However, they are independent and only apply to particular situations. Here are a few examples:
use
declarations).The fact that we have so many APIs hints that users would benefit from having general abstraction that allows them to define their own dictionaries and route them.
The following teams have been identified as stakeholders, based on the use cases listed above:
Facilitator: hjfreyer@
Reviewers:
Consulted:
Socialization:
Two internal documents preceded this RFC: a use cases and requirements doc, and a core design doc. These documents have received informal approval from stakeholders.
Information from these documents has been incorporated into this RFC, where relevant.
These are the operations dictionaries MUST support, which we have derived by analyzing the use cases and generalizing from existing grouping APIs:
B'
that inherits the contents of B
, and adds additional capabilities.A dictionary is defined as a bag of key/value pairs, where the key is a capability name (e.g., fuchsia.example.Echo
) and the value is a CF capability.
A capability name is a sequence of one or more characters from the set [A-Za-z0-9_-.]
, of size 1 to N (currently N = 100, but we may extend it in the future).
We will introduce a public FIDL protocol that provides an interface to a dictionary. As FIDL-pseudocode:
library fuchsia.component; type DictionaryEntry = resource struct { key DictionaryKey; value Capability; }; protocol Dictionary { Insert(DictionaryEntry) -> (); Remove(DictionaryKey) -> (Capability); Lookup(DictionaryKey) -> (Capability); Enumerate() -> Iterator<DictionaryKey>; Clone(); };
We will also introduce a public discoverable FIDL protocol that allows the caller to create an empty dictionary.
Follow-up design work will determine the precise type definition for Capability
.
Let‘s begin with a bit of formalization that will help with defining the operations. We’ll define four special dictionaries associated with every component: a component input dictionary, component output dictionary, program input dictionary, and program output dictionary. Together, these are the root dictionaries.
The component input dictionary is a dictionary that contains all capabilities offer
ed to the component by its parent; or in other words, all capabilities that a component can route from parent
.
The component output dictionary is a dictionary that contains all capabilities expose
d by a component to its parent; or in other words, all capabilities that the parent can route from #component
.
The program input dictionary is a dictionary containing all capabilities use
d by a component.
The program output dictionary is a dictionary containing all of a component's capability
declarations.
We can express the capability routing operations in terms of these definitions:
use
, offer
, expose
, and capabilities
are routing operations that route capabilities between dictionaries.use
routes a capability from a root dictionary to the program input dictionary.expose
routes a capability from a root dictionary to the component output dictionary.offer
routes a capability from a root dictionary to a child's component input dictionary.capabilities
makes a capability in the program output dictionary available for routing.With this design, we will generalize this to allow routing operations to use arbitrary dictionaries as a source, not just root ones:
use
routes a capability from a dictionary to the program input dictionary.expose
routes a capability from a dictionary to the component output dictionary.offer
routes a capability from a dictionary to a child's component input dictionary or another dictionary.To define a brand new, empty dictionary:
capabilities: [ { dictionary: "diagnostics-bundle", }, ],
To define a dictionary whose contents are inherited from an existing dictionary, provide a path to that dictionary with extends
(see Extension):
{ dictionary: "diagnostics-bundle", extends: "parent/logging-bundle/sys", },
To aggregate capabilities into a dictionary, route capabilities into it with the to
keyword in an offer
. Since the dictionary is defined by this component, the root dictionary containing it is self
. All the same keywords are supported as a regular offer
. For example, you can change the capability's name in the target dictionary using the as
keyword.
capabilities: [ { dictionary: "diagnostics-bundle", }, ], offer: [ { protocol: "fuchsia.logger.LogSink", from: "#archivist", to: "self/diagnostics-bundle", }, { protocol: "fuchsia.inspect.InspectSink", from: "#archivist", to: "self/diagnostics-bundle", }, { directory: "publisher", from: "#debugdata", to: "self/diagnostics-bundle", rights: [ "r*" ], as: "coverage", }, ],
Delegation means routing a dictionary:
offer: [ { dictionary: "diagnostics-bundle", from: "parent", to: "#session", }, ],
Like with other capability routes, you can change the name for the target using the as
keyword.
offer: [ { dictionary: "logging-bundle", from: "parent", to: "#session", as: "diagnostics-bundle", }, ],
An equivalent runtime API will be available for dynamic offers.
Dictionaries can be made to contain other dictionaries, by routing them into another dictionary using the aggregation syntax:
capabilities: [ { dictionary: "session-bundle", }, ], offer: [ { dictionary: "driver-services-bundle", from: "parent", to: "self/session-bundle", }, ],
Following the formalization, we will we will extend the from
keyword to accept not only a root dictionary, but a dictionary that's nested in a root dictionary.
To extract a capability from a dictionary, name the dictionary in from
. This dictionary is relative to a root dictionary (parent
, #child
, etc.)
offer: [ { protocol: "fuchsia.ui.composition.Flatland", from: "parent/session-bundle", to: "#window_manager", }, ],
This also works for use
:
use: [ { protocol: "fuchsia.ui.composition.Flatland", from: "parent/session-bundle", }, ],
Extraction also works when dictionaries are nested in other dictionaries:
use: [ { protocol: "fuchsia.ui.composition.Flatland", from: "parent/session-bundle/gfx", }, ],
Use the extends
option in a dictionary definition to inherit from another dictionary:
capabilities: [ { dictionary: "session-bundle", // `session-bundle` is initialized with the dictionary the parent // offered to this component, also called `session-bundle`. extends: "parent/session-bundle", }, ], offer: [ { dictionary: "session-bundle", from: "self", to: "#session-manager", }, { protocol: "fuchsia.ui.composition.Flatland", from: "#ui", to: "self/session-bundle", }, ],
Dictionaries constructed declaratively are immutable, which is a useful security property. For a dictionary to be mutable it must be created at runtime.
When capabilities are put into a dictionary, they retain all their type information and metadata, which is separate from any metadata associated with the dictionary itself.
For example, if a capability with optional
availability is added to a dictionary by component A
, and component B
extracts that capability, it will have optional
availability at the point of extraction, even if the availability of the dictionary itself is required
.
We may impose certain constraints on availability at the point of aggregation. For example, it could make sense to forbid putting a required
capability into an optional
dictionary, since this would violate the usual invariant that availability never gets weaker when routing from target to source.
Dictionaries created at runtime must be interoperable with dictionaries in component declarations. If this were not the case, it would force users into exclusively choosing one or the other, and would be evidence that the conceptual foundation of bundling was not sufficiently general to solve both types of use cases in a similar way.
The details of the design for interoperability will be the subject of a followup proposal.
The primary known use case for this feature is driver framework, for routing service bundles that are populated at runtime.
Landing dictionaries in cml will follow the usual pipeline for introducing new cml features. First, we will add dictionaries to the cml and component.decl schema. Then, we will update cmc
, cm_fidl_validator
, and cm_rust
, and realm_builder
to compile, validate, and represent dictionaries. Scrutiny will also be updated to recognize dictionaries and have the ability to validate dictionary routes.
Work is already in progress to integrate dictionaries (as a rust type) into the component model and routing engine. The implementation of the dictionaries API should build upon this work to use these dictionaries as the backend for the public dictionary API and as the transport for routing dictionary capabilities.
There are no special performance considerations. Routing a dictionary should be as fast or faster than it takes to route the constituent capabilities individually.
Improving the ergonomics of building component topologies was a major motive for this design.
While introducing a new feature naturally increases the complexity of the API, we believe this will be more than offset by the reduction in complexity gained by incorporating dictionaries in topologies.
There is no versioning support in cmc yet for component manifest features, so care must be taken to avoid breaking compatibility with pre-built manifests. Fortunately, all new syntax being introduced for dictionaries is compatible with the old syntax, so that makes the job easier. For example, the current name syntax in from
becomes a special case of the new path syntax.
If part of a capability route passes through a dictionary, any security policies pertaining to that capability must still apply.
When capabilities are routed in a dictionary, some transparency is lost because the identities of the capabilities inside the dictionary are hidden from the intermediate components in the routes. However, they can still be deduced by following the route backwards to the provider(s). This is a deliberate compromise to achieve the flexibility and power that dictionaries unlock.
Declaratively-constructed dictionaries are immutable. For these dictionaries, you can obtain a complete description of their contents by performing a depth-first search of the aggregate routes of the dictionary from the target to its sources.
If and when dictionaries replace environments, it will enhance the security posture of the system because dictionaries, unlike environments, are routed explicitly and in the same way as other capabilities.
This proposal has no impact on privacy.
We will test this like most component manager features, with unit tests in component_manager
and cmc
, and integration tests in component_manager/tests
. We will also add integration tests to scrutiny that exercise dictionaries and policies applied to routes with dictionaries.
We'll update the rustdoc in //tools/lib/cml
.
Add a page to //docs/concepts/components
to explain dictionaries.
Add an example to //examples/components
.
in
and into
keywords for dictionariesA dictionary capability is a cml/component.decl capability type that grants access to a dictionary.
You declare a dictionary like any other component framework capability. are two variants of dictionary creation, determined by the presence of an extends
keyword.
First, you can define a brand new, empty dictionary:
capabilities: [ { dictionary: "diagnostics-bundle", }, ],
Or, you can define a dictionary whose contents are inherited from an existing dictionary by specifying a path to a dictionary in extends
and a source in from
(see Extension):
{ dictionary: "diagnostics-bundle", extends: "logging-bundle/sys", from: "parent", },
To aggregate capabilities into a dictionary, route capabilities into it with the into
keyword:
capabilities: [ { dictionary: "diagnostics-bundle", }, ], offer: [ { protocol: "fuchsia.logger.LogSink", from: "#archivist", into: "diagnostics-bundle", }, { protocol: "fuchsia.inspect.InspectSink", from: "#archivist", into: "diagnostics-bundle", }, { protocol: "fuchsia.debugdata.Publisher", from: "#debugdata", into: "diagnostics-bundle", }, ],
Same as main design.
Dictionaries can be made to contain other dictionaries, simply by routing them into a dictionaries using the aggregation syntax:
capabilities: [ { dictionary: "session-bundle", }, ], offer: [ { dictionary: "driver-services-bundle", from: "parent", into: "session-bundle", }, ],
We introduce a new in
keyword that designates a dictionary to extract a capability from.
When in
is present, the capability keyword (protocol
etc.) refers to a capability in this dictionary, rather than a capability directly offered by the component named in from
.
in
may be a name or a path (where name can be viewed as a degenerate case). If it is a name, it refers to a dictionary presented by from
. If it's a path, the first segment of the path designates a dictionary presented by from
, while the rest of the path designates a path to a dictionary nested in this one.
in
is supported by use
, offer
, expose
. It is always optional.
offer: [ { protocol: "fuchsia.ui.composition.Flatland", from: "parent", in: "session-bundle/gfx", to: "#window_manager", }, ],
expose: [ { protocol: "fuchsia.ui.composition.Flatland", from: "#scenic", in: "gfx-bundle", }, ],
use: [ { protocol: "fuchsia.ui.composition.Flatland", in: "session-bundle/gfx", }, ],
Use the extends
keyword in a dictionary definition to inherit from another dictionary:
capabilities: [ { dictionary: "diagnostics-bundle", extends: "parent/logging-bundle/sys", }, ], offer: [ { dictionary: "diagnostics-bundle", from: "self", to: "#session-manager", }, { protocol: "fuchsia.tracing.provider.Registry", from: "#trace_manager", into: "diagnostics-bundle", }, ],
Instead of designating the path to the dictionary in from
, the path could be part of the capability identifier (protocol
, directory
, etc.)
Same as main design.
Same as main design.
Same as main design.
To extract a capability from a dictionary, specify the path to the capability within the dictionary in the capability identifier (protocol
, directory
, etc.).
offer: [ { protocol: "session-bundle/fuchsia.ui.composition.Flatland", from: "parent", to: "#window_manager", }, ],
The name in the target will be the last path element, or “dirname”, by default (fuchsia.ui.composition.Flatland
). Or you can rename it with as
:
offer: [ { protocol: "session-bundle/fuchsia.ui.composition.Flatland", from: "parent", to: "#window_manager", as: "fuchsia.ui.composition.Flatland-windows", }, ],
This also works for use
, which makes a capability from a dictionary available to the program. Like with other use
declarations, the default target path rebases the name (last path element) upon /svc
:
use: [ { protocol: "session-bundle/fuchsia.ui.composition.Flatland", path: "/svc/fuchsia.ui.composition.Flatland", // default }, ],
The path syntax also works with dictionaries nested in other dictionaries:
use: [ { protocol: "session-bundle/gfx/fuchsia.ui.composition.Flatland" }, ],
Use the origin: #...
option in a dictionary definition to inherit from another dictionary:
capabilities: [ { dictionary: "session-bundle", // Source of the dictionary to extend (in this case, the one named // "session-bundle" from the parent) origin: "#session", from: "parent", }, ], offer: [ { dictionary: "session-bundle", from: "self", to: "#session-manager", }, { protocol: "fuchsia.ui.composition.Flatland", from: "#ui", into: "session-bundle", }, ],
Officially, capability identifiers in cml are names, with no intrinsically nested structure. For example:
offer: [ { protocol: "fuchsia.fonts.Provider", from: "#font_provider", to: "#session-manager", }, ],
Capability identifiers, however, do get mapped to paths, in the capabilities
and use
section. For protocols, this is usually implicit: if no path is provided, cmc fills in a default path of /svc/${capability-name}
. For example:
use: [ { protocol: "fuchsia.fonts.Provider", // path in namespace path: "/svc/fuchsia.fonts.Provider", }, ],
capabilities: [ { protocol: "fuchsia.fonts.Provider", // path in outgoing directory path: "/svc/fuchsia.fonts.Provider", }, ],
This alternative would support paths in capability identifiers. More formally:
[A-Za-z0-9_-.]
, containing 1 to 100 characters and separated by /
characters. Leading /
is not allowed.[A-Za-z0-9_-]{1,100}(/[A-Za-z0-9_-]{1,100})*
Below, we will see how this syntax naturally lays the groundwork for bundling.
To aggregate capabilities into a dictionary, route them with the same path prefix:
offer: [ { protocol: "fuchsia.logger.LogSink", from: "#archivist", to: "all", as: "diagnostics/fuchsia.logger.LogSink", }, { protocol: "fuchsia.inspect.InspectSink", from: "#archivist", to: "all", as: "diagnostics/fuchsia.inspect.InspectSink", }, { protocol: "fuchsia.debugdata.Publisher", from: "#debugdata", to: "all", as: "diagnostics/fuchsia.debugdata.Publisher", }, ],
Delegation is simply routing a dictionary as is:
offer: [ { dictionary: "diagnostics", from: "parent", to: "#session", }, ],
Dictionaries can be made to contain other dictionaries, by making the nesting dictionary's path a prefix of the nested dictionary:
offer: [ { dictionary: "driver-services-bundle", from: "parent", to: "#session-manager", as: "session/driver-services", }, ],
Simply name the capability within the dictionary you want to extract it from:
offer: [ { protocol: "session-bundle/fuchsia.ui.composition.Flatland", from: "parent", to: "#window_manager", as: "fuchsia.ui.composition.Flatland", }, ],
This also works for use
:
use: [ { protocol: "session-bundle/fuchsia.ui.composition.Flatland", path: "/svc/fuchsia.ui.composition.Flatland", }, ],
Extraction also works when dictionaries are nested in other dictionaries (TODO: example)
Rename capabilities to have a path prefix coincident with a dictionary:
offer: [ { protocol: "session-bundle", from: "parent", to: "#session-manager", }, { protocol: "fuchsia.ui.composition.Flatland", as: "session-bundle/fuchsia.ui.composition.Flatland", from: "#ui", to: "#session-manager", }, ],
Instead of introducing dictionaries, we could use fuchsia.io
directories as the base type for bundles. In one respect, this is attractive: directories already exist, and provide their own form of hierarchical bundling. However, there are many arguments against using directories:
//sdk/lib/component/outgoing
library links in a shared library shim (//sdk/lib/svc
) instead of the VFS library directly, at the cost of functionality and transparency.A companion design will be proposed separately to target the driver use cases, which can't be completely solved with just the features in this proposal.
This design opens the door to a more economical syntax for capability routing. Instead of having the capability name and from
be separate properties, we could combine them into a single path, whose root is conceptually a dictionary that contains all the root dictionaries. For example:
offer: [ { protocol: "#ui/fuchsia.ui.composition.Flatland", to: "#session-manager", }, ],
This syntax has a nice property: it naturally generalizes to allowing one to route an entire root dictionary:
offer: [ { // Plumb all capabilities from parent to child #session-manager dictionary: "parent", to: "#session-manager", }, ],
It's also worth mentioning a more general version that would unify the syntax even more:
route: [ { // Path to source capability in dictionary src: "#ui/fuchsia.ui.composition.Flatland", // Path of target capability in dictionary dst: "#session-manager/fuchsia.ui.composition.Flatland", }, ], route: [ { src: "parent", dst: "#session-manager/parent", }, ],
Capability bundles are an old idea. There are many internal predecessor docs that propose similar ideas.