Skip to content

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:

  1. Design re-validation — the full alter design validate rule set runs again (deterministic, so re-running beats trusting a prior pass).
  2. Implementation scan — SDK-touching (see What is scanned) .py, .ts, .tsx, .mts, .cts, .js, .jsx, .mjs, and .cjs files are linted: the constructed client class must match the design’s runtime_identity (an Agent client 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 — use alter managed-secrets grants create … --principal-type agent); hardcoded key literals fail; user_token usage without idp: jwt-resolution warns. A repository with no SDK-touching files fails, because the scan verified nothing — pass --allow-no-sdk-usage to downgrade that to a warning while the SDK is still being wired.
  3. 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, never maxTokens, and requires max_tokens, messages, and model; Slack’s chat.postMessage requires channel). 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.
  4. 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).
  5. 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. Requires alter auth login.
  6. 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/--trace the check samples recent history instead, which cannot isolate the verifying call and races concurrent traffic, so it emits warnings rather than failing. Requires alter auth login.
FlagDefaultDescription
--design <path>./ALTER_INTEGRATION.mdPath 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-checkoffSkip 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-usageoffDowngrade 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.
--runtimeoffRun 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>jsonjson, 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.

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:

  1. The SDK’s App and AlterFastAPI are 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 (including const { App: Op } = require(...)), TypeScript import sdk = require(...), and await 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__.py that imports App.

    An import of a project module counts only when that module actually re-exports the SDK’s App or AlterFastAPI (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 project myapp/logging.py does not capture another directory’s import 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 own app. A name called App that is not the SDK’s (for example a React App component) 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 the App; a class body’s names are not visible to its methods; global and nonlocal are followed; and within one scope the binding before the use is the one that counts (a for target, which binds only if the loop runs, does not replace an earlier binding after the loop). When an if / try binds the name, or the client built from it, two ways and one of them is the App, it is checked as the App, and each finding says the name may be the App on one branch and gives the line of that binding. An App client built in a class body is an error: its reads (C.app, self.app, an importer’s C.app) are not followed, so bind it to a module-level or function-local name.

  2. The SDK’s App may only be constructed. Every reference to it must be the callee of a construction (new App(...) / App(...)), and every reference to AlterFastAPI the 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.

  3. Construction. An App(...) must be the initializer of a single named variable:

    • TypeScript/JavaScript: const app = new App(...) (also let, 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(...), or with 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, or import client and then client.app) is then checked too.
    • Anything else is an error: an App that 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 through module.exports.
  4. 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 App handed to a runtime integration, is reported as application-key escalation, in the file where it happens; get_agent / getAgent on 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 the App to other code, returning it, module.exports = app, using it inside a template-literal or f-string substitution, any member not listed below, and global / 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. An App with no bootstrap use anywhere is also an error.

  5. String evaluation. Only in a file where the SDK’s App, AlterFastAPI or 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 the App are 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 as dom or webworker, is read as empty; window, self and global are typed as globalThis), not the spelling of the code. These are reported:

      • a call, new, tagged template, Reflect.apply / Reflect.construct, or .call / .apply invocation whose target resolves to the global eval, to a value of type FunctionConstructor (the global Function, however it was reached: an alias, an un-annotated let assigned Function, globalThis.globalThis.Function, const g = globalThis; g.eval(...), an element taken out of an array, Map, Object.values(...), Array.from(...) or Object.assign(...), Function || x), or to a value of the Function interface invoked with at least one argument. The Function interface is the type of every .constructor, so f.constructor("..."), C.constructor(...) and const C = f.constructor; C("...") are reported when the receiver is a function. A .constructor read from a value that is not a function is that value’s own constructor and is not reported, for example this.constructor in a class or Function.name.constructor;
      • .bind on 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 Function type and given arguments (annotate callbacks with their real signature);
      • calling an element of the global object or of Function.prototype through a key the checker cannot resolve (globalThis[k](...));
      • eval or Function passed on as a call or new argument, directly or inside a spread, array or object literal (run(...[eval]), keep({ e: eval }));
      • a class that extends Function (or a .constructor of a function): the extends clause is reported.

      Nothing else is evaluation: a bare reference, a comparison, typeof, instanceof, Function && x outside a call, or a property read such as Function.name. An ambient declaration (declare function eval(...), declare const Function: any) does not hide the global. A value typed any is not checked: an untyped import from another file, the result of a library function typed any (Object.getPrototypeOf(...), a property descriptor’s value), this in an unannotated function, or anything cast to any before it is called;

    • Python: eval, exec, compile, locals, vars, globals, __import__, sys._getframe, inspect.currentframe, and importlib.import_module with a non-literal or SDK argument.

  6. AlterFastAPI. Every call must pass kind="agent" as a string literal, however the class was imported. Without kind, AlterFastAPI picks its client from the key’s prefix (alter_rk_ builds an App, alter_ak_ an Agent, and a legacy alter_key_ fails), so the code alone does not say it builds an Agent. kind="app", the operator app pattern, is not allowed in an agent design, and a kind given as a variable cannot be checked.

  7. 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 the App — because its App use cannot be checked, and a warning otherwise, since it cannot reach the App. 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 / .cjs file it detects as a script, strict-mode-only rules such as legacy octal 0755 are 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 --exclude or .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 .py file contains alter_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 in alter verify, not a problem in the file; the run fails closed. The rest of the scan always continues.

  8. Project modules. Imports resolve by exact path: relative specifiers, tsconfig (or jsconfig) paths and baseUrl from the nearest config file above the importing file (following a relative extends), and the "imports" field of the nearest package.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 a paths or "imports" alias that names no scanned file is an error, since Gate B cannot see whether it binds the App.

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 extends that names a package (@tsconfig/node20): its paths are 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.

.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, .wrangler and coverage, every directory whose name starts with a dot, and every directory that holds a pyvenv.cfg (a Python virtualenv, whatever it is called);
  • paths matched by .gitignore files, with git’s rules: the one in --dir, nested ones, and those in the directories above --dir up to the repository root, none above it. A repository root is a directory whose .git is a directory git accepts as a repository (HEAD, objects/ and refs/; not an empty .git/), or a regular file (a symbolic link to one is followed) that starts with gitdir: 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 .git file that cannot be read also counts, as it does for git. If a directory between that root and --dir is ignored, nothing is scanned. A nested repository is scanned with its own .gitignore files only. Patterns match file names as UTF-8 bytes, as git’s do (? matches one byte, so not a two-byte é). When the repository’s core.ignorecase is true, 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 /, so q**/r matches qx/r but not qr, q/x/r or qx/y/r (git before 2.52 also matched those three). A .gitignore that 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/exclude and the excludes file its git configuration names in core.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 (honouring GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM and GIT_CONFIG_NOSYSTEM), without following include directives;
  • paths matched by an --exclude pattern (.gitignore syntax, 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.

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-python in pyproject.toml (or Poetry’s python dependency), or .python-version, read from --dir and its parents up to the repository root. Candidates, in order: the interpreter the project’s virtualenv (.venv / venv) links to, then python3 and python on PATH. 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 (rule python-version), and a file it cannot parse is reported as not checked rather than as broken. Python 3.9 (the python3 macOS ships) through 3.14 are supported. It runs in isolated mode (-I -S: no user or site packages, no .pth files, no PYTHON* 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 names alter_sdk gets 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-check does not skip this analysis.
  • The installed-SDK symbol check is different, and is skipped by --skip-sdk-check. It imports the installed alter-sdk with the project’s own interpreter (.venv/venv, else python3, else python) and therefore that environment’s site packages. It runs with -I from a temporary directory, so repository files cannot shadow the standard library, but it does execute the project’s environment. Use --skip-sdk-check when 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 App only 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.

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 verify

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

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.