ctx.fetch with a vendor-relative path, the proxy adding the host and the credential. An SDK is the last resort, chosen only when the model can say why the raw calls are worse, because every package is attack surface the proxy cannot see. When it does reach for one, two rules apply: the SDK must be bound to the proxy, and the package must clear the package policy at publish.
An SDK reaches a vendor through the proxy or not at all
A vendor SDK knows nothing aboutctx.fetch. So the module constructs it with the proxy as its base and the capability token as its credential:
ctx.proxyBase() is the proxy’s route to the connection’s primary host; ctx.proxyBase("gmail.googleapis.com") names another host the connection declares, which is how one Google connection reaches several Google hosts. ctx.proxyKey is the capability token minted for this run. The proxy reads it from wherever the SDK put it, strips it, and injects the connection’s real credential on the way out. The module never holds anything longer-lived than the run.
The check enforces the binding mechanically, as the rule sdk-not-bound. A client constructed from a package must take ctx.proxyKey in its credential option and a ctx.proxyBase(...) call in its base option, under the option names each SDK uses: apiKey and apiUrl for Linear, auth and baseUrl for Notion and Octokit, apiKey and endpointUrl for Airtable, auth and rootUrl for Google, slackApiUrl for Slack, and the common spellings beside them. Refused, each with the line and a hint: a credential of the module’s own, a base of its own, a base computed at run time, options spread from a variable, an authentication header set by hand, or a construction with no credential slot the check can see. @slack/web-api must also be constructed with allowAbsoluteUrls: false, since it otherwise treats a method name that is an absolute URL as the URL to call.
Two consequences follow. An SDK that hard-codes its host, or speaks gRPC, cannot be bound and cannot be used, so the model writes those calls by hand; Stripe’s SDK has no base-path option, which makes a Stripe tool raw ctx.fetch even though stripe is on the allowlist. And the dry run works unchanged, because the SDK’s calls still cross the proxy, where the dry-run claim is enforced.
The check counts every call into an SDK as a write, because it cannot read the HTTP method inside the package. A tool built on an SDK therefore publishes as not read-only and asks once on its first call, even when it only reads. The dry run shows what it actually sends.
Packages install at publish or never
A sandbox has no npm and no route to the registry; its only egress is the proxy. Packages resolve in a separate install step whileacquire is publishing, the one step that may reach the registry, and the published version carries its own node_modules with exact pinned versions, install scripts disabled and a lockfile of its own. A run installs nothing.
A module declares what it imports under dependencies in its package.json, each at an exact version:
devDependencies, peerDependencies, overrides and the rest), and a lockfile, an .npmrc or a node_modules written into the draft. A bare import the manifest does not declare is refused by the check as import-not-vendored. Every refusal names the file, line and column and says what to change.
The package policy
Each declared package is put to the policy, in this order:- A valid name and an exact version, or the module is asked to fix them.
- On the allowlist of official vendor SDKs, in which case nothing is asked of the registry. The default list is
googleapis,@octokit/rest,@slack/web-api,@linear/sdk,@notionhq/client,airtable,stripe,@hubspot/api-client,@sendgrid/mail,twilioand@aws-sdk/*. - Otherwise known to the registry, carrying an npm provenance attestation, at least 90 days old counted from the package’s first publish, and downloaded at least 1000 times in the last week.
registry-unavailable, to retry rather than rewrite.
A package that fails the policy is a blocked shortcut, not a blocked tool. The model falls back to hand-written calls through ctx.fetch, which it can always do, and the tool ships without the package. The allowlist grows from real acquire runs through a review queue.
Self-hosted Graft tunes all three parts in .env: GRAFT_PACKAGE_ALLOWLIST adds names or @scope/* patterns for your deployment, and GRAFT_PACKAGE_MIN_AGE_DAYS and GRAFT_PACKAGE_MIN_WEEKLY_DOWNLOADS move the thresholds. See Configuration.
Prompts to try
Where next
The proxy and sandboxes
The one route out of a sandbox, and what the proxy refuses.
How credentials are protected
Why a module holds nothing longer-lived than its run.