Repository navigation
Add a Cursor SDK harness adapter - #72
Closed
jacobprall wants to merge 3 commits into
Closed
jacobprall wants to merge 3 commits into
jacobprall wants to merge 3 commits into
Conversation
An agent now names a harness value built by an adapter in app/harness/ from its library's own configuration, instead of a model tier string. The pi adapter takes a typed model from Pi's catalog, so a mistyped model ID fails typecheck. config.models, ModelTier, and modelFor are gone; the built-in choices live in app/agents/models.ts. A harness is the runtime the runner calls in the Sandbox (run and selfTest, app/runner/core/harness.ts) plus what the host needs before a run (a label and the credential env var, app/harness/types.ts). What a harness must do beyond those signatures, such as giving the model only the factory's tools and submit, is pinned by the conformance suite. The runner is split into a library-neutral core (versioned contract, emitter, submit checks, input parsing, keys), shared factory tools with a library-neutral view, and the pi loop. Agent definitions are bundled into the runner, which runs the agent named in its input on that agent's harness. Each Sandbox replaces a snapshot's runner bundle with this deploy's when their hashes differ, before verify.sh, so harness and model changes need no snapshot refresh. Keys come from the harnesses: runAgent, doctor, and the workflows env group derive them from each harness's credential. agent.finished records the model the runner reports. Tests run a conformance suite over the runner contract against the pi harness and a scripted harness with no agent library, and check that the bundle imports no host-only code. Refs render-examples#67
pydantic({ module, credential }) runs an agent on a Pydantic AI agent
built in python/agents/<module> and handed over with serve(agent). The
runner starts python -m factory_harness as the runner user and passes
the input and key on stdin; the wrapper makes itself non-dumpable before
reading the key and sets it under the credential's name for the
provider. The factory supplies the prompt as run instructions, the
submit output tool checked like core/submit.ts, the turn and time
limits, and its own tools: runner.mjs --mcp-tools serves exactly the
agent's factory tools over MCP stdio, with the same environment and
read-only policy as pi. Events are revalidated before they're forwarded,
and the self-test reports the installed Pydantic AI version.
A harness declares its Sandbox needs with provision (app/harness/types.ts):
files shipped beside the bundle and a root command keyed by a hash. The
runner bundle check becomes a payload check (bundle plus provisioned
files), swapped in root-owned when it differs, and provisions run at
snapshot build and on open before the self-test. The pydantic adapter
provisions its Python sources and builds a root-owned venv from
hash-pinned requirements with the base image's python3 and pip.
createEmitter's terminated getter was copied as a value by Object.assign
and always read false; it now reflects the stream.
The conformance suite runs the pydantic harness when python/.venv
exists, and CI gets a Python job.
Refs render-examples#69
cursor({ model }) runs an agent on a local Cursor agent inside the
Sandbox, with CURSOR_API_KEY as its credential, delivered on stdin.
Cursor's own tools are off (tools: ["mcp"]) and so are ambient settings
from the checkout (settingSources: []): the model gets the factory's
five tools and submit as custom tools, so tool authority and the
read-only shell policy match pi and Pydantic AI. The cloud runtime isn't
offered.
Replacing Cursor's system prompt is enabled per account, so by default
the agent's prompt leads the first message; replaceSystemPrompt opts in.
Turns are counted per tool batch; a run stops at max_turns, at its time
budget, after the last rejected submission, or once work continues past
an accepted one. The self-test and started event report the installed
SDK version.
The SDK's flat build doesn't load on Node and its default build can't be
single-file bundled, so the runner bundle leaves @cursor/sdk external and
the adapter provisions it: cursor/package-lock.json ships with the
payload and npm ci installs it root-owned at APP_DIR/cursor-sdk.
Cursor's models can't be scripted, so the adapter's contract tests run
live on request (CURSOR_LIVE=1).
Refs render-examples#68
jacobprall
force-pushed
the
harness-cursor
branch
from
October 27, 2026 23:10
7fd632f to
5c60019
Compare
Collaborator
Author
|
Consolidated into #73. |
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.
Fixes #68. Depends on #71, which depends on #70; their commits are included until they merge. Review the last commit.
cursor({ model: { id: "composer-2.5" } })runs an agent on a local Cursor agent inside the Sandbox, with Cursor-hosted models andCURSOR_API_KEYdelivered on stdin.Design
tools: ["mcp"]). The model gets the factory's five tools andsubmitas custom tools, the only MCP server it sees. So tool authority and the read-only shell policy match pi and Pydantic AI. This differs from Add a Cursor SDK harness adapter #68's plan, which kept Cursor's native file tools: the SDK has no nativewrite, and the extension runbook in Run agents on imported harness adapters #70 says a harness offers only factory tools.settingSources: []. The spike confirmed that a canary rule, anAGENTS.mdinstruction, and a hook in the checkout's.cursor/all had no effect.replaceSystemPrompt: trueopts in.max_turns, at its time budget, after the last rejected submission, or when work continues past an accepted one. A run that ends without submitting gets one reminder, as on pi.Packaging
In 1.0.40, the SDK's flat build doesn't load on Node: it imports
bun:sqliteand calls Bun-only code. Its default build lazy-loads its own files, so it can't be single-file bundled. So:@cursor/sdkexternal;provision(from Add a Pydantic AI harness adapter #71) shipscursor/package.jsonandcursor/package-lock.json, and runsnpm ciwith the base image's npm into a root-ownedAPP_DIR/cursor-sdk, about 2.5 seconds. The Linux platform package comes from the lockfile.Tests
CURSOR_API_KEYand ships the locked runtime; the pins incursor/package.json,package.json, andCURSOR_SDK_VERSIONagree;--test-stubruns are refused.CURSOR_LIVE=1 CURSOR_API_KEY=… npx vitest run tests/harness-cursor.test.ts. The agent reads a file and submits valid output; a read-only agent'sgit pushis rejected by the factory's policy; no key appears in the output or the tool environment. Both live tests pass on Composer 2.5.Verification
npm run checkpasses: 165 tests, plus 2 live tests skipped by default.Known gaps
costUsdis 0 for Cursor agents. Spend shows in Cursor's dashboard.Deploying
Deploy
factory-workflows, addCURSOR_API_KEYto.env, and runnpm run setup:render; the key is derived from the agents' harnesses. Use a team service-account key with a spend limit. Then refresh snapshots so Sandboxes don't each reinstall the SDK.