# Packages and SDKs

> Hand-written calls through ctx.fetch are the default. When Graft's model reaches for a vendor SDK, the SDK is bound to the proxy and the package must clear the package policy.

Source: https://docs.getgraft.ai/graft/toolbox/packages-and-sdks

The default authored tool makes its calls by hand: `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 about `ctx.fetch`. So the module constructs it with the proxy as its base and the capability token as its credential:

```ts
import { Octokit } from "@octokit/rest";

export default async (input: Input, ctx: Context) => {
  const octokit = new Octokit({ auth: ctx.proxyKey, baseUrl: ctx.proxyBase() });
  const { data } = await octokit.repos.get({ owner: input.owner, repo: input.repo });
  return data;
};
```

`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.

> **Note**
>
> 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 while `acquire` 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:

```json
{
  "dependencies": {
    "@octokit/rest": "21.1.1"
  }
}
```

The publish refuses anything else that could steer the install: a range or a tag instead of an exact version, any other dependency section (`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:

1. **A valid name and an exact version**, or the module is asked to fix them.
2. **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`, `twilio` and `@aws-sdk/*`.
3. 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.

A typosquat is a young package, which is why age is counted from the package's first publish and not the version's. Every failing package is reported at once, so the model fixes the manifest in one edit. A registry that cannot be reached is its own diagnostic, `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](https://docs.getgraft.ai/graft/self-hosting/configuration.md).

## Prompts to try

```text
Acquire a tool that lists my open GitHub issues; use the official Octokit SDK if it helps.
```

```text
Build a Linear tool that creates an issue in the Cando project. Raw calls are fine.
```

```text
Why was the package you tried refused, and what did you use instead?
```

## Where next

- [The proxy and sandboxes](https://docs.getgraft.ai/graft/concepts/the-proxy-and-sandboxes.md): The one route out of a sandbox, and what the proxy refuses.
- [How credentials are protected](https://docs.getgraft.ai/graft/security/how-credentials-are-protected.md): Why a module holds nothing longer-lived than its run.
