diff --git a/cmd/add/solution_documents.go b/cmd/add/solution_documents.go index ea2b5540..c19b4ef1 100644 --- a/cmd/add/solution_documents.go +++ b/cmd/add/solution_documents.go @@ -418,14 +418,23 @@ func solutionContract(name string) *modulecontract.Contract { ResourceKind: name + ".page", Actions: []string{readAction, writeAction}, }}, + // installer / editor / viewer, the standard set — the same three wiki + // declares, and now every solution in the fleet. + // + // `installer` is required BY NAME: the authority refuses a solution + // without it. `viewer` is what the organization is installed as, so + // the people of an installation can open the solution at all — + // without a second role only the owner holds anything and everyone + // else is refused by name. `editor` is the write position between + // them, so a solution that grows one does not have to invent a name + // for it; proposals calls its middle role `reviewer`, which is the + // same position in its own domain's word. + // + // Every role must grant something: core refuses `role "installer" + // grants nothing`. Roles: []modulecontract.Role{ {Name: modulecontract.InstallerRole, Scopes: ownScope}, - // The role the ORGANIZATION is installed as, so the people of an - // installation can open the solution at all. Without a role - // besides installer only the owner holds anything, and everyone - // else is refused by name — " holds no role on " — - // which is a correct refusal and a useless scaffold. wiki and - // proposals both declare a viewer beside their installer. + {Name: "editor", Scopes: ownScope}, {Name: "viewer", Scopes: []modulecontract.CeilingScope{{ ResourceKind: name + ".page", Actions: []string{readAction}, diff --git a/cmd/add/solution_test.go b/cmd/add/solution_test.go index 5f6f5038..4840a8a9 100644 --- a/cmd/add/solution_test.go +++ b/cmd/add/solution_test.go @@ -551,8 +551,8 @@ func TestTheContractDeclaresARoleBesidesInstaller(t *testing.T) { for index := range contract.Roles { names = append(names, contract.Roles[index].Name) } - if !slices.Contains(names, "installer") || !slices.Contains(names, "viewer") { - t.Fatalf("the contract must declare installer and viewer, got %v", names) + if !slices.Equal(names, []string{"installer", "editor", "viewer"}) { + t.Fatalf("the contract must declare the standard set installer/editor/viewer, got %v", names) } } diff --git a/docs/commands.md b/docs/commands.md index e5218cec..7f3bef01 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2425,7 +2425,7 @@ What it creates: | `services/backend` | the entry service: presents the solution's identity, serves the API the page calls, and is the workload a host installs | | `services/web` | the shell that serves the page where no host shell mounts it — not published, because a host that federates the remote needs no shell. It **mounts the solution's own `./Page`**, the same module a host federates, and its generic login/signup routes are removed: a solution has no sign-in of its own, the authority's edge signs the person in | | `solution.codefly.yaml` | what the solution exposes, and the route name it addresses each consumed module by | -| `module.contract.codefly.yaml` | the principal its credentials are issued to, with `owner_aware: true`, the solution's own `scope_ceilings`, and the **installer role** — the authority refuses to admit a solution that offers a person no role to hold on it | +| `module.contract.codefly.yaml` | the principal its credentials are issued to, with `owner_aware: true`, the solution's own `scope_ceilings`, and the standard roles **installer / editor / viewer** — the same three wiki declares. The authority requires `installer` by name and refuses a solution without it | | `module.package.codefly.yaml` | the published identity; without it the render declares no presence and nobody can install the solution | | `services/backend/solution.frontend.codefly.yaml` | the page's title, order and `./Page` remote | | `configurations/local/fixture-composition.yaml` | who, in the development fixture, has this solution **installed**. Without it the authority refuses every capability ("not installed in this organization") and the page can only render its not-installed branch. `teams` is what admits anyone but the owner |