diff --git a/README.md b/README.md index 0fbe91c..5b71ab2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A portable skill bundle for Claude Code, Codex, OpenCode, and other coding agent | Skill | Persona | What it covers | |---|---|---| -| `akash-network:akash` | Deployer | SDL syntax, Console API (with API key), the akt CLI, Akash CLI (`provider-services`), TypeScript/Go SDKs, **AkashML managed inference** (OpenAI/Anthropic-compatible LLM APIs on Akash compute), authz, bid-matching, payment in `uact` | +| `akash-network:akash` | Deployer | SDL syntax, Console API (with API key, including Console teams and projects), the akt CLI, Akash CLI (`provider-services`), TypeScript/Go SDKs, **AkashML managed inference** (OpenAI/Anthropic-compatible LLM APIs on Akash compute), authz, bid-matching, payment in `uact` | | `akash-network:akash-provider` | Provider operator | Kubernetes prereqs, provider installation, attributes & pricing, bid engine, monitoring, troubleshooting | | `akash-network:akash-node` | Node / validator operator | Full node setup, state sync, validator setup, slashing avoidance, sentry nodes, key management | diff --git a/evals/evals.json b/evals/evals.json index 4cfc83d..09c525e 100644 --- a/evals/evals.json +++ b/evals/evals.json @@ -902,6 +902,258 @@ "description": "Stays on akt instead of switching to raw HTTP" } ] + }, + { + "id": 26, + "name": "project-key-deploy", + "prompt": "Our CI deploys to Akash through the Console API with a key in $AKASH_API_KEY. A teammate created that key for our team's `checkout` project only. Give me the curl steps to deploy deploy.yaml and accept the cheapest bid, and tell me what it means if a call comes back 403 with code project_forbidden.", + "assertions": [ + { + "id": "uses_x_api_key", + "type": "regex_present", + "pattern": "x-api-key", + "description": "Uses the x-api-key header" + }, + { + "id": "creates_deployment", + "type": "regex_present", + "pattern": "/v1/deployments\\b", + "description": "Creates the deployment with POST /v1/deployments" + }, + { + "id": "accepts_with_leases", + "type": "regex_present", + "pattern": "/v1/leases\\b", + "description": "Accepts the bid with POST /v1/leases" + }, + { + "id": "explains_project_scope", + "type": "regex_present", + "pattern": "((another|other|different|second) project|outside (of )?(the|its|this|that|your) (key's )?projects?|not (in|within) (the|its|this|that) (key's )?projects?|projects? (the|this) key (can't|cannot|doesn't|does not))", + "description": "Explains project_forbidden as a request reaching outside the key's project", + "case_insensitive": true + }, + { + "id": "no_full_rights_claim", + "type": "regex_absent", + "pattern": "full (rights|access|permissions) (of|to|on|over) (the|your) (whole |entire )?(console )?account", + "description": "Does NOT claim the key carries the full rights of the account", + "case_insensitive": true + }, + { + "id": "no_authorization_bearer_for_apikey", + "type": "regex_absent", + "pattern": "Authorization:\\s*Bearer\\s*\\$?\\{?AKASH_API_KEY", + "description": "Does NOT use Authorization: Bearer with the API key" + } + ] + }, + { + "id": 27, + "name": "deploy-into-named-project", + "prompt": "Our Akash Console team has two projects, `staging` and `prod`. My API key ($AKASH_API_KEY) belongs to the team and isn't limited to a project. Give me a bash script that deploys deploy.yaml into prod rather than the default project.", + "assertions": [ + { + "id": "looks_up_projects", + "type": "regex_present", + "pattern": "/v1/projects\\b", + "description": "Finds the prod project's id with GET /v1/projects" + }, + { + "id": "names_project", + "type": "regex_present", + "pattern": "([\"']?projectId[\"']?\\s*:|x-project-id)", + "description": "Files the deployment with data.projectId, or narrows the request with x-project-id" + }, + { + "id": "creates_deployment", + "type": "regex_present", + "pattern": "/v1/deployments\\b", + "description": "Creates the deployment with POST /v1/deployments" + }, + { + "id": "uses_x_api_key", + "type": "regex_present", + "pattern": "x-api-key", + "description": "Uses the x-api-key header" + } + ] + }, + { + "id": 28, + "name": "list-and-move-by-project", + "prompt": "I'm an admin of our Akash Console team and my API key is in $AKASH_API_KEY. Write a bash script that prints the dseq and name of every active deployment in project $STAGING_PROJECT_ID, then moves deployment 1785245500842 into project $PROD_PROJECT_ID.", + "assertions": [ + { + "id": "filters_by_project", + "type": "regex_present", + "pattern": "projectId=", + "description": "Lists with GET /v1/deployments?projectId=" + }, + { + "id": "pages_on_has_more", + "type": "regex_present", + "pattern": "hasMore", + "description": "Pages on data.pagination.hasMore" + }, + { + "id": "move_route", + "type": "regex_present", + "pattern": "/v1/deployments/[^\\s\"'/]+/project\\b", + "description": "Moves with /v1/deployments/{dseq}/project" + }, + { + "id": "uses_patch", + "type": "regex_present", + "pattern": "(-[A-Za-z]*X\\s*PATCH|--request\\s+PATCH|method:\\s*[\"']PATCH|\\bPATCH\\b[^\\n]{0,80}/v1/deployments/[^\\s\"'/]+/project\\b)", + "description": "Sends the move as a PATCH" + }, + { + "id": "body_project_id", + "type": "regex_present", + "pattern": "[\"']?projectId[\"']?\\s*:", + "description": "Names the target project as data.projectId" + }, + { + "id": "no_close_to_move", + "type": "regex_absent", + "pattern": "(-[A-Za-z]*X\\s*DELETE|--request\\s+DELETE)", + "description": "Does NOT close and recreate the deployment to move it" + } + ] + }, + { + "id": 29, + "name": "team-credits-402", + "prompt": "Deploys from our team's CI key ($AKASH_API_KEY) started failing with 402 insufficient_balance, but my personal Akash Console account still has $200 of credits. Can the script spend my personal credits instead, or top the team up through the API?", + "assertions": [ + { + "id": "team_wallet_separate", + "type": "regex_present", + "pattern": "(team'?s? (own )?(wallet|balance|credits)|separate (wallet|balance|credits)|own (wallet|balance|credits))", + "description": "Explains the team pays from its own wallet, separate from the personal account", + "case_insensitive": true + }, + { + "id": "owner_or_billing_member", + "type": "regex_present", + "pattern": "(owner|billing (role|member))", + "description": "Says an owner or billing member adds the team's credits", + "case_insensitive": true + }, + { + "id": "credits_dont_move", + "type": "regex_present", + "pattern": "(can'?t|cannot|can not|no way to|won'?t|doesn'?t|does not|never)[^.\\n]{0,60}(switch|move|transfer|spend|use|draw)[^.\\n]{0,60}(personal|team|credits|wallet)", + "description": "Says the team key can't spend personal credits and credits don't move between them", + "case_insensitive": true + }, + { + "id": "no_stripe_api", + "type": "regex_absent", + "pattern": "/v1/stripe", + "description": "Does NOT script the UI-internal Stripe endpoints" + } + ] + }, + { + "id": 30, + "name": "session-only-team-actions", + "prompt": "Using my Akash Console API key ($AKASH_API_KEY), write a script that creates a new team called ml-research, then accepts the invitation link a colleague sent me to join their team.", + "assertions": [ + { + "id": "needs_session", + "type": "regex_present", + "pattern": "(session_required|signed[- ]in (console |web )?session|(console|browser|web) session)", + "description": "Says both actions need a signed-in Console session, not an API key", + "case_insensitive": true + }, + { + "id": "no_key_script", + "type": "regex_absent", + "pattern": "-[A-Za-z]*X\\s*POST\\s+[\"']?(https://console-api\\.akash\\.network|\\$\\{?API\\}?)/v1/(organizations|invitations/accept)\\b", + "description": "Does NOT script team creation or invitation acceptance with the API key" + } + ] + }, + { + "id": 31, + "name": "project-key-provider-jwt", + "prompt": "My Akash Console API key ($AKASH_API_KEY) is limited to one project of our team. I need a provider JWT to stream logs for deployment 1785245500842 (service web, gseq 1, oseq 1, provider akash1q7spv2cw06yszgfp4f9ed59lkka6ytn8g4tkjf). What's the create-jwt-token request?", + "assertions": [ + { + "id": "mints_jwt", + "type": "regex_present", + "pattern": "/v1/create-jwt-token", + "description": "Mints the JWT with POST /v1/create-jwt-token" + }, + { + "id": "granular_everywhere", + "type": "count_at_least", + "pattern": "[\"']?access[\"']?\\s*:\\s*[\"']granular", + "min_count": 2, + "description": "Uses access: granular at the top and on the provider entry" + }, + { + "id": "names_deployment", + "type": "regex_present", + "pattern": "[\"']?dseq[\"']?\\s*:\\s*\"?1785245500842", + "description": "Names the deployment by dseq" + }, + { + "id": "lists_services", + "type": "regex_present", + "pattern": "[\"']?services[\"']?\\s*:\\s*\\[", + "description": "Lists the services the deployment entry covers" + }, + { + "id": "no_full_or_scoped", + "type": "regex_absent", + "pattern": "[\"']access[\"']\\s*:\\s*[\"'](full|scoped)[\"']", + "description": "Does NOT send full or scoped access in the request body, which a project-limited key gets refused" + }, + { + "id": "ttl_within_hour", + "type": "regex_absent", + "pattern": "[\"']?ttl[\"']?\\s*:\\s*(36[0-9][1-9]|36[1-9][0-9]|3[7-9][0-9]{2}|[4-9][0-9]{3}|[0-9]{5,})\\b", + "description": "Does NOT ask for a ttl over 3600 seconds, the most a team allows" + } + ] + }, + { + "id": 32, + "name": "contractor-project-key", + "prompt": "I want to give a contractor an Akash Console API key that can only touch our team's `website` project, not billing or our other projects. Is that possible, and how should I set it up?", + "assertions": [ + { + "id": "binds_project", + "type": "regex_present", + "pattern": "(projectId|limit(ed)? (it |the key )?to (one|a single|a|the `?website`?) project|bound to (one|a|the) project|one project only)", + "description": "Says a key can be limited to one project", + "case_insensitive": true + }, + { + "id": "suggests_invite", + "type": "regex_present", + "pattern": "invit", + "description": "Suggests inviting the contractor with access to that project, since a key acts as its creator", + "case_insensitive": true + }, + { + "id": "mentions_role", + "type": "regex_present", + "pattern": "\\b(member|viewer|role)\\b", + "description": "Talks about the contractor's role in the team", + "case_insensitive": true + }, + { + "id": "no_full_rights_claim", + "type": "regex_absent", + "pattern": "(no per-key scopes|keys? (always )?(acts?|carr(y|ies)) (with )?(the )?full (rights|access)|every key (acts|has) (with )?full)", + "description": "Does NOT claim every key carries the full rights of the account", + "case_insensitive": true + } + ] } ] } diff --git a/skills/akash/SKILL.md b/skills/akash/SKILL.md index f957d23..9c4ab38 100644 --- a/skills/akash/SKILL.md +++ b/skills/akash/SKILL.md @@ -4,6 +4,7 @@ description: > Build, validate, and deploy workloads to the Akash Network — the decentralized cloud marketplace. Covers SDL syntax & examples, choosing a deployment method (Console API, the akt CLI, provider-services, TypeScript/Go SDKs), authentication (API key, JWT, self-custody wallets), + Console teams and projects (team and project API keys, team billing), deployment lifecycle, fetching logs/events via the provider proxy, and fee grants/authz. Also covers AkashML — the managed inference surface for calling open-source LLMs on Akash compute via OpenAI/Anthropic-compatible APIs. Use for "deploy to Akash", "Akash SDL", @@ -61,7 +62,7 @@ There are **five deployment paths** (you host the workload) plus one **consumpti | # | Method | Wallet model | Auth | When to use | |---|---|---|---|---| -| 1 | **Console API** | Managed (Console account owns the wallet) | `x-api-key` header | CI/CD, server-to-server, any backend that wants HTTP + an API key. No private-key handling. | +| 1 | **Console API** | Managed (Console holds the wallet of the key's personal account or team) | `x-api-key` header | CI/CD, server-to-server, any backend that wants HTTP + an API key. No private-key handling. | | 2 | **akt CLI** | Managed through a Console API key, or self-custody through a local keyring | `AKT_CONSOLE_API_KEY` or a context credential; or a local key | Terminal and agent sessions: one `akt deploy` goes from SDL to a running service, and `akt console` reads logs, status and shell. | | 3 | **Akash CLI (`provider-services`)** | Self-custody (you hold the keys) | Local key + signature | Existing `provider-services` scripts and manual chain workflows. The official docs now point new CLI users to akt. | | 4 | **TypeScript SDK** | Self-custody (browser wallet, hardware, or local key) | SDK signs locally | dApps, Node.js services, anywhere you want JS/TS code to deploy. | @@ -72,7 +73,9 @@ Rows 1–5 are paths to **host your own workload** on Akash. Row 6 is the answer ### The concept that bites everyone -**A Console account *is* a wallet.** When you sign up on `console.akash.network` and generate an API key, that API key authenticates as your Console account — and your account has a managed wallet underneath it. Deployments spend from that wallet. There is no separate "Console API + bring your own wallet" mode. +**Console holds the wallet, and a key acts in one place.** Every Console user has a personal account and can belong to teams; each of them has its own managed wallet and credits. An API key is created in one of them and acts only there, with its creator's role, and it may be limited to one project. Deployments spend from that wallet. There is no separate "Console API + bring your own wallet" mode. + +So a key does not always carry full account rights: a team `viewer` or `billing` member's key can't deploy, a project key only reaches its project (anything else answers 403 `project_forbidden`), and a key never spends outside the one personal account or team it was created in. Read the key's reach from `GET /v1/organizations` and `GET /v1/projects` instead of assuming it; see `rules/deploy/console-api/teams-and-projects.md`. If a user has a self-custody wallet (Keplr or Ledger) and wants to deploy from *that* wallet, they need the **CLI** or **an SDK**. The Console API will not sign with an external wallet. @@ -88,7 +91,7 @@ If you see **any** of these signals, commit to the matching path silently and st | If the user mentions… | Commit to… | |---|---| | `"akt"`, `"akt deploy"`, `"akt redeploy"`, `"akt console"`, `"akt context"`, `AKT_CONSOLE_API_KEY`, `"akash-network/tap/akt"`, or the user wants it done from this terminal and `command -v akt` finds it | akt CLI (its context decides Console or self-custody) | -| `"I have an API key"`, a key starting `ac.sk.`, `"$AKASH_API_KEY"`, `"x-api-key"`, `"curl"`, `"CI/CD"`, `"GitHub Actions"`, `"backend"`, `"server-to-server"`, `"deploy from CI"`, `"automate"` | Console API | +| `"I have an API key"`, a key starting `ac.sk.`, `"$AKASH_API_KEY"`, `"x-api-key"`, `"x-project-id"`, `"x-organization-id"`, a Console team or project, `"curl"`, `"CI/CD"`, `"GitHub Actions"`, `"backend"`, `"server-to-server"`, `"deploy from CI"`, `"automate"` | Console API | | `"Keplr"`, `"Ledger"`, `"hardware wallet"`, `"my wallet"`, `"self-custody"`, `"provider-services keys add"`, `"my mnemonic"` | CLI or SDK (ask only if they didn't also signal a language) | | `"React app"`, `"Next.js"`, `"akashjs"` (legacy), `"@akashnetwork/chain-sdk"`, `"my dApp"`, `"in the browser"` | TypeScript SDK | | `"Go service"`, `"golang"`, `"cosmos-sdk Go"`, `"my Go backend"` | Go SDK | @@ -151,6 +154,7 @@ A pitfall table at the end of a code response *can* mention "don't use `Authoriz ### Once committed, stay there - On the **Console API** path: don't suggest `provider-services keys add`, don't suggest mTLS certs (deprecated for Console API — see `rules/deploy/cli/mtls-legacy.md`), don't suggest `provider-services query market bid list` — every read is an HTTP call with `x-api-key` in whatever language the user picked. Console records each deployment's SDL, so: change a running deployment with `PATCH /v1/deployments/{dseq}` (never the deprecated full-SDL `PUT`), send no `manifest` to `POST /v1/leases` and no `deposit` anywhere, and treat CPU, memory, storage, GPU or replica changes as a new deployment. + - **In a team:** deploy into the intended project (`data.projectId`; a key bound to a project needs none), list with `?projectId=`, move with `PATCH /v1/deployments/{dseq}/project`, and explain `project_forbidden`, `project_required`, `project_budget_exceeded`, `organization_forbidden`, `organization_mismatch` and `session_required` from `rules/deploy/console-api/teams-and-projects.md`. Team credits, cards and Auto recharge are set on the team's Billing page by an owner or billing member, never from a script. - On the **akt** path: run `akt` commands, not curl and not `provider-services`. Check `akt version` first: 1.0.1 and later refuse any deposit on a Console deploy, and 0.1.x needs `--deposit 0.5` there (better: `brew upgrade akash-network/tap/akt`). Anything unattended passes `--bid-select` and `--yes` and reads `-o json` or `-o jsonl`. Without a terminal there is no setup wizard, so create the context (`akt context create --deploy-via console`) before the first command. On 1.0.1, secrets are `ac-secret://NAME` references with values in `--secrets-file` (never on the command line), `akt update --patch` changes single fields, and a resource change is `akt redeploy --sdl-file `, which leaves the old deployment running. Setup and recipes: `rules/deploy/akt/`. - On the **Akash CLI (`provider-services`)** path: don't suggest `/v1/deployments` HTTP calls. Don't suggest API keys. Stay on `provider-services tx ...` / `provider-services query ...`. - On the **SDK** paths: don't reach for curl examples or CLI commands; the user wants code. @@ -250,6 +254,7 @@ deployment: - `secrets.md` — `ac-secret://` references, `sealedSecrets`, `inheritSecretsFrom` - `api-key-quickstart.md` — linear walkthrough from "I have an API key" to a running deployment - `account-and-funding.md` — account model, balance, automatic funding, 402 refusals (signup and adding credits are UI-only) + - `teams-and-projects.md`: teams and roles, keys bound to a team or project, `x-organization-id` and `x-project-id`, deploying into, listing by and moving between projects, project budgets, team billing, the team errors - `operations.md` — provider JWT (`leases` shapes, scopes), logs/events/status/shell, TEE attestation - **@rules/deploy/akt/** — akt CLI (Console API key or local keyring; terminal and agent sessions) - `overview.md` — install, the 0.1.x versus 1.0 differences (deposit), contexts and credentials without a terminal, JSON/JSONL output, the MCP server, how akt maps to the Console API @@ -358,7 +363,7 @@ services: ### Payment Options - **uact** — Micro-denomination of ACT (the deployment-payment token, separate from AKT). The denom for SDL pricing (`amount: 1000`), deployment escrow, and lease payments. `uakt` is only accepted via a rare burn-mint (BME) fallback, not a normal alternative. -- Note: Console API deployments take no deposit at all — Console funds them from the account's USD credit balance. +- Note: Console API deployments take no deposit at all. Console funds them from the USD credit balance of the key's personal account or team. ## Additional Resources diff --git a/skills/akash/rules/deploy/console-api/account-and-funding.md b/skills/akash/rules/deploy/console-api/account-and-funding.md index dcbcdbc..ea57604 100644 --- a/skills/akash/rules/deploy/console-api/account-and-funding.md +++ b/skills/akash/rules/deploy/console-api/account-and-funding.md @@ -1,20 +1,27 @@ # Console Account & Funding -A Console account *is* a managed wallet. The API key authenticates as the account, and deployments spend from the account's credits automatically. There is no separate wallet to enable and nothing to fund per deployment. +Console holds the wallet. A Console user has a personal account and can belong to teams, and each of them has its own managed wallet and credits. An API key acts in the one it was created in, and deployments spend from that wallet's credits automatically. There is no separate wallet to enable and nothing to fund per deployment. ## The model ``` -Console account +Console user ├── login (email + password, or OAuth) -├── managed wallet: signs every deployment action -│ └── credits (USD), split into -│ ├── Available: free to fund new and running deployments -│ └── Escrow: held by running deployments, returned as each one closes -└── API keys: each one acts as the whole account +├── personal account (type "personal"; the user is its only member) +│ ├── managed wallet: signs every deployment action +│ │ └── credits (USD), split into +│ │ ├── Available: free to fund new and running deployments +│ │ └── Escrow: held by running deployments, returned as each one closes +│ ├── projects: a default one, plus any others +│ └── API keys: act here only, optionally limited to one project +└── teams the user belongs to, each with + ├── its own managed wallet, credits, cards and Auto recharge + ├── projects, starting with a default one + ├── members with roles (owner, admin, member, billing, viewer) + └── the user's API keys for this team: act here only, with the user's role, optionally limited to one project ``` -Each deployment still has its own escrow account on Akash; Console fills it for you. The Console UI shows the split as **Available** and **Escrow**, the same words the API uses. +Each deployment still has its own escrow account on Akash; Console fills it for you. The Console UI shows the split as **Available** and **Escrow**, the same words the API uses. Teams, projects and roles: **@teams-and-projects.md**. ## Setup happens in the UI @@ -25,6 +32,8 @@ None of these steps has a supported API: 3. **Optionally turn on Auto recharge** under Billing, so the card is charged before credits run out. 4. **Create an API key** under **Settings → API Keys** (**@authentication.md**). +A team does the same on its own **Billing** page, separately from any member's personal account. Only its owners and billing members add credits, manage cards and set Auto recharge. Teams never get a free trial, so a new team can't deploy until someone adds credits. + ## Reading the balance ```bash @@ -35,14 +44,15 @@ curl -s https://console-api.akash.network/v1/balances -H "x-api-key: $AKASH_API_ { "balance": 42.17, "deployments": 6.3, "total": 48.47 } ``` -All values are USD. `balance` is **Available**, `deployments` is the **Escrow** held by running deployments (not spent: it returns to `balance` as they close), and `total` is the sum. With the API key and no query string, the call reads your own account; `?address=akash1...` reads any address without a key. +All values are USD. `balance` is **Available**, `deployments` is the **Escrow** held by running deployments (not spent: it returns to `balance` as they close), and `total` is the sum. With the API key and no query string, the call reads the wallet the key acts in: its creator's personal account, or its team's wallet for a team key, which every member shares. `?address=akash1...` reads any address without a key. Related reads: | Endpoint | Returns | |---|---| -| `GET /v1/weekly-cost` | USD per week across your running deployments | -| `GET /v1/usage/history?address=&startDate=&endDate=` | Daily spend for an address (public; at most 366 days). Your wallet address is the `owner` of any of your deployments (`deployment.id.owner`). | +| `GET /v1/weekly-cost` | USD per week across the running deployments of the key's account or team | +| `GET /v1/usage/history?address=&startDate=&endDate=` | Daily spend for an address (public; at most 366 days). The wallet address is the `owner` of any deployment it pays for (`deployment.id.owner`); a team deployment's owner is the team wallet. | +| `GET /v1/project-spend`, `GET /v1/deployment-spend` | A team's spend per project, and per deployment and day (**@teams-and-projects.md**) | | `GET /v1/deployment-funding-config` | The constants automatic funding runs on (below) | ## How automatic funding works @@ -64,16 +74,20 @@ The one programmatic knob is a deployment's **runtime limit**, which closes it a | Response | Meaning | Do this | |---|---|---| -| 402 `insufficient_balance` | Available credit is below what the new deployment needs. `data.requiredAmountUsd` and `data.availableAmountUsd` give the gap. | Tell the user to add credits in the Console UI, or close deployments they no longer need | +| 402 `insufficient_balance` | Available credit is below what the new deployment needs. `data.requiredAmountUsd` and `data.availableAmountUsd` give the gap. | Tell the user to add credits in the Console UI, or close deployments they no longer need. For a team key, the team's credits are short: an owner or billing member adds them on the team's Billing page. | | 402 `balance_top_up_pending` | Auto recharge is already charging the card | Retry after the `Retry-After` header | | 402 `payment_required` | The fee allowance ran out, or a trial account asked for something the trial excludes | Add credits | +| 403 `project_budget_exceeded` | The project's monthly budget is spent | An owner or admin raises it (**@teams-and-projects.md**); retrying won't help | + +Credits never move between a personal account and a team, and a key can't switch to another one's wallet. ## Not programmatic These are Console UI features, and the endpoints behind them are internal (listed in **@overview.md**): - Signup, login, email verification, account deletion -- Payment methods, adding credits, invoices and billing history +- Creating a team and accepting an invitation (a Console session only; an API key gets 403 `session_required`) +- Payment methods, adding credits, invoices and billing history, for a personal account or a team - Auto recharge settings - Profile, favorites, saved templates, alerts and notification channels - **Signing arbitrary transactions.** The managed wallet's key can't be exported, and no endpoint signs a message of your choosing; `/v1/tx` is the Console UI's legacy signer and accepts only six message types (deployment, lease, deposit and certificate messages). Use the dedicated endpoints, and switch to a self-custody SDK for anything they don't cover. @@ -84,3 +98,4 @@ These are Console UI features, and the endpoints behind them are internal (liste - **@authentication.md** — API keys - **@deployment-endpoints.md** — Deployments, leases, bids, runtime limits - **@api-key-quickstart.md** — End-to-end walkthrough +- **@teams-and-projects.md**: Teams, projects, roles and team billing diff --git a/skills/akash/rules/deploy/console-api/api-key-quickstart.md b/skills/akash/rules/deploy/console-api/api-key-quickstart.md index fd45270..b513266 100644 --- a/skills/akash/rules/deploy/console-api/api-key-quickstart.md +++ b/skills/akash/rules/deploy/console-api/api-key-quickstart.md @@ -4,8 +4,8 @@ The whole flow for the API-key path: no CLI, no certificates, no private keys. I ## What you need -- An API key in the environment variable `AKASH_API_KEY`. No key yet? See **@authentication.md**. -- Credits on the Console account. A few dollars covers a small test. +- An API key in the environment variable `AKASH_API_KEY`. No key yet? See **@authentication.md**. A key created in a team deploys for that team, and a key bound to a project deploys into that project (**@teams-and-projects.md**). +- Credits on the Console account, or on the team for a team key. A few dollars covers a small test. - An SDL file, here `deploy.yaml`, and `jq`. ### Where the key lives @@ -73,7 +73,7 @@ DSEQ=$(echo "$RESPONSE" | jq -r '.data.dseq') echo "Deployment $DSEQ created" ``` -No deposit: Console funds the deployment from the account's credits. A `402` here means the account is short of credit; the body's `code` says why (**@account-and-funding.md**). Secrets in `env` need nothing extra to stay out of later reads, but to seal only some values and keep the rest readable, see **@secrets.md**. +No deposit: Console funds the deployment from the credits of the key's account or team. A `402` here means those credits are short; the body's `code` says why (**@account-and-funding.md**). In a team, add `projectId` to `data` to pick the project; a key bound to a project needs nothing extra, and `400 project_required` means a project must be named (**@teams-and-projects.md**). Secrets in `env` need nothing extra to stay out of later reads, but to seal only some values and keep the rest readable, see **@secrets.md**. ## Step 3 — Wait for bids @@ -142,7 +142,7 @@ websocat -k "wss://${HOSTURI#https://}/lease/$DSEQ/$GSEQ/$OSEQ/logs?follow=true& -H "Authorization: Bearer $JWT" ``` -`-k` is there because providers serve self-signed certificates; the JWT is the authentication. +`-k` is there because providers serve self-signed certificates; the JWT is the authentication. A key bound to a project, or a team `member`'s or `viewer`'s key, gets 403 `project_forbidden` for this `scoped` token: it names the deployment with the `granular` shape instead (**@operations.md**). ## Step 7 — Change it, then close it @@ -180,7 +180,10 @@ If a step ever needs one of these, the workflow has drifted onto the CLI or SDK | `401 Invalid API key` | Key revoked, expired or mistyped | Check it starts `ac.sk.production.`; create a new one if needed | | `401` with a valid key | Key sent as `Authorization: Bearer` | Use `x-api-key` | | `400 validation_error` on create | Body not wrapped in `{"data": {...}}`, or the SDL built by string splicing | Build the body with `jq --rawfile` | -| `402 insufficient_balance` | Not enough credit | Add credits in the Console UI | +| `402 insufficient_balance` | Not enough credit | Add credits in the Console UI; for a team key, on the team's Billing page (owner or billing member) | +| `400 project_required` on create | The key reaches several projects and can't deploy into the default one | Pass `data.projectId` from `GET /v1/projects` | +| `403 project_forbidden` | The deployment or project is outside the key's projects | Stay within the key's project, or use a key that reaches the other one | +| `400 organization_mismatch` | `x-organization-id` names another team than the key's | Drop the header; use a key created in that team | | No bids | Resources or price match no provider | `rules/bid-matching/` | | `502 provider_unreachable` on lease | The provider is down | Accept another bid | | `PUT` used to update | Deprecated full-SDL update | `PATCH` with only the fields that change | @@ -196,3 +199,4 @@ If a step ever needs one of these, the workflow has drifted onto the CLI or SDK - **@secrets.md** — Sealed secrets - **@account-and-funding.md** — Balance and automatic funding - **@operations.md** — Logs, events, status, shell, attestation +- **@teams-and-projects.md**: Team keys, projects and the team errors diff --git a/skills/akash/rules/deploy/console-api/authentication.md b/skills/akash/rules/deploy/console-api/authentication.md index 39ca40b..f1ba717 100644 --- a/skills/akash/rules/deploy/console-api/authentication.md +++ b/skills/akash/rules/deploy/console-api/authentication.md @@ -22,8 +22,9 @@ A key starting `ac.sk.` is a Console API key. AkashML keys start `akml-` and bel 1. Sign up at [console.akash.network](https://console.akash.network). The account comes with its managed wallet. 2. Add credits under **Billing** (card via Stripe). -3. Open **Settings → API Keys** (`console.akash.network/user/api-keys`) and create a key. -4. Copy it immediately. The full key is shown once; afterwards only a masked form (`ac.sk.production.abc123***def456`) is visible. +3. To deploy for a team, switch to that team first. A key acts in the personal account or team selected when it is created, and only there. +4. Open **Settings → API Keys** (`console.akash.network/user/api-keys`) and create a key. Leave it on **All projects**, or limit it to one project. +5. Copy it immediately. The full key is shown once; afterwards only a masked form (`ac.sk.production.abc123***def456`) is visible. The first key always comes from the UI: every `/v1/api-keys` call needs an existing key or a Console session. @@ -60,8 +61,8 @@ resp, err := http.DefaultClient.Do(req) | Method | Endpoint | Description | |---|---|---| -| `GET` | `/v1/api-keys` | List keys: `id`, `name`, `keyFormat` (masked), `createdAt`, `lastUsedAt`, `expiresAt` | -| `POST` | `/v1/api-keys` | Create a key. The response's `apiKey` is the only time the full key is returned. | +| `GET` | `/v1/api-keys` | List your keys in the current team: `id`, `name`, `keyFormat` (masked), `organizationId`, `projectId`, `createdAt`, `lastUsedAt`, `expiresAt` | +| `POST` | `/v1/api-keys` | Create a key in the current team, optionally limited to one project with `projectId`. The response's `apiKey` is the only time the full key is returned. | | `GET` | `/v1/api-keys/{id}` | One key's metadata | | `PATCH` | `/v1/api-keys/{id}` | Rename: `{ "data": { "name": "..." } }`. Nothing else can change. | | `DELETE` | `/v1/api-keys/{id}` | Revoke | @@ -80,6 +81,8 @@ curl -sX POST https://console-api.akash.network/v1/api-keys \ "name": "ci-prod-deploy", "apiKey": "ac.sk.production.", "keyFormat": "ac.sk.production.9f2e4d***a17c03", + "organizationId": "1b6e...", + "projectId": null, "expiresAt": "2027-01-01T00:00:00.000Z", "createdAt": "...", "updatedAt": "...", @@ -88,7 +91,17 @@ curl -sX POST https://console-api.akash.network/v1/api-keys \ } ``` -`expiresAt` is optional; omit it for a key that doesn't expire. Every key acts with the full rights of the account: there are no per-key scopes, so give each service its own key and revoke the ones you stop using. +`expiresAt` is optional and at most 1 year away; omit it and the key expires 365 days after creation. A later date answers 400. Every new key expires, so plan its rotation (below). + +### What a key can do + +A key acts as the user who created it, in one place, never more than that user can: + +- **One team.** `organizationId` is the personal account or team the key was created in, and every request runs there. It is `null` on keys from before teams, which act in their creator's personal account. A key can't switch teams: create one in each team it should deploy for. +- **The creator's role.** In a team, the key can do what its creator's role allows today: a `viewer` or `billing` member's key can't deploy, and once the creator leaves the team the key answers 403 `organization_forbidden`. +- **Optionally one project.** `projectId` limits the key to one project of its team; `null` means every project its creator reaches. A project key deploys into that project, can't touch other projects' deployments (403 `project_forbidden`), and can't manage keys, members or billing. + +The binding is fixed: `PATCH` only renames. Give each service its own key, bound as narrowly as it needs, and revoke the ones you stop using. Details and the team errors: **@teams-and-projects.md**. ### Rotation @@ -108,7 +121,7 @@ These need no key: | `POST /v1/bid-screening` | Providers that would bid on a group | | `GET /v1/deployment-funding-config` | Automatic funding constants | | `GET /v1/deployment/{owner}/{dseq}` | Read-only view of any deployment | -| `GET /v1/balances?address=` | Balance split for any address. With your key and no `address`, your own. | +| `GET /v1/balances?address=` | Balance split for any address. With a key and no `address`, the balance of the key's personal account or team. | | `GET /v1/blockchain-status` | `{ "isBlockchainReachable": true }` | ## Auth errors @@ -121,6 +134,10 @@ Errors use the shared envelope (see **@overview.md**). | 401 | The key went in `Authorization: Bearer`, or no credential was sent | Send it in `x-api-key` | | 400 | Both `Authorization` and `x-api-key` were sent | Send only `x-api-key` | | 403 | The key is valid but the account may not do this, such as a free-trial account over the trial's limits or without the Fair Use Policy accepted (`fair_use_policy_required`) | Act on the `code` and `message` | +| 403 `organization_forbidden` | The key's creator is no longer a member of the key's team | Use a key of a current member | +| 400 `organization_mismatch` | `x-organization-id` names another team than the key's | Drop the header; a key only acts in its own team | +| 403 `project_forbidden` | The request reaches a project the key can't, such as another project than the one it is bound to | Stay within the key's project (**@teams-and-projects.md**) | +| 403 `session_required` | The action needs a signed-in Console session: creating, leaving or deleting a team, accepting an invitation, deleting the account | Do it in the Console web app | ## CI/CD diff --git a/skills/akash/rules/deploy/console-api/deployment-endpoints.md b/skills/akash/rules/deploy/console-api/deployment-endpoints.md index 2ffc09e..66aaff0 100644 --- a/skills/akash/rules/deploy/console-api/deployment-endpoints.md +++ b/skills/akash/rules/deploy/console-api/deployment-endpoints.md @@ -4,7 +4,7 @@ Every endpoint below lives under `https://console-api.akash.network` and takes ` ## Quick links -- [Deployments](#deployments): create, list, read, patch, close +- [Deployments](#deployments): create, list, read, patch, close, move between projects - [Leases](#leases) - [Bids](#bids) - [Runtime limits](#runtime-limits-deployment-settings-v2) @@ -31,9 +31,10 @@ POST /v1/deployments | `name` | no | 1 to 256 characters, shown wherever the deployment is listed. Defaults to the SDL's service names joined with `+`. | | `runtimeLimitHours` | no | 1 to 48. Console closes the deployment that many hours after its lease starts and returns the unspent credits. Omit for always-on funding. | | `sealedSecrets` | no | A JWE carrying secret values the SDL references as `ac-secret://NAME`. See **@secrets.md**. | -| `inheritSecretsFrom` | no | Dseq of one of your deployments, open or closed, whose stored secrets this one starts from. See **@secrets.md**. | +| `inheritSecretsFrom` | no | Dseq of a deployment you can change in the same account or team, open or closed, whose stored secrets this one starts from. See **@secrets.md**. | +| `projectId` | no | The project to file the deployment in. Without it: the key's project for a key bound to one, otherwise the default project, and 400 `project_required` when the caller can't deploy there. See **@teams-and-projects.md**. | -Don't send `deposit`. Console funds every deployment from the account's credits and ignores the field. +Don't send `deposit`. Console funds every deployment from the credits of the key's account or team and ignores the field. **Example:** @@ -63,10 +64,10 @@ Keep `dseq`. You don't need `manifest`: Console records the deployment's definit | Status | When | |---|---| -| 400 | The SDL is invalid, or a secret reference has no value, or `sealedSecrets` is malformed, expired or names a value no service references | +| 400 | The SDL is invalid, or a secret reference has no value, or `sealedSecrets` is malformed, expired or names a value no service references. `code: project_required`: no project could be chosen, so pass `projectId`. | | 402 | Not enough credit. `code` is `insufficient_balance` or `balance_top_up_pending` (see **@overview.md**). Free-trial refusals, such as a GPU model the trial excludes, answer `payment_required`. | -| 403 | `sealedSecrets` was sealed for another user or another SDL. Free-trial accounts also get 403 when the deployment exceeds the trial's resource limits, or `code: fair_use_policy_required` until the Fair Use Policy is accepted in the Console UI. | -| 404 | `inheritSecretsFrom` names no deployment of yours | +| 403 | `sealedSecrets` was sealed for another user or another SDL. Free-trial accounts also get 403 when the deployment exceeds the trial's resource limits, or `code: fair_use_policy_required` until the Fair Use Policy is accepted in the Console UI. `code: project_budget_exceeded`: the project's monthly budget is spent. A team `viewer` or `billing` member's key can't deploy at all. | +| 404 | `inheritSecretsFrom` names no deployment you can change, or `projectId` names a project you can't deploy into | | 409 | The seal used a retired key (refetch `GET /v1/sdl-secrets-context` and seal again), or `code` is `inherited_secrets_unreadable` (permanent: supply the values in `sealedSecrets` instead) | | 503 | The key service was briefly unreachable. Retry. | @@ -83,6 +84,7 @@ GET /v1/deployments?state=active&skip=0&limit=100 | `reverse` | `false` | `true` lists newest first; the default is oldest first | | `skip` | `0` | Offset | | `limit` | `100` | Page size, 1 to 100 | +| `projectId` | none | Only the deployments filed in this project. A project you can't reach lists nothing. | **Response:** @@ -95,6 +97,7 @@ GET /v1/deployments?state=active&skip=0&limit=100 "leases": [ ... ], "escrow_account": { ... }, "name": "web-prod", + "projectId": "3f0c...", "groups": [ ... ], "settings": { "name": "web-prod", "runtimeLimitHours": null, "runtimeEndsAt": null, "closed": false } } @@ -104,7 +107,7 @@ GET /v1/deployments?state=active&skip=0&limit=100 } ``` -Page on `hasMore`, not on `total`: `total` comes from Console's index, can briefly lag, and is `null` when the index can't answer. Leases in this list carry no live `status`; read one deployment for that. A `search` over an account with more deployments than a search can span answers 422; page without `search` instead. +Page on `hasMore`, not on `total`: `total` comes from Console's index, can briefly lag, and is `null` when the index can't answer. Leases in this list carry no live `status`; read one deployment for that. A `search` over an account with more deployments than a search can span answers 422; page without `search` instead. In a team, the list covers the projects the key reaches, each deployment carries its `projectId`, and a team with more open deployments than one list can check answers 422 too: filter with `projectId`. ```bash for STATE in active closed; do @@ -160,6 +163,8 @@ This is the canonical read for a running deployment: - **Reclamation.** A lease whose provider has flagged its capacity for reclamation carries `reclamation.deadline` (unix seconds). See `rules/sdl/reclamation.md`. - **GPUs.** `offeredGpus` is what the provider bid with, which is what an "any model" request resolved to. `detectedGpus` is what Console observed inside the containers, with the driver version. +A deployment outside the projects the key reaches answers 404, as if it didn't exist. + ### Patch a deployment ``` @@ -208,6 +213,7 @@ The response is the deployment, plus the `manifestVersion` the patch recorded. E |---|---|---| | 400 | | Names a service, port or volume the stored SDL doesn't declare, leaves a secret reference without a value, or moves a port in a way that is refused | | 403 | | `sealedSecrets` was sealed for another user, or bound to an SDL other than the stored one | +| 403 | `project_forbidden` | The deployment is filed in a project the key can't change | | 404 | | Console holds no definition for this deployment; record one first (below) | | 409 | `deployment_definition_changed` | Another patch landed first. Re-read, then re-send. A 409 without this code means the seal used a retired key. | | 422 | `deployment_resources_changed` | The change needs a new deployment | @@ -236,7 +242,7 @@ Closes the deployment and its leases. Whatever escrow is left returns to the acc - Without `async`, the call returns `200 { "data": { "success": true } }` once the close went through. - With `async=true`, Console checks the request, then answers `202 { "data": { "activityId": "" } }` and finishes in the background. Poll `GET /v1/activities/{activityId}` until `status` is `succeeded` or `failed` (`meta.error` says why). The same call can still answer 200: when the deployment was already closed, or when background close isn't enabled for the account. -Closing an already-closed deployment is a no-op, so a retried close is safe. +Closing an already-closed deployment is a no-op, so a retried close is safe. A deployment filed in a project the key can't change answers 403 `project_forbidden`. ```bash RESP=$(curl -s -w '\n%{http_code}' -X DELETE "https://console-api.akash.network/v1/deployments/$DSEQ?async=true" \ @@ -254,12 +260,28 @@ fi `GET /v1/activities` lists recent actions newest first (`limit` up to 100, `cursor`, `status`, `type=deployment_close`), with a `nextCursor` for the next page. +### Move a deployment to another project + +``` +PATCH /v1/deployments/{dseq}/project +``` + +```bash +curl -sX PATCH "https://console-api.akash.network/v1/deployments/$DSEQ/project" \ + -H "x-api-key: $AKASH_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc --arg project "$TARGET_PROJECT_ID" '{data: {projectId: $project}}')" +``` + +Refiles the deployment in another project of the same account or team and answers `{ "data": { "dseq", "projectId" } }`. Nothing changes on chain: the lease, the provider and the escrow stay. Only team owners and admins may move (403 otherwise), and 404 means the deployment or the project is out of the key's reach. See **@teams-and-projects.md**. + ### Other deployment reads | Endpoint | Auth | Returns | |---|---|---| | `GET /v1/deployment-names?dseq=…&dseq=…` | key | `{ data: { "": name or null } }` | -| `GET /v1/weekly-cost` | key | `{ data: { weeklyCost } }`: USD per week across the account's running deployments | +| `GET /v1/deployment-locations/{dseq}` | key | `{ data: { organizationId, organizationSlug, projectId } }`: where the deployment is filed, within the key's reach | +| `GET /v1/weekly-cost` | key | `{ data: { weeklyCost } }`: USD per week across the running deployments of the key's account or team | | `GET /v1/deployment-funding-config` | public | The constants automatic funding runs on (see **@account-and-funding.md**) | | `GET /v1/deployment/{owner}/{dseq}` | public | A read-only view of any deployment: monthly cost, provider, events. For your own, prefer `GET /v1/deployments/{dseq}`. | @@ -305,6 +327,7 @@ The response is the deployment object. Lease status fills in once the provider s |---|---|---| | 502 | `provider_unreachable` | The provider didn't answer. No lease was created; choose another bid. | | 502 | `manifest_not_delivered` | The lease exists but the provider didn't take the manifest. Send the same request again, or close the deployment to stop paying for it. | +| 403 | `project_forbidden` | The deployment is filed in a project the key can't change | | 400 | | A bid is no longer open, or the body is malformed | There is no endpoint to close a single lease, and none to read one: close the deployment, and read leases from `GET /v1/deployments/{dseq}`. diff --git a/skills/akash/rules/deploy/console-api/operations.md b/skills/akash/rules/deploy/console-api/operations.md index d17e0ce..6e93e17 100644 --- a/skills/akash/rules/deploy/console-api/operations.md +++ b/skills/akash/rules/deploy/console-api/operations.md @@ -35,6 +35,8 @@ JWT=$(curl -sX POST https://console-api.akash.network/v1/create-jwt-token \ `ttl` is in seconds and required; the Console UI uses 1800. There is no refresh endpoint: mint a new token when one expires. The response is `201 { "data": { "token": "" } }`. +**Keys limited to some projects.** A key bound to a project, a request with `x-project-id`, and the key of a team `member` or `viewer` (only owners and admins reach every project) can't mint `full` or `scoped` tokens: those answer 403 `project_forbidden`. They name each deployment with the `granular` shape below, `granular` on every provider entry too, and only deployments the key reaches. In a team, `ttl` is at most 3600. See **@teams-and-projects.md**. + ### Self-custody (CLI or SDK) Console can't sign for a key it doesn't hold. Sign locally with `@akashnetwork/chain-sdk`: @@ -60,13 +62,13 @@ Three shapes; the JWT schema rejects anything else. { "access": "full" } ``` -Every action on every lease you own. **`full` takes no `scope`**; adding one makes the token invalid. +Every action on every lease of the wallet (your personal account's, or the key's team's). **`full` takes no `scope`**; adding one makes the token invalid. ```json { "access": "scoped", "scope": ["logs", "status"] } ``` -The listed actions on every lease you own. +The listed actions on every lease of the wallet. ```json { @@ -82,7 +84,7 @@ The listed actions on every lease you own. } ``` -Per provider: `full`, `scoped` with a `scope`, or `granular` with `deployments`. Each `deployments` entry needs `dseq` (a number), `scope` and `services` (at least one service name); `gseq` and `oseq` are optional. Each provider may appear once. +Per provider: `full`, `scoped` with a `scope`, or `granular` with `deployments`. Each `deployments` entry needs `dseq` (a number), `scope` and `services` (at least one service name); `gseq` and `oseq` are optional. Each provider may appear once. A key limited to some projects may only use the `granular` form, on every provider. **Scopes:** `send-manifest`, `get-manifest`, `logs`, `shell`, `events`, `status`, `restart`, `hostname-migrate`, `ip-migrate`, `attestation`. Grant the narrowest set that works: a token with `shell` can read the container's environment, sealed secrets included. @@ -250,7 +252,8 @@ ws.on("close", () => process.exit(0)); | Symptom | Cause | Fix | |---|---|---| | 401 or 403 from the provider | Token expired, or its scope lacks the action | Mint a new token with the needed scope | -| 400 from `create-jwt-token` | Malformed `leases` claim, such as `scope` on `access: "full"` | Use one of the three shapes above | +| 400 from `create-jwt-token` | Malformed `leases` claim, such as `scope` on `access: "full"`, or a `ttl` over 3600 in a team | Use one of the three shapes above | +| 403 `project_forbidden` from `create-jwt-token` | The key is limited to some projects and the token doesn't name its deployments, or names one outside them | `granular`, listing each deployment of the key's projects | | 404 on `/events` | `events` is a UI alias | Use `/kubeevents` | | Connection refused | Wrong host | Resolve `hostUri` from `GET /v1/providers/{address}` | | TLS certificate rejected | Provider certificate is self-signed | Skip verification server-side, or use a provider proxy | @@ -263,5 +266,6 @@ ws.on("close", () => process.exit(0)); - **@authentication.md** — API keys - **@deployment-endpoints.md** — Where `provider`, `gseq` and `oseq` come from - **@api-key-quickstart.md** — End to end, with logs at the end +- **@teams-and-projects.md**: Tokens for keys limited to some projects - **@../../sdk/typescript/provider-sdk.md** — The same flow through the SDK's provider client - **AEP-64** — https://akash.network/roadmap/aep-64/ — the JWT specification diff --git a/skills/akash/rules/deploy/console-api/overview.md b/skills/akash/rules/deploy/console-api/overview.md index 229106f..ee58925 100644 --- a/skills/akash/rules/deploy/console-api/overview.md +++ b/skills/akash/rules/deploy/console-api/overview.md @@ -1,6 +1,6 @@ # Console API Overview -The Console API is the REST surface behind [Akash Console](https://console.akash.network). An API key authenticates as a Console account, and that account's managed wallet funds and signs every deployment action. This file covers the deployment-management subset; the rest of the spec powers the Console UI. +The Console API is the REST surface behind [Akash Console](https://console.akash.network). An API key authenticates as the Console user who created it, acting in one personal account or team, and that account's or team's managed wallet funds and signs every deployment action. A key can also be limited to one project; see **@teams-and-projects.md**. This file covers the deployment-management subset; the rest of the spec powers the Console UI. With `akt` installed, `akt deploy` and the `akt console` commands make these calls for you: see [../akt/overview.md](../akt/overview.md). @@ -29,6 +29,8 @@ Every path carries its own version prefix: `/v1/...` for almost everything, `/v2 **Never put an API key in `Authorization: Bearer`.** Keys are created in the Console UI; see **@authentication.md**. Calls to a *provider* (logs, shell, status) use a different JWT, minted by `POST /v1/create-jwt-token`; see **@operations.md**. +A key always acts in the team it was created in. Two optional headers narrow a request: `x-organization-id` (can only repeat the key's own team) and `x-project-id` (one project the key reaches). Scripts rarely need either; see **@teams-and-projects.md**. + ## Envelopes **Requests.** Write endpoints wrap the payload in `data`: @@ -68,6 +70,12 @@ The specific codes a deployment script should handle: | 502 | `manifest_not_delivered` | `POST /v1/leases`: the lease exists but the provider did not take the manifest | Send the identical request again, or close the deployment | | 422 | `deployment_resources_changed` | An update tried to change compute resources, replica counts, groups or globally exposed ports | Create a new deployment instead | | 409 | `deployment_definition_changed` | A concurrent `PATCH` changed the deployment first | Re-read the deployment, then re-send | +| 403 | `project_forbidden` | The request reaches outside its projects: a write on a deployment filed in a project the key can't change, an `x-project-id` it can't reach, or a provider JWT that doesn't name its deployments | Stay within the key's projects; see **@teams-and-projects.md** | +| 400 | `project_required` | `POST /v1/deployments` found no project to file the deployment in | Pass `data.projectId` | +| 403 | `project_budget_exceeded` | The deposit would take the project past its monthly budget | An owner or admin raises the budget; don't retry | +| 403 | `organization_forbidden` | The key's creator is no longer a member of its team | Use a key of a current member | +| 400 | `organization_mismatch` | `x-organization-id` names another team than the key's | Drop the header, or use that team's key | +| 403 | `session_required` | Creating, leaving or deleting a team, or accepting an invitation, with an API key | Do it in the Console web app | `PATCH /v1/deployments/{dseq}` and the secrets flow have a few more; they are listed next to those endpoints in **@deployment-endpoints.md** and **@secrets.md**. @@ -90,6 +98,9 @@ The specific codes a deployment script should handle: | Secrets that can't be read back | `GET /v1/sdl-secrets-context` + `sealedSecrets` | @secrets.md | | Balance and funding | `GET /v1/balances` | @account-and-funding.md | | API keys | `/v1/api-keys` | @authentication.md | +| The key's team and projects | `GET /v1/organizations`, `GET /v1/projects` | @teams-and-projects.md | +| Deploy into a project, list by project | `data.projectId` on create, `GET /v1/deployments?projectId=` | @teams-and-projects.md | +| Move a deployment to another project | `PATCH /v1/deployments/{dseq}/project` | @teams-and-projects.md | | Logs, events, status, shell, attestation | `POST /v1/create-jwt-token`, then the provider | @operations.md | For a linear walkthrough from an API key to a running deployment: **@api-key-quickstart.md**. @@ -110,7 +121,7 @@ The spec also exposes endpoints that exist to power the Console UI. They change | Surface | Endpoint pattern | Instead | |---|---|---| | Signup, login, email verification, account deletion | `/v1/auth/*`, `/v1/register-user`, `/v1/send-verification-*`, `/v1/verify-email*`, `/v1/user/me/*-deletion` | Console UI | -| Stripe payments, adding credits, Auto recharge | `/v1/stripe/*`, `/v1/wallet-settings` | Console UI → Billing | +| Stripe payments, adding credits, Auto recharge | `/v1/stripe/*`, `/v1/wallet-settings` | Console UI → Billing (for a team, its owner or a billing member, on the team's Billing page) | | Profile, favorites, saved templates, newsletter | `/v1/user/*`, `/v1/favorite-providers` | Console UI | | Alerts and notification channels | `/v1/alerts/*`, `/v1/deployment-alerts/*`, `/v1/notification-channels/*` | Console UI → Alerts | | Deploy-flow drafts, hardware requests, feed state | `/v1/configure-drafts/*`, `/v1/hardware-requests`, `/v1/activities/seen` | Console UI | @@ -126,3 +137,4 @@ The spec also exposes endpoints that exist to power the Console UI. They change - **@api-key-quickstart.md** — From an API key to a running deployment - **@account-and-funding.md** — Account model, balance, automatic funding, 402s - **@operations.md** — Provider JWT, logs, events, status, shell, attestation +- **@teams-and-projects.md**: Teams, projects, keys bound to them, team billing, the team errors diff --git a/skills/akash/rules/deploy/console-api/secrets.md b/skills/akash/rules/deploy/console-api/secrets.md index 91b9fa9..d9693e9 100644 --- a/skills/akash/rules/deploy/console-api/secrets.md +++ b/skills/akash/rules/deploy/console-api/secrets.md @@ -104,7 +104,7 @@ A patch that writes env values through `services..env` stores them encrypt ## Redeploying without re-entering values -`inheritSecretsFrom: ""` on create starts the new deployment from the secrets another of your deployments stored, even a closed one. Values in `sealedSecrets` override inherited names. This is how to resize: take `consoleSettings.sdl` from the old deployment (its references are already in place), change the resources, create with `inheritSecretsFrom`, lease it, then close the old one. +`inheritSecretsFrom: ""` on create starts the new deployment from the secrets stored for another deployment you can change in the same account or team, even a closed one. Values in `sealedSecrets` override inherited names. This is how to resize: take `consoleSettings.sdl` from the old deployment (its references are already in place), change the resources, create with `inheritSecretsFrom`, lease it, then close the old one. ## Errors diff --git a/skills/akash/rules/deploy/console-api/teams-and-projects.md b/skills/akash/rules/deploy/console-api/teams-and-projects.md new file mode 100644 index 0000000..3d9b4df --- /dev/null +++ b/skills/akash/rules/deploy/console-api/teams-and-projects.md @@ -0,0 +1,188 @@ +# Console API: Teams and Projects + +Every Console user has a **personal account** and can belong to any number of **teams**. The API calls both *organizations* (`type` is `personal` or `team`). Each one has its own managed wallet and credits, and files its deployments into **projects**. Every organization, personal accounts included, has a **default project**. + +An API key is created in one organization and acts only there, with its creator's role, optionally limited to one project. It never reaches its creator's other teams or personal account. A user who belongs to no team sees no change: requests run in the personal account and deployments land in its default project. + +## Roles + +Team roles, as `role` in `GET /v1/organizations`: + +| Role | Deploys and changes deployments | Reads deployments | Changes team billing | Members, projects, budgets | +|---|---|---|---|---| +| `owner` | Every project | Every project | Yes | Everything, including deleting the team | +| `admin` | Every project | Every project | No | Everything except owners | +| `member` | Its projects | Its projects | No | Only projects where it is a project admin | +| `billing` | No | No | Yes | No | +| `viewer` | No | Its projects | No | No | + +A member or viewer reaches **all projects** or **selected ones**. On a selected project its project role decides: `viewer` reads it, `member` also deploys into it and changes its deployments, `admin` also renames it and manages who else reaches it. A team `viewer` only reads, whatever its project role. In a personal account the user is the owner. + +## Which team and project a request runs in + +| Credential | Team | Projects | +|---|---|---| +| API key | The team the key was created in, always. A key from before teams (`organizationId: null`) acts in its creator's personal account. | A key bound to a project reaches only that one. Otherwise, every project its creator reaches. | +| Console session (`Authorization: Bearer`, the web app) | `x-organization-id`, else the team used last, else the personal account | Every project the user reaches | + +The key follows its creator: if the creator's role changes, so do the key's rights, and once the creator leaves or is removed from the team, the key answers 403 `organization_forbidden`. + +Two optional headers narrow a request: + +| Header | Value | With an API key | Refused with | +|---|---|---|---| +| `x-organization-id` | Team id or slug | Can only repeat the key's own team; a key never switches teams | 400 `organization_mismatch` for another team than the key's; 403 `organization_forbidden` for a session that isn't a member | +| `x-project-id` | Project id (a UUID) | Limits the request to one project the key reaches | 403 `project_forbidden` | + +Scripts with an API key need neither header. To act in another team, use a key created in that team. + +### Find where a key acts + +```bash +API=https://console-api.akash.network +curl -s "$API/v1/organizations" -H "x-api-key: $AKASH_API_KEY" | jq '.data[] | {id, name, slug, type, role}' +curl -s "$API/v1/projects" -H "x-api-key: $AKASH_API_KEY" | jq '.data[] | {id, name, isDefault, canDeploy: .access.canDeploy}' +``` + +With an API key, `GET /v1/organizations` lists only the key's team. `GET /v1/projects` lists the projects the request reaches, most recently active first; a key bound to a project sees only its own. `access.canDeploy` says whether the caller may deploy into that project and change its deployments. + +## API keys bound to a team or a project + +In Console, switch to the team, then open **Settings → API Keys**; the dialog offers **All projects** or one project. Over the API, `POST /v1/api-keys` creates the key in the team the request runs in, and `data.projectId` binds it to one project the caller reaches (404 otherwise): + +```bash +curl -sX POST "$API/v1/api-keys" \ + -H "x-api-key: $AKASH_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"data": {"name": "ci-checkout", "projectId": ""}}' +``` + +Every key in `GET /v1/api-keys` shows where it acts: + +| Field | Meaning | +|---|---| +| `organizationId` | The team the key acts in. `null` for a key created before teams, which acts in its creator's personal account. | +| `projectId` | The only project the key reaches, or `null` when it reaches every project its creator does | + +`GET /v1/api-keys` lists the caller's own keys in the current team only. A binding never changes (`PATCH` only renames), so to move a key, create a new one and delete the old one. + +A key bound to a project: + +- deploys into its project without being told (below), and reads and changes only that project's deployments +- reads its team and its project, but can't manage API keys, members, invitations, projects or billing settings (403), nor rename the team +- mints provider JWTs only for deployments it names (see [Provider tokens](#provider-tokens)) + +To give someone access to one project only, invite them to the team as a `member` with that project selected, and let them create their own key. A key you create acts with your role, so anyone holding it acts as you. + +## Deploy into a project + +`POST /v1/deployments` takes `data.projectId`. Without it, Console files the deployment: + +1. in the only project the request may deploy into, when there is exactly one: a key bound to a project, an `x-project-id` header, or a member who may deploy into a single project; +2. otherwise in the team's default project. + +When the caller may not deploy into that project, the create answers **400 `project_required`**: pass `data.projectId`, taken from `GET /v1/projects` where `access.canDeploy` is true. A `projectId` the caller can't deploy into, or that doesn't exist, answers 404. + +```bash +PROJECT_ID=$(curl -s "$API/v1/projects" -H "x-api-key: $AKASH_API_KEY" \ + | jq -r '.data[] | select(.name == "prod" and .access.canDeploy) | .id') + +curl -sX POST "$API/v1/deployments" \ + -H "x-api-key: $AKASH_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc --rawfile sdl deploy.yaml --arg project "$PROJECT_ID" '{data: {sdl: $sdl, projectId: $project}}')" +``` + +Bids, `POST /v1/leases`, `PATCH`, the provider JWT and the close work on the dseq as usual; the deployment stays in its project until someone moves it. + +## List by project, move, locate + +``` +GET /v1/deployments?projectId= +PATCH /v1/deployments/{dseq}/project +GET /v1/deployment-locations/{dseq} +``` + +- **List.** `projectId` keeps the deployments filed in one project, and each listed deployment carries its `projectId`. A project the caller can't reach lists nothing, like one that doesn't exist. Without the filter the list spans every project the caller reaches; a team with more open deployments than one list can check answers 422, so filter by project. +- **Move.** `PATCH /v1/deployments/{dseq}/project` with `{ "data": { "projectId": "" } }` refiles a deployment in another project of the same team and answers `{ "data": { "dseq", "projectId" } }`. Owners and admins only (403 otherwise; 404 when the deployment or the project is out of reach). Nothing changes on chain: the lease, the provider and the escrow stay; the deployment's alerts follow it. A deployment never moves between teams. +- **Locate.** `GET /v1/deployment-locations/{dseq}` answers `{ "data": { "organizationId", "organizationSlug", "projectId" } }`. With an API key it only looks within the key's reach, and answers 404 outside it. + +## Project budgets + +Owners and admins can give a project a monthly budget in USD (calendar months, UTC) with `POST /v1/project-budgets` (`projectId`, `monthlyAmountUsd`, `alertThresholdPercent`, `stopTopUps`). A create whose first deposit would take the project past it answers **403 `project_budget_exceeded`**, and `data.projectIds` names the project. With `stopTopUps`, the automatic top-ups of the project's running deployments stop at the budget too, so they run on their escrow until it is spent. Retrying doesn't help: an owner or admin raises the budget (`PATCH /v1/project-budgets/{id}`), or the next month starts. `GET /v1/project-budgets` shows `currentPeriod.committedUsd`. + +## Team billing + +A team pays from its own wallet, separate from every member's personal account: its own on-chain address, credits, cards and Auto recharge. + +- Teams never get a free trial. A new team has no credits until an owner or billing member adds them. +- `GET /v1/balances` with a team's key reads the team's balance, in the same `balance`, `deployments` and `total` fields (**@account-and-funding.md**). Every member can read it. +- A team deployment's `deployment.id.owner` is the team wallet's address, not a member's. +- Credits, cards and Auto recharge are set on the team's **Billing** page in Console, by an owner or a billing member. There is no supported API for them, and nothing moves credits between a personal account and a team. +- A 402 (`insufficient_balance`, `balance_top_up_pending`) on a team key is about the team's credits. Ask an owner or billing member to add credits. `hasPaymentMethod` in `GET /v1/organizations` says whether the team has a card saved. + +## Provider tokens + +`POST /v1/create-jwt-token` mints tokens for the wallet of the request's team. Two rules apply on top of **@operations.md**: + +- In a team, and for any request limited to some projects, `ttl` is at most 3600 seconds; more answers 400. +- A request is **limited to some projects** when its key is bound to a project, it sends `x-project-id`, or the key's creator is a team `member` or `viewer` (only owners and admins reach every project). Such a request must name each deployment: `access: "granular"` at the top and on every provider entry, each with its `deployments`. `full` and `scoped` answer 403 `project_forbidden`. Granting more than `status`, `events` and `logs` also needs the right to change those deployments. + +```json +{ + "data": { + "ttl": 1800, + "leases": { + "access": "granular", + "permissions": [ + { + "provider": "akash1prov...", + "access": "granular", + "deployments": [{ "dseq": 1785245500842, "gseq": 1, "oseq": 1, "services": ["web"], "scope": ["logs", "events", "status"] }] + } + ] + } + } +} +``` + +## Only from a signed-in session + +Creating a team, accepting an invitation, leaving a team, deleting a team and deleting the account need a signed-in Console session. With an API key they answer **403 `session_required`**: do them in the Console web app. + +## Errors + +| Status | `code` | Meaning | Do this | +|---|---|---|---| +| 400 | `organization_mismatch` | `x-organization-id` names another team than the API key's | Drop the header, or use a key created in that team | +| 403 | `organization_forbidden` | The caller isn't a member of the team: the header names a team the user doesn't belong to, or the key's creator left or was removed | Use a key of a current member | +| 403 | `project_forbidden` | The request reaches outside its projects: `x-project-id` names a project the caller can't reach (or another project than the key's), a write targets a deployment filed in a project the caller can't change, or a provider JWT doesn't name its deployments one by one | Stay within the key's projects, name the deployments in the JWT, or use a key that reaches the other project | +| 400 | `project_required` | A create found no project to file the deployment in | Pass `data.projectId` | +| 403 | `project_budget_exceeded` | The deposit would take the project past its monthly budget; `data.projectIds` names it | An owner or admin raises the budget. Don't retry. | +| 403 | `project_limit_reached` | `POST /v1/projects` found the team at its project limit, given in `data.limit`: 10, counting the default project | Use an existing project, or have an owner or admin delete one it no longer uses (one with no open deployment) | +| 403 | `organization_limit_reached` | Creating a team found the user at their limit of teams they created that still exist, given in `data.limit`: 3 | Delete a team you created and no longer use, or join an existing team by invitation | +| 403 | `session_required` | A session-only action was sent with an API key | Do it in the Console web app | + +A read of a deployment outside the caller's projects answers 404, as for one that doesn't exist. A `viewer` or `billing` member's key gets a plain 403 (`forbidden`) on a create, lease, patch or close. + +## Team endpoints + +All of these run in the request's team and take the same `x-api-key`; the live spec has their bodies. + +| Task | Endpoint | +|---|---| +| The team, rename it (owner, admin) | `GET /v1/organizations`, `PATCH /v1/organizations/{id}` | +| Projects (only owners and admins create one; up to 10 per team, counting the default) | `GET`, `POST /v1/projects`; `GET`, `PATCH`, `DELETE /v1/projects/{id}` (delete only with no open deployment; the default project can't be deleted) | +| Who reaches a project | `GET /v1/projects/{id}/members`; `POST /v1/project-members`; `PATCH`, `DELETE /v1/project-members/{id}` | +| Members | `GET /v1/organization-members`; `PATCH`, `DELETE /v1/organization-members/{id}` | +| Invitations by email (accepting is session-only) | `GET`, `POST /v1/organization-invitations`; `DELETE /v1/organization-invitations/{id}` | +| Budgets | `GET`, `POST /v1/project-budgets`; `PATCH`, `DELETE /v1/project-budgets/{id}` | +| Spend per project, per deployment | `GET /v1/project-spend`, `GET /v1/deployment-spend` | +| What happened in the team | `GET /v1/organization-activities` | + +## Related files + +- **@authentication.md**: API keys +- **@deployment-endpoints.md**: create, list, move and close deployments +- **@account-and-funding.md**: balance and automatic funding +- **@operations.md**: provider JWTs, logs and shell diff --git a/skills/akash/rules/deploy/overview.md b/skills/akash/rules/deploy/overview.md index 3efedbf..7818df4 100644 --- a/skills/akash/rules/deploy/overview.md +++ b/skills/akash/rules/deploy/overview.md @@ -13,10 +13,10 @@ This page exists because conflating these methods is the most common mistake. Th **2. (Deployment only) Do you want Console to manage the wallet, or do you want self-custody?** -- **Console-managed wallet** → Console API, or the akt CLI on a Console context. You sign up on console.akash.network, generate an API key, and authenticate every request with `x-api-key` (akt sends it for you). The Console account owns the on-chain wallet, holds the funds, and signs transactions on your behalf. No private keys ever leave Console's infrastructure. +- **Console-managed wallet** → Console API, or the akt CLI on a Console context. You sign up on console.akash.network, generate an API key, and authenticate every request with `x-api-key` (akt sends it for you). Console holds the on-chain wallet of your personal account, or of the team the key belongs to, keeps its funds, and signs transactions on your behalf. No private keys ever leave Console's infrastructure. - **Self-custody** → the akt CLI on a keyring context, the Akash CLI (`provider-services`), TypeScript SDK, or Go SDK. You hold the private key (in `~/.akash/keys`, a browser wallet like Keplr, a Ledger, or wherever), and your code signs each transaction locally before broadcasting. -Within the deployment paths there is no "Console API + my own wallet." A Console account IS the wallet — the API key authenticates as that account, period. If a user wants to deploy from a self-custody wallet (Keplr or hardware), they need the CLI or an SDK. +Within the deployment paths there is no "Console API + my own wallet." Console holds the wallet: an API key acts in the personal account or team it was created in, with its creator's role and optionally limited to one project, and that wallet pays (`console-api/teams-and-projects.md`). If a user wants to deploy from a self-custody wallet (Keplr or hardware), they need the CLI or an SDK. AkashML sits *outside* the deploy/self-custody axis entirely: it is a managed consumption surface, not a wallet model. Bringing it up alongside Console API in a "managed vs self-custody" comparison is a category error — they solve different problems. @@ -57,7 +57,7 @@ REST API at `https://console-api.akash.network/v1`. Authentication is `x-api-key **Limitations** - Depends on Console as a service -- Can only spend from the Console account's wallet +- Can only spend from the wallet of the key's personal account or team, within what the key's role and project allow - Rate limits apply See **@console-api/** for full reference. diff --git a/skills/akash/rules/overview.md b/skills/akash/rules/overview.md index dc174b1..87edbaf 100644 --- a/skills/akash/rules/overview.md +++ b/skills/akash/rules/overview.md @@ -63,7 +63,7 @@ Providers run Kubernetes clusters and the Akash provider software to: ### Console API The [Console API](https://console-api.akash.network/v1/swagger) provides REST endpoints, authenticated with an API key, for: -- Creating, updating and closing deployments from a Console account's managed wallet +- Creating, updating and closing deployments from the managed wallet of a Console account or team, filed into projects - Accepting bids and reading lease status - Provider data, GPU and region availability diff --git a/skills/akash/rules/sdk/typescript/provider-sdk.md b/skills/akash/rules/sdk/typescript/provider-sdk.md index 2016173..79d44fe 100644 --- a/skills/akash/rules/sdk/typescript/provider-sdk.md +++ b/skills/akash/rules/sdk/typescript/provider-sdk.md @@ -83,7 +83,7 @@ curl -X POST https://console-api.akash.network/v1/create-jwt-token \ -d '{"data":{"ttl":1800,"leases":{"access":"scoped","scope":["logs"]}}}' ``` -See **@../../deploy/console-api/operations.md**. +See **@../../deploy/console-api/operations.md**. A key limited to some projects (bound to a project, or held by a team `member` or `viewer`) gets 403 `project_forbidden` for this `scoped` claim and must name its deployments with the `granular` form. ## Resolve the provider's `hostUri`