Crux Daemon · 2. Integration packs

An integration pack is one JSON document. The daemon reads it, validates it, and stores it. Nothing in a pack executes in the daemon process unless its entry.kind names a runtime the daemon actually ships (three do today; see the table below).

The manifest type is IntegrationManifest (crates/crux-integrations/src/lib.rs:98). validate() is the single gate every install path runs (lib.rs:481).

2.0 In plain English

An integration pack is a description of how to connect the daemon to something else, written down as a single JSON document. Think of it as a recipe card rather than a program. It names what the integration is, who published it, what it wants permission to do, and what its entry point is. The daemon reads the card, checks it is well-formed, and files it. In the common case nothing in the card ever executes inside the daemon.

That is the design decision worth understanding, because it explains everything else on this page. A format that mostly cannot execute is a format that mostly cannot go wrong. Most of what people want from an integration is configuration rather than code, and a declarative document can be reviewed by a human, signed by a publisher, checked by a machine and diffed between versions in a way that arbitrary code cannot. Nine entry kinds exist, and only three of them name a runtime the daemon actually ships today; the rest describe something a client is expected to carry out. §2.3 states which is which, and you should read it before you assume a kind will run.

You will write one of these when you want to connect the daemon to a system it does not know about and the connection is mostly a matter of endpoints, credentials and shapes. Start from the minimal valid manifest in §2.1, get it installing, and then add fields. The field reference in §2.2 is a lookup, not a reading list.

The thing people get wrong is expecting an installed pack to start working. Install and enable are separate steps, on purpose: a pack that has been installed but never granted does nothing at all and says nothing about it. If your pack is silent, check the grant before you check anything else. It is the most common cause by a wide margin, and §2.8 covers both steps in order.

2.1 Minimal valid manifest

{
  "schema": "crux.integration.v1",
  "id": "example.hello",
  "name": "Hello",
  "version": "0.1.0",
  "publisher_passport_fpr": "p_6ca61039700c94bf57c2e158d282ef1e",
  "summary": "Does nothing, correctly.",
  "entry": { "kind": "http_recipe", "path": "recipes/hello.json" },
  "capabilities": ["facts:read"]
}

Everything below capabilities has a serde default. summary does not - it is a required key at deserialisation time, not at validation time, so a missing summary fails as a JSON decode error rather than a validation error (lib.rs:104).

2.2 Field reference

FieldTypeRequiredValidation rule (and where)
schemastringyesMust equal crux.integration.v1; else invalid integration schema '<x>' (lib.rs:482)
idstringyesNon-empty, and every char in [A-Za-z0-9._-]; else invalid identifier '<id>' (lib.rs:485, lib.rs:1400)
namestringyesNon-empty after trim (lib.rs:486)
versionstringyesNon-empty after trim. No semver check exists (lib.rs:487)
publisher_passport_fprstringyesNon-empty. The value cuecrux:first-party is reserved for packs compiled into the daemon (lib.rs:31, lib.rs:488)
summarystringyes (serde)No content rule (lib.rs:104)
entry.kindenum, snake_caseyesOne of the nine variants in §2.3 (lib.rs:186)
entry.pathstringyesNon-empty. Relative-path safety is enforced by the community CI gate, not by validate() (lib.rs:490)
capabilitiesstring[]no ([])Every entry must be in the 14-item allowlist; else unknown capability '<c>' (lib.rs:492)
network.allowed_hostsstring[]no ([])Literal host names, optional :port. Enforced at dispatch, not at validate (lib.rs:217)
network.requires_user_tokenboolno (false)Declarative only, no daemon code branches on it
data_access.tenant_scopesstring[]no (["selected"])Declarative; feeds risk scoring only (lib.rs:232)
data_access.content_previewboolno (false)Raises risk_level to high (lib.rs:1461)
data_access.private_factsboolno (false)Raises risk_level to high (lib.rs:1462)
safety.sandboxnone\command\wasmno (none)Declarative label; the WASM sandbox is selected by entry.kind, not by this field (lib.rs:265)
safety.max_runtime_msu64no (0)0 = daemon default. Non-zero tightens only the outbound timeout (lib.rs:243, extension_outbound.rs:462)
safety.max_output_bytesu64no (0)0 = daemon default. Non-zero tightens only the response cap (extension_outbound.rs:469)
hashes.manifeststringnoWhen present must equal the recomputed hash; else manifest hash mismatch: expected <a>, actual <b> (lib.rs:612)
hashes.bundlestringnoNot checked by validate(). Studio pack verify checks it (studio_pack.rs:379)
signatureobjectconditionalRequired unless a policy bypass applies (§2.5)
external_tool_endpointstringexternal_tool onlyMust start https:// or http://; forbidden on every other kind (lib.rs:505)
tools[]object[]external_tool, wasmNon-empty; each entry needs non-empty name + description (lib.rs:518)
wasm_module_pathstringwasm (xor URL)Must not start / nor contain .. (lib.rs:577)
wasm_module_urlstringwasm (xor path)Must start https:// (lib.rs:584)
wasm_module_sha256stringwasmExactly 64 lowercase hex chars (lib.rs:564)

tools[] entry

ExternalToolDefinition (lib.rs:154):

FieldTypeRequiredNotes
namestringyesBecomes the MCP tool name verbatim. Must be globally unique across the daemon's catalogue. The MCP layer only treats a name as an extension tool when it starts with ext. (crux-mcp/src/tools/extensions.rs:180)
descriptionstringyesNon-empty
input_schemaJSONyesPassed through to MCP tools/list unmodified. The daemon never validates arguments against it (lib.rs:160)
consequence_metadataJSONnoSurfaced to MCP discovery; when absent the daemon substitutes its own derived metadata (extensions.rs:69)
auth_shared_secret_idstringnoNames an entry in the daemon's encrypted-secrets store, forwarded as Authorization: Bearer. Forbidden for wasm (lib.rs:546). See §5.6 for its current wiring status

2.3 Entry kinds and their runtime status

Nine variants exist (lib.rs:186). Only three have a daemon-side runtime. The rest are declared vocabulary: the daemon stores them, the console lists them, and a client (an SDK, an editor, an installer script) is expected to act on the recipe at entry.path.

entry.kindRuntime in this repoWhat actually happens
mcp_confignoneDeclarative. Two first-party packs use it to describe MCP client wiring
http_recipenoneDeclarative
sdk_recipenone (carrier)Declarative. Also the carrier kind for Studio packs, POST /v1/studio/pack/build stamps sdk_recipe (studio_pack.rs:147)
cli_recipenoneDeclarative
file_watcheryescrates/corecruxd/src/vault_watcher.rs, a granted pack of this kind plus a roots env var activates the markdown-vault watcher (chapter 7)
webhook_adapternoneDeclarative
external_helperblockedvalidate() rejects it unless policy.allow_executable_helpers is true (default false): external helpers are disabled by policy. risk_level is blocked (lib.rs:498, lib.rs:1458)
external_toolyesDaemon proxies tools/call to external_tool_endpoint (chapter 5)
wasmyes, feature-gatedIn-process wasmtime sandbox; requires building corecruxd with --features wasm-extensions, else the invoke route returns 501 (chapter 6)

2.4 Derived fields the daemon computes for you

FieldValuesRule
manifest_hashblake3:<hex>BLAKE3 over the canonical signing payload, an 11-field subset, not the whole document (lib.rs:650)
trust_tierfirst_party, locally_signed, community_reviewed, unknownChapter 4
install_stateavailable, installed, enabled, blocked, update_availableenabled means at least one grant exists and is enabled (lib.rs:844)
risk_levellow, medium, high, blockedexternal_helper → blocked; content_preview / private_facts / admin:read → high; any *:write, integrations:grant, integrations:install → medium; else low (lib.rs:1457)

The signing payload is a subset: read this before you sign

manifest_hash() hashes ManifestSigningPayload (lib.rs:466): schema, id, name, version, publisher_passport_fpr, summary, entry, capabilities, network, data_access, safety.

Fields outside the signature: external_tool_endpoint, tools[], wasm_module_path, wasm_module_url, wasm_module_sha256, hashes, signature.

Consequences you must design around:

  • A signature does not bind the endpoint URL or the tool list of an external_tool pack. What binds them is the registry's manifest_sha256, a SHA-256 over the whole manifest file, published by the curator and enforced at install (http/extensions.rs:310).
  • A WASM module's bytes are bound by wasm_module_sha256, which is itself outside the signature, again, the registry manifest_sha256 is the binding layer.
  • Install by pasting a manifest into POST /v1/extensions/register therefore gives you signature integrity over the policy surface only. Install from a curated registry gives you integrity over the whole document.

2.5 When a signature is required

validate() requires a signature unless one of two policy flags lets it through (lib.rs:622):

Policy flagDefaultEffect
allow_unsigned_first_partytrueAn unsigned manifest passes iff publisher_passport_fpr == "cuecrux:first-party". This is what lets the compiled-in packs load (lib.rs:443)
allow_unsignedfalseAn unsigned manifest passes regardless of publisher. Dev only; the HTTP layer wires it to CORECRUXD_EXTENSIONS_ALLOW_UNSIGNED (lib.rs:449)

The extension installer sets allow_unsigned_first_party: false explicitly, so the first-party bypass never applies to the /v1/extensions/* family (extension_registry.rs:142).

Missing signature with no bypass gives signature is required.

2.6 The four built-in packs

builtin_manifests() (lib.rs:1132) returns four first-party manifests, asserted by builtin_manifests_validate (lib.rs:1520):

idversionkindcapabilities
mcp.claude-desktop0.1.0mcp_configintegrations:read, passport:read
mcp.cursor0.1.0mcp_configintegrations:read, passport:read
sdk.typescript.quickstart0.1.0sdk_recipeintegrations:read, facts:read, facts:write, sessions:read
vault.markdown-watcher0.1.0file_watcherfacts:write, facts:read

vault.markdown-watcher is the only one with a runtime. It is inert until both its grant and its roots environment variable are present (chapter 7).

A github.pr-facts recipe used to ship here and was removed in favour of the live GitHub connector (lib.rs:1236).

2.7 Pack registry on disk

Packs installed through the console pack API live as flat files under <data_dir>/integrations/ (lib.rs:818):

<data_dir>/integrations/
  index.json                                  # schema crux.integration.index.v1
  packs/<pack_id>/<version>/manifest.json     # verbatim manifest
  grants/<passport_fpr>/<pack_id>.json        # one grant per pack per passport
  audit.jsonl                                 # append-only event log

Every path component (pack_id, version, passport_fpr) is passed through safe_path_component, which permits only [A-Za-z0-9._-] and rejects . and ..; a violation gives invalid path component '<x>' (lib.rs:1363). Writes are atomic - serialise to <name>.json.tmp, then rename (lib.rs:1352).

index.json carries schema, updated_at_unix_ms, and a packs[] array of InstalledPackRecord (id, version, manifest_hash, trust_tier, installed_at_unix_ms) (lib.rs:326). A wrong schema value gives invalid index schema '<x>' (lib.rs:1255).

audit.jsonl is one IntegrationAuditEvent per line (lib.rs:350). Appending is best-effort: a broken audit path is warn-logged and never changes the outcome of the primary operation (lib.rs:1320), which the test audit_append_failure_does_not_fail_install pins (extension_registry.rs:430).

Extensions installed through /v1/extensions/* do not live here; they are facts. See chapter 3.

2.8 Installing and granting a pack

Three console routes back the pack model (http/mod.rs:1422):

# List the library: builtins + installed, with grants and the audit tail.
curl -s http://127.0.0.1:14800/v1/console/integrations \
  -H "Authorization: Bearer $CRUX_AGENT_TOKEN"

# Install a builtin by id (version defaults to "0.1.0").
curl -s -X POST http://127.0.0.1:14800/v1/console/integrations/vault.markdown-watcher/install \
  -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \
  -H 'Content-Type: application/json' -d '{"version":"0.1.0"}'

# Grant capabilities to the daemon's own passport.
curl -s -X POST http://127.0.0.1:14800/v1/console/integrations/vault.markdown-watcher/grant \
  -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"version":"0.1.0","capabilities":["facts:read","facts:write"],"reason":"vault ingest"}'

# Revoke.
curl -s -X POST http://127.0.0.1:14800/v1/console/integrations/vault.markdown-watcher/disable \
  -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \
  -H 'Content-Type: application/json' -d '{"reason":"done testing"}'

Scopes: integrations:install, integrations:grant, integrations:disable respectively (console.rs:2216, console.rs:2251, console.rs:2289).

Install accepts either a manifest object (whose id must equal the path id) or a bare version naming a builtin (console.rs:3231). Trust tier is assigned by manifest_trust_tier: first-party publisher → first_party, otherwise signed → locally_signed, otherwise unknown (console.rs:3255).

Grant rules (lib.rs:963):

  • The pack must be installed on disk, else pack '<id>' version '<v>' is not installed.
  • Each requested capability must be in the global allowlist and declared by the manifest, else capability '<c>' is not declared by pack '<id>'.
  • Capabilities are sorted and deduped before storage.
  • disable flips enabled to false and stamps disabled_at_unix_ms; it does not delete the file (lib.rs:1011).

Three operator switches

Env varDefaultEffect
CORECRUXD_INTEGRATIONS_ENABLEDon (unset = on)Off → install and grant return 403 integrations are disabled (config.rs:1377)
CORECRUXD_INTEGRATIONS_SAFE_MODEoffOn → install returns 403 integration safe mode blocks install, grant returns 403 integration safe mode blocks grants, and every non-first-party enabled pack is reported as blocked (config.rs:1380, console.rs:3211)
CORECRUXD_INTEGRATIONS_ALLOW_EXECUTABLE_HELPERSoffOn → external_helper manifests validate instead of being rejected (config.rs:1383)

Accepted truthy values for all three are exactly 1, true, TRUE, yes, YES.

Ground truth