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
11 changes: 10 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ mints through the host's *real* gateway handlers at that same ref
| `datasource/` | the same facade over `DatasourceService` |
| `settings/` | presence-aware `Field[M, T]` runtime; `JSONCodec` is the only typed↔ProtoJSON boundary |
| `settings/catalog/` | Go / TypeScript / proto catalog renderers that `module-compose` calls |
| `moduleauthority/` | a module's own authority: gateway broker mints; direct Accounts authority or explicitly selected gateway authority transport. Work Contexts are opaque `string`s, shape-checked only by `core/workcontext.Decode` |
| `moduleauthority/` | a module's own authority: gateway broker mints; direct Accounts authority or explicitly selected gateway authority transport. Work Contexts are opaque `string`s, shape-checked only by `core/workcontext.Decode`. `verifier_sources.go` serves core's three live-authority sources to a callee's verifier without verifying anything itself |
| `gen/` | generated output. Regenerated, never edited |
| `conformance/hostgateway/` | `moduleauthority` minting through the host's real gateway at the `SOURCE.txt` ref |
| `internal/apiboundary` | the public-API boundary gate, with fixtures that prove it still looks |
Expand Down Expand Up @@ -145,6 +145,15 @@ examples. Read it rather than re-deriving the API from the tree.
hand-written parser, and never a decoded claim treated as a finding.
Verifying belongs to the receiver (`sdk-go/workcontext`); a callee-side
verifier does not come back here.
**A SOURCE is not a verifier.** `WorkContextSources` implements core's
`SealSource`, `RevisionSource` and `GrantSource` over the host's
`ResolveWorkContextAuthority`, and that belongs here: it holds no key, reads
no signature and decides nothing about a capability — it is a client of the
ISSUER's live state, which is this SDK's whole job. The rule above is about
the decision, and the decision stays in `sdk-go/workcontext`. A source may
read a claim off a capability to NARROW what it asks (the owner it questions
is the capability's own) and never to settle anything: the host checks the
same claim against the same capability, so a misreading refuses.
- **The release version tracks the saas-starter module version**, and
`SOURCE.txt` records the proto ref `gen/` came from. Both move together with
a regeneration, or the tag claims a contract it does not carry.
Expand Down
63 changes: 63 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@

### Added

- **`moduleauthority.WorkContextSources`**, the three live-authority sources a
callee's verifier needs, over
`ModuleCapabilitiesService/ResolveWorkContextAuthority`
(module-saas-starter#1107). A composed module that receives a Work Context
could previously only run core's forwarding-hop `Inspector` — no seal, no
epoch, no revision, nothing consumed — so a capability whose installation was
revoked, whose build was replaced, whose binding was withdrawn or whose
principal's epoch had moved stayed accepted for its whole life. These
implement `corework.SealSource`, `corework.RevisionSource` and
`corework.GrantSource`, so a consumer hands one value to
`sdk-go/workcontext`'s `NewVerifier` as all three and brings its own replay
store (durable and shared when it runs more than one replica).

This module still **verifies nothing**: it holds no key, reads no signature
and decides nothing about a capability. A source is a client of the ISSUER's
live state, which is this SDK's job; verification stays in
`sdk-go/workcontext`.

`WorkContextSources` is built per presented capability and takes exactly one
required field, the capability itself — the host authenticates it, requires it
to be addressed to the calling module and to its tenant, and answers only
about the identifiers it names, so there is no tenant, audience or owner for a
caller to supply or get wrong. Answers are translated and never interpreted: a
state this client does not understand, an answer on another arm, and an answer
about another principal, installation or binding are each
`ErrUnreadableAuthorityAnswer`. Positive answers are cached for at most
`AuthorityCacheBound` (2s, a ceiling rather than a default, because a cached
answer delays a revocation by exactly that long); refusals are never cached.
A host refusal arrives with its connect code, which is how a caller tells
`CodeFailedPrecondition` ("the issuer decided; mint again") from
`CodeUnavailable` ("the lookup failed; retry").

- **`moduleauthority.WorkloadTokenFile()`**, the one answer to "where is this
module's projected token": the file `CODEFLY__WORKLOAD_TOKEN_FILE` names
when it is set to a non-blank value, and the pod mount
Expand All @@ -20,6 +52,37 @@
`TOKEN_FILE` in its `workload-identity` configuration and passes
`ProjectedTokenFile(path)` itself.

### Changed

- **`gen/` moves from module-saas-starter v0.0.94 to `9ab36e7e`** (the merge
commit of #1119 on `main`), which is where `ResolveWorkContextAuthority`
first exists. It carries every accounts contract change made since v0.0.94 —
`datasource`, `installations`, `api_keys`, `module_registration` and
`work_contexts` all move — and those changes are the host's, not this SDK's.

- **`moduleauthority.ErrNoHostStateToMintFor`**, for HTTP **412** on the module
Work Context and operation-context mints. The gateway at this ref relays
accounts' `FailedPrecondition` as 412, where v0.0.94 relayed it as a 502 — so
a workload the host admits that is installed in no organization it may mint
for used to read as "the host is broken" and now reads as "the host works and
there is nothing to mint for you yet". Nothing is wrong with the credential
and nothing is denied; a mint succeeds once the module is installed. Found by
`conformance/hostgateway` against the new ref, which is what that suite is
for. The source mint's own 412 keeps its own meaning
(`ErrDelegationMissing`).

### Removed (breaking)

- **`SessionId` is gone from three Work Context mint requests**, with their
`GetSessionId()` accessors: `StartTaskWorkContextRequest`,
`StartChildSessionWorkContextRequest` and `StartInstallationTaskRequest`. The
host stopped accepting a caller-named session — every session a capability
carries is now the one its caller authenticated with, or a fresh one the
issuer assigns and returns — so there is no replacement field and nothing to
set. A consumer that set one was naming a session it did not own; it drops the
assignment. These are the only exported symbols the regeneration removed:
nothing else in `gen/` lost a type, a field or an RPC.

## v0.1.2 — core v0.19.1; Work Contexts are core's, carried opaque

Closes #38. The SDK requires `github.com/codefly-dev/core v0.19.1` and no
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,65 @@ or binding is required. It checks current parent authority and delegation,
authenticated module/audience/tenant alignment, current membership, and exact
active record identity. A refusal never invokes a bearer-based fallback.

### Verifying a Work Context presented to you, at full strength

A module that RECEIVES a Work Context is its callee, and a callee verifies with
`sdk-go/workcontext`'s `PinnedVerifier` — core's verifier, with its
configuration settled. That verifier needs three live sources only the issuer
holds, and this SDK is how you reach them:

```go
sources, err := client.WorkContextSources(moduleauthority.WorkContextSourceSettings{
Capability: presented, // the capability this module is verifying
})
// Handle err before using sources.
verifier, err := workcontext.NewVerifier(workcontext.VerifierSettings{
Issuer: issuer, Audience: audience,
Seals: sources, Revisions: sources, Grants: sources,
Replay: replayStore, // yours, and durable+shared across replicas
}, keySet)
```

Without them a module can only run core's forwarding-hop `Inspector`, which
checks the encoding, signature, issuer, audience, window and shape and nothing
live — so a capability whose installation was revoked, whose build was replaced,
whose operation binding was withdrawn or whose principal's epoch has moved stays
accepted until it expires.

The capability is the only required setting, and it is the whole authorization:
the host authenticates it, requires it to be addressed to your module (its
registration prefix, or the route alias its delivered presence declares) and to
the organization your credential is bound to, and answers only about the
identifiers it names. There is no tenant, audience or owner to supply. Build one
per presented capability; a module verifying a stream of messages that carry the
same capability keeps one.

What the answers mean:

- `corework.ErrNoSeal`, `ErrNoApprovedBuild` and `ErrNoBinding` are core's own
sentinels, which its verifier turns into a refusal of the capability.
- `ErrUnreadableAuthorityAnswer` is a host answer this client will not read — a
state it does not understand, another arm, or an answer about another
principal, installation or binding. It is a refusal, never a permissive
default.
- `ErrNoApprovalGrant` is an approval the issuer never recorded.
- A host refusal arrives with its **connect** code (`connect.CodeOf`):
`CodeFailedPrecondition` means the issuer decided and the authority has
changed — mint again, do not retry — while `CodeUnavailable` means the lookup
failed and a retry may succeed.

Positive answers are cached for at most `AuthorityCacheBound` (2 seconds), which
is a **ceiling**: a cached answer delays a revocation by exactly its bound, so a
larger one is refused rather than clamped, and `CacheEntries: -1` turns caching
off. Refusals are never cached. One verification costs five or more round trips
(the revision, the seal, an epoch and a build for the owner and for each actor
hop, a binding), so decide the cadence for a stream deliberately rather than
discovering it from a latency graph.

This module verifies nothing: it holds no key, reads no signature and decides
nothing about a capability. The decision is core's, through
`sdk-go/workcontext`.

### Gateway authority transport and audit declarations

`moduleauthority.NewGatewayAuthority(seams, credentials)` explicitly selects
Expand Down
9 changes: 7 additions & 2 deletions SOURCE.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
gen/ generated from codefly-dev/module-saas-starter@v0.0.94
gen/ generated from codefly-dev/module-saas-starter@9ab36e7efff8cfd15d48f319dfa540e52c630725
(accounts public proto: module/services/accounts/proto)
(auth gateway: module/services/auth-gateway/code)

module version: 0.1.0 (module/module.package.codefly.yaml)
This SDK's release is matched to the saas-starter module version above.

Released upstream tag: v0.0.94.
Released upstream tag: none — this is the merge commit of
codefly-dev/module-saas-starter#1119 on `main`, which is where
ResolveWorkContextAuthority first exists. A commit is a ref this repository's
gen-drift job resolves, so the SDK does not wait on a deploy tag to carry a
contract its consumers need; the next regeneration moves to whichever tag
overtakes it.
Regeneration does not publish an SDK release.
2 changes: 1 addition & 1 deletion conformance/hostgateway/testdata/sdk_conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ func TestSDKConformanceRefusals(t *testing.T) {
{"work execution unestablished", mintWork, refusal(codes.Unavailable, ""), 503, moduleauthority.ErrExecutionUnestablished},
{"work invalid request", mintWork, refusal(codes.InvalidArgument, ""), 400, nil},
{"work unauthenticated", mintWork, refusal(codes.Unauthenticated, ""), 502, nil},
{"work failed precondition", mintWork, refusal(codes.FailedPrecondition, ""), 502, nil},
{"work failed precondition", mintWork, refusal(codes.FailedPrecondition, ""), 412, moduleauthority.ErrNoHostStateToMintFor},

{"operation unproven", mintOperation, refusal(codes.Unauthenticated, ""), 401, moduleauthority.ErrInvalidCredentials},
// The same organization refusal is a 403 on the operation mint.
Expand Down
Loading
Loading