Repository navigation
fix: declare the versioned static-key registration in the packaged manifest - #23
Merged
Merged
Conversation
added 8 commits
October 20, 2026 00:15
…nifest The host reads a static export's credential registration from the manifest before it composes the connector, and it refuses a static export that does not declare one. The published release declares only the legacy `credential_mode`, so every host that requires the versioned registration refuses this plugin at discovery time: the enabled kind never resolves. This declares the registration the host holds for the cursorsdk kind - one static API key from the host environment root CURSOR_API_KEY, admitted once per session, in the cursor quota domain - in the packaged manifest, and makes the packager and both verifiers refuse a tree that drops it or names a different registration. The operator-facing verifiers also read the manifest's own identity fields with the exports array removed, because the export declares a nested "version" of its own and a scan of the whole document would read that one instead. The certification gate asserts the registration in the certified manifest, so a certification cannot be cited for a manifest the host would refuse.
…o end The packaged manifest declares a host-environment credential contract (CURSOR_API_KEY, one api_key slot, session_admission), but the connector still accepted a literal api_key in backend YAML, fell back to it when no host secret was present, and silently ignored additional secret slots. The real-host gate then certified the legacy inline mechanism instead of the declared host-secret path. Port the host-side ingress invariants into the connector: strict YAML decoding with a forbidden secret-field scan, exactly one ordered host slot, no YAML key material, no environment or file fallback. The ported regression suite pins absent, duplicate, numbered, unknown, and valid single-slot cases, plus the manifest posture. The certification harness now binds the synthetic host secret through backend_discovery.instances secret_refs and forwards it as CURSOR_API_KEY in the host process environment, so every enabled-backend case proves the host-secret path. A new real-host case asserts a literal api_key is rejected at composition and at serve, naming the field and the approved environment root without carrying key material. Release metadata is honest about what changed: the posture flips to uncertified with the reason, because no host release carrying the v2 credential-contract ABI exists yet. Publication stays blocked until a v2-capable host release exists and the gate is re-run against it.
The gate asserts the host states the approved channel. Current main words it peer-authenticated rather than secure; the assertion follows the wording without weakening what it proves.
The renderer writes host_certification_platforms as null for an uncertified record rather than omitting the key. The PowerShell verifier treated a present key as a present list and iterated one null entry, which fails closed under Set-StrictMode on the missing platform field instead of reporting the uncertified posture. The shell verifier already reads null as absent; the PowerShell twin now does the same.
The manifest dialect changed under an unchanged version. cursorsdk-v0.1.0 is already published with the v1 registration, so an archive built from this source would have claimed the same version as an artifact with a different ABI - the one collision a digest cannot resolve for an operator. Bump the declared and compiled version to 0.2.0 and the tag to cursorsdk-v0.2.0 in release.yaml, the manifest template and the descriptor the host compares against the manifest. The packaging fixtures now read the declared version instead of hardcoding one, so the verifier's identity check cannot drift from release.yaml again. History stays history: the published cursorsdk-v0.1.0 archives, the host v0.1.0 measurement, and the rollback-to-first-release guidance still name 0.1.0.
compatibility.json records the bridge package version next to the plugin version, and the packaged archive check asserts they agree: the companion is shipped, verified and rolled back as part of one artifact, so it cannot advertise a version the release does not have. The Go-side bridge protocol identity (go-cursorsdk/...) is a separate thing and stays as it is.
The decoder accepts canonical api_key, api_key_2..N, and Configure then required exactly one decoded slot. A lone api_key_2 satisfied both, so the connector admitted a binding the contract says cannot exist: the environment root is the bare CURSOR_API_KEY, with no numbered form. Require the single admitted slot to be named exactly api_key and pin it with a negative test. The real-host negative case asserted doctor and serve rejections against pooled output from both invocations, so doctor rejecting correctly could hide a serve that died of something else. Each command's stderr is now checked independently for the connector's forbidden-field classification, the approved root, and the absence of key material.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Standalone Cursor SDK plugin: v2 credential registration, enforced end to end
What this does
The host now requires a versioned credential contract on a static export (
credential_contractv2:class: api_key,source: host_env,env_root: CURSOR_API_KEY,slot: api_key,selection: session_admission,quota_domain: cursor). A host built from currentmainrefuses a manifest without it —backendplugins/catalog: enabled kind unresolved— while the published hostv0.1.0refuses a manifest that has it, so the two dialects are mutually exclusive.This PR declares the contract, makes the connector behave exactly as the declaration says, and stops the release metadata from claiming a certification the changed manifest no longer has.
Changes
1. Manifest registration. The contract is declared in the manifest template, the packager's renderer, both package verifiers, and the certification gate, so a built archive cannot describe a registration the source does not declare.
2. Credential ingress, enforced (F1, R2-F2).
internal/service/credentials.goports the host-side invariants: strict YAML decode with the forbidden secret-field scan (same field list and normalization as the host), orderedapi_key/api_key_2..Ndecode with the host's budgets, exactly one admitted slot, and that slot must be named exactlyapi_key— the environment root has no numbered form, so a loneapi_key_2is rejected rather than treated as a synonym. No YAML key material, no environment or file fallback, no rotation, no pooling. The regression suite deleted with the in-tree module is restored here: absent, duplicate, numbered, unknown, valid single slot, per-session binding, no replay after effects.3. Certification harness, host-secret path (F2). The harness now binds a fixed synthetic key through
backend_discovery.instances.secret_refsand forwardsCURSOR_API_KEYin the host process environment; the inlineapi_keyline is gone, so every enabled-backend case proves the declared path. A real-host negative case asserts a literalapi_keyis rejected — and it asserts each command independently:doctorandservemust each fail with the connector's forbidden-field classification, name the approved root, and carry neither the rejected value nor the bound secret. Pooled output would let one command fail correctly while the other died of something else.4. Release provenance and version identity (F5).
release.yamldeclareshost_certification: uncertifiedwith a reason naming the blocker, and the release workflow refuses to publish an uncertified revision — so this source cannot be published by accident, and the old v0.1.0 certificate is not carried forward as coverage for the v2 manifest. The source is versioned 0.2.0 (release.yaml, manifest template, compiled descriptor) with the private bridge package bumped in lockstep;cursorsdk-v0.1.0is already published with the v1 registration, and two different ABIs must not share a version. Packaging fixtures now read the declared version instead of hardcoding one.5. PowerShell verifier. The renderer writes
host_certification_platformsas JSON null for an uncertified record; the PowerShell verifier treated a present-but-null value as a present list and failed underSet-StrictMode. It now reads null as absent, matching the shell verifier.Evidence
b7c6878: all 7 lanes pass — Go (released contracts only), Go race, native Windows full suite, macOS development evidence, both native package lanes, Node bridge.main@bc0ddabfplus the extracted Configure-rejection fix (#1403, host revision39287274, binary sha256b1b4b1a9b53b785290f7ed5063747db351b36eceb64837ad3139224c7480c716), against a tree rendered from this branch atb7c6878(manifest0.2.0, contract v2). The run includes discovery, trust, manifest identity, inactive discovery, negotiation, inventory, canonical execution, explicit refusal, multi-user denial, all three prerequisite failures, the per-command inline-key rejection, the companion contract, and the unchanged-tree check. This is linux/amd64 evidence on this machine; Windows is covered by the release workflow's own leg.ef784ffb(sha2564baa163f…), notmain. That host contained the fix, which is exactly why the fix was extracted into #1403 before a release is cut frommain.go test ./...green;go vet ./...clean.Not in this PR
secret_refsremains the explicit binding an operator writes; the docs, config sample, and example now say so. Auto-provisioning agent runtimes from the manifest would change shared composition and billing-adjacent secret flow; that needs its own decision.Ordering
Merge this (it publishes nothing) → merge #1403 → release the v2-capable host from
mainwith the in-tree Cursor fallback retained → certify and publish plugin0.2.0against those artifacts → the host removal merges and task 6.3 closes.Watch item: the shared-conformance cancellation case (
transport_death) failed twice and passed twice on identical code across earlier runs; the path is the pinned rc.1 adapter and the untouched run stream, and it is green at this head.