The built-in Kinds — a commented catalog¶
Kinds — identity & composition explains the mechanics of a Kind; this page is the commented catalog of the composition-plane Kinds that ship with the SDK beyond the core prompt family, grouped by what they are for. Two–four honest sentences each: what it is, its role, when you would reach for it. Field-by-field schemas live in the generated Kinds reference, which cannot drift from the source.
The core prompt-composition family — Genome, Agent, Skill, Soul,
AgentDefinition, Guardrail, KindDefinition and friends — is covered
in Kinds and the first-Kind
tutorial. The record-plane SDLC family
(Story, Feature, Issue, …) is covered in Your git log is your
SDLC. What follows is everything else.
Composition mechanics¶
Hook¶
A Hook (helix-hook) is a
declarative lifecycle interceptor: it attaches to a kernel hook point such
as pre_build_prompt and runs an action — inject_fields merges YAML
key-value pairs into the prompt context, log emits a structured message,
script executes inline code. Hooks are stored as hooks/<name>/HOOK.md
bundles and auto-register when ManifestInstance.apply_hooks() is called.
Use one when behavior around prompt building must change without a deploy —
injecting a feature flag or contextual field into every prompt is the
canonical case.
Background automation¶
Automation¶
An Automation
(dna-automation) declares background work as data: on says when it
fires and runner says what runs. One doc, zero deploy — adding,
retargeting or pausing an automation is a YAML edit, not a code change.
The Kind is a direct port of the internal SDK's unification of three
structurally identical trigger Kinds (async-tool job / event hook / cron
schedule) into one schema discriminated by on.type:
cron— a 5-field cron expression (0 10 * * 1,3,5). The write path parses it with a zero-dependency validator (numbers,*, ranges, lists, steps; noJAN/MONname aliases), so61 * * * *is vetoed at write time, not discovered at 3 a.m.hook— a kernel lifecycle hook name (post_save,post_build_prompt, …). The name must belong to the kernel's typed vocabulary (KNOWN_HOOK_NAMES): a misspelled hook would be declared, listed and silently never fire, so an unknown name is a veto, not a warning.tool— an async dispatch tool the host exposes to the model (tool_name+ a declaredinput_schema+primary_input), for "fire-and-forget from a conversation" work like deep research.
Validation happens at the write, not at scan: the write guard runs the
Kind's schema (per-trigger required fields, runner enum) and the semantic
checks above before anything persists, so a broken Automation is vetoed
while the author is still present. One authoring note for hand-edited
YAML: prefer quoting the trigger key ('on':) — YAML 1.1 parsers such as
PyYAML read a bare on as a boolean. The Python write path heals the
boolean-key form before validating, and docs emitted by the SDK are
always quoted, so the round-trip is safe either way.
The runner is a reference to a real Kind — an Agent (kind: agent) or a
Tool (kind: tool) by name — plus the shared directive surface:
agent_directive (the dispatch instruction, with {arg} placeholders),
input (structured context), result_kind/result_spec_template
(deterministic persistence of the output), running_message/done_message
(user-facing copy) and a safety block (debounce, cooldown, rate cap,
fan-out cap, idempotency key) that the host enforces as loop protection.
Automation is an inheritable _lib default: declare the fleet once in the
library scope, let every scope inherit it, and let a tenant override one
doc in its overlay.
Contrast with Hook: a Hook is an in-process interceptor the
SDK itself runs around prompt building; an Automation is out-of-process
work — a report at 03:00, reindexing after a save — that only a host
runtime can execute.
The execution extension point¶
Deliberately, execution does not ship in the SDK — there is no
scheduler, bus or worker in a notation library. The contract is split the
same way as the CLI's post-transition hooks
(dna_cli.sdlc_cmd.register_post_transition_hook, where the CLI declares
the hook point and the host registers the executor): the SDK declares,
validates and lists; the host reads and runs. The read side is the
query helpers automations_for(instance, trigger_type) and
trigger_key(doc) (automationsFor / triggerKey in TypeScript), built
on the blessed instance query surface. A minimal cron runner is ~20 lines:
# host_runner.py — a minimal cron executor over declared Automations
import time
from dna.kernel import Kernel
from dna.extensions.automation import automations_for, trigger_key
mi = Kernel.quick("my-scope")
while True:
for doc in automations_for(mi, "cron"): # enabled-only by default
if cron_matches_now(trigger_key(doc)): # your cron matcher
runner = doc.spec["runner"]
run_agent( # your agent runtime
name=runner["ref"],
directive=doc.spec.get("agent_directive", ""),
inputs=doc.spec.get("input", {}),
)
time.sleep(60)
A hook-triggered executor is the same pattern on the kernel's own event
channel: for each doc from automations_for(mi, "hook"), subscribe
kernel.hooks.on(trigger_key(doc), ...) and dispatch the runner from the
listener. Whatever the trigger, honor spec.safety before firing — the
doc declares the loop protection, the host enforces it.
Collaboration¶
Comment¶
A Comment (collab-comment)
is a remark, status change or assignment attached to any other
instance via target_ref (Kind:name). It is how discussion and history
become data: the SDLC timelines, review notes on an eval finding, or an
agent narrating its work all land as Comment instances. Reach for it
whenever "who said what about this doc, when" needs to be queryable rather
than buried in chat.
Canvas¶
A Canvas (helix-canvas) is
a shared whiteboard between a voice/chat agent and the user, backed by a
serialized tldraw snapshot in spec. The user draws; the agent reads the
shapes as JSON (or vision-interprets free strokes) and writes back through
discrete shape tools. It exists as a Kind — rather than ephemeral UI state —
so boards are persisted, searchable and embeddable like any other
instance. It is a product-facing Kind: useful when you build an assistant
UI on DNA, irrelevant for headless setups.
Safety & governance¶
SafetyPolicy¶
A SafetyPolicy
(helix-safety-policy) declares runtime enforcement rules for agent input
and/or output: PII, content safety, topic restriction, prompt injection,
banned words, custom regex. Tier 1 (regex — CPF/CNPJ/email/phone/credit
card plus injection heuristics) is built in; ML and LLM-judge tiers are
opt-in extras. The action decides what happens on a hit: mask redacts
inline, block rejects the message, log passes through with violation
metadata. This is the Kind to write when compliance asks "prove nothing
personal reaches the model".
Recognizer¶
A Recognizer
(presidio-recognizer) is a Presidio
ad-hoc PII recognizer as data: an entity type (say BR_CPF), regex
patterns with scores, deny lists and context words. SafetyPolicy instances
reference Recognizers via dep_filters, and the runtime exports them to
the Presidio engine. Write one when the built-in entities miss a
domain-specific identifier — an internal employee ID format, a
country-specific instance number.
EvidencePolicy¶
An EvidencePolicy
(evidence-policy) controls which event types are automatically captured
as immutable Evidence instances — the content-level audit trail. It
declares the event list to watch, whether auto-capture is on, and the
retention period. Use it to tune the audit surface per scope: capture
everything in a regulated project, only writes elsewhere.
UserRoleAssignment¶
A UserRoleAssignment
(audit-userroleassignment) maps a user identity to a role list within a
tenant — the instance name is the user id. It backs role-gated endpoints
in a hosting platform's admin surface and is the persistent mirror of IdP
group membership. You only touch it when running DNA multi-user behind
auth; single-user local setups never see one.
MCPFederation¶
An MCPFederation
(federation-mcp) declares an external MCP server whose tools agents may
consume: transport (stdio command or streamable HTTP URL), an
allowed_tools bound, and an enabled: false kill switch. An Agent lists
the doc's name in spec.mcp_servers and the runtime loads the remote
tools as first-class agent tools — zero code, zero deploy. Secrets never
live in the doc: the auth block carries env-var names, read at connect
time.
Runtime bindings¶
RuntimeBinding¶
A RuntimeBinding
(runtime-binding) selects how a portable Agent is attached to a runtime.
It names the runtime protocol and optional provider, points to a deployment
host by reference, and declares policies for confirmations, reconnects, and
session authority. Deployments resolve the host reference; the binding never
stores a raw endpoint, credentials, transport objects, subscriptions, or
sessions. Those remain the runtime host's responsibility, so the same agent
definition and binding can move between environments without embedding
deployment state in DNA.
For a local consumer run, resolve the binding with
dna run --binding local-copilot --prompt "Continue the analysis". The command
uses the binding's Agent and provider; it does not treat spec.host.ref as an
endpoint.
GenUIComponent and GenUIBinding¶
A GenUIComponent
(runtime-gen-ui-component) describes portable governance for a generative UI
surface: supported protocols and host capabilities, a non-visual fallback, and
the bindings a consumer may use. mcp names a discovered MCP tool and optional
MCP Apps resource; MCP remains the owner of its input schema and tool result.
a2ui selects a trusted component from a host-owned catalog. grounding
declares when rendered operational values must come from an MCP tool result.
The legacy tool_name, input_schema, and symbolic renderer_ref fields remain
supported as one complete host-rendered binding. The reference is an external
ui-host identifier, not executable code or a DNA relation. New components may
instead declare mcp, a2ui, or both without duplicating an MCP tool schema in
DNA. Every component must provide at least one complete rendering binding.
A GenUIBinding
(runtime-gen-ui-binding) assigns one or more component instance names to one
DNA Agent. Keeping assignment separate from the component contract allows a
component to be reused by several Agents without duplicating its JSON Schema.
Consumers can resolve only the components assigned to an Agent; an Agent with
no binding receives none, and a binding that names a missing component fails
instead of silently dropping the reference.
Model registry¶
ModelProfile¶
A ModelProfile
(modelreg-model-profile) records one LLM model's hard limits and
capabilities — instruction_token_cap, context_window, tools_cap,
max_output_tokens, modalities, cost — as first-class data instead of
implicit knowledge scattered through code. Profiles are GLOBAL and live in
the _lib scope (model-profiles/<model_id>.yaml); resolve one with
kernel.model_profile(id_or_alias) (modelProfile in TypeScript), which
matches model_id first and the aliases list second, regardless of the
caller's scope.
The registry exists for one contract: never hardcode token caps. The
kernel's write path enforces it — when an Agent that declares a model
(or a voice_persona) is written, the prompt-budget guard estimates the
instruction's token count and compares it against the profile's
instruction_token_cap. A strict model — a voice persona write, or any
profile with realtime: true — over the cap vetoes the write with a
didactic error; a chat model over the cap writes but warns loud; an Agent
with no declared model, or a model with no profile, passes untouched
(enforcement is opt-in: writing a profile with a cap arms the guard). The
estimate is a deliberate over-count (chars ÷ 3.5), so the guard never
under-blocks; DNA_PROMPT_BUDGET_ENFORCE=0 is the ops kill-switch that
downgrades the veto to a warning. This ports a lesson paid for in a real
outage: a 17,269-token voice persona silently exceeded a realtime model's
16,384-token session-instructions cap because the cap lived in nobody's
code.
# _lib/model-profiles/gpt-realtime-2.yaml
apiVersion: github.com/ruinosus/dna/modelreg/v1
kind: ModelProfile
metadata:
name: gpt-realtime-2
spec:
model_id: gpt-realtime-2
provider: openai
realtime: true # strict: over-cap Agent writes are vetoed
context_window: 32768
instruction_token_cap: 16384
modalities: [text, audio]
aliases: [gpt-realtime-2-2026-05-07]
Preferences & personalization¶
Setting¶
A Setting (helix-setting)
is a reusable configuration snippet — env vars plus nested config plus
setup prose for one domain ("configure Vertex AI", "corporate proxy").
Selected Settings compose into a coding agent's settings.json or the
runtime env. Atomic and idempotent by design, so a "complete workstation
setup" is just a scope of docs.
Theme¶
A Theme (helix-theme)
declares a UI color palette — primary/accent/success in light and dark
HSL — plus optional typography and radius overrides, applied as CSS
variables at runtime with no rebuild. Because it is an instance, a tenant
ships its brand by publishing themes/brand.yaml in its overlay. Only
meaningful when a web UI sits on top of DNA.
UserProfile¶
A UserProfile
(helix-user-profile) holds per-user personalization data — display name,
language, communication style, opt-in personal context — that an agent may
inject into its prompt. It is consent-gated: without
consent.profile_used_in_prompts: true (or without a doc at all) the
agent treats the user as anonymous, and each user can read/write only
their own profile. Use it when an agent should remember who it is
talking to across sessions.
Testkit¶
TestGuide¶
A TestGuide
(testkit-test-guide) is a declarative test script: an ordered list of
steps (action → expected, optionally where in the product) that verifies
one or more work items via verifies refs. It turns the test roteiro that
used to live in chat into a versioned, schema-validated, re-runnable
instance. Write one per feature you expect a human (or agent) to smoke
again later.
TestRun¶
A TestRun
(testkit-test-run) records one execution of a TestGuide: outcome
(pass/fail/partial/blocked), who ran it, per-step results and
evidence. A passing run whose verifies points at a work item drives that
item's verify phase in the derived journey — it is the proof, where the
guide is the promise.
Evaluation¶
Four Kinds make evaluation authoring data — ported from the internal
SDK's eval extension minus its runtime (the upstream runner was a
Temporal worker driving live agents through LLM judges; none of that
belongs in a notation library). What ships instead is a local,
synchronous, offline runner whose default target is the kernel itself:
composing a prompt is a deterministic function of the declared instances,
so "does my agent compose the prompt I expect?" is a real evaluation of
declarative config — no LLM required. The evaluating agents
guide walks the full workflow, including
how a host registers an LLM target (EvalTargetPort — the same
declare-here/execute-in-the-host split as Automation
runners).
EvalCase¶
An EvalCase (eval-eval-case)
is one scenario: a target (what produces the text under test — default
{type: prompt, agent: X}, the composed system prompt) and a list of
deterministic checks (contains, not_contains, regex, not_regex,
equals, min_length, max_length) that ALL must pass. Upstream fields
that presuppose a live agent loop (trajectory matching, HITL policies,
judge engines) deliberately did not travel.
EvalSuite¶
An EvalSuite
(eval-eval-suite) groups cases and configures the run: the cases list
(empty = every EvalCase in the scope), a default target the cases
inherit, and stop_on_fail. Execute it with dna eval run <suite> —
offline, in seconds, in CI.
EvalRun¶
An EvalRun (eval-eval-run) is
the persisted ledger of one execution: counts, timestamps, the resolved
target and per-case results with the outcome of every declared check.
dna eval run --save writes it; being an instance, runs are queryable,
diffable and versioned like everything else.
EvalBaseline¶
An EvalBaseline
(eval-eval-baseline) pins one EvalRun as the "known good" reference for
a suite (dna eval pin <run>). Future runs compared against it
(dna eval run <suite> --baseline <name>) report regressions,
improvements and unchanged cases — and exit non-zero only on a
regression, so a pre-existing failure doesn't re-fail your CI.
Domain content¶
Doc¶
A Doc (dna-doc) is one page of
in-product documentation: a markdown body plus sidebar metadata (icon,
order, locale, Diátaxis kind_of, free-form category), authored as a
docs/<name>/DOC.md bundle and read back by dna docs list/show — so a
DNA-based product serves its own help pages straight from the kernel. It
is a record-plane Kind shipped as a pure descriptor (content as data, no
port class), ported from the internal SDK's doc extension minus its
product-specific help-center machinery (live data/diagram placeholders,
landing-page curation, asset aggregation).
HtmlArtifact¶
An HtmlArtifact
(sdlc-html-artifact) stores an HTML page as a first-class, linkable output of
a work item (Story/Feature/Epic/Spike). It is a bundle: ARTIFACT.html holds
the raw HTML byte-faithful (the writer never injects frontmatter or
re-escapes, so a design doc or rendered report round-trips untouched) plus an
optional artifact.json companion with structured metadata — the same mechanic
as a Soul's SOUL.md + soul.json. Attach one to the board with dna sdlc
produces add <WiKind>/<wi> HtmlArtifact/<name>; the SDLC
guide walks the flow.
Lesson¶
A Lesson (lesson-lesson)
is a short, structured educational activity an agent can run with a
pre-reader child — subject, target concepts, spoken prompts — born in an
AAC (augmentative and alternative communication) product built on DNA. It
is deliberately data, not code, so caregivers and therapists curate
content in a UI without code review. It doubles as the reference example
of a narrow domain Kind carried by an extension: your equivalent might be
Recipe or Workout.
DNA Cloud¶
PricingPlan¶
A PricingPlan (cloud-pricing-plan)
declares one DNA Cloud pricing plan as data: its hard caps (calls_per_day,
rate_per_sec, max_tenants), the feature families it unlocks
(definitions / sdlc / memory / emit), memory_mode, and price. The hosted
MCP server resolves a request's plan and enforces the caps at the same seam
that binds a token to a tenant — so changing a limit is a file edit, not a
redeploy. Free and Pro ship as seed docs. (The Kind was renamed from Tier in
0.29.0; its storage container stays tiers and the binding field stays tier_id.)
PlanBinding¶
A PlanBinding
(cloud-plan-binding) maps a DNA Cloud billing account to its current
PricingPlan — the billing→enforcement bridge.
The subscription belongs to the account, not to a workspace. One
PlanBinding covers every workspace whose Workspace.account_id matches, so
creating a second workspace is never a second charge and needs no billing write
at all. The account is an opaque id recorded on the workspace at creation from
the caller's verified account claim — whatever the IdP block's tenant_claim
names (Entra tid, WorkOS/Clerk/Auth0 org_id, Google Workspace hd). No new
entity: it is the same string the billing portal and the Stripe customer already
key on.
DNA Cloud's Stripe webhook writes the doc on subscribe / cancel
(PUT /v1/account-plan); the MCP server resolves workspace → account_id →
plan (kernel.account_for_workspace then kernel.account_plan) when the token
carries no explicit plan claim. A workspace with no resolvable account falls to
the Free floor — fail-closed, never another account's plan and never a paid
default. Zero Stripe or billing code lives in the OSS SDK; it only reads the
assignment.
PlanBinding replaces the retired per-workspace WorkspacePlan, which is now a
write-block tombstone in Kernel._REMOVED_KINDS. Keying the plan per workspace
forced whoever owned billing to fan out one doc per workspace, and workspace
enumeration is by membership, not ownership — so a workspace somebody else
founded and invited you into would have been swept into that fan-out and handed a
plan the account never bought. Re-keying on the account removes the question
instead of answering it: one write, one truth. (Renamed from AccountPlan in
0.29.0; the _lib storage container stays account-plans/<account_id>.yaml and
the REST op stays PUT /v1/account-plan.)
Intelligence layer¶
The intel extension is the data foundation for the DNA's intelligence layer
(automated research → ranked insights → feedback). It ships two record Kinds,
both TENANTED — they hold a tenant's own portfolio data, not a shared _lib
default, and are deliberately not inheritable.
IntelSource¶
An IntelSource (intel-source)
is a watched portfolio source — the Direction stage of the pipeline: what the
DNA observes. One doc per source (a repo, a scope, or an external URL)
declares its research cadence (manual / event / daily / weekly), an
actionability threshold below which insights are suppressed, its Priority
Intelligence Requirements (pirs — focus areas that get prioritized), and a
muted flag to pause research without deleting the source.
IntelInsight¶
An IntelInsight
(intel-insight) is the dissemination unit — a ranked, actionable insight
produced from an IntelSource that the ranker, digest, dedup and feedback
stages all reference. It carries a title, the cited fact, an optional
why / action, an actionability score the ranker sets, matched pirs,
citations, an evidence_rating, and a feedback state (new / actioned /
dismissed / snoozed). It is embeddable (title + fact) so a later dedup
stage can recall semantically similar insights. Named IntelInsight rather
than Insight because the bare Insight name already belongs to the SDLC
oracle Kind.
Portfolio console¶
The portfolio extension is the data foundation for the DNA Cloud portfolio
console — the enterprise multi-tenant model of
adr-portfolio-project-model. It ships five
record Kinds, all TENANTED (per-tenant portfolio data, not a shared _lib
default, and deliberately not inheritable). The shape follows the Azure DevOps
container model: an Organization owns Projects, a Project is a multi-repo
container that owns its board + intel + memory, and RBAC is a standard role
ladder.
Organization¶
An Organization
(portfolio-org) is the tenant's own org profile — the enterprise-familiar
top-level container (as in GitHub / Azure DevOps) whose portfolio of Projects
the console aggregates. It carries a name, a URL-safe slug, an optional
display_name, and a plan_ref annotation naming a DNA Cloud Tier the org
is on. One Organization per tenant; it is distinct from the platform-level
Tenant provisioning-identity Kind (the editable profile inside the tenant's
portfolio, not the global identity row).
Project¶
A Project (portfolio-project) is
the key Kind — the multi-repo development-space container. It owns a SDLC
board_scope (convention <slug>-development), the intel_source_refs the
intelligence layer observes, and scoped memory, and it is the permission
boundary. Repos are attached by reference via repo_refs — an N—N edge
kept on the Project side, so a repo can belong to many Projects without
duplication. A Project has a visibility (private / shared) and an org_ref
to its Organization.
Repo¶
A Repo (portfolio-repo) is a code
repository the portfolio references — its name, url, provider (github /
gitlab / azure-devops / other) and default_branch. It is attached to N
Projects via Project.repo_refs; a Repo carries no project back-ref, so
the N—N edge has a single source of truth (the Project) and a shared repo is
never duplicated. "Which projects use this repo" is a query over Projects, not
a stored reverse list.
Membership¶
A Membership
(portfolio-membership) is the RBAC join — a user's role at an org- or
project-scope (scope_type + scope_ref). The role is the standard ladder
(owner > admin > member > guest); resolution is highest-role-wins across a
user's memberships, with the org owner a superuser. It carries an invitation
status (invited / active). It is distinct from the platform-level
TenantMembership (which links a user to a provisioning Tenant); this grants
access inside the tenant's own Organization / Project graph.
Role¶
A Role (portfolio-role) is one rung of
the RBAC ladder expressed as data (the DNA thesis: everything declarative)
— its role_id, display_name, rank (higher = more access), the
capabilities it grants, and a can_delete flag protecting built-in rungs.
Modelling the ladder as data (not a hardcoded enum) makes it extensible: a
tenant can add a custom role without a code change, and highest-role-wins
simply compares rank. The four standard rungs (owner / admin / member /
guest) ship as per-tenant seed docs under
examples/dna-cloud/.dna/.../roles/.
Provenance — the original behind a derived instance¶
Some instances are not authored, they are extracted. A file arrives, an
agent reads it, and writes typed instances from what it found. Those instances
are a projection: lossy, interpreted, and — on their own — an assertion
nobody can check. SourceArtifact is what makes the claim testable.
SourceArtifact¶
A SourceArtifact
(artifact-source) records the original a projection came from: the sha256
of its bytes, a uri naming where they live, and derived_refs — the typed
instances extracted from it.
Three properties are worth understanding before you use it, because each is a deliberate choice rather than an obvious one.
The sha256 is the point, not bookkeeping. Without a content address,
"this instance was derived from that file" is unfalsifiable — and an
unfalsifiable provenance claim is worth less than no claim at all, because it
invites a trust it has not earned. With one, anyone holding the bytes can
verify the pairing, and re-uploading identical content is recognisably the
same artifact rather than a second one.
The edge points from the artifact to its derivations, never the reverse.
The obvious shape would be a source_ref field on each extracted instance, and
it fails twice: an agent composing schemas would have to remember that field
every time (and Kinds nobody authored have nowhere to put it), and one upload
commonly yields many instances — a PDF holding twelve invoices should state
"these came from one file" once, not twelve times.
The uri is an identity, never a credential. It names where the bytes
live; it does not grant access to them. A signed URL stored here would make the
instance itself the capability — copy the instance, copy the access. The schema
is closed (additionalProperties: false) precisely so a token cannot be
attached beside it, and a host is expected to serve artifact reads through an
authenticated route that checks the caller's membership first.
The Kind is deliberately vendor-neutral: where the bytes actually live is a
deployment's own concern (a blob store in a hosted product, a directory in a
self-host), and the kernel treats uri as opaque so that stays true.
KnowledgeChunk¶
Extraction is one thing a file can become. Retrieval is the other: a passage
of the original, kept so an agent can find it and quote it. A
KnowledgeChunk
(artifact-knowledge-chunk) is one such passage — the text, the collection it
belongs to, and the SourceArtifact it came from.
A chunk is an instance, and that is the whole design. Everything a corpus
needs already exists for instances: embed: makes the text searchable, tenancy
isolates one workspace's corpus in the index rather than at the door, and the
search door already returns an envelope that says whether the semantic plane
actually ran. A parallel document store would have had to re-earn each of those,
and would have put the corpus outside the Kind system — no version, no tenancy,
no relation back to the file it came from.
A collection is a name prefix. Instance names are structured
(<collection>.<sha12>.<ordinal>), so a corpus partitions without a new column
and without a Kind per collection. The narrowing is applied where the search
provider chooses candidates, never to the list it already chose — both planes
over-fetch a fixed number of rows, so a post-filter would let one voluminous
collection fill that budget and crowd every other row out while the envelope
still reported a healthy hybrid search.
The separator is a dot, and both halves of that choice were measured rather than
picked. An instance name is written to disk as a single path component, so
validate_instance_name refuses a / before any adapter is reached — the
format was <collection>/<sha12>/<ordinal> until ingestion tried to write one
and could not. And the separator has to sit outside the alphabet a collection
name may use ([a-z0-9-]), or the prefix of handbook would sweep up every
chunk of handbook-2024 — which is exactly the leak the trailing separator
exists to prevent.
A chunk is produced, never authored. dna.application.knowledge's
ingest_knowledge_impl takes already-extracted Markdown plus the sha256 of
a registered SourceArtifact, cuts it with
chonkie, and writes one instance per
passage. Bytes stop at the host on purpose: uri is opaque by contract, so a
vendor-neutral SDK that could resolve blob:// would have to know about
credentials, and the byte→Markdown converter is the host's dependency already.
The chunk's provenance (source_uri, source_filename) is read from the
artifact instance, never accepted from the caller — a citation the caller could
dictate is not evidence.
Re-ingesting the same file rewrites the same instances, because the name is
derived from the content address. Re-ingesting it with a different cut is the
case that needs help: fewer passages leave the surplus ordinals behind, in the
store and in the index, where a search would return them as text from a dead
version of the document. The port prunes them — through the kernel directly,
which is the path record.append-only deliberately leaves open for a
purpose-built verb, exactly as forget retires an Engram.
It is embeddable but NOT memory.recallable, and the distinction is the one
that trait exists for. memory.recallable means a Kind participates in the
memory verbs — retention decay, affect weighting, reconsolidation on recall. A
passage of a document does not decay, has no affect, and does not change for
having been read: it is what the file says. Declaring the trait would have put a
corpus — voluminous by construction — into recall beside a person's Engrams,
ranked by properties it does not have. embed: [text] is what makes it
findable; a caller who wants the corpus asks for it by name.
Only text is embedded. Without that restriction the indexed body would
collect every string on the instance — the sha256 in hex, the collection name
repeated on every chunk — and push that noise into both the vector and the
full-text index. A hex digest is not language, and a collection name repeated
across a whole corpus makes a search for that name match all of it.
⚠️ Retrieval over a corpus ranks; it does not filter. DNA ships no relevance
floor, because on a real corpus none separates the relevant from the irrelevant
(the measurement is in dna.kernel.query.relevance). So a chunk comes back with
its score and its source, and a caller presenting chunks to a person or a model
is expected to pass both along rather than assert on their strength.
Delegation — the Agent Card of a remote agent¶
A2A (Agent2Agent, governed by the Linux
Foundation) standardized exactly the thing DNA already treats as an instance: a
self-describing capability descriptor. The A2A protocol — the JSON-RPC/gRPC
call, the task lifecycle — is transport, not an instance, and stays out of the
Kind system entirely. What A2A calls the Agent Card — the descriptor a
server publishes at /.well-known/agent-card.json naming who it is, what it
can do, and where to reach it — is exactly instance-shaped, and RemoteAgent
is it.
RemoteAgent¶
A RemoteAgent
(a2a-remote-agent) is the Agent Card of a third-party A2A agent, held as a
DNA instance: name, description and supported_interfaces (the A2A 1.0
interface list — each entry a protocol_binding of JSONRPC / GRPC /
HTTP+JSON with its own url, plus an optional protocol_version)
come straight from the A2A spec in snake_case; skills, capabilities and
security_schemes describe what it can do and how to authenticate to it.
It is deliberately a separate Kind from Agent rather than a mode of it.
The two Kinds' fields are disjoint: Agent declares behaviour (instruction,
model, tools) that DNA composes and executes; RemoteAgent describes and
points (interfaces, skills, security schemes) at something DNA locates and
calls over the network. A single Kind carrying both would let a local agent
declare security_schemes and a remote one declare instruction, which is
exactly the kind of nonsense a closed schema cannot express as "these fields OR
those." What the two Kinds share is delegation_target_for — the block that
lets a supervisor's roster of delegation targets span both a local Agent and
a remote RemoteAgent by asking "who declared this block, and does their
allowlist include me?" rather than by enumerating Kinds.
Three properties are worth understanding before you register one:
data_scope is required, and it is DNA's own field, not A2A's. A
RemoteAgent is, by construction, an exfiltration channel: registering one
means DNA will send workspace data to a URL the tenant chose. A2A has no
opinion on that — it is transport protocol. So the scope of what may be sent
is ours, mandatory, and explicit: data_scope.kinds names which Kinds'
instances this endpoint may receive. An implicit scope would mean "everything,"
and nobody approves that knowingly — an empty list is the honest "registered,
but permitted to receive nothing," not an error.
The schema is closed. additionalProperties: false exists so a credential
— a bearer token, an API key — cannot be smuggled into the instance alongside
security_schemes (which only says how to authenticate, never with what).
An instance that could carry its own access token would let anyone who can read
it also call the endpoint with it, the same reasoning that keeps
SourceArtifact closed.
signature_state is tri-state on purpose. A Card that arrives unsigned is
unsigned; one that carries an A2A signatures block is present_unverified
— cryptographic verification of that signature is out of scope for this
version, because it requires deciding a trust chain (whose signatures count?),
which is a product decision, not a schema one. verified is reserved for when
that lands. A plain boolean signed would make "we didn't check" look
identical to "there was nothing to check," and those are different states a
reviewer needs to be able to tell apart.
AgentGrant¶
A AgentGrant (a2a-agent-grant) is
the inbound twin of RemoteAgent, and the two only make sense as a pair:
| says | |
|---|---|
RemoteAgent |
we may send data over there |
AgentGrant |
they may act on our behalf |
A third party's agent — Claude, an integration, a partner's automation — arrives holding a token of one of your users, and acts as that user. Whether it may is not a property of the token: a valid token only proves the user signed in. Without a grant, "the user authorized Acme" and "the user is logged in" are the same statement, and a permissions screen built on that would display a permission that does not exist.
Three properties carry the design.
state is tri-state — pending, active, revoked — for the same reason
RemoteAgent.signature_state is. A boolean granted would make "it asked and
nobody decided" look identical to "denied", and those are different: the first
needs to surface on a screen so a human can decide; the second was already
decided and asks for nothing.
What was REQUESTED and what was GRANTED are separate fields.
requested_scope_kinds is what the agent asked for; scope_kinds is what a
human allowed. One field would make asking equal to receiving. An agent that
declares nothing leaves the request empty and nothing is pre-selected — silence
never becomes permission, not even a suggestion of one.
Everything that is not active closes. Absent, pending, revoked,
malformed and unrecognised all deny. The allowed list has exactly one entry
and the rest is the rest — a gate written the other way around (denying what it
recognises) would open for whatever it does not, including a state some future
version adds, and it would open silently.
scope_kinds speaks the same vocabulary as RemoteAgent.data_scope.kinds on
purpose: it is the same question in both directions, and answering it in two
different shapes would be a trap for whoever reads them side by side.
The instance never carries a credential — the schema is closed precisely so a token cannot be attached to a grant. Who may act, and over what, lives here; the secret they authenticate with belongs to the deployment.
AgentCatalogEntry¶
An AgentCatalogEntry
(a2a-agent-catalog-entry) gives a human-readable name to an agent whose
client_id is opaque — and it is deliberately the lesser half of agent
identity.
The primary mechanism is CIMD (Client ID Metadata Instances): the client_id
IS an HTTPS URL serving the client's metadata, so the name can only be declared
at acme.com by whoever controls acme.com, and DNS plus TLS already prove
that. It is the browser padlock's mechanism — you do not trust the text, you
trust where it came from.
This Kind covers what CIMD cannot: legacy clients with opaque ids, which publish no metadata and which nothing can anchor.
Two rules keep the difference visible, and both are structural.
client_id may not be an HTTPS URL — the schema refuses it. A client that
publishes metadata already has an anchored identity, and letting someone type a
name over it would swap proof for typing. That swap is exactly the mistake an
earlier design made without noticing: it said "the name must come from whoever
verified" and then proposed that the verifier be a person filling in a form.
Typing is not verification; it protects against the caller lying, not against
the registrar being wrong or deceived.
registered_by is required, because it is what makes the label possible. A
consent screen can honestly say "Sprint Assistant · registered by Maria" — a
sentence the user can weigh. "Sprint Assistant" alone, on an opaque id, is a
claim nobody stands behind; sitting next to an anchored name, it teaches the
reader to trust both the same way.
The entry names, it does not authorize. There is no scope, no state, no
grant field: authorization lives in AgentGrant, and registering an agent in a
catalog must never be a path to granting it access.
Run dna kind list for the live registry in your install, and dna kind
describe <Kind> for the exact schema the write boundary enforces — the
CLI tour shows both in action.