92 — Ports: type, extension, binding, grant
BuiltRendered from 92-ports-as-four-layers.md in the public repository. Edits belong there.
A port is a seam through which an instance uses an outside service (email, SMS, an AI provider, site hosting, payments, a package source). Whether a port works on an instance is four separate facts, each in its own module:
| # | Layer | Question | Module |
|---|---|---|---|
| 1 | Type | Which ports can exist. | micyte/ports/port_catalog/contracts.py |
| 2 | Extension | Which installed package can fill a port. | micyte/ports/tool_package/contracts.py (PortFill) |
| 3 | Binding | Which extension this instance chose for a port. | micyte/ports/port_binding/contracts.py |
| 4 | Grant | Whether the bound calls may be made. | micyte/ports/external_call_policy/ |
A port works when all four hold.
1. Type
port_types() returns the declared port types, in display order:
| Port | Kind |
|---|---|
email_provider | Mail aliases, forwarding, held delivery. |
sms_provider | Text messages. |
ai_provider | Model messages. |
site_hosting | A published website. |
commerce_offering | A storefront. Its operations are the payment vendor's (for example paypal:order.capture), so PortType.names_its_operations is false. |
package_source | Where packages are pulled from (93). |
payment_instrument | A stored means of payment. |
The list is written in the module, not derived from the micyte/ports/ directory. NOT_BINDABLE lists the port modules that are not bindable, each with a reason (for example datum_write_policy and port_binding, which are the policies a binding is judged by). fnd_app/tests/unit/test_port_catalog.py asserts the two lists together cover the directory exactly. Each type's operations are imported from its port's contract module. port_type() raises UnknownPortType for an id that is not declared.
2. Extension
An extension is a package whose manifest declares PortFill entries: which port it fills and the functions it performs. It installs no tools. eligible_fills(port_id, packages) returns the installed packages that can fill a port, and raises ToolPackageError for an unknown port.
3. Binding
A PortBinding records which extension fills which port for which msn. Its calls are built from the extension's declared functions; the operator does not type them. declared_call_requests(binding) and declared_write_requests(binding) turn a binding into the requests layer 4 and the datum write policy check. The Ports surface writes bindings through fnd_app/instances/_shared/runtime/port_binding_write_runtime.py (bind_extension, unbind_port). Neither form writes a grant.
4. Grant
require_external_call checks an ExternalCallRequest against the instance's ExternalCallGrant entries and raises ExternalCallDenied when none matches. There is no default grant: every actor is denied until an external_call_grants entry is added to the instance's configuration. fnd_app/scripts/check_external_call_grants.py prints the entry a binding needs.
Datum writes differ: their policy derives a default from the store (the operator may write the instance's own sandboxes). Nothing in the store implies a right to call an outside service, so external calls have no default.
The host derives the actor for each call site. A grant written for a different actor than the one a call site resolves to matches nothing and the call is refused.
Binding and granting are separate writes, so a port can be bound and not granted. The Ports surface shows that state as waiting, not as a fault.
The instance diagram
The Utilities surface opens on This instance, drawn by micyte/tools/instance_diagram.py:
| Position | Shows | Selecting it opens |
|---|---|---|
| Top | Each app installed on this msn | Install and update, at that app |
| Right | Each sandbox the instance holds, linked to the app that owns it | The compendium, filtered to that sandbox |
| Left edge | A socket for each port an installed app uses | Ports |
| Beyond the sockets | Each installed extension that fills or could fill a socket | Ports |
| Bottom | The msn_profile that sign-in resolves through | — |
Socket states:
| State | Label | Layers |
|---|---|---|
| open | "nothing fills it yet" | 1–2: no binding |
| refused | "waiting to be allowed" | 3 without 4 |
| working | "working" | 3 and 4 for every bound function |
Only a binding is drawn as a line. With static=True the diagram has no links and nothing focusable; accounts that cannot change their instance see this form (96).