HTTP API · 7. Extensions and Studio

Thirty-eight registrations govern code and content the daemon did not ship with: extensions it may call, integrations it may connect to, packs it may install, and artefacts it may publish.

Planes covered: Community extensions · Integrations (GitHub) · Integrations (OpenAI) · OpenAI function-calling shim · Studio board packs and template library · RCX Registry publish · Actions enrichment.

Read chapter 0 first for the auth model, request-field notation and error shape. The daemon developer guide covers the manifest formats, signing and the capability model in depth; this chapter is the route contract only.

Two rules run through the whole plane and explain most of its shape:

  • Install is not enable. Registering an extension does not make it callable. A per-passport grant naming the permitted tools and prefixes is a separate, explicit action.
  • Mutating an extension needs two scopes at once. Every non-read route under /v1/extensions/* requires admin:read and facts:write together (extensions.rs:126). This is one of only two all-of pairs in the daemon. Either scope alone is a 403.

7.1 Community extensions

Thirteen registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/extensionsextensions.rs:76 list_extensionsadmin:read · Read-keys extensions, count, allow_unsigned_dev-,R
POST/v1/extensions/registerextensions.rs:121 register_extensionall-of admin:read and facts:write · Writebody: manifest (IntegrationManifest, req)Registration record400, 409, 413, 502-W · facts
GET/v1/extensions/registryextensions.rs:228 list_registry_entriesadmin:read · Read-keys entries, curator_passport_fpr, updated_at_unix_ms, schema404-R
POST/v1/extensions/install-from-registryextensions.rs:284 install_from_registryall-of admin:read and facts:write · Writebody: id (String, req), index_path (PathBuf, dflt)keys installed, manifest_sha256, registry_entry, schema400, 404, 409, 502-W · facts
GET/v1/extensions/{id}extensions.rs:95 get_extensionadmin:read · Readpath idExtension record404-R
DELETE/v1/extensions/{id}extensions.rs:464 delete_extensionall-of admin:read and facts:write · Writepath idDeletion result400, 404-W · facts
GET/v1/extensions/keysextensions.rs:503 list_trusted_keysadmin:read · Read-keys keys, count500-R
POST/v1/extensions/keysextensions.rs:522 add_trusted_keyall-of admin:read and facts:write · Writebody: passport_fpr (String, req), public_key_hex (String, req), trust_tier (TrustTier, req), added_by (String, dflt)keys passport_fpr, trust_tier500-W
DELETE/v1/extensions/keys/{passport_fpr}extensions.rs:870 delete_trusted_keyall-of admin:read and facts:write · Writepath passport_fprDeletion result404, 500-W
GET/v1/extensions/{id}/grantsextensions.rs:587 list_grantsadmin:read · Readpath idkeys grants, count, extension_id-,R
POST/v1/extensions/{id}/grantsextensions.rs:610 issue_grantall-of admin:read and facts:write · Writepath id; body: passport_fpr (String, req), allowed_tool_names (Vec String, dflt), allowed_prefixes_read (Vec String, dflt), allowed_prefixes_write (Vec String, dflt), rate_limit_per_min (u32, dflt)Grant record400, 404, 409-W · facts
DELETE/v1/extensions/{id}/grants/{passport_fpr}extensions.rs:837 revoke_grantall-of admin:read and facts:write · Writepath id, passport_fprRevocation result400, 404-W · facts
POST/v1/extensions/{id}/tools/{tool_name}/invokeextensions.rs:686 invoke_extension_toolall-of admin:read and facts:write · Writepath id, tool_name; header X-Corecrux-Passport-Id; body: passport_fpr (String, dflt), args (JSON, dflt InvokeToolBody::empty_args)Tool result400, 403, 404, 429, 500, 502-W · receipt, facts

The invoke route's own rate limit

POST /v1/extensions/{id}/tools/{tool_name}/invoke is rate-limited independently of the ingress limiter, by a process-wide sliding 60-second window keyed by extension id and passport fingerprint, capped per grant (rate_limit_per_min) or by a daemon default (mod.rs:386). The 429 from this route means the grant's budget, not the IP budget.

502 means your extension's endpoint failed or returned something unusable. 403 means the grant does not permit that tool. A write outside allowed_prefixes_write is not a 403; it is dropped and reported back in the result.

WASM extensions

Extension entries of kind: wasm need the wasm-extensions Cargo feature, which is off by default. Without it, dispatch returns 501 (mod.rs:393). The route exists in every build; the runtime behind one entry kind does not.

Route ordering

/v1/extensions/registry and /v1/extensions/keys are static segments that must stay registered before /v1/extensions/{id} (mod.rs:1073). An extension whose id is registry or keys is unreachable.

7.2 Integrations: GitHub

Nine registrations. Note the scope split: reads take admin:read, writes take integrations:install or integrations:disable, not admin:write.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/integrations/github/statusintegrations_github.rs:31 get_statusadmin:read · AdminRead-Connection status-CORECRUXD_INTEGRATIONS_ENABLED, default onR
POST/v1/integrations/github/connectintegrations_github.rs:39 post_connectintegrations:install · Writebody: pat (String, req), skip_verify (bool, dflt), username_override (String, dflt)Connection result400, 500CORECRUXD_INTEGRATIONS_ENABLED, default onW
POST/v1/integrations/github/disconnectintegrations_github.rs:91 post_disconnectintegrations:disable · Write-Disconnect result500CORECRUXD_INTEGRATIONS_ENABLED, default onW
GET/v1/integrations/github/reposintegrations_github.rs:251 get_selected_reposadmin:read · AdminRead-keys repos, count-CORECRUXD_INTEGRATIONS_ENABLED, default onR
GET/v1/integrations/github/repos/accessibleintegrations_github.rs:112 get_accessible_reposadmin:read · AdminRead-keys accessible, selected, count412, 500, 502CORECRUXD_INTEGRATIONS_ENABLED, default onR
POST/v1/integrations/github/repos/{owner}/{repo}/selectintegrations_github.rs:148 post_select_repointegrations:install · Writepath owner, repoSelection result412, 500CORECRUXD_INTEGRATIONS_ENABLED, default onW
DELETE/v1/integrations/github/repos/{owner}/{repo}/selectintegrations_github.rs:208 delete_selected_repointegrations:disable · Writepath owner, repoDeselection result500CORECRUXD_INTEGRATIONS_ENABLED, default onW
PUT/v1/integrations/github/repos/{owner}/{repo}/planningintegrations_github.rs:193 put_planning_flagintegrations:install · Writepath owner, repo; body: planning (bool, req)Updated flag400CORECRUXD_INTEGRATIONS_ENABLED, default onW
POST/v1/integrations/github/syncintegrations_github.rs:222 post_syncintegrations:install · Write-Sync result412, 500, 502CORECRUXD_INTEGRATIONS_ENABLED, default onW

412 means the integration is not connected, call connect first. 502 means GitHub itself failed or rate-limited the daemon.

POST /v1/integrations/github/connect accepts a personal access token in the request body. It is sealed into a credential envelope on disk rather than kept in the response. skip_verify: true stores it without a round-trip to GitHub; use it only when you know the network path is unavailable at connect time.

7.3 Integrations: OpenAI

Five registrations, the same scope pattern.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/integrations/openai/statusintegrations_openai.rs:38 get_statusadmin:read · AdminRead-Connection status-CORECRUXD_INTEGRATIONS_ENABLED, default onR
POST/v1/integrations/openai/connectintegrations_openai.rs:46 post_connectintegrations:install · Writebody: api_key (String, req), organization_id (String, dflt), default_model (String, dflt), skip_verify (bool, dflt)Connection result400, 500CORECRUXD_INTEGRATIONS_ENABLED, default onW
POST/v1/integrations/openai/disconnectintegrations_openai.rs:107 post_disconnectintegrations:disable · Write-Disconnect result500CORECRUXD_INTEGRATIONS_ENABLED, default onW
PATCH/v1/integrations/openai/settingsintegrations_openai.rs:117 patch_settingsintegrations:install · Writebody: default_model (String, dflt), organization_id (String, dflt)Updated settings412, 500CORECRUXD_INTEGRATIONS_ENABLED, default onW
POST/v1/integrations/openai/chatintegrations_openai.rs:170 post_chatintegrations:install · Writebody: messages (JSON, req), model (String, dflt), max_tokens (u32, dflt), temperature (f32, dflt)keys messages, model, duration_ms412, 500, 502CORECRUXD_INTEGRATIONS_ENABLED, default onW

POST /v1/integrations/openai/chat makes an outbound call to a third party using the operator's stored key. It is classed as a write because it spends the operator's credit with an external provider. integrations:install is the scope that authorises it; there is no separate call scope. If you do not want a token to be able to spend on OpenAI, do not give it integrations:install.

7.4 OpenAI function-calling shim

Two registrations, gated off by default. The shim projects the daemon's MCP tool registry into OpenAI function-calling schemas so an OpenAI-shaped client can drive it.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/openai/tools.jsonopenai_shim.rs:168 get_tools_jsonany-of query:read, facts:write, sessions:write, admin:read, admin:write · FeatureGated-keys tools, count, source, generated_from401, 404, 503CORECRUXD_OPENAI_SHIM, default offR
POST/v1/openai/invokeopenai_shim.rs:219 post_invokeany-of query:read, facts:write, sessions:write, admin:read, admin:write · FeatureGatedbody: name (String, dflt), arguments (Value, dflt), function (Value, dflt)keys name, arguments, result, receipt_ref, message, error400, 401, 403, 404, 503CORECRUXD_OPENAI_SHIM, default offW · receipt

The scope set on this plane is unusually wide and it is any-of. Any one of five scopes, including query:read, reaches a route that can invoke any MCP tool (openai_shim.rs:42). The individual tool's own authorization still applies inside the dispatch, but the shim's front door does not narrow it. That is the reason the flag is off by default. Turn it on deliberately.

503 means the MCP registry is unavailable, usually CORECRUXD_MCP_ENABLED is off, since the shim generates its schemas from the MCP tool list. 404 means the flag is off.

receipt_ref on a successful invoke points at the mediation receipt for the call.

7.5 Studio: board packs and template library

Four registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/studio/pack/buildstudio_pack.rs:96 post_build_packquery:read · Readbody: studio (Value, req), id (String, req), name (String, req), version (String, req), publisher_passport_fpr (String, req), summary (String, dflt)pack, signed, capabilities, manifest_hash, bundle_hash, sign_instructions, trust_note400, 500-R, mounted as a write-class POST
POST/v1/studio/pack/verifystudio_pack.rs:429 post_verify_packquery:read · Readbody: pack (Value, req)ok, schema_ok, manifest_hash_ok, bundle_hash_ok, signature, signed, manifest, capabilities, studio, errors400, 500-R, mounted as a write-class POST
GET/v1/studio/librarystudio_library.rs:116 get_studio_libraryquery:read · Read-keys entries, curator_passport_fpr, updated_at_unix_ms, schema404-R
POST/v1/studio/library/{id}/installstudio_library.rs:287 post_studio_library_installall-of admin:read and facts:write · Writepath id; body: index_path (PathBuf, dflt)keys written, library_id, entry, kind, version, pack_sha256, publisher_passport_fpr, provenance, remaps, required_tier, schema, signed, allow_unsigned_dev400, 403, 404, 409, 500, 502-W · receipt, facts

Build and verify are pure functions. Both need only query:read and neither persists anything. An unsigned result from build is the expected outcome on a bare daemon, and sign_instructions in the response tells you the two ways forward.

The install route is the one Studio route that mutates, and it is classified ahead of the read-class /v1/studio/* prefix rule specifically so that a read token can never authorise an install (route_auth.rs:144). It also requires admin:read and facts:write at the handler. If you add a mutating route under a read-class prefix anywhere in this daemon, do the same.

403 on install means the pack is unsigned and the daemon is not in a dev-unsigned posture, or the publisher is not in the trusted keyring. 409 means an id collision, the installer never overwrites an existing board.

/v1/studio/library is registered before /v1/studio/pack/* (mod.rs:674).

7.6 RCX Registry publish

Four registrations. Preview and emit, for two subject kinds.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/rcx/publish/passports/{passportId}/previewrcx_publish.rs:35 preview_passportadmin:read · Writepath passportId; body: registry_url (String, dflt), operator_metadata (Value, dflt)Preview payload-,R, mounted as a write-class POST
POST/v1/rcx/publish/passports/{passportId}/emitrcx_publish.rs:50 emit_passportall-of admin:read and facts:write · Writepath passportId; body: registry_url (String, dflt), operator_metadata (Value, dflt)Emit result-,W
POST/v1/rcx/publish/projects/{projectId}/previewrcx_publish.rs:66 preview_projectadmin:read · Writepath projectId; body: registry_url (String, dflt), operator_metadata (Value, dflt)Preview payload-,R, mounted as a write-class POST
POST/v1/rcx/publish/projects/{projectId}/emitrcx_publish.rs:81 emit_projectall-of admin:read and facts:write · Writepath projectId; body: registry_url (String, dflt), operator_metadata (Value, dflt)Emit result-,W

Preview and emit are deliberately different scopes. Preview needs only admin:read; emit needs admin:read and facts:write (rcx_publish.rs:56). Emit is the route that sends data to an external registry. Run preview first and read exactly what would leave the daemon.

registry_url in the body overrides the configured destination. Treat that as a privileged field: a caller who can emit can choose where the emission goes.

7.7 Actions enrichment

One registration.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/actions/enrichactions.rs:35 post_action_enrichany-of query:read, admin:read, admin:write · Writebody: tool_name (String, req), tenant_id (String, dflt), tool_parameters (Value, dflt), action_description (String, dflt), include_first_party_enrichers (bool, dflt)keys proposal, capability, status, detail, basic_available, fallback, first_party_enrichers_used, schema400, 402, 500first-party enrichers need enrichers:first_party or admin:writeW · receipt

include_first_party_enrichers: true triggers a second scope check requiring any-of enrichers:first_party or admin:write (actions.rs:54). Without one of those you get a 402 rather than a 403, the response's capability and fallback fields tell you which service is missing and what the basic path returned instead.

basic_available tells you whether the unenriched proposal is usable, so a client can degrade rather than fail.

7.8 Failure modes on this plane

SymptomMost likely cause
403 on any /v1/extensions/* mutation with facts:write aloneEvery extension mutation needs admin:read and facts:write. So do Studio library install and both RCX emits.
501 from an extension invokeThe extension is kind: wasm and the wasm-extensions Cargo feature is off. It is off by default.
429 from an extension invoke while other traffic is fineThe grant's rate_limit_per_min window, not the IP rate limiter.
502 from an extension invokeYour extension's HTTPS endpoint failed or returned something unusable.
A tool call that succeeds but writes nothingWrites outside the grant's allowed_prefixes_write are dropped and reported, not rejected.
412 on any GitHub or OpenAI routeThe integration is not connected. Call connect first.
502 on /v1/integrations/github/*GitHub failed or rate-limited the daemon.
404 on /v1/openai/tools.jsonCORECRUXD_OPENAI_SHIM is off. It is off by default.
503 on /v1/openai/invokeThe MCP registry is unavailable; the shim generates its schemas from it. Check CORECRUXD_MCP_ENABLED.
403 on Studio library installThe pack is unsigned on a daemon that requires signing, or the publisher is not in the trusted keyring.
409 on Studio library installAn id collision. The installer never overwrites an existing board.
402 from /v1/actions/enrichYou asked for first-party enrichers without enrichers:first_party or admin:write. Read capability and fallback.
An extension id of registry or keys that cannot be fetchedStatic route segments win over the parameter. Rename the extension.

Sources