Developer guide

Crux Daemon · 2. Integration packs

Crux Daemon developer guide

2. Integration packs — the crux.integration.v1 manifest

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.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