Reference
alter verify
Verify the implementation matches the ALTER_INTEGRATION.md design.
alter verify checks that the code in a repository actually matches its design document — locally, against the real files and the installed SDK, so the check cannot drift from the SDK version the project uses. It is CI-friendly: one command, one exit code. The offline gates report 0 pass, 1 violations, 4 design file missing; the authenticated checks (--key, --runtime) add the standard CLI codes, including 2 when no app resolves and 3 when not signed in. See exit codes before branching on them.
alter verify [--design <path>] [--dir <path>] [--exclude <pattern>]... [--skip-sdk-check] [--allow-no-sdk-usage] [--key <key-id>] [--runtime] [--grant <grant-id>] [--trace <trace-id>] [--agent <agent-id>] [--app <app-id-or-name>] [--output json|jsonl|table]What runs, in order:
- Design re-validation — the full
alter design validaterule set runs again (deterministic, so re-running beats trusting a prior pass). - Implementation scan — SDK-touching (see What is scanned)
.py,.ts,.tsx,.mts,.cts,.js,.jsx,.mjs, and.cjsfiles are linted: the constructed client class must match the design’sruntime_identity(anAgentclient in an app design fails; in an agent design, see App bootstrap code in an agent design); impersonation (get_agent) in agent designs fails; creating an agent-bound managed-secret grant from the SDK fails (the platform rejects it — usealter managed-secrets grants create … --principal-type agent); hardcoded key literals fail;user_tokenusage withoutidp: jwt-resolutionwarns. A repository with no SDK-touching files fails, because the scan verified nothing — pass--allow-no-sdk-usageto downgrade that to a warning while the SDK is still being wired. - Provider request-shape lint — request bodies aimed at documented provider endpoints are checked against a generated contract built from pinned OpenAPI request-schema projections sourced from the providers’ official references/specs (for example, Anthropic’s Messages API takes
max_tokens, nevermaxTokens, and requiresmax_tokens,messages, andmodel; Slack’schat.postMessagerequireschannel). The package test fails if the generated contract is stale. The body is forwarded to the provider verbatim, so this is the only pre-runtime catch for wrong wire-format fields. - Installed-SDK symbol check — every symbol imported from the Alter SDK must exist in the version actually installed in the project (catches hallucinated APIs at the source).
- Scope least-privilege (optional,
--key) — infers the runtime scopes the scanned code actually needs and compares them with the minted key’s real scopes: a wildcard scope fails, missing scopes fail (with the exact rotate command), unused scopes warn. Requiresalter auth login. - Runtime attribution (optional,
--runtime) — after the first real call, the audit attribution must match the design’s identity (agent designs must attribute to the agent via--agent; app designs must be app-attributed). Pass--grant <grant-id>(or--trace <trace-id>) to check the exact verifying call deterministically — the latest audit row for that grant/trace. Without--grant/--tracethe check samples recent history instead, which cannot isolate the verifying call and races concurrent traffic, so it emits warnings rather than failing. Requiresalter auth login.
| Flag | Default | Description |
|---|---|---|
--design <path> | ./ALTER_INTEGRATION.md | Path to the design document (relative to --dir). |
--dir <path> | . | Repository root to scan. |
--exclude <pattern> | — | Leave matching paths out of the scan; repeatable. A .gitignore-style pattern relative to --dir, for example tests/fixtures/ or **/*.generated.ts. A leading / or ./ anchors it to --dir (/gen and ./gen match gen but not src/gen); a pattern is never a filesystem path, and the disk is consulted only for --dir’s real path. An absolute path naming --dir or a path under it, compared after collapsing //, . and .. with --dir as given, with its real path and, for a relative or omitted --dir, with the same path under the shell’s $PWD (which may pass through a symbolic link), exits 2, naming the pattern to write when there is one (/gen for /home/me/app/gen with --dir /home/me/app); write ./home/me/app/gen for a pattern that only looks like one. Trailing spaces are dropped unless escaped (\ ), as in .gitignore; leading spaces are kept. An empty pattern, one with a . or .. segment, one containing a line break, one starting with # or ! (write \# or \! for a name that starts with one), on Windows a drive or UNC path (C:\gen, C:/gen, \\server\share; elsewhere these are patterns that can match), or one ending in an unescaped \, also before a final / (git never matches it), exits 2. Use it for files kept deliberately unparseable (fixtures, templates). See What is scanned. |
--skip-sdk-check | off | Skip the installed-SDK symbol check — before dependencies are installed, and on any repository you do not trust: that check runs the project’s own Python environment and resolves its node_modules, which executes repository code. The agent-design App analysis still runs, in an isolated interpreter that executes no repository code. |
--allow-no-sdk-usage | off | Downgrade the zero-SDK-files finding to a warning. By default a repository with no SDK-touching files fails, because the implementation scan verified nothing. |
--key <key-id> | — | Least-privilege check: compare this key’s scopes against what the code requires. |
--runtime | off | Run the audit-attribution check (authenticated; run after the first real call). |
--grant <grant-id> | — | With --runtime: check the attribution of this grant’s latest audit row, deterministically (no recent-history sampling). The grant_id the SDK returned from connect. |
--trace <trace-id> | — | With --runtime: pin the single call by distributed-trace id — the most precise filter. |
--agent <agent-id> | — | The agent the audit rows should attribute to. Required with --runtime for agent designs. |
--app <app-id-or-name> | — | App the authenticated checks run against. Falls back to ALTER_APP_ID or the workspace pin (alter link); --key and --runtime exit with a usage error if no app resolves. |
--output <format> | json | json, jsonl, or table. Because the report is one object, jsonl falls back to pretty-printed JSON. |
JSON output reports status, files_scanned, excluded (what the scan left out: gitignore, exclude and virtualenv lists of paths relative to --dir, a directory ending in / and listed once, . when --dir itself is ignored, sorted, each with a gitignore_count, exclude_count or virtualenv_count and a gitignore_truncated, exclude_truncated or virtualenv_truncated flag that is true when the list stops at its first 200 entries; table output prints the three counts on stderr), required_scopes (the inferred minimal set), app_bootstrap_files (the files whose App use was classified operator/bootstrap-only in an agent design), and each finding’s gate (A design, B implementation, C runtime), rule, severity, file, line, and message.
App bootstrap code in an agent design
Section titled “App bootstrap code in an agent design”An agent integration legitimately contains application-side code: the operator step that creates the Connect session delegating a user’s account to the agent (create_connect_session(..., agent=...) / createConnectSession({ ..., agent })) and polls it runs with an application key, while the workload itself calls providers through Agent with the agent’s own key. In an agent design the implementation scan accepts an App only in that operator shape.
The source is parsed — the TypeScript parser for .ts/.tsx/.mts/.cts/.js/.jsx/.mjs/.cjs, Python’s own ast for .py — so comments, docstrings and string contents are never read as code. The checks:
-
The SDK’s
AppandAlterFastAPIare found by their imports, not by spelling:- named, aliased and namespace imports:
import { App as Op },import * as sdk,from alter_sdk import App as A,import alter_sdk as sdk; - CommonJS
require(includingconst { App: Op } = require(...)), TypeScriptimport sdk = require(...), andawait import(...)bound to a name; - members of the SDK module:
sdk.App,sdk["App"],getattr(sdk, "App"), and SDK submodules (alter_sdk.fastapi.AlterFastAPI); - project modules that re-export them, followed through every scanned file: TypeScript
export { App } from "@alter-ai/alter-sdk",export * from …,export default App, and Python module-level imports such as a package__init__.pythat importsApp.
An import of a project module counts only when that module actually re-exports the SDK’s
ApporAlterFastAPI(or the SDK module), directly or through further project modules; an import of any other project module is ordinary code. Modules resolve by their exact path — a relative import from the importing file’s directory, a TypeScript/JavaScript alias (see rule 8), a Python absolute import against the repository root,src/, and the importing file’s own package root (the directory Python puts on its import path) — never by a file of the same name elsewhere, so a projectmyapp/logging.pydoes not capture another directory’simport logging.Names are resolved by scope: a parameter or local variable that happens to be called
App(function g(App),def g(App)) is a different variable, and so is an inner function’s ownapp. A name calledAppthat is not the SDK’s (for example a ReactAppcomponent) is not affected. In Python, each function, class body and comprehension resolves a name in its own scope, as Python does: a name that another function imports, or that a later import or assignment rebinds, is not theApp; a class body’s names are not visible to its methods;globalandnonlocalare followed; and within one scope the binding before the use is the one that counts (afortarget, which binds only if the loop runs, does not replace an earlier binding after the loop). When anif/trybinds the name, or the client built from it, two ways and one of them is theApp, it is checked as theApp, and each finding says the name may be theAppon one branch and gives the line of that binding. AnAppclient built in a class body is an error: its reads (C.app,self.app, an importer’sC.app) are not followed, so bind it to a module-level or function-local name. - named, aliased and namespace imports:
-
The SDK’s
Appmay only be constructed. Every reference to it must be the callee of a construction (new App(...)/App(...)), and every reference toAlterFastAPIthe callee of a call. These are errors:- an alias (
const C = App,C = App), a subclass (class B extends App),Reflect.construct(App, …), or passing, returning or comparing the class (isinstance(x, App)); - the SDK module object used as a value (
const s = sdk), or indexed by a computed key (sdk[name],getattr(sdk, name)); - a
require/import()of the SDK that is not a simple binding.
Type-only uses — annotations, TypeScript types and
implements— and pure re-export statements are allowed. - an alias (
-
Construction. An
App(...)must be the initializer of a single named variable:- TypeScript/JavaScript:
const app = new App(...)(alsolet,var,using,await), in a function or at module scope. A module-scope variable may be exported (export const app = ...,export { app },export { app as client },export default app); every project file that imports it, directly or through re-exports, is then checked too. - Python:
app = App(...),app: App = App(...), orwith App(...) as app/async with App(...) as app, in a function or at module level. Every project module that imports a module-level variable (from client import app, orimport clientand thenclient.app) is then checked too. - Anything else is an error: an
Appthat is not bound to a name, assigned to a property or attribute (this.app = new App(...),self.app = App(...)), destructured, part of a tuple target, assigned with:=, built in a class body, or exported throughmodule.exports.
- TypeScript/JavaScript:
-
Every reference to the variable. Every occurrence of that variable’s name in its function (or the whole module, for a module-level variable), including nested functions, and in every project file that imports it, must be one of these uses:
- a direct call of a bootstrap method,
app.<method>(...); - a namespace,
app.<namespace>...; - a read-only property,
app.<property>(not called); - an argument of a bootstrap integration;
- Python only:
with app:/async with app:.
A runtime method, or the
Apphanded to a runtime integration, is reported as application-key escalation, in the file where it happens;get_agent/getAgenton the variable is reported as impersonation, also in the file where it happens, including an importing file that never names the SDK. Any other reference is an error, reported at its line. That covers aliasing, destructuring, computed access (app["request"]), passing theAppto other code, returning it,module.exports = app, using it inside a template-literal or f-string substitution, any member not listed below, andglobal/nonlocal. A variable of the same name declared in an inner scope (a parameter, or a local of a nested function) is a different variable and is not checked. AnAppwith no bootstrap use anywhere is also an error. - a direct call of a bootstrap method,
-
String evaluation. Only in a file where the SDK’s
App,AlterFastAPIor the SDK module is actually bound — imported directly or through a project module that re-exports it — anything that can reach a variable by a name computed at run time is an error. Files that import nothing that carries theAppare not affected.-
TypeScript/JavaScript: the check uses the TypeScript type checker, with the full ECMAScript standard library types (no DOM: a
/// <reference lib>to a library outside ECMAScript, such asdomorwebworker, is read as empty;window,selfandglobalare typed asglobalThis), not the spelling of the code. These are reported:- a call,
new, tagged template,Reflect.apply/Reflect.construct, or.call/.applyinvocation whose target resolves to the globaleval, to a value of typeFunctionConstructor(the globalFunction, however it was reached: an alias, an un-annotatedletassignedFunction,globalThis.globalThis.Function,const g = globalThis; g.eval(...), an element taken out of an array,Map,Object.values(...),Array.from(...)orObject.assign(...),Function || x), or to a value of theFunctioninterface invoked with at least one argument. TheFunctioninterface is the type of every.constructor, sof.constructor("..."),C.constructor(...)andconst C = f.constructor; C("...")are reported when the receiver is a function. A.constructorread from a value that is not a function is that value’s own constructor and is not reported, for examplethis.constructorin a class orFunction.name.constructor; .bindon any of those targets, whatever it binds (Function.bind(null),f.constructor.bind(null));- a call to a variable or parameter annotated with the bare
Functiontype and given arguments (annotate callbacks with their real signature); - calling an element of the global object or of
Function.prototypethrough a key the checker cannot resolve (globalThis[k](...)); evalorFunctionpassed on as a call ornewargument, directly or inside a spread, array or object literal (run(...[eval]),keep({ e: eval }));- a class that extends
Function(or a.constructorof a function): theextendsclause is reported.
Nothing else is evaluation: a bare reference, a comparison,
typeof,instanceof,Function && xoutside a call, or a property read such asFunction.name. An ambient declaration (declare function eval(...),declare const Function: any) does not hide the global. A value typedanyis not checked: an untyped import from another file, the result of a library function typedany(Object.getPrototypeOf(...), a property descriptor’svalue),thisin an unannotated function, or anything cast toanybefore it is called; - a call,
-
Python:
eval,exec,compile,locals,vars,globals,__import__,sys._getframe,inspect.currentframe, andimportlib.import_modulewith a non-literal or SDK argument.
-
-
AlterFastAPI. Every call must passkind="agent"as a string literal, however the class was imported. Withoutkind,AlterFastAPIpicks its client from the key’s prefix (alter_rk_builds anApp,alter_ak_anAgent, and a legacyalter_key_fails), so the code alone does not say it builds anAgent.kind="app", the operator app pattern, is not allowed in an agent design, and akindgiven as a variable cannot be checked. -
Parse errors (rule
app-parse-error). A scanned project source file that does not parse is reported at its line. It is an error when the file binds an SDK name — it imports@alter-ai/alter-sdk/alter_sdk, or a project module that carries theApp— because itsAppuse cannot be checked, and a warning otherwise, since it cannot reach theApp. TypeScript/JavaScript parse-ability is judged by the TypeScript parser bundled with the CLI (the parser recovers, so a broken file’s imports are still read; in a.js/.cjsfile it detects as a script, strict-mode-only rules such as legacy octal0755are not failures); Python’s by the Python interpreter verify chooses (see What runs). A TypeScript/JavaScript file the parser cannot process at all (for example a very deeply nested generated file) is an error. Leave a deliberately broken file, such as a test fixture, a template, or a file in a syntax outside standard TypeScript/JavaScript such as Flow, out of the scan with--excludeor.gitignore. A UTF-8 byte-order mark is ignored. Nothing is rewritten. Each TypeScript/JavaScript file is checked as its own module, so a file’s global declarations (interface Array<T>,/// <reference no-default-lib="true"/>) never change how other files are checked. Which files are parsed: TypeScript/JavaScript files are parsed when at least one scanned TypeScript/JavaScript file contains@alter-ai/alter-sdk, and Python files when at least one scanned.pyfile containsalter_sdk; then every scanned file of that language is parsed, since a project-local module can re-export the SDK. A language with no file naming the SDK cannot use it, and its files are not parsed. If verify’s own analysis fails on a file that parsed, the file gets an error that says it is an internal error inalter verify, not a problem in the file; the run fails closed. The rest of the scan always continues. -
Project modules. Imports resolve by exact path: relative specifiers, tsconfig (or jsconfig)
pathsandbaseUrlfrom the nearest config file above the importing file (following a relativeextends), and the"imports"field of the nearestpackage.json. Python absolute imports resolve against the scanned directory,src/and the importer’s package root. Configuration files are read, never executed. When the project re-exports the SDK, an import through apathsor"imports"alias that names no scanned file is an error, since Gate B cannot see whether it binds theApp.
Not covered. The following are outside this check, which is a guide rather than a sandbox:
- obfuscation beyond the forms above, for example building the module name from strings, iterating the SDK module’s exports, or patching the SDK;
- a tsconfig
extendsthat names a package (@tsconfig/node20): itspathsare not read; - code outside the scanned directory;
- whether Node, a bundler or a given Python version accepts a file the TypeScript parser or the chosen interpreter rejects, or the reverse: parse-ability is that of the bundled TypeScript parser and the chosen Python.
What is scanned
Section titled “What is scanned”.py, .ts, .tsx, .mts, .cts, .js, .jsx, .mjs and .cjs files under --dir, up to 20,000 files, except:
- the directories
node_modules,.git,.venv,venv,dist,build,__pycache__,__pypackages__,.next,.wranglerandcoverage, every directory whose name starts with a dot, and every directory that holds apyvenv.cfg(a Python virtualenv, whatever it is called); - paths matched by
.gitignorefiles, with git’s rules: the one in--dir, nested ones, and those in the directories above--dirup to the repository root, none above it. A repository root is a directory whose.gitis a directory git accepts as a repository (HEAD,objects/andrefs/; not an empty.git/), or a regular file (a symbolic link to one is followed) that starts withgitdir:and names such a directory, relative to the file’s directory or absolute (read as bytes, as git reads it: trailing line breaks are dropped from the whole file, then it ends at the first NUL byte); a.gitfile that cannot be read also counts, as it does for git. If a directory between that root and--diris ignored, nothing is scanned. A nested repository is scanned with its own.gitignorefiles only. Patterns match file names as UTF-8 bytes, as git’s do (?matches one byte, so not a two-byteé). When the repository’score.ignorecaseistrue, ASCII letters match case-insensitively, as in git (Éandéstay distinct). Patterns match as git 2.52 and later matches them, whatever git is installed: a**crosses directories only at the start of a pattern or after a/, soq**/rmatchesqx/rbut notqr,q/x/rorqx/y/r(git before 2.52 also matched those three). A.gitignorethat is a symbolic link or not a regular file, or is larger than 100 MiB (git’s limit), is not read. Below every.gitignore, the repository’s.git/info/excludeand the excludes file its git configuration names incore.excludesFile(else git’s default,$XDG_CONFIG_HOME/git/ignore) apply, as in git; the configuration is read from the system, global and repository config files (honouringGIT_CONFIG_GLOBAL,GIT_CONFIG_SYSTEMandGIT_CONFIG_NOSYSTEM), without followingincludedirectives; - paths matched by an
--excludepattern (.gitignoresyntax, relative to--dir), which win over a.gitignore!re-include.
The walk does not follow symbolic links, to files or to directories, so it never leaves --dir.
What runs
Section titled “What runs”The scan never executes repository code:
- TypeScript: the parser is bundled with the CLI.
- Python: the interpreter decides whether a file parses, so verify chooses one for the version the project targets:
requires-pythoninpyproject.toml(or Poetry’spythondependency), or.python-version, read from--dirand its parents up to the repository root. Candidates, in order: the interpreter the project’s virtualenv (.venv/venv) links to, thenpython3andpythononPATH. The first at least as new as the target runs; with no target, the first found. A candidate inside the scanned directory runs as the real interpreter it links to, and is skipped when that is inside the directory too. When every candidate is older than the target, the newest runs, verify warns (rulepython-version), and a file it cannot parse is reported as not checked rather than as broken. Python 3.9 (thepython3macOS ships) through 3.14 are supported. It runs in isolated mode (-I -S: no user or site packages, no.pthfiles, noPYTHON*variables, nothing from the working directory on the import path), from a temporary directory outside the repository, with the sources passed on standard input; the virtualenv’s packages are never loaded. If the analysis cannot run, every Python file that namesalter_sdkgets an error that says which case occurred: no interpreter found, the interpreter failed (with its exit status), its output was unreadable, or the analysis did not finish within 30 seconds.--skip-sdk-checkdoes not skip this analysis. - The installed-SDK symbol check is different, and is skipped by
--skip-sdk-check. It imports the installedalter-sdkwith the project’s own interpreter (.venv/venv, elsepython3, elsepython) and therefore that environment’s site packages. It runs with-Ifrom a temporary directory, so repository files cannot shadow the standard library, but it does execute the project’s environment. Use--skip-sdk-checkwhen verifying a repository you do not trust.
Files whose App construction passes these checks are listed in app_bootstrap_files.
camelCase names are the TypeScript SDK, snake_case the Python SDK.
- Bootstrap methods:
createConnectSession/create_connect_session,createManagedSecretConnectSession/create_managed_secret_connect_session,createConnectSessionForError/create_connect_session_for_error,pollConnectSession/poll_connect_session,connect,createAuthSession/create_auth_session,pollAuthSession/poll_auth_session,authenticate,verifyUserToken/verify_user_token,resolveIdentity/resolve_identity,assertIdentity/assert_identity,listGrants/list_grants,revokeGrant/revoke_grant,revokeDelegation/revoke_delegation,mintGrant/mint_grant,createManagedSecretGrant/create_managed_secret_grant,getApprovalStatus/get_approval_status,awaitApproval/await_approval,close. - Namespaces:
agents,keys,scopes,spans,oauthProviders/oauth_providers,providerSpecs/provider_specs. - Read-only properties:
actorId/actor_id,lastRetryInfo/last_retry_info,lastRateLimit/last_rate_limit,baseUrl/base_url. - Bootstrap integrations (they use the
Apponly for sign-in and Connect sessions):AlterAuthProvider. - Runtime methods (reported as application-key escalation):
request,proxyRequest/proxy_request,withConstraints/with_constraints,boto3_client. - Runtime integrations (they make provider calls with the
App):AlterMCP,AlterContext,alter_tool.
Runtime attribution (--runtime --grant <grant-id> --agent <agent-id>) remains the check that the actual provider call ran under the agent.
The verification failure status is also used for invalid authenticated-check
identifiers: a malformed --key, a malformed --grant used with
--runtime, or a missing/malformed --agent required by an agent-runtime
design exits with code 1.
Run it in CI
Section titled “Run it in CI”alter verify is built for CI: it reads the committed ALTER_INTEGRATION.md and the working tree, prints JSON by default, and exits non-zero on any violation — so the design document stays continuously validated instead of rotting after onboarding. A minimal GitHub Actions job:
alter-verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm install -g @alter-ai/cli - run: npm ci # or pip install -r requirements.txt — the SDK must be installed for the symbol check - run: alter verifyThe offline gates need no credentials. To add the authenticated checks (--key, --runtime), expose a CI-scoped PAT from your secret store as the ALTER_PAT environment variable (the CLI reads it ahead of any stored login, so no alter auth login step is needed) and set ALTER_APP_ID to the app’s ID. CI has no workspace pin: alter link adds .alter/ to .gitignore, so the pin is never committed. Or keep CI offline-only and run the authenticated checks locally.
When a package import has multiple conditional targets and the project re-exports the SDK, verification cannot prove which App binding runs. It reports the ambiguity; use a relative import to identify the intended module. Extensionless JSX re-exports are included in the analysis.