Capabilities · 11. Making it do more

The daemon lets you add capability without forking it and without running a stranger's code inside your process. That single sentence is the design of this whole family. Everything below is a consequence of it.

This chapter is explanation. It says what each extensibility capability is for, why it was built that way, and what it deliberately refuses to do. It does not repeat the manifest schema, the grant vocabulary or the route tables; those live in the Extending the daemon set, and each explainer ends with a pointer to the exact chapter.

11.0 In plain English

You will eventually want the daemon to do something it does not do. Read your issue tracker. Call your internal pricing service. Show a board of tiles your team actually cares about. There are two normal ways a product answers that, and both are bad. It can accept plugins that run as code inside itself, which means every plugin author has your memory, your keys and your process. Or it can accept nothing, and you fork it.

Crux takes a third route. An extension is a description, not a program. You hand the daemon a signed manifest that says "there is a tool called ext.pricing.quote, it lives at this HTTPS endpoint, and it is allowed to talk to these hosts and nothing else." The daemon reads that description, shows the tool to your agent, and when the agent calls it the daemon makes the outbound request itself, under limits you set. The extension code never runs on your box. That is why version 1 of the pack format executes nothing in-process, and it is the security claim the rest of this chapter rests on.

Two words recur and are worth pinning down before you meet them. Installing a pack means the daemon now knows about it. Granting means a named identity may actually use it. They are separate acts on purpose, because "I have read this manifest" and "I trust this manifest with my facts" are different decisions, usually made by different people, sometimes weeks apart.

The second half of this chapter is a different kind of extension. Where packs add tools, the substrate adds shapes: typed objects and edges beside the plain fact store, so a thing with structure (a capability, a repository, a release) can be stored as that thing rather than as flattened text. A lens is a package of kinds plus the analytics over them. The Feature Registry is the smallest complete one, which is why it is used here as the worked example.

11.1 Integration packs

Status SHIPPED · Reached through POST /v1/extensions/register, GET /v1/extensions, DELETE /v1/extensions/{id}, Console System › Extensions · Who it is for both

What it does. An integration pack is a declarative crux.integration.v1 manifest that describes what an extension adds: MCP tools, HTTP recipes, SDK snippets, CLI commands, file-watcher roots and webhook shapes. You register the manifest with the daemon, the daemon validates it, and the described surfaces become available to callers who have been granted them. Version 1 of the format deliberately does not execute any code inside the daemon process.

Why it works this way. The obvious design is an in-process plugin API: fast, expressive, and it hands every extension author your memory, your credentials and your address space. The pack format rejects that trade outright. A manifest is data, so it can be signed, diffed, reviewed in a pull request, and reasoned about before it is trusted; a shared library cannot be any of those things. The cost is real and was accepted: an extension cannot do anything the declared recipes cannot express, and anything genuinely novel needs either an outbound HTTPS endpoint you host (§11.4) or the sandboxed WASM path (§11.5). The team took the smaller feature surface in exchange for being able to say, without qualification, that installing a pack does not run foreign code on your box.

What changes for you.

  • As an operator: you can review what an extension will do before you allow it, because the manifest is the whole story. There is no runtime behaviour hiding behind the description.
  • As an agent: new tools appear in your tool list already tagged with where they came from, so you can tell a built-in from an addition.

What it does not do.

  • It does not run extension code inside the daemon in version 1. A pack that needs to compute something needs an endpoint you host, or a WASM module under the compile-time feature in §11.5.
  • Registering a pack does not enable it. Nothing an extension declares is usable until a grant exists for the calling identity, which is §11.2.
  • The daemon validates the manifest, not the service behind it. A manifest that describes an endpoint accurately today describes it inaccurately the moment you change that endpoint, and nothing in the daemon detects the drift.

Where the detail lives. Manifest schema, registration flow and every field: extending/02 Integration packs. Format definition in source: lib.rs:30.

11.2 Capability grants

Status SHIPPED · Reached through GET/POST /v1/extensions/{id}/grants, DELETE /v1/extensions/{id}/grants/{fingerprint}, Console System › Extensions · Who it is for human

What it does. A grant records that one named identity may use one installed extension's declared capabilities. There are fourteen capabilities an extension can declare, and some fact prefixes are privacy-gated so they can never be granted to an extension at all, whatever the manifest asks for. Two grant models exist side by side: flat-file pack grants and fact-backed extension grants. They share the capability vocabulary and share no storage.

Why it works this way. Install and authorise are separated because they are separate human decisions. An operator evaluating a pack wants to read it, register it, and think; a fleet lead deciding that the analytics agent may write facts is answering a different question on a different day. Collapsing the two into one "install" verb is how extension systems end up with everything trusted by default. The privacy-gated prefixes exist because some memory should not be reachable by an extension under any grant, and a policy expressed as "do not tick this box" is a policy that will eventually be ticked.

What changes for you.

  • As an operator: revoking one identity's access to one extension is a single delete, and it does not disturb anyone else using the same pack.
  • As an agent: a tool you can see is not necessarily a tool you can call. A missing grant is refused at call time, not hidden at list time.

What it does not do.

  • It does not merge the two grant stores. A flat-file pack grant and a fact-backed extension grant are not interchangeable, and reading one tells you nothing about the other.
  • It does not make privacy-gated prefixes grantable. That set is closed in code, not configured.
  • A grant is not an identity check. It says which passport may use the extension; it does not verify that the caller is that passport beyond the normal auth rails.

Where the detail lives. The fourteen capabilities, the grant shapes and the gated prefixes: extending/03 Capabilities and grants. Capability enumeration in source: lib.rs:40.

11.3 Signed distribution, keyring and trust tiers

Status SHIPPED · Reached through GET/POST /v1/extensions/keys, GET /v1/extensions/registry, POST /v1/extensions/install-from-registry, corecruxctl extensions sync|review|install · Who it is for human

What it does. Manifests are Ed25519-signed over a defined subset of their own fields. The daemon holds an operator keyring which is authoritative over any key embedded in the manifest itself, so a pack cannot vouch for itself. Four trust tiers are derived and surfaced to the agent inside the tool description. Two curator-signed indexes exist, one for community extensions and one for Studio templates, and they share the one keyring.

Why it works this way. Signing answers "who wrote this", not "is this safe", and the design is careful not to blur them. The keyring outranks inline keys because a self-signed manifest is a manifest that signed itself, which is worth nothing; only a key you added is evidence. The trust tier is put into the tool description rather than being kept as operator metadata because the agent is the party that will decide whether to call the tool, and a trust signal an agent cannot read is a trust signal that does nothing. Documented development bypasses exist so that local authoring is not painful, and they are enumerated rather than hidden.

What changes for you.

  • As an operator: you decide which publishers exist, by adding or removing keys. Removing a key retroactively downgrades everything it signed.
  • As an agent: the trust tier is visible at the moment of choosing, in the same text as the tool's description.

What it does not do.

  • A valid signature does not mean the extension is safe. It means the manifest was not altered after the named key signed it, and nothing more.
  • The two registries are catalogues, not review boards. Curator signing attests to publication, not to an audit of behaviour.
  • Development bypasses weaken the chain when enabled. They are documented so that finding one in a production configuration is an alarm rather than a surprise.

Where the detail lives. Signing, the keyring and the tier ladder: extending/04 Signing and trust. Publishing into the registries: extending/09 Registry and publishing.

11.4 External tools over HTTPS

Status SHIPPED · Reached through MCP ext.* tools, POST /v1/extensions/{id}/tools/{name}/invoke · Who it is for agent

What it does. This is the extension path that carries real work. A manifest declares one endpoint and an allowed_hosts list, and the daemon enforces a per-extension egress allowlist, safety budgets that may only be tightened and never loosened at call time, a sliding-window rate limit keyed on the extension and the calling passport together, and filtering of the facts the extension is permitted to write. Every invocation emits an audit event.

Why it works this way. The direction of travel is the whole point: the extension calls out, it does not call in. Nothing foreign is admitted to the process, and the network boundary becomes the enforcement point, which is a boundary an operator already understands and can already monitor. Rate limiting is keyed on the pair rather than on the extension alone because a shared extension used by twelve agents is twelve independent workloads, and a single global bucket would let one busy agent starve the rest. Budgets tighten only, because a call-time parameter that can raise its own limit is not a limit.

What changes for you.

  • As an agent: an ext. tool behaves like any other tool, and its failure modes are ordinary ones: refused host, exhausted rate limit, upstream error.
  • As an operator: the allowlist is per extension, so one integration's reach does not become every integration's reach.

What it does not do.

  • It does not inspect the response body for anything beyond size and the declared fact-write filter. A permitted endpoint returning nonsense returns nonsense to the agent.
  • The allowlist constrains hosts, not behaviour at those hosts. An endpoint you allowed can do anything an endpoint can do.
  • Rate limits are per daemon. Two daemons calling the same upstream do not share a budget.

Where the detail lives. Endpoint contract, limits and the audit event: extending/05 External tools. Invocation route in source: extensions.rs:686.

11.5 The WASM extension host

Status FEATURE wasm-extensions, compiled out by default · Reached through the same invoke route as §11.4 · Who it is for agent

What it does. Behind the compile-time cargo feature wasm-extensions, which is off in the default build, the daemon can run extension code in-process inside a wasmtime sandbox with fuel metering, epoch interruption, memory limits and a wall-clock cap, with the module hash pinned and re-verified on every dispatch. Without the feature, a kind: wasm dispatch returns 501. The shipped host ABI is the foundation only: log, now_unix_ms and current_passport_json. The richer fact ABI is a declared follow-up and is not built.

Why it works this way. WASM is the answer to the one thing §11.4 cannot do, which is compute something without a network round trip, and it is the only in-process path the design admits, because a sandbox with fuel and memory limits is auditable in a way that a dynamic library is not. Shipping the foundation before the fact ABI was deliberate: traps, resource limits and hash pinning are the parts that must be right, and they were landed with their own tests before any capability was bolted on top. The hash is re-verified per dispatch rather than once at load, because a module verified at install time and swapped afterwards is exactly the attack the pinning exists to stop.

What changes for you.

  • As an operator: the default build has no WASM runtime in it at all. Enabling it is a rebuild, not a configuration change, so the decision is visible in your artefact.
  • As an agent: until the feature is compiled in, a WASM-kind tool call returns 501 rather than degrading to something else.

What it does not do.

  • The shipped ABI cannot read or write facts, query, read secrets or emit receipts. Those five host functions are a declared follow-up, not a flag you can turn on.
  • It is not enabled by an environment variable. wasm-extensions is a cargo feature, so an operator who wants it needs a build that has it.
  • The sandbox bounds resource use, not intent. A module that stays inside its fuel and memory budget can still return whatever it likes.

Turn it on. Build with the cargo feature wasm-extensions. There is no runtime toggle.

Where the detail lives. Limits, traps, the wire contract and the ABI: extending/06 WASM extensions. Host module in source: wasm_host.rs:6.

11.6 Extension tools in the agent's tool list

Status SHIPPED · Reached through MCP tools/list, tools/call · Who it is for agent

What it does. When an extension is both installed and granted, its tools appear in tools/list beside the built-ins, annotated with the extension id, the manifest hash, the trust tier and consequence metadata. The agent therefore sees provenance at the moment it chooses a tool rather than after it calls one.

Why it works this way. The alternative, a separate "extensions" namespace the agent has to go and look in, produces agents that never use extensions. Putting them in the one list makes them usable; putting the provenance in the annotation is what makes that safe to do. Consequence metadata is included because an agent choosing between two plausible tools needs to know which one writes something.

What changes for you.

  • As an agent: you can read an extension tool's origin and trust tier from the listing, without a second call.
  • As an operator: whatever you granted is what the agent sees. The list is the grant made visible.

What it does not do.

  • It does not validate tool names against the ext. prefix. A tool declared without it is advertised and then is not dispatchable, which presents to the agent as a tool that exists and always fails.
  • It does not detect name collisions with built-ins. A colliding name appears twice in the listing and every call silently routes to the built-in.
  • Under a shaped tool surface (§13.3), extension tools have no affinity score, so they are invisible in minimal mode.

Where the detail lives. Listing shape and annotations: extending/10 The MCP surface. Collision and prefix hazards in source: extensions.rs:39.

11.7 Studio packs and the signed template library

Status SHIPPED · Reached through POST /v1/studio/pack/build, POST /v1/studio/pack/verify, GET /v1/studio/library, POST /v1/studio/library/{id}/install, corecruxctl studio sync, Console Studio › Board and Studio › Library · Who it is for human

What it does. A Studio pack is a console tile board exported as a portable artefact, wrapped in a valid integration manifest so it travels on the same signed trust rails as everything else in this chapter. You can build one, verify one, and install one from a curator-signed central catalogue. Install can only ever add tiles.

Why it works this way. A board someone spent a week arranging is knowledge, and the normal fate of that knowledge is a screenshot in a chat thread. Wrapping the export in an integration manifest rather than inventing a second format means Studio packs inherit signing, keyring and trust-tier handling for free, and an operator learns one trust model instead of two. Install is additive by construction because the failure mode of a destructive install is losing a board you built, and no amount of confirmation dialogue makes that acceptable.

What changes for you.

  • As an operator: a board you designed is a file you can sign, review and hand to another team.
  • As an agent: nothing. Studio packs are a human surface.

What it does not do.

  • Install never removes or overwrites your existing tiles. If you wanted the pack's board exactly, you still have to remove yours.
  • required_tier in a template is advisory. The daemon does not refuse an install because a tier is unmet, so it must be presented to users as guidance and not as a gate.
  • A verified pack is a pack whose manifest is intact. It says nothing about whether the board is useful or whether its queries return anything on your data.

Where the detail lives. Building, verifying and the workspace model: extending/08 Studio packs and workspaces. Using the library from the console: operations/06 Console explorer and graphs.

11.8 The substrate: entities, edges, kinds and relations

Status SHIPPED · Reached through MCP entity_upsert, entity_get, entity_list, entity_delete, entity_history, edge_upsert, edge_get, edge_list, edge_delete, kind_list, kind_get, plus /v1/entities, /v1/edges, /v1/kinds and /v1/relations, Console Canvas › Graph · Who it is for agent

What it does. The substrate is a typed object graph that sits beside the fact store: versioned entities with their own history, edges between them, a registry of kinds, and a relations graph you can traverse in either direction and expand. It is a second shape for memory, not a replacement for the first.

Why it works this way. Facts are deliberately flat, and flatness is what makes them legible and cheap. But some of what an agent needs to remember genuinely has structure: a capability that depends on three others, a repository that contains modules, a release that supersedes a release. Encoding that as string keys works until you need to traverse it, at which point you are writing a graph database in prose. The substrate exists so the structured case gets a structured store, and the two are kept separate rather than fused so that neither pays for the other's complexity. The trade accepted is that you now have two stores and must know which one your data belongs in.

What changes for you.

  • As an agent: relationships are first-class, so "what depends on this" is a traversal rather than a substring search over keys.
  • As an operator: entities are versioned with history, so you can see what a typed object looked like before it changed.

What it does not do.

  • It does not replace the fact store, and it is not searched by the fact recall path. A thing stored as an entity is not found by query_facts.
  • Kinds are a registry, not a schema enforcer in the database sense. Registering a kind describes it; it does not retroactively validate what was already stored.
  • Edges are typed links, not transactions. There is no referential-integrity guarantee that both ends of an edge still exist.

Where the detail lives. Entity, edge and relation contracts: api/01 Facts and memory. Tool-by-tool reference: api/13 MCP tool reference continued.

11.9 Lenses, and the Feature Registry as the reference lens

Status SHIPPED · Reached through MCP feature_file_search, feature_coverage_report, feature_trigger_audit, feature_suggest_next, get_gaps, plus /v1/features/capabilities · Who it is for both

What it does. A lens registers entity kinds on the substrate and adds analytics over exactly those kinds. The Feature Registry is the first and smallest complete lens: it registers capability and repository kinds, then adds gap analysis, promise coverage and coverage reporting, wired into both the HTTP surface and the MCP tool list.

Why it works this way. Once the substrate exists, the interesting question is not "can I store a graph" but "can a domain add its own vocabulary and its own analytics without touching the daemon's core". A lens is the answer, and the Feature Registry was built as a real one rather than a demonstration precisely so the extension point would be proved by something the project itself depends on. Keeping the lens small was the point: if the smallest useful lens had needed core changes, the abstraction would have been wrong.

What changes for you.

  • As an agent: you can ask for the gaps in a system's capability coverage and get a structured answer, rather than reading a spreadsheet.
  • As an operator: the pattern is copyable. A lens is kinds plus analytics, and the Feature Registry is the worked example of the whole shape.

What it does not do.

  • A lens is not a plugin. It is compiled into the daemon, so adding one is a code change, not an install.
  • The Feature Registry's analytics are only as good as what was loaded into it. An empty registry reports no gaps, which is not the same as having none.
  • Lens analytics do not read the fact store. They read the substrate kinds the lens registered.

Where the detail lives. Registry endpoints and response shapes: api/01 Facts and memory. What the Feature Registry is for at platform level: platform/03 FeatureCrux.

11.10 Projection read models

Status SHIPPED for entity projections; several artifact projections return 501 in the Community Edition · Reached through /v1/projections/*, /v1/admin/projections/* · Who it is for human

What it does. A projection is a materialised read model over the substrate: entity counts, current state, timelines, artifact living-state, relations, dependents and pressure events, plus a rebuild action and a batch lookup. It answers shaped questions without re-deriving them from the underlying records on every call.

Why it works this way. The substrate stores what happened; a projection stores what that adds up to. Recomputing "current state" from history on every read is correct and slow, and the read pattern for a console tile or a dashboard is overwhelmingly repetitive. Materialising the answer and offering an explicit rebuild keeps the derivation honest, because a projection you can rebuild is a projection you can prove is not stale. The four artifact projections that return 501 do so because they are backed by a dataplane the Community Edition does not carry; returning 501 rather than an empty result was chosen so that "not available here" is never confusable with "nothing to report".

What changes for you.

  • As an operator: a projection that looks wrong can be rebuilt rather than reasoned about, which turns a debugging session into one action.
  • As an agent: a projection is a read model. It is a faster way to ask a question you could already ask, not a new fact source.

What it does not do.

  • Artifact living-state, artifact relations, artifact dependents and pressure events return 501 in this edition. That is a stated absence, not a transient failure, and no configuration changes it.
  • Projections are derived. Rebuilding one cannot recover source data that was never written.
  • A projection is not a cache with an expiry. It is maintained, and a rebuild is the operator-driven correction path.

Where the detail lives. Projection routes, parameters and response shapes: api/03 Query and retrieval. Projection handlers in source: projections.rs:14.

11.11 Typed action traces and session mining

Status FLAG CORECRUXD_FEATURE_TOOL_TRACES, default on; set it to 0 to disable · Reached through MCP tool_trace_recent, learn · Who it is for agent

What it does. Behind CORECRUXD_FEATURE_TOOL_TRACES, which is on by default and disabled by setting it to 0, the daemon records typed traces of tool calls into a bounded ring, and learn mines that ring to weight recurring loops so the advertised tool surface can adapt to what this agent actually does. One flag governs both, because learn's only data source is the trace ring.

Why it works this way. Tool surfaces are configured once and then never revisited, and the configuration is always someone's guess about what an agent will need. A trace ring turns that guess into a measurement. It is one flag rather than two because splitting them produces a configuration where learn is enabled and silently has nothing to learn from, which is worse than either state. The ring is bounded rather than persisted because the value is in recent behaviour, and an unbounded behavioural log is a liability with no matching benefit.

What changes for you.

  • As an agent: the shaped tool surface (§13.3) reflects your recent use, not a static list.
  • As an operator: tool_trace_recent tells you what an agent has actually been calling, which is usually not what its prompt says it calls.

What it does not do.

  • The ring is bounded and in-process. It is not an audit log, it does not survive a restart, and it must not be cited as evidence of what an agent did.
  • learn weights loops. It does not add, remove or rewrite tools.
  • Disabling the flag removes learn's input entirely, so the tool remains present and has nothing to work with.

Turn it on. It is already on. CORECRUXD_FEATURE_TOOL_TRACES=0 disables both the ring and learn; note that an empty value also disables.

Where the detail lives. Trace and learn tool contracts: api/13 MCP tool reference continued. Flag parsing in source: traces.rs:75.


Each explainer above carries its own grounding in its final block. Statuses in this chapter reconcile against the daemon feature inventory taken at 93b41a7d9735c4f7d1186c7a57b861d746273366; the complete status table for all 119 capabilities is chapter 15.