Source capabilities — the optional slices¶
A source adapter's mandatory contract is WritableSourcePort. Everything a store might additionally be able to do — keep versions, hold drafts, resolve overlays, store bundle entries — is a separate, opt-in Protocol here.
These exist so the kernel never has to ask hasattr(source, ...). That matters more than it sounds: the kernel needs to know what your store cannot do before it reads, so a face can refuse honestly instead of serving a confident empty answer. Read what your declaration turns on before you implement any of them.
Generated from the source
Names, signatures and docstrings are parsed out of packages/sdk-py/dna
by scripts/gen_ports_docs.py. The prose around each contract is
hand-written in scripts/ports_prose.py, and the generator fails
if a port has none — so a new port cannot ship undocumented.
BundleEntryReadable¶
dna.kernel.capabilities.BundleEntryReadable · @runtime_checkable · extension point
Mandatory in practice. The adapter guide lists it alongside WritableSourcePort and KernelAttachable as what every adapter implements, and the port-contract test asserts it of all of them.
From the source
Source adapter capability: fetch a single bundle entry by name.
The kernel uses this to read large binary payloads (graph.json,
tree.json, ...) without rehydrating the whole bundle through the
Reader pipeline. Implementing adapters store bundle entries in
their backing store (filesystem dir, dna_bundle_entries SQL
table) and serve byte payloads directly.
Implementations may be sync (-> bytes) or async
(-> Awaitable[bytes]). The kernel's
Kernel.fetch_bundle_entry and
Kernel.fetch_bundle_entry_async handle both shapes via
inspect.isawaitable on the return value.
Tenant overlay routing: when tenant is provided and the
adapter supports it, the tenant-scoped copy is preferred over
the base layer (see FilesystemWritableSource and SqlAlchemySource
impls for the canonical 2-step lookup).
Raises:
- FileNotFoundError when the bundle or entry is absent
(after the tenant overlay → base layer fallback).
The kind kwarg is the kind name (e.g. "GraphifyArtifact")
that owns the bundle. It's optional for backwards compatibility
— adapters that don't need it (e.g. filesystem, where each
container is a directory namespace) may ignore it. SQL adapters
use it to disambiguate between two bundles that share the same
name in the same scope but live in different containers
(e.g. a Skill and a Persona both named "foo").
Without kind, SQL adapters fall back to a name+entry-only
match and accept the rare collision risk documented in
SqlAlchemySource.fetch_bundle_entry.
The contract
| Member | Signature | What it must do |
|---|---|---|
fetch_bundle_entry |
def fetch_bundle_entry(self, scope: str, container: str, name: str, entry: str, *, tenant: str | None=None, kind: str | None=None) -> bytes | Awaitable[bytes] |
|
list_bundle_entries |
def list_bundle_entries(self, scope: str, container: str, name: str, *, tenant: str | None=None, only_tenant: bool=False, kind: str | None=None) -> list[str] | Awaitable[list[str]] |
List entry paths for a bundle. Composed = tenant overlay ∪ base (tenant rows shadow base by path). only_tenant returns just the tenant's own override rows. Empty list when the bundle is absent. |
Swap it when — Always. Treat this as part of the floor, not as optional.
The minimum that works — fetch_bundle_entry and list_bundle_entries. Both may be sync or async — the kernel awaits what is awaitable, so the filesystem adapter returns bytes directly and the SQL ones return coroutines, and both are conformant.
What it lights up — Declared as bundle_read. The production gate is an isinstance(source, BundleEntryReadable) in dna.kernel.bundle.io, not the flag — skip the Protocol and every bundle read raises NotImplementedError naming this Protocol, which is at least a good error message.
How you prove it — source_conformance_suite case bundle_entry_round_trip.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
BundleEntryWritable¶
dna.kernel.capabilities.BundleEntryWritable · @runtime_checkable · extension point
The write half of the pair above. Same rules, same gate.
From the source
Source adapter capability: persist a single bundle entry payload.
Write twin of BundleEntryReadable — used by tools and HTTP
handlers that need to put a binary payload (PNG/JPG/JSON blob)
into a bundle's storage WITHOUT going through the WriterPort
serialize pipeline (which only emits text entries).
Source-agnostic by design: the kernel's
Kernel.write_bundle_entry_async dispatches to this method on
the active source, so callers can switch between FS / SQLite /
Postgres without rewriting the binary persistence path. Adapters
bound their own atomicity guarantees — Postgres uses a single
transaction with the doc write; filesystem writes the file
directly under the bundle dir; SQLite uses a single sqlite
transaction.
Tenant scoping: writes MUST honor the active tenant just like
the doc index — see WritableSourcePort.write_instance. A
mismatch produces orphan bundle rows that the delete path can't
reach (bug observed 2026-05-21 with generate_image hardcoding
tenant='').
Args: - scope: scope identifier - container: Kind name owning the bundle (e.g. "ImagePrompt") - name: doc name (e.g. "img-iu-dna-overview-kernel") - entry: entry path within the bundle (e.g. "output.png") - content: raw bytes - tenant: active tenant; pass through from the caller's context
The contract
| Member | Signature | What it must do |
|---|---|---|
write_bundle_entry |
def write_bundle_entry(self, scope: str, container: str, name: str, entry: str, content: bytes, *, tenant: str | None=None, kind: str | None=None) -> None | Awaitable[None] |
|
delete_bundle_entry |
def delete_bundle_entry(self, scope: str, container: str, name: str, entry: str, *, tenant: str | None=None, kind: str | None=None) -> bool | Awaitable[bool] |
Delete ONE entry row for tenant (base sentinel '' when None). Returns True if a row existed. Reverting a tenant fork deletes the tenant row so the base entry composes through again. |
Swap it when — Whenever your source is writable at all.
The minimum that works — write_bundle_entry and delete_bundle_entry; sync or async.
What it lights up — Declared as bundle_write; gated in production by isinstance in dna.kernel.bundle.io. Skip it and bundle writes raise, naming the Protocol.
How you prove it — source_conformance_suite case bundle_entry_round_trip.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
Draftable¶
dna.kernel.capabilities.Draftable · @runtime_checkable · extension point
Instances that exist before they are live.
From the source
Source adapter capability: draft/publish lifecycle.
A Draftable source keeps unpublished drafts (load_drafts)
and can promote a draft to the live instance (publish). All 3
production adapters implement this — the old capabilities()
dicts that reported drafts: False for the filesystem were
lying; isinstance(src, Draftable) reports the truth.
The contract
| Member | Signature | What it must do |
|---|---|---|
load_drafts |
async def load_drafts(self, scope: str) -> list[dict] |
|
publish |
async def publish(self, scope: str, kind: str, name: str) -> str |
Swap it when — Your store can hold an unpublished instance that reads do not see. If publishing is a no-op for you (as on the filesystem), declare drafts=False.
The minimum that works — load_drafts and publish — the probe requires both.
What it lights up — The drafts flag and the draft lifecycle across the faces. Undeclared, everything written is immediately live.
How you prove it — source_conformance_suite case drafts_lifecycle.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
KernelAttachable¶
dna.kernel.capabilities.KernelAttachable · @runtime_checkable · extension point
Mandatory. The kernel hands itself to the adapter once wiring is done, so the adapter can reach the Kind registry it needs to interpret what it is storing.
From the source
Source adapter capability: accept post-init kernel wiring.
H2 unification: Kernel.auto(source=...) previously had a
hardcoded isinstance(source, FilesystemWritableSource) check
that wired source._writers and source.set_kernel(k). SQLite
and Postgres sources required the same wiring but only got it via
the runtime source factory — leaving direct
Kernel.auto(source=SqlAlchemySource(...)) callers with a
half-broken kernel that silently dropped bundle writes.
Adapters now declare attachability by implementing
attach_kernel(kernel). The kernel calls this method on every
source it accepts — uniformly. Implementations install the
kernel's _writers, _readers, and (optionally) a back-ref
to the kernel itself for the source's save path to consult
storage_for_kind.
The contract: attach is idempotent. Calling twice with the same kernel produces the same wired state.
The contract
| Member | Signature | What it must do |
|---|---|---|
attach_kernel |
def attach_kernel(self, kernel: object) -> None |
Swap it when — Always. The port-contract test asserts every adapter implements it.
The minimum that works — attach_kernel(kernel), and it must be idempotent — it can be called more than once, and a non-idempotent implementation corrupts state in ways that surface far from the cause.
What it lights up — Declared as kernel_attachable; gated by isinstance at boot. Uniquely on this page the failure is fail-soft — boot logs a warning and continues, so a missing attach_kernel shows up later as an adapter that cannot resolve Kinds rather than as a boot error. Do not rely on boot to tell you.
How you prove it — packages/sdk-py/tests/test_port_contract.py asserts it of every adapter; the kit's port_surface case covers the shape.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
LayerAware¶
dna.kernel.capabilities.LayerAware · @runtime_checkable · extension point
The layer= twin of TenantAware, with the identical caveat.
From the source
Source adapter capability: writes accept a layer overlay kwarg.
Same runtime_checkable caveat as :class:TenantAware — use
:func:write_kwarg_support for the runtime kwarg decision; this Protocol is
for documentation + static typing.
The contract
| Member | Signature | What it must do |
|---|---|---|
save_instance |
async def save_instance(self, scope: str, kind: str, name: str, raw: dict, *, layer: tuple[str, str] | None=...) -> str |
Swap it when — Same as TenantAware — you want the contract expressed in types.
The minimum that works — save_instance accepting layer=, plus the matching write_kwargs entry.
What it lights up — The layer half of tenant_layer_writes. Same trap: use write_kwarg_support, not isinstance.
How you prove it — source_conformance_suite case declared_write_kwargs_accepted.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
Layered¶
dna.kernel.capabilities.Layered · @runtime_checkable · extension point
Resolving an instance through an overlay — the mechanism behind tenancy and per-customer forks.
From the source
Source adapter capability: layer (overlay) resolution.
A Layered source can resolve an instance from a specific layer
via load_layer — the method the Composition Engine consults
for overlay/inheritance reads. sqlite/postgres and the composite
filesystem router implement it; the flat filesystem writable does
not (it can list layer values but not resolve them).
The contract
| Member | Signature | What it must do |
|---|---|---|
load_layer |
async def load_layer(self, scope: str, layer_id: str, layer_value: str, kind: str, name: str) -> dict | None |
Swap it when — Your store can key an instance by an overlay dimension as well as by name.
The minimum that works — load_layer, returning None for an unknown layer rather than raising.
What it lights up — The layers flag and the overlay engine. ⚠️ The known divergence to inherit or fix: the SQLite dialect's instances primary key omits tenant, so an overlay publish clobbers the base row. It is a strict=True xfail in both the matrix and the conformance kit's _KNOWN divergence table. Postgres passes with identical logic — this is schema debt, not a design limit — so if your store can key by tenant, key by tenant.
How you prove it — source_conformance_suite case tenant_overlay_shadows_base, and the matrix's test_tenant_overlay_shadows_base row.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
TenantAware¶
dna.kernel.capabilities.TenantAware · @runtime_checkable · extension point
Documentation and static typing. Read the warning — this Protocol is the one place on this page where isinstance is the wrong tool, and the source says so itself.
From the source
Source adapter capability: save_instance/delete_instance accept a
first-class tenant kwarg (the modern WritableSourcePort write contract,
Phase 2). All 3 production adapters satisfy it.
NOTE: runtime_checkable isinstance only checks that the methods
exist, NOT that they accept a tenant keyword — Protocols can't express a
kwarg-level capability. So this Protocol instances the contract + serves
static checking, while the kernel's runtime branch that decides whether to
pass tenant= uses :func:write_kwarg_support (a memoized signature
probe) instead. Don't isinstance(src, TenantAware) to gate the tenant
kwarg — it would be True for any source with a save_instance at all.
The contract
| Member | Signature | What it must do |
|---|---|---|
save_instance |
async def save_instance(self, scope: str, kind: str, name: str, raw: dict, *, tenant: str | None=...) -> str |
|
delete_instance |
async def delete_instance(self, scope: str, kind: str, name: str, *, tenant: str | None=...) -> None |
Swap it when — You are writing a source adapter and want your editor and type-checker to hold you to the modern write contract. Declare the behaviour through capabilities().write_kwargs, which is what the kernel actually reads.
The minimum that works — save_instance / delete_instance accepting tenant=, and "tenant" present in your declared write_kwargs / delete_kwargs.
What it lights up — The tenant_layer_writes flag and the tenant kwarg being passed at all. ⚠️ Never gate on isinstance(src, TenantAware). A runtime_checkable Protocol checks that methods exist, never that they accept a keyword — so that check is True for any source with a save_instance at all, including ones that would reject the kwarg. The kernel uses write_kwarg_support(src), a memoized signature probe, and so should you.
How you prove it — source_conformance_suite case declared_write_kwargs_accepted, which checks your declaration against what your signatures really take.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
Versionable¶
dna.kernel.capabilities.Versionable · @runtime_checkable · extension point
Your store can return a specific published version of an instance. Worth reading the caveat below before you declare it.
From the source
Source adapter capability: per-Kind semver versioning.
Backs the catalog versioning flow: an adapter that is Versionable
supports get_version(scope, kind, name, version_id) and
list_versions(...).
The runtime gate is the DECLARED SourceCapabilities.versions flag,
not an isinstance against this Protocol — there is no such check
anywhere in the tree. Implement the Protocol for static typing and for
the reader; declare the flag for the kernel.
⚠️ versions says version rows are READABLE and nothing more. The
filesystem adapter declares it while keeping no history at all
(list_versions returns []), which is precisely why answering
"what did you believe at T?" had to become its OWN flag
(:attr:SourceCapabilities.as_of_reads) rather than riding on this
one. Do not widen this docstring's promise again: an earlier version
of it claimed an isinstance check and a
/catalog/{owner}/{name}/versions endpoint, and neither existed.
The production adapters (FilesystemWritableSource and SqlAlchemySource
on both dialects) implement this. An adapter that does not track
per-instance versions declares versions=False and the faces refuse
the version reads rather than inventing an answer.
The contract
| Member | Signature | What it must do |
|---|---|---|
get_version |
async def get_version(self, scope: str, kind: str, name: str, version_id: str) -> dict |
Swap it when — Your store keeps version rows and can hand back a past one by id. If it does not, do not declare versions — see the trap.
The minimum that works — get_version.
What it lights up — The versions flag, which says version rows are readable and nothing more. ⚠️ It does not mean history exists: the filesystem adapter declares versions=True and list_versions returns []. That gap is exactly why the ability to answer what did you believe at time T became a separate flag, as_of_reads — a store that cannot reconstruct the past must let the face raise AsOfUnsupported (REST 501) rather than serve today's instance under yesterday's timestamp. And when history existed but was pruned, that is AsOfTruncated (REST 410), never a LookupError: the instance did not exist yet is an answer, and must not render the same as I no longer hold the record.
How you prove it — source_conformance_suite case versions_surface.
Shipped implementations — none in-tree. This port has no reference adapter yet: you would be writing the first one.
What your declaration turns on¶
Every capability above is declared, not sniffed. Your adapter returns one
SourceCapabilities literal from capabilities(), and the kernel consults
that declaration — never hasattr, never inspect.
That indirection is the point. It exists because the previous design was a
per-adapter dict of magic strings whose keys and sync/async shape drifted
between backends, and which lied: the filesystem adapter claimed
versions: False while implementing get_version. A declaration can be
checked against reality; a hasattr cannot.
The declaration¶
def capabilities(self) -> SourceCapabilities:
return SourceCapabilities(
source="mystore",
drafts=False, versions=False, layers=True,
bundle_read=True, bundle_write=True, kernel_attachable=True,
granular_list=True, granular_one=True,
query_pushdown=True, tenant_layer_writes=True,
api_version_identity=True, as_of_reads=False, edge_graph=False,
valid_time=False, key_lookup=False, key_lookup_indexed=False,
write_kwargs=frozenset({"tenant", "layer", "if_absent"}),
delete_kwargs=frozenset({"tenant"}),
)
⚠️ valid_time and key_lookup_indexed are the two flags whose value may
depend on the binding rather than on the class. SqlAlchemySource serves
Postgres and SQLite from one class, and only Postgres has the tstzrange
column plus the EXCLUDE constraint that makes overlapping validity periods
impossible — so it declares valid_time=self._is_pg and sets an instance
attribute (supports_valid_time) that the reflection oracle reads. Probing for
the method would derive True on SQLite, where load_one_valid_at exists
and refuses, and the oracle would then certify a declaration that lies.
key_lookup_indexed has the identical shape and the identical reason.
find_instances_by_spec_key — the read that lets a relation declared
by: workspace_id be followed rather than merely declared — is defined for
both bindings, but only Postgres serves it from an index: dna_insts_spec_gin_idx
(baseline revision 0001) is a GIN over (content::jsonb->'spec'), generic over
the key, and answers a containment lookup over 200 000 instances in 1,8 ms
against 15 ms scanned. SQLite has no GIN and no containment operator, and the
filesystem adapter has no index at all: both answer honestly by walking, and
both say so through the flag rather than through a profiler.
The pair is deliberately not one flag. key_lookup=False means "cannot
answer" and changes what a face may claim; key_lookup_indexed=False means
"answers the slow way" and changes what a deployment may be asked to hold.
Collapsing them would leave an honest O(N) store with no way to be honest.
Declare conservatively. An undeclared capability means a feature is off; an
over-declared one means the kernel hands you work you will silently drop, and
the conformance kit's capabilities_declared_honestly case exists to catch
exactly that before a user does.
⭐ Why an honest False is a feature, not an admission¶
This is the part of the port contract that is genuinely non-obvious, and it is what separates a trustworthy adapter from a merely working one.
A store that cannot answer must refuse, not approximate.
Take the reference graph. A store with no edge table does not return an empty list. The kernel's own words:
dna/kernel/query/graph.py
A store without an edge table does not return an empty list. [] reads as
"nothing points at this instance", which is a claim only a store that
actually records edges may make; the filesystem adapter has neither a
transaction to write edges in nor a table to write them to, so the answer
is GraphUnsupported and the face says so. Serving a confident empty
answer from a store that cannot know is the fail-open silence this codebase
treats as a defect, not a convenience.
The same reasoning produced as_of_reads as a flag separate from versions,
and it is why these questions must be askable before the read rather than
discovered during it.
| You declare | Asked anyway, the face answers | Not |
|---|---|---|
edge_graph=False |
GraphUnsupported → REST 501 |
[] |
as_of_reads=False |
AsOfUnsupported → REST 501 |
today's instance under a past timestamp |
| history pruned | AsOfTruncated → REST 410 |
LookupError — "it did not exist yet" is a different answer from "I no longer hold the record" |
no find_instances_by_id_prefix |
InstanceIdLookupUnsupported → REST 501 |
an empty result set |
valid_time=False |
ValidTimeUnsupported → REST 501 |
the instance unfiltered — which asserts "yes, it was true then" |
key_lookup=False |
KeyLookupUnsupported → REST 501, and the write path records the edge with reason unsupported |
None — which reads as "no instance carries that key" |
edges not in write_kwargs |
the kernel never hands you edges | your adapter silently dropping them |
The as_of traversal (spec-topologia-do-grafo fatia 4) reads that table
twice, and the reason is worth stating because the obvious reading was wrong.
"The graph as it was at T" is not dna_edges filtered by from_version:
that table is deleted and re-inserted on every write, so — measured on the
dna-cloud database, 07/08/2026 — 0 of 33 rows carry a from_version older
than their instance's current version, and a stale row cannot exist. The edge
table has no history by construction. The walk therefore RE-DERIVES from
dna_versions.content, which means it needs as_of_reads (plus
load_kind_as_of for direction=in) and refuses without them. AsOfTruncated
→ 410 applies to the ANCHOR; a node the walk merely reaches and cannot
read that far back is named in as_of_truncated beside the edges, because
dropping it would let a reader conclude "nothing pointed at this" out of "we
cannot know what these said".
⚠️ One limit no flag can express, stated because it is invisible from the
answer: delete_instance removes an instance's dna_versions rows, so an
instance alive at T and deleted since leaves no history to read. Walking out,
the delete's own to_deleted_at stamp on the SURVIVING incoming edges rescues
it — the target existed at T, only its content is unknowable, and that lands in
as_of_truncated. Walking in there is no witness at all, because the delete
takes the deleted instance's outgoing edges with it. That blind spot is pinned
by test rather than papered over.
Neither GraphUnsupported nor InstanceIdLookupUnsupported is a
KernelRefusal, and the distinction is deliberate: a refusal is a verdict on
the request, which the caller might appeal. These are statements about the
deployment. The caller's remedy is a different adapter, not a different
request.
The three tiers¶
The eight Protocols on this page are not equally optional.
| Tier | Protocols | What it means |
|---|---|---|
| Mandatory | KernelAttachable, BundleEntryReadable, BundleEntryWritable |
Gated by a real isinstance in production. The port-contract test asserts every shipped adapter implements them. Treat them as part of the floor. |
| Optional, declaration-driven | Versionable, Draftable, Layered |
No isinstance anywhere today — the runtime gate is the corresponding flag (versions / drafts / layers). Implement the Protocol for typing and clarity; the flag is what actually decides. |
| Typing only — do not gate on these | TenantAware, LayerAware |
⚠️ runtime_checkable checks that a method exists, never that it accepts a keyword. isinstance(src, TenantAware) is True for any source with a save_instance at all. Use write_kwarg_support(src). The source file warns about this in both classes. |