Reference
alter design
Validate the ALTER_INTEGRATION.md design document before provisioning.
The design document (ALTER_INTEGRATION.md) records how an integration uses Alter — the goal, the runtime identity (app vs agent), the key type, principals, providers, and how grants are acquired — in machine-checkable YAML frontmatter. alter design validate runs a deterministic rule set over it so identity mistakes (for example, an app runtime key serving an agent workload) fail before anything is provisioned. Pair with alter verify, which re-validates the design and then checks the implementation against it.
alter design validate [--design <path>] [--remote] [--app <app-id-or-name>] [--output json|jsonl|table]The design document
Section titled “The design document”ALTER_INTEGRATION.md is a file committed at the repository root. YAML frontmatter carries the machine-checkable fields; the prose below it explains the intent. The Alter Docs MCP server returns a complete, already-valid starter template for each setup flow, so the frontmatter never has to be written from scratch: get_started includes it in the flow plan, and next_step repeats it at the design step.
Each template below is the exact artifact that server returns. Copy the one matching the goal and replace its values.
goal: user-data — call an API as the signed-in user
Section titled “goal: user-data — call an API as the signed-in user”---use_case: "Post as our Slack app bot in a workspace a user connects"goal: user-data # user-data | backend-secret | agentruntime_identity: app # app | agent — WHO makes the provider callkey_type: runtime # runtime (alter_rk_…) | agent (alter_ak_…) — must match runtime_identityprincipals: [user] # user | group | system | agent — never mix user and agent. # Names the Alter grant OWNER, not the upstream token principal.providers: [slack] # the OAuth providers this design callsgrant_acquisition: connect-consent # user-data always acquires grants via browser consentidp: none # none | jwt-resolution (jwt-resolution must be listed in dashboard_steps)dashboard_steps: [] # operator-owned human actions, listed honestlylanguage: typescript # python | typescript — the two languages with SDK wiring---
Why: the signed-in user approves the workspace installation, and Alter bindsthe grant to that user. Slack API calls use the installed Slack app bot; thisflow does not request a Slack user token or post as the human.Here, principals: [user] identifies the Alter grant owner. It does not claim
that every provider issues a human-user token: Slack’s standard Alter adapter
stores an installed app-bot token, while other providers may issue a credential
whose upstream principal is the user. The provider reference is authoritative.
goal: backend-secret — a service holds its own credential
Section titled “goal: backend-secret — a service holds its own credential”---use_case: "Call the Stripe API from our billing worker"goal: backend-secret # user-data | backend-secret | agentruntime_identity: app # app | agent — WHO makes the provider callkey_type: runtime # runtime (alter_rk_…) | agent (alter_ak_…) — must match runtime_identityprincipals: [system] # system | user | group — an agent-bound secret is the agent flowproviders: [] # OAuth providers only; a managed secret is not listed heregrant_acquisition: managed-secret-grant # backend-secret always acquires via a managed-secret grantidp: none # none | jwt-resolution (jwt-resolution must be listed in dashboard_steps)dashboard_steps: [] # operator-owned human actions, listed honestlylanguage: typescript # python | typescript — the two languages with SDK wiring---
Why: the billing worker calls Stripe as itself. There is no end user in theloop, so the credential is a managed secret bound to the workload.goal: agent — an agent calls under its own identity
Section titled “goal: agent — an agent calls under its own identity”---use_case: "Let our support agent read threads available to an installed Slack app bot"goal: agent # user-data | backend-secret | agentruntime_identity: agent # app | agent — WHO makes the provider callkey_type: agent # agent (alter_ak_…) — an agent identity must NOT use an app runtime keyprincipals: [agent] # never mix user and agent on one credential pathproviders: [slack] # the OAuth providers this design callsgrant_acquisition: agent-delegation # agent-delegation | managed-secret-grant (the latter is CLI/portal-only)idp: none # none | jwt-resolution (jwt-resolution must be listed in dashboard_steps)dashboard_steps: [] # operator-owned human actions, listed honestlylanguage: typescript # python | typescript — the two languages with SDK wiring---
Why: the agent acts under its own Alter identity so the audit trail attributeseach call to the agent, not to the application. The delegated Slack credentialstill represents the installed app bot, not the human who approved it.Each template’s inline comments are scoped to its own goal, because the accepted value depends on the goal. The tables below give the full range.
| Field | Required | Accepted values |
|---|---|---|
use_case | yes | Free text — one sentence describing the integration. |
goal | yes | user-data, backend-secret, agent |
runtime_identity | yes | app, agent — which identity makes the provider call. |
key_type | yes | runtime, agent — must match runtime_identity. |
grant_acquisition | yes | connect-consent, managed-secret-grant, agent-delegation |
idp | yes | none, jwt-resolution |
language | yes | python, typescript |
principals | list — required for every goal | user, group, system, agent — user and agent never share one credential path. Each goal constrains the set; see the table below. |
providers | list | OAuth provider ids this design calls. Required for goal: user-data. A managed secret is not listed here. |
dashboard_steps | list | Human, operator-owned steps (for example identity-provider setup). |
Valid goal combinations
Section titled “Valid goal combinations”The goal decides which identity makes the provider call, which key that identity carries, and how the credential is acquired. Every combination outside this table is a rule violation:
goal | runtime_identity | key_type | principals | grant_acquisition |
|---|---|---|---|---|
user-data | app | runtime | must include user | connect-consent |
backend-secret | app | runtime | at least one of system, user, group; never agent | managed-secret-grant |
agent | agent | agent | exactly [agent] | agent-delegation, or managed-secret-grant — the latter is valid but emits a warning, because an agent-bound managed-secret grant can only be created with alter managed-secrets grants create --principal-type agent or the dashboard, never the SDK. Exit code stays 0. |
A user-delegated OAuth grant consumed by an agent is still goal: agent with principals: [agent]. principals records the identity on the runtime credential path, not who consented — the user’s consent is recorded by grant_acquisition: agent-delegation.
validate
Section titled “validate”Validates the design document. Local by default — no network and no login required, so it can gate automation.
Offline exit codes: 0 passed (warnings allowed); 1 rule violations, or a design file that exists but cannot be read; 2 a bad flag or --output value; 4 the design file was not found. Adding --remote can also produce the standard authenticated codes — 3 not signed in, 7 forbidden (e.g. insufficient PAT scope) — plus 5/6 from the backend, and 2/4 when the --remote app itself cannot be resolved or found. See exit codes before branching on them in CI.
| Flag | Default | Description |
|---|---|---|
--design <path> | ./ALTER_INTEGRATION.md | Path to the design document. |
--remote | off | Additionally verify the design’s providers against the app’s actual configuration, when the design lists any. With an empty providers list the remote check is a no-op and nothing is contacted. Otherwise requires alter auth login and an app (pass --app, link a workspace, or set ALTER_APP_ID). |
--app <app-id-or-name> | resolved app | App for --remote; falls back to ALTER_APP_ID or the linked workspace. |
--output <format> | json | json, jsonl, or table. Because the report is one object, jsonl falls back to pretty-printed JSON. |
The rules enforced include:
- Identity ↔ key type —
runtime_identity: agentrequireskey_type: agent, a scoped agent key (alter_ak_…) fromalter keys mint --key-type agent;runtime_identity: apprequireskey_type: runtime(alter_rk_…, fromalter keys mint). A managed-agent-bound key fromalter agents mint-keyis a separate credential with a legacy prefix and does not satisfykey_type: agent. - Goal ↔ runtime identity —
user-dataandbackend-secretrun as the app;agentruns as the named agent. Identity and key agreeing with each other is not enough: an internally consistentapp+runtimepair still cannot implementgoal: agent. - Agent principal —
goal: agentrequires exactlyprincipals: [agent]. User, group, and system principals are separate credential paths and belong to their own design. - Backend-secret principal —
goal: backend-secretrequires at least onesystem,user, orgroupprincipal and neveragent. A design that names no principal never says who holds the secret. - Goal ↔ grant acquisition —
user-dataacquires grants via browser consent;backend-secretvia managed-secret grants;agentvia delegation or an agent-bound managed-secret grant (created via the CLI or dashboard — never the SDK). - Supported SDK language —
languagemust bepythonortypescript. Any other value is an error because the next onboarding step cannot produce SDK wiring for it. - Principal hygiene —
userandagentprincipals never share one credential path. - Human-owned trust steps declared —
idp: jwt-resolutionrequires identity-provider setup to be listed indashboard_steps. The field records that an operator must confirm this trust decision; the operator may runalter identity-providers createor use the dashboard. Identity-provider update and deletion remain dashboard-only.
JSON output reports status, design_path, the rules_version, each violation’s rule, severity, and message, and — only when --remote could not complete — a remote_note explaining why. A --remote run whose authenticated phase failed reports status: "fail" with no remote-provider-configured violation, so check remote_note before treating that as a design problem.
Two rule ids in the violations array come from the command rather than the versioned rule set: parse (the file has no readable YAML frontmatter) and remote-provider-configured (a designed provider is not configured on the app). A consumer switching on rule ids should expect them alongside the rule-set ids below.
Rule-set versions
Section titled “Rule-set versions”rules_version identifies the rule set that produced a report. A stored report is only comparable against the version that emitted it:
rules_version | CLI version | Rules |
|---|---|---|
1 | v0.7.0 – v0.7.3 | Initial set: identity ↔ key type, goal ↔ grant acquisition, principal hygiene, IDP dashboard step, user-data principal and provider, the backend-secret agent exclusion (already reporting as backend-secret-principal), and the agent managed-secret acquisition warning. |
2 | never released | language and Gate B’s empty-SDK-scan became errors. Superseded before any release, so no published binary reports 2. |
3 | 0.8.0 | Adds goal ↔ runtime identity, the agent principal requirement, and the backend-secret principal presence requirement (its agent-exclusion half has reported as backend-secret-principal since 1). Goal-coherence rules now stay silent for a field that already failed its required or enum check, so one typo reports once. |
Rules that reject a previously-passing design ship in a new CLI version, so pin the CLI version in CI if a design must keep validating unchanged.