# Aspian Cloud docs, in full > Aspian Cloud is change control for software. It governs every change to production made by engineers, contractors and AI agents, on whatever you deploy with, and gives you proof your auditors, board and customers can check. Generated from https://cloud.aspianalliance.com/docs. Shorter index: https://cloud.aspianalliance.com/llms.txt. Each section below is one guide; its page is linked under its title. ## Quickstart Source: https://cloud.aspianalliance.com/docs/quickstart The first five minutes: install the GitHub App, see your scan, and turn on a policy in Evaluate. Nothing is blocked until you choose. ### 1. Install the GitHub App [Run the free scan](https://cloud.aspianalliance.com/scan): no account, email confirmation or passkey. Install the read-only Aspian Cloud Scan GitHub App on your account or organisation, on all repositories or the ones you choose. It suggests fixes as links to your GitHub settings and never changes anything. For a governed workspace, [tell us about your team](https://cloud.aspianalliance.com/signup?plan=team) and we set it up with you within one working day. ### 2. See your scan The scan runs as soon as you come back from GitHub, without waiting for a nightly run. In under a minute it shows: - every repo scored against the baseline: branch protection or rulesets, required reviews, stale approvals dismissed, code owner review, required status checks, force pushes and deletion blocked, read-only workflow tokens, CODEOWNERS and CI; - the last 90 days of merges to the default branch with no independent review, by name; - commits pushed to the default branch with no pull request, and production deploys to environments with no required reviewer; - a fix list, each fix one click away in your GitHub settings, and the whole report as a JSON download. > **Note:** The free scan covers the 25 most recently pushed repos and stores nothing: the report exists only in your browser tab. It says plainly what a read-only app can’t see. Run it again any time. ### 3. Turn on a policy in Evaluate Every policy starts in **Evaluate**. It shows how many recent changes it would have blocked, and whose, before it blocks anything. When you are ready, promote it to **Warn**, then **Block**, one group of repos at a time. Promoting a policy is itself a protected change with an approver. ### Next: choose a control level per tool | Level | What it does | Where | | --- | --- | --- | | Observe | Watches deploy events and what is running, and flags anything without an approved change | Every integration | | Gate | A required check inside your tool, so production waits for an Aspian approval | [Integrations](https://cloud.aspianalliance.com/integrations) that support it | | Hold | Aspian holds the only production deploy credentials and deploys approved, prebuilt output | Regulated and Sovereign | ### How a change reaches production 1. A person or an AI agent opens a change that closes an agreed work item and declares its scope. 2. Aspian works out the risk from the diff. Protected paths raise it; automation can never lower it. 3. Routine changes with all their evidence are approved by policy, and the policy version is recorded. Risky ones go to named approvers, who approve with a **passkey** bound to the change’s hash. The person who asked can never approve. 4. The change deploys through your own tool, and is verified when the live service reports the shipped version. 5. Every step is written to a signed, hash-chained **audit trail**, which feeds your Trust page and audit exports. ### Verify a signed snapshot yourself The quickest way to see how Aspian Cloud proves things is to check a signed snapshot yourself. Open the [Trust sample](https://cloud.aspianalliance.com/trust), or run the verifier from the repository with Node 24: ```bash node packages/trust/scripts/verify-snapshot.ts snapshot.json --public-key # exit code 0 = valid, 1 = invalid, 2 = bad usage ``` ### Conventions - Secrets are write-only. No API, tool, log or export returns a stored secret value. - Money is in whole minor units everywhere in the API. --- ## Deploys and the build pipeline Source: https://cloud.aspianalliance.com/docs/deploys How a GitHub commit becomes a signed image, and what the pipeline refuses. ### What the pipeline accepts - A GitHub repository as `owner/name`, at a full commit SHA (40 or 64 hex characters). Branch names, tags and short SHAs are refused, so a build is always of an exact commit. - A `Dockerfile` at the repository root. Builds without a Dockerfile (framework detection) are not available yet. ### What every build goes through 1. **Secret scan** with gitleaks. Any finding fails the build. Ignore files and allow comments in your repository are not honoured. 2. **Build** with rootless BuildKit. 3. **Bill of materials** with Syft, in SPDX and CycloneDX. 4. **Vulnerability gate** with Trivy. A CRITICAL finding fails the build unless a waiver of 90 days or less, from a different named approver, covers it. 5. **Signing** with cosign (ECDSA P-256), plus attestations for the SBOM, SLSA v1 provenance and OpenVEX. Every stage fails closed: if a tool is missing or errors, the build stops. Cells admit only images signed by this pipeline, enforced by Kyverno admission policies. ### Deploying a build A deploy is a change in a [plan](https://cloud.aspianalliance.com/docs/plans-and-approvals). In production it is a `PROD_CHANGE`. Add a canary (1–50% of traffic, a maximum error rate and a window of 60–3,600 seconds) and a release-only deploy can go out without an approver; the canary rolls the release back if errors rise. A deploy change: ```json { "kind": "deploy", "environment": "production", "imageDigest": "sha256:…", "newCode": true, "canary": { "percent": 10, "maxErrorRate": 0.01, "windowSeconds": 600 } } ``` ### Not done yet - Builds without a Dockerfile (Railpack or Cloud Native Buildpacks). - Publishing to a registry in a running cell, and admission on a live cell. - A public transparency log for signatures. --- ## Plans and approvals Source: https://cloud.aspianalliance.com/docs/plans-and-approvals Risk classes, which changes need a person, and how a passkey approval binds to the exact plan. ### Change kinds and their risk | Change | Risk class | Notes | | --- | --- | --- | | `deploy` | `PROD_CHANGE` in production, otherwise `REVERSIBLE` | Can send data out when it ships new code. Optional canary | | `rollback` | `REVERSIBLE` | Returns to an earlier deployment. Never reverses a migration | | `scale` | `REVERSIBLE` | 0–50 replicas | | `env_change` | `PROD_CHANGE` in production, otherwise `REVERSIBLE` | Values that look like secrets are refused; pass a secret handle instead | | `webhook_change` | `PROD_CHANGE` in production, otherwise `REVERSIBLE` | HTTPS only | | `domain_change` | `IRREVERSIBLE` | | | `migration` (expand) | `PROD_CHANGE` | Needs a down-migration digest | | `migration` (contract) | `IRREVERSIBLE` | Cannot share a plan with a deploy | | `drop_database` | `IRREVERSIBLE` | | | `export` | `IRREVERSIBLE` | Always needs a passkey approval | A plan takes the risk class of its riskiest change. ### When a person must approve - `IRREVERSIBLE` plans and exports: always. - `PROD_CHANGE` plans: when the plan is tainted, or when a deploy in it has no canary. - Anything else: only when the plan is tainted and can send data out (a preview of new code, settings, webhooks, domains, exports). How many people must approve depends on your tier and policy. Business and above can require two for production database changes and irreversible actions. See [Pricing](https://cloud.aspianalliance.com/editions). ### The approval ceremony 1. Create an approval request for a plan. Only named approvers can be asked, and the requester is never one of them. 2. A WhatsApp message (falling back to SMS or email) carries a single-use link. Opening it shows what changes, what it affects, how it rolls back and who asked. 3. The approver’s browser asks the server for a challenge bound to the request, the plan hash and the approver. 4. The approver signs it with a passkey. If the plan has changed since, the approval is refused. ### Create a plan ```http POST /v1/plans x-aspian-tenant: Content-Type: application/json { "riskClass": "PROD_CHANGE", "summary": "Add partial refunds to payments", "steps": [{ "kind": "deploy", "idempotent": true, "params": { … } }] } ``` Then `POST /v1/plans/{planId}/approval-requests` to ask for approval, and `POST /v1/plans/{planId}/apply` once it is approved. Full list: [API reference](https://cloud.aspianalliance.com/docs/api). --- ## MCP server for AI agents Source: https://cloud.aspianalliance.com/docs/mcp The ten tools an AI agent can use, the risk class of each, and the rules that keep agents safe. > **Important:** The MCP server is built and tested, but its backends are not yet connected to a running cell, and there is no hosted endpoint with OAuth sign-in yet. Hosted clients such as Claude.ai and ChatGPT cannot connect until there is. ### Tools and risk classes | Tool | Risk class | Taints the key | What it does | | --- | --- | --- | --- | | `open_session` | `READ` | No | Opens a session bound to your agent key | | `list_projects` | `READ` | No | Projects the key may see | | `get_status` | `READ` | No | Status of a plan or a project | | `read_logs` | `READ` | Yes | Logs for a project and environment, up to 500 lines | | `inspect_request` | `READ` | Yes | One request by its ID | | `query_rows` | `READ` | Yes | Up to 100 rows from one table. No free-form SQL | | `read_webhook` | `READ` | Yes | One webhook delivery | | `create_plan` | `REVERSIBLE` | No | 1–20 changes. The plan’s own class decides who must approve | | `request_export` | `REVERSIBLE` | No | Creates an export plan, which always needs a passkey approval | | `apply_plan` | `IRREVERSIBLE` | No | Applies a plan that is allowed to run | There is no tool that reads a secret value, and none will be added. ### Taint - The four tools that read customer content (`read_logs`, `inspect_request`, `query_rows`, `read_webhook`) taint the session and the agent key before they fetch anything, so even a failed read taints. - Content they return is wrapped in a boundary marker so an agent can tell it apart from instructions. - Taint on a key never wears off by itself. Only a person can clear it, and a new session inherits it. - A plan created while tainted stays tainted. Tainted production plans always need a person’s approval. ### Limits - Each agent key can create 20 plans an hour (`RATE_LIMITED` after that). - After three refused or failed applies in a row, the key is paused and the owner is alerted. An owner, admin or the key’s owner can unpause it. - A key’s scope names its projects and the highest risk class it may apply. New keys default to `READ` with no projects. ### Connecting The server speaks MCP over streamable HTTP at `POST /mcp`, with JSON responses. Agent keys are bearer tokens of the form `aak__`; only a hash is stored. Browser origins must be on the allow-list. Claude Code, against a server you run: ```bash claude mcp add --transport http aspian http://127.0.0.1:3820/mcp \ --header "Authorization: Bearer $ASPIAN_AGENT_KEY" ``` Cursor (.cursor/mcp.json), against a server you run: ```json { "mcpServers": { "aspian": { "url": "http://127.0.0.1:3820/mcp", "headers": { "Authorization": "Bearer ${env:ASPIAN_AGENT_KEY}" } } } } ``` Errors come back as tool results with `isError: true` and a body of `{"error": {"status", "code", "message"}}`. Codes are listed in [Errors](https://cloud.aspianalliance.com/docs/errors). --- ## Trust Page and verifying evidence Source: https://cloud.aspianalliance.com/docs/trust-page What a Trust Page fact contains, how snapshots are signed, and how to check one without asking us. ### What a fact contains - A statement computed from platform records: the audit trail, restore drills, storage inventory or telemetry. - When it was observed, and a freshness window. Past the window it reads as **stale**; with no record it reads as **missing**. - Its coverage: what it includes and, as importantly, what it excludes. - The source records it was computed from. Facts from partners, such as physical protection of a facility, are shown separately as attested by that partner, never as platform-verified. ### Verifying a snapshot offline A snapshot is canonical JSON signed with Ed25519. The verifier needs Node 24 and nothing from the platform. ```bash # against a published key node packages/trust/scripts/verify-snapshot.ts snapshot.json --public-key --key-id # or, with the key in the environment ASPIAN_TRUST_PUBLIC_KEY= node packages/trust/scripts/verify-snapshot.ts snapshot.json ``` > **Note:** Without `--public-key` the verifier checks against the key inside the file, which only proves the file is self-consistent. Always verify against a key you got from us separately. ### Sharing with an auditor Share links expire and can be revoked. A reviewer sees the evidence without an account and without being able to change anything. --- ## Messaging and alerts Source: https://cloud.aspianalliance.com/docs/messaging How alerts fall back across channels, how acknowledgement works, and what crosses borders. > **Important:** The alert bus is tested against a simulator. No live provider sends messages yet; a 50-alert test against real providers comes before go-live. ### Fallback order By default an alert tries WhatsApp, then SMS, then voice, then email. Voice is used only where an approved voice provider is enabled. Each organisation can set its own order. ### Delivery and acknowledgement - Delivery is at least once and is measured by delivery receipts. - Acknowledgement is exactly once: an alert carries one ID and can be acknowledged once, whichever channel it arrived on. - Delivered and acknowledged are reported separately. A delivery receipt does not mean a person read the alert. - If every channel fails, the alert is marked undelivered and escalated. ### Minimal content for Nigerian organisations For organisations whose data is in Nigeria, WhatsApp and email messages carry only an alert code and a link. The details open after sign-in, inside the cell. Providers are listed on [Sub-processors](https://cloud.aspianalliance.com/legal/subprocessors). ### Providers Receipt webhooks are verified for Meta WhatsApp Cloud, Termii, Africa’s Talking and Resend. Sending through them is not wired yet. --- ## Cells, runtimes and databases Source: https://cloud.aspianalliance.com/docs/cells-and-databases What a cell is, which runtime supports which operation, and which database engines exist. ### Cells A cell is a self-contained Kubernetes cluster in one jurisdiction: Lagos, Abuja, London, the EU or your own premises. Apps, databases, logs, backups and audit records stay inside it, and it keeps serving if the control plane is unreachable. ### What each runtime supports | Operation | Kubernetes (cells) | Docker (laptop and CI only) | | --- | --- | --- | | Run a workload | Yes | Yes | | Postgres database | Yes, Postgres 17 | Yes | | Rollback | Not yet | Yes | | Migrations | Not yet | No | | Restore drills | Not yet | No | | Export | Not yet | No | | Sleep when idle | Yes, never in production | Yes, never in production | ### Postgres Postgres 17 runs on CloudNativePG with three instances, synchronous replicas and Barman backups. Other engines, including Valkey and MySQL, are refused on cells with `ENGINE_NOT_IMPLEMENTED`. ### Restore drills A restore drill restores a backup, measures data lost and time taken, and compares checksums. The result is signed and becomes a Trust Page fact. Drills are weekly and signed on Business and above. --- ## API reference Source: https://cloud.aspianalliance.com/docs/api Every HTTP route in the platform API, with who may call it. > **Important:** The API is built and tested but not yet hosted for customers. It refuses to start in production until a real executor is connected. ### Calling the API - All routes are under `/v1`. Sign-in is by OIDC single sign-on and passkeys; the session is a cookie. - Requests that change something and carry the cookie must send an `Origin` matching the platform, or get `BAD_ORIGIN`. - Tenant routes need an `x-aspian-tenant` header with your organisation’s UUID, and membership of it. - Errors return `{"error": "CODE", "message": "…"}`. See [Errors](https://cloud.aspianalliance.com/docs/errors). ### Sign-in and passkeys | Route | What it does | | --- | --- | | `GET /v1/auth/sso/login` | Starts single sign-on | | `GET /v1/auth/sso/callback` | Completes single sign-on | | `POST /v1/auth/logout` | Ends the session | | `GET /v1/me` | The signed-in person | | `POST /v1/passkeys/registration/options` | Starts registering a passkey | | `POST /v1/passkeys/registration/verify` | Finishes registering a passkey | ### Plans and approvals | Route | Who | What it does | | --- | --- | --- | | `POST /v1/plans` | Owner, admin, developer | Create a plan | | `GET /v1/plans/{planId}` | Members | Read a plan and its canonical bytes | | `POST /v1/plans/{planId}/apply` | Owner, admin, developer | Apply an approved or low-risk plan | | `POST /v1/plans/{planId}/approval-requests` | Owner, admin, developer | Ask named approvers to approve | | `POST /v1/approval-links/open` | The approver | Open a single-use approval link | | `POST /v1/approval-requests/{requestId}/challenge` | The approver | Get a challenge bound to the plan hash | | `POST /v1/approval-requests/{requestId}/verify` | The approver | Submit the passkey signature | ### Operators | Route | What it does | | --- | --- | | `GET /v1/operator/tenants/{tenantId}/incidents` | Incidents an operator’s grant covers | | `POST /v1/operator/tenants/{tenantId}/incidents/{incidentId}/acknowledge` | Acknowledge an incident | | `POST /v1/operator/tenants/{tenantId}/runbooks/{runbookId}/run` | Run a named runbook the grant allows | Operator routes need an active operator and a current grant for that organisation and runbook. ### Add-ons and partner attestations | Route | Who | What it does | | --- | --- | --- | | `GET /v1/addons` | Members | List add-ons | | `POST /v1/addons` | Owner, admin | Request an add-on | | `POST /v1/addons/{addonId}/status` | Owner, admin | Change an add-on’s status | | `POST /v1/partner/attestations` | A partner, by signature | Submit a signed attestation. Returns 201, or 200 for a duplicate | --- ## Error codes Source: https://cloud.aspianalliance.com/docs/errors Every error code the API and the MCP server return, with its HTTP status. ### API errors Body: `{"error": "CODE", "message": "…"}`. Match on the code; the message is for people and may change. | Status | Code | Meaning | | --- | --- | --- | | 400 | `INVALID_REQUEST` | The body or parameters failed validation | | 400 | `TENANT_REQUIRED` | The `x-aspian-tenant` header is missing or not a UUID | | 400 | `LOGIN_EXPIRED`, `SSO_FAILED` | Single sign-on did not complete | | 400 | `REGISTRATION_INVALID`, `INVALID_ASSERTION` | A passkey registration or assertion was malformed | | 401 | `UNAUTHENTICATED` | Not signed in | | 403 | `BAD_ORIGIN` | A state-changing request came from another origin | | 403 | `NOT_A_MEMBER`, `FORBIDDEN` | You are not a member, or your role cannot do this | | 403 | `EMAIL_NOT_VERIFIED`, `NO_ACCOUNT` | The account cannot sign in yet | | 403 | `PLAN_REQUIRES_APPROVAL` | The plan needs an approval before it can apply | | 403 | `REQUESTER_CANNOT_APPROVE`, `NOT_AN_APPROVER` | This person may not approve this plan | | 403 | `ASSERTION_INVALID` | The passkey signature did not verify | | 403 | `NOT_AN_OPERATOR`, `NO_GRANT`, `RUNBOOK_NOT_IN_GRANT`, `RUNBOOK_NOT_PERMITTED` | The operator’s grant does not cover this | | 403 | `SESSION_NOT_USABLE`, `NOT_YOUR_REQUEST` | The agent session or approval request belongs to someone else | | 404 | `NOT_FOUND` | The plan, organisation, request, link, incident or add-on does not exist | | 404 | `SSO_NOT_CONFIGURED`, `RUNBOOK_UNKNOWN` | Not set up for this organisation | | 409 | `PLAN_CHANGED` | The plan changed after the approval was requested | | 409 | `ALREADY_APPROVED`, `APPROVAL_NOT_NEEDED` | Nothing more to approve | | 409 | `NO_APPROVERS`, `NO_PASSKEY` | Nobody can approve yet: add a named approver with a passkey | | 409 | `PLAN_NOT_PENDING`, `PLAN_NOT_APPLICABLE`, `PLAN_CONFLICT`, `ILLEGAL_TRANSITION` | The plan is not in a state that allows this | | 410 | `CHALLENGE_USED_OR_EXPIRED`, `LINK_USED`, `PLAN_EXPIRED` | Single-use or time-limited, and already used or expired | | 422 | `REQUESTER_CANNOT_APPROVE`, `NOT_AN_APPROVER` | When creating an approval request: this person cannot be asked | | 422 | `UNKNOWN_PARTNER` | A partner attestation came from an unknown partner | | 500 | `PLAN_INTEGRITY` | The stored plan no longer matches its hash. Nothing was applied | | 500 | `INTERNAL` | Something went wrong and has been logged | ### MCP errors Tool results with `isError: true` and `{"error": {"status", "code", "message"}}`. Transport errors are JSON-RPC errors with the same code in `data`. | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHENTICATED`, `SESSION_INVALID` | Bad agent key or session handle | | 403 | `KEY_PAUSED` | The key was paused after three refused or failed applies | | 403 | `SCOPE_EXCEEDED`, `NOT_AUTHORISED` | Outside the key’s projects or maximum risk class | | 403 | `SESSION_TAINTED`, `PLAN_REQUIRES_APPROVAL` | Taint or risk means a person must approve | | 403 | `PLAN_NOT_OWNED`, `PLAN_EXPIRED` | Not this key’s plan, or too old | | 400 | `PLAN_INVALID` | The changes are malformed or cannot be combined | | 400 | `SECRET_VALUE_REFUSED` | A value looked like a secret. Use a secret handle | | 404 | `PLAN_NOT_FOUND`, `KEY_NOT_FOUND` | Does not exist | | 409 | `PLAN_STATE_CONFLICT`, `KEY_NOT_PAUSED` | Wrong state for this action | | 429 | `RATE_LIMITED` | More than 20 plans in an hour from this key | | 502 | `APPLY_FAILED` | The cell refused or failed the change | | 500 | `INTERNAL` | Something went wrong and has been logged | --- ## Deploy with a Dockerfile Source: https://cloud.aspianalliance.com/deploy/dockerfile Any language or framework, as long as the repository has a Dockerfile at its root and the server listens on $PORT. ### What we detect A file named `Dockerfile` at the root of the repository. Nothing else is detected yet: there are no framework presets, and a repository without a Dockerfile is refused before the build starts. ### What your app must do - Listen on the port in the `PORT` environment variable, on all interfaces (`0.0.0.0`). - Read settings from environment variables. Secrets arrive the same way and are never written into the image. - Keep secrets out of the repository. The build runs a secret scan, and any finding fails it, whatever your ignore files say. - Avoid CRITICAL vulnerabilities in the image. They fail the build unless a time-limited waiver covers them. ### Example: a Node.js server Dockerfile: ```dockerfile FROM node:24-slim WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . ENV NODE_ENV=production USER node CMD ["node", "server.js"] ``` server.js: ```js import { createServer } from 'node:http'; const port = Number(process.env.PORT ?? 3000); createServer((req, res) => res.end('ok')).listen(port, '0.0.0.0'); ``` ### Example: a Python app Dockerfile: ```dockerfile FROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT}"] ``` ### What happens next The build is signed and gets a bill of materials. Deploying it is a change in a plan; see [Deploys and the build pipeline](https://cloud.aspianalliance.com/docs/deploys) and [Plans and approvals](https://cloud.aspianalliance.com/docs/plans-and-approvals).