Skip to main content
Everything Charter reads at run time, in one place: how a pack is given its credential, which parameters change behaviour rather than shape, and what the library does with logging.

configure()

Every shipped pack builds its tools at import, so from charter.packs.gmail import TOOLS works before anything is configured — schemas, llm_schema() and to_json_schema() need no credential. The credential arrives afterwards, through the pack’s configure().
A credential is resolved per request, not captured when the tool is built. That is why configure() reaches tools that already exist, and why a reference taken before configuration keeps working after it. Calling a tool on a pack that has none raises CredentialError naming both the configure() call and the environment variable it would have accepted. It fails locally, before the request.
Three shapes, depending on how the API authenticates.

Bearer packs

gmail, gcalendar, gsheets, gdocs, gdrive, gforms, slack, github, notion, outlook.
Pass any CredentialProvider: a StaticTokenProvider for a script, an OAuth2Client for a process that stays up, a SubjectProvider wrapping one of those per end user.

API-key packs

stripe, linear, firecrawl, granola, tavily, luma. Exactly one of the two: api_key is one account for the whole process, and credential_provider is read on every call, so a SubjectProvider serves each of your users their own key or OAuth token. Both, or neither, raises TypeError; an empty key raises CredentialError.
The key goes out as Authorization: Bearer <key>, except on Linear, which sends a personal key (lin_api_...) bare and anything else — an OAuth access token — as Bearer. Connect your users’ accounts has each pack’s per-user setup.

Shopify

A store and a credential, because the host is a property of the store rather than of the API. shop is the subdomain — "my-store" for my-store.myshopify.com — and the full domain or URL is accepted too; left out, it comes from $SHOPIFY_SHOP. The credential is at most one of client_id with client_secret, a Dev Dashboard app’s pair, which the pack trades for a 24-hour token and renews; access_token, held as given; or credential_provider. Two at once raise TypeError, and so does moving to another store while holding an access_token given for the previous one — a token belongs to the store that issued it, while the client ID and secret mint per store. Nothing changes when a call raises. The token is sent in X-Shopify-Access-Token. shop may also be a function returning the store, called on every request, for an application whose merchants each connect their own. Pair it with a credential_provider that answers per user too; with an access_token it raises TypeError, since that token belongs to one store.

Environment fallbacks

A pack with no configure() call falls back to a documented variable, read on first use. Every variable is read per call, so one set after import is still found. The Google packs take a renewable grant. A raw access token expires in about an hour and nothing renews it, so the six Google packs also look for a grant, tried first:
  1. GOOGLE_TOKEN_FILE — the path of an authorized-user JSON file: the token.json google-auth writes, Hermes Agent’s google_token.json, or gcloud’s application_default_credentials.json. Only client_id, client_secret and refresh_token are read, and ~ is expanded.
  2. GOOGLE_REFRESH_TOKEN with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET — the three values Set up Google ends with.
  3. GOOGLE_ACCESS_TOKEN.
A grant is renewed by one OAuth2Client shared by all six packs, so a call to Gmail and one to Calendar spend one refresh. The file is read per call, and a different grant in it is picked up without a restart. A grant that is named but unusable — a file that cannot be read, an OAuth client file in place of a token, a refresh token without its client — raises a CredentialError saying which, rather than falling through to GOOGLE_ACCESS_TOKEN. The token file holds a refresh token: keep it readable by its owner alone (chmod 600). Charter warns once per process when other users can read it, and never changes it. This is the path the MCP entry point uses, which is why it needs no configuration code. It is one account per process — the shape the MCP spec prescribes for a stdio server, which reads its credentials from the environment. Serving many users means a grant per user, through a SubjectProvider over your own store; persisting a rotated refresh token is an OAuth2Client with on_refresh, in your own process.

Settings that tune behaviour

These change how a request is made rather than what it contains. All are factory parameters unless noted, and each can be overridden per tool.
int
default:"20"
Request timeout in seconds. Per tool: timeout_override.
int
default:"10"
How dead a bearer token must be before the runtime refuses to send it. On oauth_tool_factory only. This is a guard, not a refresh policy: keeping a token fresh is the provider’s job, and OAuth2Client renews 90 seconds early by default. Ten seconds covers clock skew and the request itself — refusing a token the server still calls valid for a minute would be the runtime overruling the server.
Optional[Iterable[int]]
default:"{401}"
Which statuses raise CredentialError rather than APIError. Declare {401, 403} for an API that means “your token” by 403.
Optional[CallSink]
default:"None"
Where finished ToolCall records go. Declared on the factory because observability belongs to the deployment, not to one endpoint. Per tool: on_call_override.
Optional[Dict[str, str]]
default:"None"
Headers sent verbatim on every request — an Azure api-version, a Notion-Version. Not case-converted, absent from the LLM schema, applied after tool input so a schema field cannot overwrite them.
Optional[Dict[str, Any]]
default:"None"
Query parameters sent verbatim on every request. Same three properties.
Optional[Dict[str, Any]]
default:"None"
Body keys sent verbatim. Per tool only: a constant in the body belongs to one operation, the GraphQL query document being the case that motivated it.
KeyCase
default:"\"camel\", \"snake\", \"snake\""
How keys are spelled on the wire. Per tool: body_case_override, query_case_override, path_case_override. Per field: Case. See the cascade.
"json" | "form"
default:"\"json\""
"form" for application/x-www-form-urlencoded. Per tool: body_format_override.
"repeat" | "bracket"
default:"\"repeat\""
How structured query values serialise. Factory-wide; there is no per-tool override.
Nothing here is read from a config file or an environment variable other than the credentials above. A setting is a parameter, in code, at the place the tool is declared.

Logging

The library attaches a NullHandler to the charter logger and configures nothing else for its host: emitting a record is a no-op until the application opts in. Enabling INFO also switches on payload measurement, because something is then listening for the sizes.