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/*requiresadmin:readandfacts:writetogether (extensions.rs:126). This is one of only two all-of pairs in the daemon. Either scope alone is a403.
7.1 Community extensions
Thirteen registrations.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/extensions | extensions.rs:76 list_extensions | admin:read · Read | - | keys extensions, count, allow_unsigned_dev | - | , | R |
POST | /v1/extensions/register | extensions.rs:121 register_extension | all-of admin:read and facts:write · Write | body: manifest (IntegrationManifest, req) | Registration record | 400, 409, 413, 502 | - | W · facts |
GET | /v1/extensions/registry | extensions.rs:228 list_registry_entries | admin:read · Read | - | keys entries, curator_passport_fpr, updated_at_unix_ms, schema | 404 | - | R |
POST | /v1/extensions/install-from-registry | extensions.rs:284 install_from_registry | all-of admin:read and facts:write · Write | body: id (String, req), index_path (PathBuf, dflt) | keys installed, manifest_sha256, registry_entry, schema | 400, 404, 409, 502 | - | W · facts |
GET | /v1/extensions/{id} | extensions.rs:95 get_extension | admin:read · Read | path id | Extension record | 404 | - | R |
DELETE | /v1/extensions/{id} | extensions.rs:464 delete_extension | all-of admin:read and facts:write · Write | path id | Deletion result | 400, 404 | - | W · facts |
GET | /v1/extensions/keys | extensions.rs:503 list_trusted_keys | admin:read · Read | - | keys keys, count | 500 | - | R |
POST | /v1/extensions/keys | extensions.rs:522 add_trusted_key | all-of admin:read and facts:write · Write | body: passport_fpr (String, req), public_key_hex (String, req), trust_tier (TrustTier, req), added_by (String, dflt) | keys passport_fpr, trust_tier | 500 | - | W |
DELETE | /v1/extensions/keys/{passport_fpr} | extensions.rs:870 delete_trusted_key | all-of admin:read and facts:write · Write | path passport_fpr | Deletion result | 404, 500 | - | W |
GET | /v1/extensions/{id}/grants | extensions.rs:587 list_grants | admin:read · Read | path id | keys grants, count, extension_id | - | , | R |
POST | /v1/extensions/{id}/grants | extensions.rs:610 issue_grant | all-of admin:read and facts:write · Write | path 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 record | 400, 404, 409 | - | W · facts |
DELETE | /v1/extensions/{id}/grants/{passport_fpr} | extensions.rs:837 revoke_grant | all-of admin:read and facts:write · Write | path id, passport_fpr | Revocation result | 400, 404 | - | W · facts |
POST | /v1/extensions/{id}/tools/{tool_name}/invoke | extensions.rs:686 invoke_extension_tool | all-of admin:read and facts:write · Write | path id, tool_name; header X-Corecrux-Passport-Id; body: passport_fpr (String, dflt), args (JSON, dflt InvokeToolBody::empty_args) | Tool result | 400, 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/integrations/github/status | integrations_github.rs:31 get_status | admin:read · AdminRead | - | Connection status | - | CORECRUXD_INTEGRATIONS_ENABLED, default on | R |
POST | /v1/integrations/github/connect | integrations_github.rs:39 post_connect | integrations:install · Write | body: pat (String, req), skip_verify (bool, dflt), username_override (String, dflt) | Connection result | 400, 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
POST | /v1/integrations/github/disconnect | integrations_github.rs:91 post_disconnect | integrations:disable · Write | - | Disconnect result | 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
GET | /v1/integrations/github/repos | integrations_github.rs:251 get_selected_repos | admin:read · AdminRead | - | keys repos, count | - | CORECRUXD_INTEGRATIONS_ENABLED, default on | R |
GET | /v1/integrations/github/repos/accessible | integrations_github.rs:112 get_accessible_repos | admin:read · AdminRead | - | keys accessible, selected, count | 412, 500, 502 | CORECRUXD_INTEGRATIONS_ENABLED, default on | R |
POST | /v1/integrations/github/repos/{owner}/{repo}/select | integrations_github.rs:148 post_select_repo | integrations:install · Write | path owner, repo | Selection result | 412, 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
DELETE | /v1/integrations/github/repos/{owner}/{repo}/select | integrations_github.rs:208 delete_selected_repo | integrations:disable · Write | path owner, repo | Deselection result | 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
PUT | /v1/integrations/github/repos/{owner}/{repo}/planning | integrations_github.rs:193 put_planning_flag | integrations:install · Write | path owner, repo; body: planning (bool, req) | Updated flag | 400 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
POST | /v1/integrations/github/sync | integrations_github.rs:222 post_sync | integrations:install · Write | - | Sync result | 412, 500, 502 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/integrations/openai/status | integrations_openai.rs:38 get_status | admin:read · AdminRead | - | Connection status | - | CORECRUXD_INTEGRATIONS_ENABLED, default on | R |
POST | /v1/integrations/openai/connect | integrations_openai.rs:46 post_connect | integrations:install · Write | body: api_key (String, req), organization_id (String, dflt), default_model (String, dflt), skip_verify (bool, dflt) | Connection result | 400, 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
POST | /v1/integrations/openai/disconnect | integrations_openai.rs:107 post_disconnect | integrations:disable · Write | - | Disconnect result | 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
PATCH | /v1/integrations/openai/settings | integrations_openai.rs:117 patch_settings | integrations:install · Write | body: default_model (String, dflt), organization_id (String, dflt) | Updated settings | 412, 500 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
POST | /v1/integrations/openai/chat | integrations_openai.rs:170 post_chat | integrations:install · Write | body: messages (JSON, req), model (String, dflt), max_tokens (u32, dflt), temperature (f32, dflt) | keys messages, model, duration_ms | 412, 500, 502 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/openai/tools.json | openai_shim.rs:168 get_tools_json | any-of query:read, facts:write, sessions:write, admin:read, admin:write · FeatureGated | - | keys tools, count, source, generated_from | 401, 404, 503 | CORECRUXD_OPENAI_SHIM, default off | R |
POST | /v1/openai/invoke | openai_shim.rs:219 post_invoke | any-of query:read, facts:write, sessions:write, admin:read, admin:write · FeatureGated | body: name (String, dflt), arguments (Value, dflt), function (Value, dflt) | keys name, arguments, result, receipt_ref, message, error | 400, 401, 403, 404, 503 | CORECRUXD_OPENAI_SHIM, default off | W · 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
POST | /v1/studio/pack/build | studio_pack.rs:96 post_build_pack | query:read · Read | body: 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_note | 400, 500 | - | R, mounted as a write-class POST |
POST | /v1/studio/pack/verify | studio_pack.rs:429 post_verify_pack | query:read · Read | body: pack (Value, req) | ok, schema_ok, manifest_hash_ok, bundle_hash_ok, signature, signed, manifest, capabilities, studio, errors | 400, 500 | - | R, mounted as a write-class POST |
GET | /v1/studio/library | studio_library.rs:116 get_studio_library | query:read · Read | - | keys entries, curator_passport_fpr, updated_at_unix_ms, schema | 404 | - | R |
POST | /v1/studio/library/{id}/install | studio_library.rs:287 post_studio_library_install | all-of admin:read and facts:write · Write | path 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_dev | 400, 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
POST | /v1/rcx/publish/passports/{passportId}/preview | rcx_publish.rs:35 preview_passport | admin:read · Write | path 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}/emit | rcx_publish.rs:50 emit_passport | all-of admin:read and facts:write · Write | path passportId; body: registry_url (String, dflt), operator_metadata (Value, dflt) | Emit result | - | , | W |
POST | /v1/rcx/publish/projects/{projectId}/preview | rcx_publish.rs:66 preview_project | admin:read · Write | path 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}/emit | rcx_publish.rs:81 emit_project | all-of admin:read and facts:write · Write | path 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
POST | /v1/actions/enrich | actions.rs:35 post_action_enrich | any-of query:read, admin:read, admin:write · Write | body: 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, schema | 400, 402, 500 | first-party enrichers need enrichers:first_party or admin:write | W · 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
| Symptom | Most likely cause |
|---|---|
403 on any /v1/extensions/* mutation with facts:write alone | Every extension mutation needs admin:read and facts:write. So do Studio library install and both RCX emits. |
501 from an extension invoke | The 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 fine | The grant's rate_limit_per_min window, not the IP rate limiter. |
502 from an extension invoke | Your extension's HTTPS endpoint failed or returned something unusable. |
| A tool call that succeeds but writes nothing | Writes outside the grant's allowed_prefixes_write are dropped and reported, not rejected. |
412 on any GitHub or OpenAI route | The integration is not connected. Call connect first. |
502 on /v1/integrations/github/* | GitHub failed or rate-limited the daemon. |
404 on /v1/openai/tools.json | CORECRUXD_OPENAI_SHIM is off. It is off by default. |
503 on /v1/openai/invoke | The MCP registry is unavailable; the shim generates its schemas from it. Check CORECRUXD_MCP_ENABLED. |
403 on Studio library install | The pack is unsigned on a daemon that requires signing, or the publisher is not in the trusted keyring. |
409 on Studio library install | An id collision. The installer never overwrites an existing board. |
402 from /v1/actions/enrich | You 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 fetched | Static route segments win over the parameter. Rename the extension. |
Sources
- crates/corecruxd/src/http/extensions.rs:126, all-of
admin:readandfacts:writeon extension mutations - crates/corecruxd/src/http/extensions.rs:77, extension read scope
- crates/corecruxd/src/http/mod.rs:386, the extension dispatch rate table
- crates/corecruxd/src/http/mod.rs:393,
501without thewasm-extensionsfeature - crates/corecruxd/src/http/mod.rs:1073, extension route ordering
- crates/corecruxd/src/http/integrations_github.rs:32, GitHub read scope
- crates/corecruxd/src/http/route_auth.rs:326, integrations write class
- crates/corecruxd/src/http/openai_shim.rs:42,
SHIM_SCOPES - crates/corecruxd/src/config.rs:1376,
CORECRUXD_OPENAI_SHIMdefault - crates/corecruxd/src/config.rs:1377,
CORECRUXD_INTEGRATIONS_ENABLEDdefault - crates/corecruxd/src/http/studio_pack.rs:96, pack build
- crates/corecruxd/src/http/studio_library.rs:293, library install, all-of pair
- crates/corecruxd/src/http/route_auth.rs:144, the Studio install carve-out
- crates/corecruxd/src/http/rcx_publish.rs:56, emit, all-of pair
- crates/corecruxd/src/http/actions.rs:16,
enrichers:first_party - crates/corecruxd/src/http/actions.rs:54, the first-party enricher gate

