adkit drives a live ad account and can spend money, so safety is a first-class design goal, not an afterthought. This document describes the threat model, the controls that back it, and how to report a problem.
adkit assumes:
- You trust the machine it runs on. It reads your credentials from a local
.envand calls the Meta and Google APIs as you. It is not a multi-tenant service and does not sandbox the host. - You may not fully trust the agent driving it. Because adkit exposes an MCP server, an LLM agent (or a prompt injection reaching one) can call its tools. The controls below assume the caller can be careless or adversarial.
The assets worth protecting are your access token, your ad budget, and the files on your machine.
- Every credential is read from the environment via a local
.env, never hardcoded (adkit/config.py)..envis gitignored; only.env.example(placeholders) is tracked. - The Gemini key is passed to the gemskills subprocess through the environment
at call time (
adkit/creative_gen.py), never written to a file adkit creates.
- The Meta token is sent in the
Authorization: Bearerheader, never as a query parameter (adkit/graph.py). URLs are what leak into exception messages, proxy logs, and CI output, so keeping the token out of them removes that class of leak. - Every string that could carry the token (a network-error message, a non-JSON
body) is scrubbed to
<redacted-token>before it is raised, and the original exception is dropped (raise ... from None) so it cannot be chained into a traceback.adkit verifyreports validity, expiry, and scopes, never the token.
- Campaigns, ad sets, and ads are always created PAUSED. Delivery starts
only on an explicit
adkit ad activate. adkit automate launchis a dry run until--go.- Over MCP, the actions that spend money or go live (
activate_ad,generate_image,generate_video,launch_brief(go=True)) are refused unless the operator setsADKIT_ALLOW_SPEND=1in the server environment. This is enforced server-side, so an auto-approving client or a prompt injection cannot bypass it with a persuasive tool call. ADKIT_GENERATION_DAILY_CAP_USDsets a hard per-day ceiling on AI generation spend, enforced across both the CLI and MCP.
- The MCP
generate_*andcreate_ad_from_imagetools resolve every path against the creatives working directory (ADKIT_CREATIVE_DIR, default./creatives) and reject anything that escapes it. This blocks an agent from writing arbitrary files or reading and uploading/etc/passwd.
- Runtime dependencies are
requestsandclick(plus optionalpyyamlandmcp). They are version-floored, not hash-pinned; for a locked, auditable install, generate a lock file (pip-compileoruv pip compile) and install from it. - AI creative generation executes third-party code.
adkit/creative_gen.pyshells out to the gemskills toolkit via thebunruntime. That code runs with your privileges and your Gemini key. Only pointGEMSKILLS_ROOTandBUNat binaries you trust, and treat a gemskills update like any other dependency bump. adkit invokes it with an argument list (never a shell), so a creative prompt cannot inject shell commands, but the toolkit itself is still code you are choosing to run.
git grep -nE 'EAA[A-Za-z0-9]{20,}|AIza[A-Za-z0-9_-]{20,}' -- . ':!*.example' || echo "clean"EAA... is the shape of a Meta token, AIza... a Google API key. If either
matches outside an example file, remove it and rotate the credential.
Open a private security advisory on the repository, or email the maintainers. Please do not file public issues for suspected vulnerabilities.