Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d3d0729
Bind hop admissions to exact methods, audiences and single-use nonces
antoinetoussaint-byte Oct 26, 2026
d87cc7e
Merge current main and preserve authority migration and key-refresh r…
antoinetoussaint-byte Oct 26, 2026
fb2208c
Pin the published admission client and migrate merged conformance cal…
antoinetoussaint-byte Oct 26, 2026
8413772
Clarify replay volume isolation and shared-store deployment scope
antoinetoussaint-byte Oct 26, 2026
33c436b
Pin admission consumers to the verified merged client revision
antoinetoussaint-byte Oct 26, 2026
4b1c57a
Merge main: signed hop admissions over Connect routing and bare verif…
antoinetoussaint-byte Oct 26, 2026
c189ca1
Pin admission consumers to the merged client revision
antoinetoussaint-byte Oct 26, 2026
3d0a11c
Fix the security review's blockers: admission window, REST, replay de…
antoinetoussaint-byte Oct 26, 2026
2a8cb59
Pin admission consumers to the review-fix client revision
antoinetoussaint-byte Oct 26, 2026
8a3e953
Say in hoptest that the memory store is refused outside local
antoinetoussaint-byte Oct 26, 2026
073a40e
Consume a single-use admission exactly once: reserve at arrival, hold…
antoinetoussaint-byte Oct 26, 2026
0d04d8b
Merge origin/main (#30 local setup, #7 installation-scoped grants) in…
antoinetoussaint-byte Oct 26, 2026
fc6fca1
feat(authority): Keys publishes the hop's admission keys (authority 0…
antoinetoussaint-byte Oct 26, 2026
d534e62
feat(stamp): choose the published admission key a proof is signed by
antoinetoussaint-byte Oct 26, 2026
c5b5e23
fix(stamp): the admission entry names no generated type
antoinetoussaint-byte Oct 26, 2026
f1a7f05
fix(hop): verifiers take the hop's admission key from Keys; judge arr…
antoinetoussaint-byte Oct 26, 2026
b184af7
fix(authority-client): Python and TypeScript verifiers take the hop's…
antoinetoussaint-byte Oct 26, 2026
4984c5e
docs: the hop's admission key is published in Keys; StatsHandler; col…
antoinetoussaint-byte Oct 26, 2026
611932f
Merge origin/main (#31 one destination per service) into fix/hop-stam…
antoinetoussaint-byte Oct 26, 2026
2552d5c
chore: gofmt the local authority's imports
antoinetoussaint-byte Oct 26, 2026
e1966db
feat(authority): the hop registers its admission key (authority 0.3.3)
antoinetoussaint-byte Oct 26, 2026
b9b716b
fix(hop): the hop registers its admission key; the local authority de…
antoinetoussaint-byte Oct 26, 2026
655fe4f
fix(hop): name the published admission key in the hop's log
antoinetoussaint-byte Oct 26, 2026
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
9 changes: 9 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ jobs:
with:
go-version-file: ${{ matrix.module }}/go.mod
cache-dependency-path: ${{ matrix.module }}/go.sum
# The hop library's single-use admission regressions run over the real
# transport and a real Postgres: the runner's own, started here, reached
# over its socket as the runner's user, with no password written down.
- name: start Postgres for the hop admission regressions
if: matrix.module == 'libraries/hop/go'
run: |
sudo systemctl start postgresql.service
sudo -u postgres createuser --superuser "$USER"
echo "HOP_TEST_POSTGRES=host=/var/run/postgresql dbname=postgres" >> "$GITHUB_ENV"
- name: go vet
run: go vet ./...
- name: go test -race
Expand Down
55 changes: 38 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ staging validated.
**Rung 1 first; it is the one that matters (owner, 2026-10-21).** The loop:

1. Cut a branch in module-saas-starter. accounts declares in its module
interface that it implements `codefly.dev/authority@0.3.1`, `codefly.dev/composition@0.2.0`,
interface that it implements `codefly.dev/authority@0.3.2`, `codefly.dev/composition@0.2.0`,
`codefly.dev/installation@0.1.0` and `codefly.dev/session@0.2.0`, takes this repo's
protos (bindings via `codefly generate proto`), and serves the four services as
adapters over what it already has: `Issue` → StartTask / StartInstallationTask /
Expand Down Expand Up @@ -276,7 +276,7 @@ Gap 7 was a decision, taken on 2026-10-22: the interface carries the host's name
## The namespace, settled (2026-10-22)

The interfaces are the host's contract, so they carry the host's names: publisher
`codefly.dev` (ids `codefly.dev/authority@0.3.1`, `composition`, `installation`,
`codefly.dev` (ids `codefly.dev/authority@0.3.2`, `composition`, `installation`,
`installable`, `session`) and protobuf packages `host.authority.v1`,
`host.composition.v1`, `host.installation.v1`, `host.session.v1` under `proto/host/`.
Not `codefly.*`: the host's buf configuration excludes a `codefly/` proto directory
Expand Down Expand Up @@ -505,32 +505,28 @@ gRPC-only composition. An empty HTTP routing table does not remove that
endpoint from Codefly's readiness checks. Requests still pass through the
authority decision and are refused when no corresponding HTTP upstream exists.

The module behind it carries one interceptor from `libraries/hop/go` and no
other authority code. The stamp binds the digest of the Work Context the hop
checked, so a module that needs the full claims decodes that exact token
without verifying it again. The interceptor gives each call the identity the hop
stamped, and refuses a call that did not come through the hop. It verifies no
token and asks no authority. The stamp is believed only with the run's proof,
which Codefly generates and delivers to the hop and the module alone. In a cell
the mesh plays that part.
The module behind it carries the interceptor from `libraries/hop/go`. It
verifies a short-lived Ed25519 admission bound to the exact method, audience and
token digest, authenticates the identity, and atomically consumes the call nonce
in a durable store shared by replicas. The hop alone receives the Codefly-generated
signing seed; modules receive its public key. See the breaking
[admission protocol and delivery migration](libraries/hop/README.md).

The hop and the edge ask the host the composition names in the `authority`
group's `endpoint`, the key every module reads for the same host, so pointing
that key at saas accounts' Connect listener moves them with the modules:
`host:port` and `http://` are the run's plaintext transport (h2c), `https://` is
TLS. A composition that names none reaches the authority module's local service.
Under `hop/ENFORCEMENT=warn` the hop lets through a call the authority verified
and the method's policy would have refused, stamped with that refusal as its
advisory; a call with a missing, unsound or misaddressed Work Context, or one
the authority could not be asked about, is refused in either mode.
Signed admissions require enforced method policy in every environment.
Legacy `hop/ENFORCEMENT=warn` configuration is refused with a reason.

This run's hop fronts the console, whose one operation answers who the call is
for. Run it from the playground workspace, `playground/`, which composes this
repository's modules with the platform's stores and the directory's page:

```bash
cd playground
codefly config generate hop PROOF
codefly config generate hop-signing SEED --service authority/hop
codefly config generate internal-auth CODEFLY_INTERNAL_TOKEN
codefly run service console/api authority/hop --local-agents
```
Expand Down Expand Up @@ -595,7 +591,8 @@ resource IDs does not satisfy that requirement, even with a wildcard action;
several restricted scopes do not become a whole-kind grant. A whole-kind grant
alongside restricted scopes still satisfies its declared action. Refusals mark
resource-limited holdings in the diagnostic. `Advise` reports the same refusal
without widening or discarding the checked capability, so warn mode can log it.
without widening or discarding the checked capability. An advisory result is
not sufficient to issue a hop admission.
Checking a capability without a method and reading its resource scope remain
supported. Modules still own resource visibility, ownership and installation
checks; method admission does not replace them.
Expand Down Expand Up @@ -979,7 +976,7 @@ interface evolution rules require a minor bump below 1.0. Installation Issue
requires its installed caller's workload credential; an installation ID alone
is never authority. Workloads name the work item's organization; the local
sole-installation convenience is not required of other hosts. Advisory Check
is a local development extension, and the hop rejects warn outside local runs.
is a local diagnostic extension; the hop requires enforcing Check in every environment.

`codefly.dev/session@0.2.0` adds person and organization display names to Current
and SignIn (fields 7 and 8). Missing fields fail conformance. These implement the
Expand Down Expand Up @@ -1023,6 +1020,30 @@ installation-owned rows contain only attached ids, and an empty owned set emits
no grant. The local host is an adapter to that core. Accounts adoption and
its durable adapter are not qualified by a local authority test.

`codefly.dev/authority@0.3.2` adds `KeysResponse.admission_keys` and
`PublicKey.purpose`: `Keys` publishes the hop's admission verification keys
(purpose `hop-admission`) beside the Work Context keys (purpose
`work-context`), and every verifier takes the admission keys from the `Keys`
read it already caches and refreshes, so no consumer is configured with a hop
public key. The admission keys are a field of their own, never entries of
`keys`, so a verifier that reads `keys` alone (an earlier release, or the JSON
Web Key Set) can never trust a hop key to sign a Work Context. More than one is
published while a hop key rotates. It is additive, so `>=0.2.0 <0.4.0`
consumers admit it; a host that publishes no admission key leaves every hop
admission refused (`keys_unavailable`), so `accounts` must publish its hop's key
before a cell's modules adopt this verifier.

`codefly.dev/authority@0.3.3` adds `RegisterAdmissionKey`: the hop presents its
public key under its own workload credential (it declares `module-identity`),
the authority refuses any other workload by name, and `Keys` publishes the key
until its registration lapses (`AdmissionLease`, two minutes) unless the hop
renews it, which it does while it runs. A restarted authority therefore learns
the key again at the next renewal, a restarted hop re-registers the same entry,
and a key no running hop signs with is withdrawn without being named. The seed
never leaves the hop; nothing about the hop is configured on the authority. It
is additive too. The local authority implements it; `accounts` implementing the
same registration is follow-up work in the host.

The 0.3.1 authority version is an additive pre-stable revision selected for
this refresh so existing `>=0.2.0 <0.4.0` consumers admit it. It retains 0.3.0's
`AuthorizeTransfer` and adds attachment operations. Composition remains 0.2.0.
4 changes: 2 additions & 2 deletions interfaces/authority/interface.codefly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ kind: interface
publisher: codefly.dev
name: authority
description: What a module or solution calls on its host to be issued, checked and verified authority. Implemented by accounts and by the local authority.
version: 0.3.1
version: 0.3.3
type: connect
protobuf:
package: host.authority.v1
services:
- name: AuthorityService
methods: [Issue, Check, Keys, ListResources, AuthorizeTransfer, AttachResource, DetachResource, Delegate, RevokeDelegation, GetDelegation]
methods: [Issue, Check, Keys, RegisterAdmissionKey, ListResources, AuthorizeTransfer, AttachResource, DetachResource, Delegate, RevokeDelegation, GetDelegation]
2 changes: 1 addition & 1 deletion libraries/authority-client/contract/catalog.codefly.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"kind": "protobuf",
"package": "api",
"path": "contract.binpb",
"digest": "sha256:58c434f9e507929a6cb98a6056464df2cbc27ad486b7c85c88c282d6fc092146",
"digest": "sha256:1c51ec903d141b1df6c0eff0fcaa10aa48b6633e0753a13718bef83d086dc0cb",
"services": [
{
"name": "LocalService",
Expand Down
Binary file modified libraries/authority-client/contract/contract.binpb
Binary file not shown.
14 changes: 6 additions & 8 deletions libraries/authority-client/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authenticates the Work Context the hop forwarded and projects its claims:
import claims "github.com/codefly-dev/authority/libraries/authority-client/go/workcontext"

md, _ := metadata.FromIncomingContext(ctx)
c, err := claims.Authenticate(md, proof, audience, trust)
c, err := claims.Authenticate(md, audience, trust)
// err wraps claims.ErrUnauthenticated and names the reason.
allowed := c.Allows(resourceKind, action, exactResourceID)
```
Expand All @@ -17,13 +17,11 @@ A module does not call this directly: `hop.Verifier`
runs it on every call. `Authenticate` is the generated-code-free check it rests
on, which the shared conformance suite also drives against both hosts.

It checks, in order: this run's hop proof, exactly one token and the stamp's
SHA-256 digest of it (`stamp.Context`); then Core's `Inspector`: canonical
decode, the issuer pin, the Ed25519 signature under the published key the token
names, the audience is this module, not-before and expiry within Core's skew,
and structure and attenuation. It consumes nothing and asks the authority
nothing: liveness is the authority's Check the hop made, bound to this call by
the digest. Proof and digest alone authenticate nothing; the signature does.
It checks one digest-bound token with Core's Inspector: canonical decode,
issuer, signature, audience, window and attenuation. This authenticates identity
only. Method authorization and replay protection require `hop.Verifier` and the
[signed admission protocol](../../hop/README.md). A token digest alone does not
prove that the hop made a live Check.

`Scopes()` returns deep copies of the final actor's grants, or the owner's grants
when there is no actor. An empty final actor grants nothing. `Allows` evaluates
Expand Down
Loading
Loading