Org membership & project grants
Org membership & project grants (r.orgs, r.org(id), r.grants — org-owned control plane; first-class orgs)
Section titled “Org membership & project grants (r.orgs, r.org(id), r.grants — org-owned control plane; first-class orgs)”A wallet authenticates (SIWX → a control-plane principal); an org owns projects, and what a principal may do is decided by its org membership role (owner > admin > developer > billing > viewer) or a per-project grant — never wallet_address == signer. The collection + identity lives on r.orgs; per-org operations on the scoped sub-client r.org(id) (the org analog of r.project(id) — the id is bound once). Memberships carry org_id + display_name.
r.orgs.create({ displayName? })→{ org_id, display_name, tier, lease_started_at, lease_expires_at }(POST/orgs/v1). Creates an empty org on the prototype tier; you become owner. Accepts onlydisplayName— no tier input. Step-up gated; the soft per-owner free-org cap answers 403FREE_ORG_OWNER_LIMIT_EXCEEDEDwithnext_actions(a policy refusal, never retried; the SDK raises it asUnauthorizedwith thatcode).r.orgs.list()→ orgs you are an active member of (OrgMembership[], each{ org_id, display_name, role, status }).r.orgs.whoami()→{ principal, memberships[], authenticator_id }(GET/agent/v1/whoami). The REMOTE, gateway-resolved identity — distinct fromr.whoami()(local + network-free wallet/profile label, used byrun402 status).r.org(id).get()→{ org_id, display_name, tier, lease_started_at, lease_expires_at, role }. Any active member; a non-member (incl. a guessed id) gets the same non-revealing 403.r.org(id).rename(displayName | null)→{ org_id, display_name, tier, lease_started_at, lease_expires_at }. Owner-only; set or clear the label (null/""clears). Step-up gated.r.org(id).setSlug(slug, { idempotencyKey? })→{ org_id, slug, previous_slug, created }(POST/orgs/v1/:org_id/slug, repo-first-onramp design D6). Owner-only. A genesis set debits a small one-time fee; a rename is free but releases the OLD slug into a ~90-day cooldown (typedSLUG_RELEASEDrefusal thereafter, naming this org’s new slug as successor). A paid, side-effecting mutation — requiresIdempotency-Key; the SDK generates a fresh one per call whenidempotencyKeyis omitted, so a retried call after a dropped response cannot double-bill.OrgSummary/OrgDetailgain an additiveslug: string | nullfield.r.org(id).members.list()/.add({ wallet, role? })/.setRole(principalId, { role })/.revoke(principalId)— owner-gated; a new wallet is provisioned as ahumanprincipal,roledefaults todeveloper. Removing/demoting the org’s only active owner throwsApiError code: "LAST_OWNER"(409).r.org(id).invites.list()/.create({ email, role, inviteTtlHours? })/.revoke(principalId)— email invites, claimed automatically at the invitee’s first login.r.org(id).audit({ limit?, before? })→ control-plane audit trail (admin+), newest-first; page withbefore.r.grants.create(projectId, { wallet, capability, policy?, expiresAt?, key? })/r.grants.list(projectId)/r.grants.revoke(projectId, grantId)— per-project capability grants for agent/CI principals; requires owner of the project’s org.capabilityexamples:"deploy","functions:write".listreturns each grant with its grant keys nested underkeys, never a token. Revoking a grant revokes every key minted against it.- Grant keys — a credential minted against exactly one grant: narrower than it, spend-capped, expiring, revocable on its own, never an owner.
createwithkey: { kind?, scope?, spendCap?, expiresAt?, publicSubject? }({}takes every default: kindrun402_agent_key, scope = the grant’s capability) mints the grant’s first key in the same transaction;r.grants.createKey(projectId, grantId, key?)mints another;r.grants.revokeKey(projectId, keyId)/r.grants.rotateKey(projectId, keyId)revoke or replace one.key.tokenis returned ONCE fromcreate,createKey, androtateKey— persist it immediately. An agent presents it asRUN402_GRANT_KEY(orrun402({ grantKey })from@run402/sdk/node), which makes it the sole credential class for that client. - Also project-scoped:
r.project(id).grants.create({...})/.list()/.revoke(grantId)/.createKey(grantId, key?)/.revokeKey(keyId)/.rotateKey(keyId).
Control-plane denials throw NotAuthorizedError (403 NOT_AUTHORIZED, carrying requiredRole / requiredCapability / reason). Bad input is ApiError code: "VALIDATION_ERROR" (400). Exported types: OrgRole, Principal, OrgMembership, OrgMember, WhoAmIResult, OrgSummary, OrgDetail, CreateOrgInput, ProjectGrant, GrantKey, GrantKeyInput, MintedGrantKey, plus input/result types.