Plan 0006
Hosted credentials for external tool services
- Status
- Draft
- Ambition
- 5 requirements surface landed, hosted boxes are the speculative half
- Impact
- 6 removes the bring-your-own-key wall for meterable services
- Effort
- 5 gateway or provisioner plus billing glue
- Risk
- 6 holds user credit and vendor keys, billing mistakes are real money
- Maturity
- 3 requirements surface shipped, hosted path not started
- Leverage
- 6 every credentialed tool onboards through one door
- Taste
- 7 billing UX and credential trust decisions are judgment calls with real money attached
- Authors
- Created
- Updated
- Tracking issue
- -
- Supersedes
- -
- Superseded by
- -
Summary
The ix-mcp kernel bundles tooling that calls paid or personal external services: search needs a Mixedbread credential, the bundled exa_py SDK needs EXA_API_KEY, linear needs LINEAR_API_KEY, and slack / google_auth need a stored login. This plan does two things. First, it documents the requirements surface that landed alongside it: every such credential is declared once, in the registry that already feeds api() and the server instructions, and probed locally, so ix-mcp serve yells about gaps once at startup, ix-mcp requirements gives setup scripts one gateable command, and the instructions name the credentialed modules without drifting. Second, it proposes the next step: hosted credentials (“boxes”), where a user who has not brought a vendor key for the meterable services (Exa, Mixedbread) can pay with ix credits through an ix-operated gateway or an ix-side provisioner instead. The hosted path is a proposal only; nothing here commits to a vendor, a billing system, or a deployment.
Motivation
Every vendor-backed tool multiplies setup friction. The bundled modules each fail clearly at call time (linear names LINEAR_API_KEY; slack points at slack.login), which is good but late and scattered: an agent or user discovers each gap one failing call at a time, and nothing answers “what does this kernel still need from me?” in one place. There was also no machine-gateable check a provisioning script could run before handing the server to an agent.
That is two distinct problems:
- Discovery. Which credentials does the bundled tooling want, which are visible right now, and what exactly fixes each missing one? Fixed by the requirements surface below.
- Acquisition. Even with perfect reporting, the metered services still demand a vendor signup, a payment method, and a key per user. For fleets of agent VMs and for end users who just want search to work, “go create a Mixedbread account” is the wall itself. That is what the hosted-credential proposal addresses.
What landed with this plan
The companion change implemented the requirements surface in packages/mcp (the ix_notebook_mcp server). This section doubles as the report of that change.
- One declaration per credential, in the registry.
registry.Credential(service name, env vars in resolution order, token-file path, login clause, key URL) attaches to the existingModulerows and to a new typedLibraryrow, sosearch(Mixedbread),slack,linear,google_auth, andexa_py(Exa) each declare their credential exactly where they already declare themselves.registry.credentialed()is the one iteration everything else builds from; adding a credentialed service is one field on its registry row. - Local-only probes.
ix_notebook_mcp/requirements.pychecks env vars and token-file existence, never a file’s content, the network, or a secret’s value, so the report is free and safe anywhere. A present credential is reported by source (search: Mixedbread credential via MXBAI_API_KEY); a missing one carries the full remedy (set MXBAI_API_KEY (get one at https://www.mixedbread.com) or runmgrep login“). - Three fail-fast consumers.
ix-mcp serveprints one status line per credential on stderr at startup; the newix-mcp requirementssubcommand prints the same report and exits non-zero when anything is missing; and the server instructions gain a registry-generated sentence naming the credentialed modules (guide.credentials_note()), so the list cannot drift from the probes. - Checks. A nix check (
requirementsSmoke) proves the contract in the credential-less build sandbox end to end through the wrapped binary, plants credentials to flip the probes, and pins the registry’s slack declaration against the slack module’s own resolution constants so the declared probe cannot drift from the code that actually resolves the token.packages/mcp/tests/test_requirements.pycovers the probe and message contracts under pytest.
Call-time behavior is unchanged: each module still owns its own resolution and its own clear failure. The registry rows mirror those documented sources (the Mixedbread row mirrors the resolution order owned by packages/mixedbread/src/auth.rs), and the smoke pins the one module that exports its constants.
Detailed design: hosted credentials
The goal is that a user without vendor accounts can run the metered tools by holding exactly one relationship: an ix account with a credit balance. The bundled services split into three classes, and only two of them are hostable at all.
Service classification
| Stateless metered (Exa shape) | Stateful per-tenant (Mixedbread shape) | Personal identity (Slack / Google / Linear shape) | |
|---|---|---|---|
| Upstream state | None per user; pure request/response | Durable per-user data (vector stores, indexes) | The user's own account at the service |
| Hosted path | Gateway pass-through on ix's platform key | Per-user upstream resource plus scoped key, provisioned by ix | None. The credential is the user's identity; ix mediating it would mean ix holding the user's mailbox or workspace |
| Isolation risk | Rate/spend only | One shared key would commingle tenants: any user could query any other user's index | Not applicable; BYO login flows stay the only path |
| Metering | Per request/result at the gateway | Provider-side usage records per sub-credential | Not billed by ix |
Stateless services get a gateway. ix operates a thin reverse proxy. The user holds an ix-issued virtual key; the gateway authenticates it, debits the credit balance per metered unit, applies rate and spend caps, swaps in ix’s platform key, and forwards upstream. The vendor sees one ix account. Exa is the canonical worked example: one upstream key, per-request metering, no tenant state.
Stateful services get a provisioner. For Mixedbread, ix creates one upstream store (or store family) per user and a key scoped to it, through the vendor’s platform API, and hands the user that scoped credential. Where a vendor cannot scope keys narrowly enough, the gateway substitutes: it terminates the user’s virtual key and validates or rewrites tenant identifiers (for Mixedbread, the store_identifiers field of every search request) against the caller’s tenant before forwarding. Teardown is the reverse: revoke the key, delete the store.
Personal-identity services stay BYO by construction. Slack, Google, and Linear credentials authenticate as the user; the modules already gate them to incognito sessions so personal data cannot reach a shared room. A hosted pool of such credentials is a non-goal, permanently.
The box
A box is the per-user bundle the hosted path materializes: the gateway virtual key plus any provisioned per-service credentials, written by an ix login-style flow (OAuth device flow against the ix platform, then a credential file under ~/.config/ix/, mode 0600). The name is deliberately mundane: a container of credentials, not a new abstraction.
kernel tooling (search, exa_py, ...)
|
| registry.Credential resolution: env key? vendor login token? ix box?
v
credential source ----------------------------- BYO key: call vendor directly
|
| ix box (virtual key)
v
ix gateway: authenticate, meter, debit credits,
enforce caps, swap to platform key,
scope tenant identifiers
|
v
vendor API (Exa, Mixedbread, ...) The requirements surface is the seam that makes this incremental: a hosted credential is one more entry in a registry row’s declared sources (after explicit env vars and vendor login tokens), and the probe, the startup report, the subcommand, and the instructions all pick it up from that single declaration. Explicit vendor keys always win, so the hosted path is a convenience, not lock-in, and a gateway outage cannot break a user who brought their own key. There is no silent fallback between sources at call time: whichever source resolves first is the one used, and its failure is that source’s clear error.
Relationship to Plan 0002
Plan 0002 designs partner-mediated integrations for ix VMs: per-VM scoped credentials, host-attested identity, fleet-time binding, boot-time materialization. This proposal is the user-identity sibling for tooling that runs anywhere (a laptop running ix-mcp, not a fleet VM), and inherits 0002’s invariants where they apply: no secrets in the Nix store or evaluated configuration; scoped, revocable credentials per principal; billing truth from gateway or provider-side records, never from counters the client can forge; typed, fail-closed failures. The identity primitive differs (user login instead of VM attestation); when both exist, the gateway and provisioner should be one service with two principal types.
Implementation shape and sequencing
- Now (landed): the requirements surface. Every new vendor-backed module or library must declare its
Credentialon its registry row; this is the contract that keeps both the BYO and hosted paths honest. - Next, as services arrive: new rows only. No hosted machinery is needed to add a credentialed service.
- Hosted, behind a human decision: stand up the gateway as a small service with a credit ledger backed by the billing system the ix platform already uses. For LLM-API traffic specifically, evaluate operating an existing gateway (for example LiteLLM) before writing one; for arbitrary vendor APIs like Exa and Mixedbread the proxy is small and the metering logic is the actual product, so ownership is likely justified (the repo already owns a typed Mixedbread client in
packages/mixedbread). Do not build any of this speculatively: the trigger is a decision to sell hosted access, not this plan merging.
Drawbacks
- Operating surface. A gateway in the request path is an availability and latency liability for exactly the tools that work fine with BYO keys. Mitigation: BYO keys always bypass it.
- Money handling. Credits, refunds, abuse (a leaked virtual key burns ix’s upstream balance), and vendor terms around resale all carry real exposure. Spend caps per key and provider-side reconciliation are necessary, not optional.
- Tenant isolation by rewrite is fragile. Where a vendor lacks scoped keys and the gateway enforces tenancy by rewriting identifiers, every proxied endpoint must be audited first. An allowlist of understood endpoints fails closed.
- Declared probes can lag the code. The registry mirrors each module’s documented resolution rather than executing it. The smoke pins the module that exports its constants (slack); the others rely on review. A wrong declaration miscolors the report but never changes call behavior.
Alternatives
- Do nothing beyond the requirements surface. BYO keys with crisp, aggregated reporting is a respectable resting state, and is exactly where this plan leaves the repo until a human decides to host. The acquisition wall stays.
- Per-user upstream sub-accounts for everything, no gateway. Cleanest isolation, but only works where vendors offer platform APIs, and leaves metering to the vendor’s billing granularity. Right for Mixedbread-shaped services, insufficient alone for Exa-shaped ones where ix wants its own metering and caps.
- Share one ix key directly with users. Rejected: no per-user metering, no revocation story, instant commingling for stateful services.
- Ask each module’s own status function. Rejected for the report: only
slackandgoogle_authexpose one, several modules import heavy dependencies, and the report must work in a bare server process. The registry declaration plus a pinned smoke keeps the report cheap and the modules authoritative at call time.
Prior art
- Plan 0002 in this repo: per-VM credential brokering with the same fail-closed and billing-truth invariants.
packages/mixedbread/src/auth.rs: the env-then-login resolution order the search row mirrors and a box would extend.- The registry pattern itself (
packages/mcp/ix_notebook_mcp/registry.py): modules declare themselves once and the instructions,api(), and now the requirements report derive from the table. - LLM gateways (LiteLLM, OpenRouter, Cloudflare AI Gateway): virtual keys, per-key metering and caps, upstream key swap. OpenRouter demonstrates the pay-with-credits model this proposes.
- Cloud reseller and marketplace APIs: sub-account issuance, scoped keys, revocation, usage reporting keyed by sub-credential.
Unresolved questions
- Identity and ledger. Which ix platform service owns user login and the credit balance, and is Stripe the merchant of record?
- First hosted vendor. Exa (trivially proxyable, low risk) or Mixedbread (highest user value here, but needs the provisioner or tenant-rewrite path)?
- Terms. Do Exa’s and Mixedbread’s terms permit platform-mediated resale on one umbrella account, or do they require a partner agreement first?
- Box file format. One JSON file under
~/.config/ix/versus per-service files; and whether the requirements report should also be exposed as an MCP resource so agents can self-diagnose without shelling out.
Future possibilities
- Auto-provisioning on first use: a call whose credential is missing offers the hosted path interactively instead of only printing the remedy.
- Unifying this surface with Plan 0002’s claims, so a fleet VM and a laptop resolve the same requirement through principal-appropriate machinery.
- A fleet-wide
requirementsreport: every host’s missing credentials in one table, via the bundledfleetfan-out.
Claude
Fable
5
(AI
agent)
commissioned
by
andrewgazelka