Skip to content
Like what we’re building? Star on GitHub

Tenancy, credentials and spend — package invariants ​

One entry per package, verbatim from the engine map this tree replaces. The generated map-packages.md lists every package with its purpose and interfaces; these leaves carry the invariants. iterion map find|impact|path answer the exact questions.

  • pkg/identity/ — Two-level tenancy domain (ADR-048): Org (top level — members via OrgMembership, SSO, monthly run/cost/memory budget, billing) → Team (the resource tenant: every store keys on Team.ID; carries OrgID + team-level concurrency/launch-rate caps). A user is an org member granted 0..N teams. Active context = (org_id, team_id), both on the JWT. Personal org+team auto-created on signup; iterion migrate orgs backfills legacy teams. Store (mongo + memory) is the source of truth for both. An Org also carries the governance it runs itself: RequireProvisionApproval + ProvisionApprovalScope (park every repo request, or only the teams funded by a shared credential tier) and CredentialAudience (which of its teams may spend the org's own LLM keys — zero value admits NOBODY, since lending is an explicit act). A Team is renamable, suspendable (Team.Status, read by the launch gate) and deletable when EMPTY, and an existing account is placed in one directly rather than emailed an invitation — see docs/ticket-context.md's team-lifecycle table. The store has no transactions, so a team grant is kept inside a confirmed org membership by compensation: handlePutTeamMember re-reads the org membership AFTER writing the grant — handleUpdateMember too, for a role that is mirrored up to one — and takes the grant back when that boundary is gone, or when a write the store ACKNOWLEDGED sits behind one it can no longer read; handleRemoveOrgMember sweeps the user's grants and, where their account row names an address, the invitations addressed to them TWICE — invitations first, since an accept CLAIMS its invitation before it writes any access — then deletes the membership row once more. The second sweep, that last delete and the grant guard's own re-read and revoke run on contexts detached from the request, a client that goes away being itself a reason a boundary cannot be re-confirmed. What they bound is the WINDOW, not the race: every ERROR answer names the residual it leaves, state by state in the org-member and team-member rows of docs/cloud-rest-api.md, and closing the accept that straddles every pass takes a conditional boundary write — a pkg/identity seam — rather than one more sweep. What they prevent is a live team grant with no org membership behind it, which GET /api/admin/users/{id} reports as orphan_grant for ANY role, off_ladder naming the sanctioned cause beside it since a role off the ladder (config_editor) is mirrored up to no org membership at all (ADR-078). pkg/auth's invitation accepts write the org membership BEFORE the team grant, as the SSO path does: the residual they choose is an org member with no team, never a team grant with no boundary.

  • pkg/auth/ — Operator authentication primitives (SSO, session cookies, password reset) for cloud-mode endpoints. Mints the JWT carrying (OrgID, OrgRole, TeamID, Role); SwitchOrg/SwitchTeam re-issue it (org-then-team validation).

  • pkg/audit/ — Tenant + platform audit log (control-plane mutations and sign-ins, identity-data-lifecycle.md; Mongo TTL store, /api/{teams,orgs}/{id}/audit + /api/admin/audit)

  • pkg/orgusage/ — Per-org monthly run/cost counters (Mongo CAS) feeding the launch gate + usage views (see docs/quotas-and-limits.md)

  • pkg/credusage/ — Per-credential monthly counters, the question the org bucket structurally cannot answer ("what did this key cost"): keyed {fingerprint, provider, tier, tenant} × month, Mongo CAS + memory twins + conformance, fed by recordCredentialSpend beside recordOrgSpend. Two load-bearing properties: the spend is taken per (backend, model) route — one run can consume two credentials, so a run total belongs to neither, and a route iterion cannot attribute is charged to NOBODY — and every amount is typed metered (real money on an invoice) or estimate (a subscription's would-have-cost figure), the line credpool.CredentialSource.Metered() draws, with metered_usd/estimated_usd kept apart in the responses. GET /api/teams/{id}/credentials/usage + GET /api/admin/credentials/usage (?tier=/?fingerprint=) + iterion remote usage --by-credential. See docs/quotas-and-limits.md

  • pkg/credpool/ — Mutualised credential pool: LLM capacity individual contributors lend to a deployment — a subscription (source: oauth) or a personal metered API key of any provider (source: api_key). CredentialSource.Metered() is the predicate every surface reads: a subscription's dollar figures are ESTIMATES against a plan already paid for, a key's are ACTUAL charges on the lender's invoice — so a metered pledge must carry a spend ceiling, only a personal key may be lent (never a team-scoped one), and metered keys are asked for LAST, after every subscription. A Pledge is one donor's standing offer of a credential they already connected (pkg/secrets OAuth), bounded by ceilings THEY set (spend/day + /week, runs/day, concurrency, sharing window, bot allow-list) and revocable instantly. The Broker is the fifth credential tier in cloudpublisher.resolveAndSealCredentials (the org tier having taken fourth place) — consulted only when a run resolved NO key of its own, neither its team's nor its org's, and before the DB-backed platform tier (the deployment's own fallback keys/forfait, super-admin-managed — see docs/cloud-llm-credentials.md) — and picks the least-consumed eligible pledge by fraction of what each offered, records a Lease (the concurrency unit AND the donor's audit trail), and hands back the blob for the ordinary sealing path. Audience decides who may draw: a union of independent predicates (teams / orgs / contributors reciprocity / all_teams) whose zero value is "the owning org only". Enforcement is the run's own max_cost_usd, clamped to the donor's remaining allowance — the post-hoc ledger charge is the final truth but arrives too late to protect anyone. Dollar figures are ESTIMATES (a subscription bills nothing per call); the hard guard is the provider's usage window, which puts a donor to rest until its reset. Depends on the delegate cost signal reaching metricsEmitter.RunTotals — without it every donor reads $0 forever. See docs/credential-pool.md

  • pkg/pat/ — Personal access tokens (iap_ bearers for programmatic API access)

  • pkg/secrets/ — Secret storage + resolution + AES-256-GCM sealing (Sealer) shared across backends and sandbox. Domains: BYOK API keys, generic named secrets (GenericSecretStore — Mongo in cloud, file-backed FileGenericSecretStore/LayeredGenericSecretStore for the local desktop/CLI store), bot-secret bindings, per-run sealed bundle, OAuth-forfait. The local store (~/.iterion/secrets.json global + <store-dir>/.iterion/secrets.json project override) is sealed with a master key from the OS keychain (go-keyring) or a secrets.key keyfile fallback (LoadOrCreateMasterKey), resolved into runs by ResolveLocalCredentials → WithCredentials in runview.BuildExecutor (the in-process equivalent of the cloud runner's injectCredentials); managed via iterion secret set|list|rm, the studio Secrets view (server_info.secrets_enabled), and /api/local/secrets. There is no KMS backend yet — the Sealer interface is the seam for one. See docs/secrets.md

  • pkg/platformcfg/ — Platform runtime-settings families beyond the usage caps (ADR-090 doctrine: env/const = default, DB record = runtime override, ≤30s TTL resolvers, super-admin API/CLI): platform_credentials (an OPT-IN audience over the deployment's own LLM keys — absent or unenforced admits everyone, so naming a team never cuts the fleet off in one write — plus the shared tiers' ordering: keys_first, the escape hatch that puts their keys before their forfaits, and facade_default (auto|never|always), which says when a z.ai/Moonshot key may be the anthropic wire's default; env defaults ITERION_PLATFORM_KEYS_FIRST / ITERION_PLATFORM_FACADE_DEFAULT, checked at boot; and routing, the platform level of the adaptive-routing policy — ADR-121, pkg/llmroute, env dials ITERION_PLATFORM_PAIR_ORDER / _TRIGGERS / _REFUSED_PINNED_KEY / _ROUTING_STRICT, plus ADR-129's typed overrides cells (literal values) and a providers allowlist carrying a refusal of its own, which names the node, the provider and the level that set the list), bot_roles (the webhook role→bot bindings that were hardcoded constants — reviewer/revi_converse/brancher/implementer, consumed via Server.roleBots()) and sandbox (the sandbox: auto fallback image, resolved at publish and pinned on the RunMessage). One doc per family in the shared platform_settings collection. See docs/platform-bots.md

  • pkg/mail/ — Stdlib SMTP mailer (invitations + password reset) with a log fallback when unconfigured

  • pkg/valkey/ — go-redis client construction + health for ephemeral cross-replica server state (forge OAuth/CSRF, board-MCP run tokens, auth rate-limit buckets); single-node URL or Sentinel-HA topology

  • pkg/configshare/ — Shared-config read/write through forge.FileClient under a synthetic auth.KindShare grant: reads project down to the grant's visible paths, writes merge onto the server-read file with an if-match SHA. See docs/config-share.md