Context
0462 decision 1 keepsPermissions, Permission and Risk in @stackra/iam. It rejected option B, moving them with re-exports from iam,
because “a permission is IAM’s concept”, and because the move “only worked around an edge that should not exist”. That
edge was the one type import from iam to segments, which 0462 removed.
The vocabulary is now needed below iam, not only by iam:
@stackra/tenancymust declare aPermissionsclass.PruneApprovalBindingsActiondeclaresprune = Permission.L4.system(), and without a class gen refuses it withpermission_unknown.@stackra/iamdepends on@stackra/tenancyat runtime (iam → rules → ai → tenancy), so tenancy cannot import iam without closing a package cycle, which turbo refuses.- Every module that declares permissions needs the same vocabulary, so the reason that held for segments, a single consumer on the wrong side of an edge, does not hold for permissions. The owner’s decision of 2026-11-01 is that the vocabulary is shared, and that iam keeps evaluation.
Risk needs only Enum from @stackra/contracts.
Permission needs only Risk. The Permissions kind base needs only registerKind, which @stackra/container
exports and iam already imports. registerPermissions, permissionKey and the registry read kindOf, which
@stackra/container exports.
The architecture-guardian reviewed this placement and found nothing blocking, except the clash with 0462 that this
record resolves.
Decision
RiskandPermissionmove to@stackra/contracts. Both are dependency-free values:Riskis anEnum.ofvalue, andPermissionis the frozenPermission.Lxvalue with.system()and.restricted().@stackra/contractskeeps importing nothing. Its charter allows this: itsstackra.summarysays it “is the dependency-free leaf every member imports, so shared cross-member vocabulary lands here”, and its README says “Every other Stackra package may import it; it imports nothing.” The README’s table of what is here gets rows forRiskandPermissionin the code PR, not in this one.- The permission registry moves to
@stackra/container. ThePermissionskind base,registerPermissions,permissionKey,PERMISSION_REGISTRYand thePermissionRegistrationtype move beside the Driver, Explorer and Service kind bases. The registry useskindOf. A container’s charter covers defining a kind, which thePermissionsbase is. Itsstackra.summarywas checked; its README was not re-read for this record. @stackra/iamre-exports what moves, and does not copy it.Risk,Permission,Permissions,registerPermissions,permissionKeyandPermissionRegistrationare re-exported from iam, so existingimport { Permissions, Permission, Risk } from "@stackra/iam"keep working. Because the symbols are the same objects, one registry is shared, andregisterPermissions’instanceof Permissioncheck holds across both entries.- Iam keeps evaluation.
coversPermission,selectsPermission, the permission snapshot code,registerRoles, roles, policies and the decision point stay in@stackra/iam. Iam imports the vocabulary; it does not own it. - gen writes
registerPermissionsfrom@stackra/container.emit-registrations.function.tsimports it from@stackra/containerinstead of@stackra/iam. A member that declares permissions but no roles gets no@stackra/iamimport.registerRolesstill comes from@stackra/iam. - Nothing moves to
@stackra/actions. The rest of 0462 decision 1 stands. Rejected option B there, “move with re-exports from iam”, is reconsidered only for@stackra/contractsand@stackra/container, never for actions. - Dependency direction.
@stackra/tenancydeclares itsPermissionsclass from@stackra/container, which it already depends on. No edge goes from iam to tenancy, and no edge goes upward from contracts or container.
Consequences
- Tenancy, and every other module that declares permissions, can declare them without depending on iam.
- Permission vocabulary now lives in two packages: values in contracts, the registry and the kind base in container. Anyone changing a permission’s shape edits contracts, and anyone changing how keys are registered edits container.
@stackra/contractsgrows by two symbols, which every member imports. It stays a leaf, so the cost is a larger surface in a package every member already loads.- Iam’s snapshot and decision code now imports
Permissionfrom contracts. Iam’s public names do not change. - Two copies of
@stackra/contractsin one install would breakinstanceof Permission, which is the same failure the existingEnumandPortbases already have, so this record adds no new class of risk. - A member that declares permissions but no roles stops importing
@stackra/iamin its generated entry.
Enforcement
- planned — the code PR after this one (STA-1890): a test in
@stackra/iamasserts thatRisk,Permission,Permissions,registerPermissions,permissionKeyandPermissionRegistrationre-exported from iam are the same objects as their contracts and container exports. - planned — the code PR after this one (STA-1890): a test in gen’s
emit-registrationsasserts thatregisterPermissionsis imported from@stackra/container, and that a member declaring permissions but no roles gets no@stackra/iamimport. - Existing:
turbo boundariesand turbo’s package-cycle refusal (ADR 0462, Enforcement) fail any edge from iam back to tenancy, or any upward edge out of contracts or container. - Existing: gen’s
permission_unknown(ADR 0462, decision 1) refuses an action whose permission noPermissionsclass declares, which is the check tenancy needs to pass.
Supersedes and sources
Supersedes: the clause of 0462 decision 1 that reads “Permissions, Permission and Risk stay in @stackra/iam”. 0462 is otherwise in force, so
its status becomes “Accepted, amended by 0513”, not “Superseded by 0513”: only one clause changes.
Amends, as amendment notes on each page (their status stays Accepted, and their bodies are not edited):
- 0099:
Risklives in@stackra/contractsand is re-exported from@stackra/iam. The five levels and every other decision stand. - 0132: the
Permissionskind base lives in@stackra/container, and thePermissionvalue in@stackra/contracts. Keys and decisions stand. - 0208: decision 6 holds. The vocabulary that names permissions moves; roles, policies and the decision point stay in iam.
- 0462: decision 1, as described above; its option B is reconsidered for contracts and container only.
@stackra/contracts and @stackra/container charters
(stackra.summary in each package.json, and the contracts README).