Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This repository holds two things, and only these two:
2. **The local authority** (`modules/authority/services/local`): the development
implementation. One process that signs in fixture people with their roles,
issues and verifies Work Contexts, knows the composition from the rendered
manifests, and holds the fixture installations in memory. No accounts
manifests, and holds the installations its composition declares in memory. No accounts
service, no stores, no platform frontend. Running a solution locally is the
solution, the modules it consumes, and this.

Expand Down Expand Up @@ -130,7 +130,7 @@ design.
|---|---|
| The solution registers its backend and page | `/modules/_register`, `/modules/_registration-token` (gateway `gateway_module_routes.go`); `/.well-known/solution.json` and `/.well-known/capabilities` served by the runtime and proxied |
| Routes to the solution | `/solutions/<id>/*` (gateway), `/api/solutions/<id>/proxy/*` (frontend), module-federation assets, the registry snapshot, the 120 s lease |
| Who may use it | `ListSolutionEntitlements` (gateway `handleSolutionEntitlements`); `InstallationService`: `InstallSolution`, `GrantInstallationToTeam`, `ListInstallations`; installations come from the fixture file |
| Who may use it | `ListSolutionEntitlements` (gateway `handleSolutionEntitlements`); `InstallationService`: `InstallSolution`, `GrantInstallationToTeam`, `ListInstallations`; installations come from the composition's fixture-composition document |
| The solution is installed | host → solution `POST /_codefly/installations` with a Work Context, retried until acknowledged |
| The solution acts for a viewer against a module | `StartTask` under resource and installation confinement (`x-codefly-resource`, installation header) |

Expand Down Expand Up @@ -427,12 +427,17 @@ The local fixture includes `no-roles` in Acme for that case.
The suite proves it as a matrix: the fixture states, for each person at each
audience, which kind and action is allowed and which refused, and the suite asks
for exactly that scope and expects the answer. The local authority's fixture carries
two organisations, a person with no roles or teams, an automatically generated
developer role, and a solution installed under different roles. The host supplies its own matrix from its
two organisations, a person with no roles or teams and an automatically generated
developer role; the test kit's composition document installs a solution under
different roles in each. The host supplies its own matrix from its
own fixture. When the fixture signs, the suite composes its solution as a carrier
with the authority's document carrying its roles; the sketch remains for a
fixture that does not sign.

The fixture names only the authority's own vocabulary. A composing workspace
adds what its modules and solutions need: grants on the fixture's roles,
installations and stand-in modules, through the versioned `fixture-composition`
document ([what a composition adds to the fixture](modules/authority/services/local/README.md#what-a-composition-adds-to-the-fixture)).
The local authority also logs a delivery-time role × method report and
generates its local developer role from concrete delivered permissions. See
[local permission diagnostics](modules/authority/services/local/README.md#permission-diagnostics-during-local-development)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The default of the local authority's fixture-composition document: a
# composition that composes the authority and supplies no document of its own
# adds nothing to the fixture. A composing workspace's
# configurations/local/fixture-composition.yaml replaces this one whole
# (services/local/README.md, "What a composition adds to the fixture").
schema: codefly.dev/authority/fixture-composition/v1
60 changes: 56 additions & 4 deletions modules/authority/services/local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,57 @@ Generated code (`pkg/gen`, `*_gen.go`, `main.go`) comes from `codefly generate`
and the agent; `work.go` and everything under `pkg/business`, `pkg/infra` and
`plugins` is written here.

## What a composition adds to the fixture

The embedded fixture (`code/fixtures/local.yaml`) says who exists and which
organisation roles they hold, in the authority's own vocabulary only
(`console.identity`). It names no module's kinds, no solution and no stand-in
module. Each of those belongs to the workspace that composes it, which adds it
through the workspace configuration document `fixture-composition`
(`configurations/local/fixture-composition.yaml` in that workspace):

```yaml
schema: codefly.dev/authority/fixture-composition/v1
roles: # grants added to roles the fixture defines
- organization: acme
name: editor
scopes:
- {kind: ledger.entry, actions: [read, write]}
installations: # installations of a solution the run composes, the fixture's own path
- {organization: acme, target: atlas, role: viewer, owner: alex, teams: [finance]}
modules: # a module the run supervises itself, until its own delivery
- module: oracle
kinds: [{kind: oracle.answers, actions: [read]}]
```

The authority module ships an empty default
(`modules/authority/configurations/local/fixture-composition.yaml`, the schema
line alone). A composing workspace's own document replaces it whole, so a
composition that supplies none boots on the fixture alone.

**The contract.** The `schema` line versions the document. Its sections, and
the fixture's organisation, role and person ids it names (`acme`, `globex`,
`viewer`, `editor`, `admin`, `alex`, …), are a contract with every composing
repository. A breaking change is a new schema version, never an edit to v1.

**What is refused.** Each of these refuses the boot by name and changes no
role:
- an empty or null document, a section spelled `null`, an unknown field, or a
schema other than v1;
- an entry naming a role the fixture does not define, or the generated
`developer` role (it is derived from the delivered catalog);
- no scope, or a scope with no action;
- a wildcard kind or action (`*`): a composition grants the concrete
permissions its modules serve, so viewer, editor and administrator stay
distinct;
- a kind or action outside the grammar, surrounding whitespace included;
- a malformed resource matcher;
- an installation whose organisation or owner the fixture does not define.

The boot log names each effect, for example `the composition grants
acme/editor: ledger.entry:[read write]`, or says that the composition adds
nothing. The local authority refuses to start in a pod.

## Permission diagnostics during local development

The local fixture's `developer` role uses `generated: true` and declares no
Expand All @@ -61,10 +112,11 @@ regressions. This does not qualify the production host or live browser journeys.

Person issuance sorts and deduplicates a copy of the session's team attribution
before signing. Fixture order therefore does not affect a module's ability to
consume the context; the original session remains unchanged. The local Globex
administrator explicitly holds `documents.collection:read` alongside document
read/write; the role name itself implies no additional grants. Issuer regressions
and the shared fixture matrix cover this grant and a person who lacks it.
consume the context; the original session remains unchanged. A role name implies no
grants: the test kit's composition
(`code/pkg/business/testkit/testdata/composition.yaml`) gives the Globex
administrator its collection read explicitly. Issuer regressions and the shared
fixture matrix cover that grant and a person who lacks it.

## Signed delivery lifecycle

Expand Down
84 changes: 84 additions & 0 deletions modules/authority/services/local/code/composition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"gateway/pkg/business/state"
"gateway/pkg/business/testkit"

"github.com/codefly-dev/core/configurations"
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
"github.com/codefly-dev/core/resources"
codefly "github.com/codefly-dev/sdk-go"
)

// Exercise the production boot hook through Codefly's file loader and SDK
// document carrier, rather than passing fixture bytes straight to state.
func TestCompositionReachesBootThroughCodefly(t *testing.T) {
ctx := context.Background()
t.Setenv(resources.EnvironmentPrefix, "local")
t.Cleanup(func() { _ = codefly.InjectConfigurations() })
for _, tc := range []struct {
name, document, path, reason string
granted bool
}{
{name: "absent", reason: "no fixture-composition document was delivered"},
{name: "shipped default", path: testkit.DefaultCompositionPath()},
{name: "composition", document: "schema: codefly.dev/authority/fixture-composition/v1\nroles:\n - organization: acme\n name: editor\n scopes:\n - {kind: ledger.entry, actions: [write]}\n", granted: true},
{name: "unknown role", document: "schema: codefly.dev/authority/fixture-composition/v1\nroles: [{organization: acme, name: missing, scopes: [{kind: ledger.entry, actions: [write]}]}]", reason: "acme/missing is not a role"},
{name: "null", document: "null", reason: "not a mapping"},
{name: "unversioned", document: "{}", reason: "schema is"},
} {
t.Run(tc.name, func(t *testing.T) {
if err := codefly.InjectConfigurations(); err != nil {
t.Fatal(err)
}
if tc.path != "" {
data, err := os.ReadFile(tc.path)
if err != nil {
t.Fatal(err)
}
tc.document = string(data)
}
if tc.document != "" {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "fixture-composition.yaml"), []byte(tc.document), 0600); err != nil {
t.Fatal(err)
}
infos, err := configurations.LoadConfigurationInformationsFromFiles(ctx, dir)
if err != nil {
t.Fatal(err)
}
if err := codefly.InjectConfigurations(&basev0.Configuration{Origin: resources.ConfigurationWorkspace, Infos: infos}); err != nil {
t.Fatal(err)
}
}
k := testkit.New(t)
ledger := state.Manifest{Module: "ledger", Principal: "ledger", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}}
if _, err := k.State.ApplyManifest(ledger); err != nil {
t.Fatal(err)
}
err := applyComposition(ctx, k.State)
if tc.reason != "" {
if err == nil || !strings.Contains(err.Error(), tc.reason) {
t.Fatalf("got %v, want %s", err, tc.reason)
}
} else if err != nil {
t.Fatal(err)
}
found := false
for _, scope := range k.State.RoleScopes("acme", "editor") {
if scope.GetResourceKind() == "ledger.entry" {
found = true
}
}
if found != tc.granted {
t.Fatalf("composition grant present = %v, want %v", found, tc.granted)
}
})
}
}
91 changes: 11 additions & 80 deletions modules/authority/services/local/code/fixtures/local.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# The local authority's fixture: who exists, what they hold, what is installed.
# The local authority's fixture: who exists and which organisation roles they
# hold, in the authority's own vocabulary only. What a role holds at a module,
# which solutions are installed and which modules a run supervises itself
# belong to the composition that composes them: each composing workspace adds
# them through its fixture-composition document (services/local/README.md,
# "What a composition adds to the fixture"). The organisation, role and person
# ids below are that document's contract with every composing repository.
organizations:
- id: acme
name: Acme
Expand All @@ -8,73 +14,17 @@ roles:
- organization: acme
name: viewer
scopes:
- {kind: documents, actions: [read]}
- {kind: records.records, actions: [read]}
- {kind: records.types, actions: [read]}
- {kind: directory.entry, actions: [read]}
# The authority console: a viewer reads who they are.
- {kind: console.identity, actions: [read]}
# Robin: a viewer reads the conversations they own, and runs no turn.
- {kind: robin.conversations, actions: [read]}
# Tracing: a viewer lists the organisation's agent runs, never their text.
- {kind: tracing.trace, actions: [list_completed, read]}
# Evaluation: a viewer reads the runs and the online scores.
- {kind: evaluation.runs, actions: [read]}
- {kind: evaluation.online, actions: [read]}
# A viewer reads only some document collections: the directory's, the
# wiki's, and every shared one. The authority answers this when asked
# (ListResources); the token carries the kind alone. A collection's id
# is the store's namespace, "derived" and its producer joined by a unit
# separator (\x1f).
- kind: documents.collection
actions: [read]
resources:
- {prefix: "derived\x1fdirectory"}
- {prefix: "derived\x1fwiki"}
- {pattern: "shared-[a-z]+"}
- organization: acme
name: editor
scopes:
- {kind: documents, actions: [read, write]}
- {kind: documents.collection, actions: [read]}
- {kind: records.records, actions: [read, write]}
- {kind: records.types, actions: [read]}
- {kind: directory.entry, actions: [read, write]}
# An editor invokes the model: robin's model child is the authority's to
# grant from what the editor holds at the model, and a turn the editor
# runs calls a profile.
- {kind: modelservice.profiles, actions: [read, invoke]}
# Robin: an editor runs turns and keeps their conversations.
- {kind: robin.tasks, actions: [execute, read]}
- {kind: robin.runs, actions: [read, start, cancel]}
- {kind: robin.conversations, actions: [read, write, share, delete]}
- {kind: robin.intents, actions: [read, run]}
# Tracing: an editor reads their organisation's agent runs, text
# included where the composition keeps it.
- {kind: tracing.trace, actions: [list_completed, read, read_content]}
# Evaluation: an editor runs an evaluation (creates a run, submits its
# outputs, completes it) and reads the scores.
- {kind: evaluation.runs, actions: [read, write]}
- {kind: evaluation.online, actions: [read]}
scopes: []
- organization: acme
name: admin
scopes:
- {kind: documents, actions: [read, write, ingest]}
- {kind: documents.collection, actions: [read, transfer, admin]}
# Robin: an administrator sees and changes what the intent store holds.
- {kind: robin.intents, actions: [read, read_config, import, configure, activate]}
# Evaluation: an administrator publishes definitions, registers and
# freezes datasets, labels them and approves labels (a reviewer who is
# not the author); the developer, who writes them, holds everything.
- {kind: evaluation.definitions, actions: [publish]}
- {kind: evaluation.datasets, actions: [write]}
- {kind: evaluation.labels, actions: [write, approve, admin]}
- {kind: evaluation.runs, actions: [read]}
- {kind: evaluation.online, actions: [read]}
scopes: []
- organization: globex
name: admin
scopes:
- {kind: documents, actions: [read, write]}
- {kind: documents.collection, actions: [read]}
scopes: []
# Generated from the current delivered catalog, recomputed on each delivery,
# in Acme. A playground signs its person in as dev.
- organization: acme
Expand Down Expand Up @@ -129,22 +79,3 @@ people:
organization: acme
roles: [developer]
teams: [engineering]
installations:
# Acme's wiki reaches finance and engineering as viewers; its owner installs.
- organization: acme
target: wiki
role: viewer
owner: alex
teams: [finance, engineering]
- organization: globex
target: wiki
role: editor
owner: zoe
teams: [ops]

# A model a local run supervises itself (a playground's bridge to its model
# provider) is an audience like any composed module.
modules:
- module: model
kinds:
- {kind: modelservice.profiles, actions: [read, invoke]}
Loading