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 viaOrgMembership, SSO, monthly run/cost/memory budget, billing) →Team(the resource tenant: every store keys onTeam.ID; carriesOrgID+ 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 orgsbackfills 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) andCredentialAudience(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:handlePutTeamMemberre-reads the org membership AFTER writing the grant —handleUpdateMembertoo, 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;handleRemoveOrgMembersweeps 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 — apkg/identityseam — rather than one more sweep. What they prevent is a live team grant with no org membership behind it, whichGET /api/admin/users/{id}reports asorphan_grantfor ANY role,off_laddernaming 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/SwitchTeamre-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 byrecordCredentialSpendbesiderecordOrgSpend. 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 typedmetered(real money on an invoice) orestimate(a subscription's would-have-cost figure), the linecredpool.CredentialSource.Metered()draws, withmetered_usd/estimated_usdkept 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.mdpkg/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. APledgeis one donor's standing offer of a credential they already connected (pkg/secretsOAuth), bounded by ceilings THEY set (spend/day + /week, runs/day, concurrency, sharing window, bot allow-list) and revocable instantly. TheBrokeris the fifth credential tier incloudpublisher.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 aLease(the concurrency unit AND the donor's audit trail), and hands back the blob for the ordinary sealing path.Audiencedecides who may draw: a union of independent predicates (teams/orgs/contributorsreciprocity /all_teams) whose zero value is "the owning org only". Enforcement is the run's ownmax_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 reachingmetricsEmitter.RunTotals— without it every donor reads $0 forever. See docs/credential-pool.mdpkg/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-backedFileGenericSecretStore/LayeredGenericSecretStorefor the local desktop/CLI store), bot-secret bindings, per-run sealed bundle, OAuth-forfait. The local store (~/.iterion/secrets.jsonglobal +<store-dir>/.iterion/secrets.jsonproject override) is sealed with a master key from the OS keychain (go-keyring) or asecrets.keykeyfile fallback (LoadOrCreateMasterKey), resolved into runs byResolveLocalCredentials→WithCredentialsinrunview.BuildExecutor(the in-process equivalent of the cloud runner'sinjectCredentials); managed viaiterion secret set|list|rm, the studio Secrets view (server_info.secrets_enabled), and/api/local/secrets. There is no KMS backend yet — theSealerinterface is the seam for one. See docs/secrets.mdpkg/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, andfacade_default(auto|never|always), which says when a z.ai/Moonshot key may be the anthropic wire's default; env defaultsITERION_PLATFORM_KEYS_FIRST/ITERION_PLATFORM_FACADE_DEFAULT, checked at boot; androuting, the platform level of the adaptive-routing policy — ADR-121,pkg/llmroute, env dialsITERION_PLATFORM_PAIR_ORDER/_TRIGGERS/_REFUSED_PINNED_KEY/_ROUTING_STRICT, plus ADR-129's typedoverridescells (literal values) and aprovidersallowlist 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 viaServer.roleBots()) andsandbox(thesandbox: autofallback image, resolved at publish and pinned on the RunMessage). One doc per family in the sharedplatform_settingscollection. See docs/platform-bots.mdpkg/mail/— Stdlib SMTP mailer (invitations + password reset) with a log fallback when unconfiguredpkg/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 topologypkg/configshare/— Shared-config read/write throughforge.FileClientunder a syntheticauth.KindSharegrant: 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
