Skip to content

fix: declare the versioned static-key registration in the packaged manifest - #23

Merged
matdev83 merged 8 commits into
mainfrom
feat/credential-contract-v2
Oct 23, 2026
Merged

matdev83 merged 8 commits into
mainfrom
feat/credential-contract-v2

Conversation

@matdev83

@matdev83 matdev83 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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_contract v2: class: api_key, source: host_env, env_root: CURSOR_API_KEY, slot: api_key, selection: session_admission, quota_domain: cursor). A host built from current main refuses a manifest without it — backendplugins/catalog: enabled kind unresolved — while the published host v0.1.0 refuses 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.go ports the host-side invariants: strict YAML decode with the forbidden secret-field scan (same field list and normalization as the host), ordered api_key/api_key_2..N decode with the host's budgets, exactly one admitted slot, and that slot must be named exactly api_key — the environment root has no numbered form, so a lone api_key_2 is 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_refs and forwards CURSOR_API_KEY in the host process environment; the inline api_key line is gone, so every enabled-backend case proves the declared path. A real-host negative case asserts a literal api_key is rejected — and it asserts each command independently: doctor and serve must 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.yaml declares host_certification: uncertified with 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.0 is 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_platforms as JSON null for an uncertified record; the PowerShell verifier treated a present-but-null value as a present list and failed under Set-StrictMode. It now reads null as absent, matching the shell verifier.

Evidence

  • CI at b7c6878: all 7 lanes pass — Go (released contracts only), Go race, native Windows full suite, macOS development evidence, both native package lanes, Node bridge.
  • Real-host certification gate: 14/14, against a host binary built from main@bc0ddabf plus the extracted Configure-rejection fix (#1403, host revision 39287274, binary sha256 b1b4b1a9b53b785290f7ed5063747db351b36eceb64837ad3139224c7480c716), against a tree rendered from this branch at b7c6878 (manifest 0.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.
  • Earlier run corrected: the previously reported pass used this repository's host-removal branch tip ef784ffb (sha256 4baa163f…), not main. That host contained the fix, which is exactly why the fix was extracted into #1403 before a release is cut from main.
  • Local go test ./... green; go vet ./... clean.

Not in this PR

  • The host panic fix — extracted to #1403 so the release line can carry it independently of the blocked removal PR.
  • Host provisioning behavior (F3) — secret_refs remains 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.
  • ARM64 — the in-tree manifest declared it and no assembled or certified standalone artifact ever covered it. The host spec records it as a withdrawn unverified claim needing owner sign-off, not an approved reduction.
  • Publication — blocked until a host release carrying the v2 ABI exists; the gate is then re-run against those exact published binaries on both declared platforms.

Ordering

Merge this (it publishes nothing) → merge #1403 → release the v2-capable host from main with the in-tree Cursor fallback retained → certify and publish plugin 0.2.0 against 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.

AIProxer Delivery 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.
@matdev83
matdev83 merged commit 4a1462c into main Oct 23, 2026
7 checks passed
@matdev83
matdev83 deleted the feat/credential-contract-v2 branch October 23, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant