Skip to content

Commit afd03e6

Browse files
fix!: build on core v0.19.1, sdk-go v0.3.2 and workcontext/v0.1.0 (#64)
The pins move to released tags only: core v0.9.0 -> v0.19.1, sdk-go v0.3.0 -> v0.3.2, and sdk-go/workcontext from the dd6ca01 pseudo-version to its first tag, workcontext/v0.1.0 (8 commits ahead of dd6ca01). Each break the move exposed is fixed where this runtime owns it: - core v0.15 renamed the structural read Inspect -> Decode. The broker credential reads the token it was issued through corework.Decode. - core's old spelling is now Inspector, a forwarding hop's SIGNATURE check, and Authenticator is a callee's; neither contains "Verif", so the verifier gate named neither. It refuses both by name now, with a probe for each and a control showing Decode is not refused. - sdk-go v0.3.1 resolves no endpoint until codefly.Init has pinned the consumer module from CODEFLY__MODULE. The boot only called LoadEnvironmentVariables, so against v0.3.2 it resolved no gateway, no frontend and no port and blamed provisioning. start() calls Init; loadConfig separates its two failures; validate() refuses an unidentified module naming CODEFLY__MODULE, overrides or not, which is the SDK's stated contract ("a process that fails Init must not serve"). - core added the empty-audience conformance fixture (40 at v0.19.1). It is schema-refused inside Decode, so it is refused here, before a call. - core enforces endpoint visibility: the on-disk workspace fixture's gateway endpoint now declares itself public, as a host's must for a solution in another module to resolve it. Breaking for consumers: the module's minimum core and sdk-go move with it, and Serve refuses a process that names no CODEFLY__MODULE (every Codefly-managed flow sets it). No exported Go API changes. Closes #62 Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 274ba70 commit afd03e6

11 files changed

Lines changed: 272 additions & 64 deletions

‎AGENTS.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ embedding it.
171171
suite until this file describes the exception — and so does a read reachable
172172
from the serving surface, because a helper boot shares with serving code sits
173173
in that tree while running long after `validate()`.
174+
What precedes `loadConfig` is `codefly.Init`, not a bare
175+
`LoadEnvironmentVariables`: since sdk-go v0.3.1 the SDK resolves no endpoint
176+
until `Init` has pinned the consumer module from `CODEFLY__MODULE`, so a boot
177+
that only loaded the carriers resolved nothing and blamed provisioning.
178+
`loadConfig` separates `Init`'s two failures and `validate()` refuses an
179+
unidentified module by name, overrides or not — the SDK's contract is that a
180+
process failing `Init` does not serve.
174181
- Those gates ask the compiler, not the source text, and that is not a style
175182
choice: four review rounds each found one more spelling a syntactic rule
176183
lost. Constants come from `types.Info` (a conversion, a chain, a local
@@ -206,7 +213,7 @@ embedding it.
206213
built from named constants or cased differently is the same shape to it.
207214
- **Never sign, parse or verify a Work Context here.** There is one
208215
implementation, Core's; this runtime obtains a credential through the native broker
209-
client and delegates structural inspection to Core, carries it as the string it travels as, and lets the far end
216+
client and delegates its structural read to Core, carries it as the string it travels as, and lets the far end
210217
decide. It declares none of a capability's fields and holds no verifier —
211218
core's needs the issuer's live revision, replay, grant and seal sources, which
212219
a solution does not have. `work_context_boundary_test.go` fails on a signing
@@ -449,8 +456,11 @@ embedding it.
449456
every mint, checks live trust and admission before and after it, disables
450457
redirects/proxies/reuse, and holds the presented leaf to the frozen principal.
451458
The SDK still owns the certificate reloader and authority reader. Core's
452-
`Inspect` is the only structural reader of the returned token; no signed
453-
message is constructed or decoded here, and inspection verifies no signature.
459+
`Decode` is the only structural reader of the returned token; no signed
460+
message is constructed or decoded here, and that read verifies no signature.
461+
It was `Inspect` until core v0.15, and the old name is now core's
462+
`Inspector` — a forwarding hop's **signature check** — so it is a verifier to
463+
this package and the verifier gate refuses it, with `Authenticator`, by name.
454464
A private credential view keeps the public SDK `CredentialSource` compatible.
455465
- **Only 503 and transient transport failures retry.** Timeout, connection
456466
refused and connection reset are identified by error types/identities, never

‎README.md‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ The request is `POST /modules/_work-context`, with exactly
6969
`{"prefix":"<Manifest.ID>"}`, `Authorization: Bearer <projected token>`, and
7070
`X-Codefly-Internal-Token`. The prefix is the declared solution ID, separate from
7171
the frozen SPIFFE principal. The answer carries `token`, `expiresAt`,
72-
`principalId`, and `tenant`. Core alone inspects the returned capability; the
73-
runtime does not implement a signed encoding or verify an issuer's authority.
72+
`principalId`, and `tenant`. Core alone reads the returned capability, through
73+
its structural `Decode` (named `Inspect` before core v0.15); the runtime does not
74+
implement a signed encoding or verify an issuer's authority, and that read
75+
checks no signature.
7476

7577
`WorkContextRefusal.Code` and `.Message` have been removed (breaking API change).
7678
Consumers should inspect `.StatusCode` or unwrap to `GatewayError`. Broker bodies,
@@ -138,9 +140,13 @@ up, after a retry, an eviction or a restart.
138140

139141
## Configuration
140142

141-
On boot `Serve` calls
142-
`codefly.LoadEnvironmentVariables()` to load Codefly's injected carriers, then
143-
`loadConfig` resolves every address, port, and path through the codefly Go SDK
143+
On boot `Serve` calls `codefly.Init(ctx)`, which loads Codefly's injected
144+
carriers and pins the module this process belongs to from `CODEFLY__MODULE`.
145+
Every Codefly-managed flow sets that variable; the SDK (v0.3.1 and later)
146+
resolves no endpoint for a process it has not pinned, so a boot that names no
147+
module — or a different one from the module the process was first pinned to —
148+
is refused naming `CODEFLY__MODULE`, whether or not every address below was
149+
overridden. Then `loadConfig` resolves every address, port, and path through the codefly Go SDK
144150
(`github.com/codefly-dev/sdk-go`), falling back to the local native workspace
145151
map when not running under the runtime. Most values have an explicit env
146152
override for an operator with a deployment the resolver cannot see; the
@@ -156,7 +162,7 @@ identifier and the issuer](#the-resource-identifier-and-the-issuer).
156162
| What | SDK resolution | Env override (default) |
157163
|---|---|---|
158164
| Own listen port | `codefly.For(ctx).Endpoint("http").NetworkInstance()` — the Codefly-assigned port, not a fixed default | `PORT` |
159-
| Gateway URL (auth-gateway `rest`) | resolved by role — the single module owning the `auth-gateway` `rest`/`rest` endpoint, discovered from the injected carriers (or the workspace, run locally). Must be `https` | `GATEWAY_URL` |
165+
| Gateway URL (auth-gateway `rest`) | resolved by role — the single module owning the `auth-gateway` `rest`/`rest` endpoint, discovered from the injected carriers (or the workspace, run locally, where core's endpoint visibility applies: an endpoint private to the host's module is refused, not resolved, for a solution in another module). Must be `https` | `GATEWAY_URL` |
160166
| Credential mint URL | Explicit HTTPS endpoint ending in `/modules/_work-context`. The host's broker path is published; its origin must still be supplied. A 404 is a terminal endpoint/configuration error | `CODEFLY__CREDENTIAL_MINT_URL` (**required**) |
161167
| Broker perimeter credential | `codefly.For(ctx).WorkspaceSecret("module-authority", "INTERNAL_TOKEN")`, required at boot. Sent only to the execution broker as `X-Codefly-Internal-Token`; it does not replace the projected token or workload identity | — (declared workspace secret, no legacy env fallback) |
162168
| Projected service-account token | `codefly.For(ctx).WorkspaceConfiguration("workload-identity", "TOKEN_FILE")` — a **path**, re-read at every mint; defaults to `/var/run/secrets/codefly/token`, the CLI projection | `CODEFLY__WORKLOAD_TOKEN_FILE` |
@@ -1447,12 +1453,11 @@ descriptor, with protobuf JSON names.
14471453
> configuration-profile rule and `core/workcontext` come from core; and the
14481454
> certificate reloader, the authority reader and the mint client come from
14491455
> sdk-go. The `core` and `sdk-go` pins in `go.mod` carry all of it, and there is
1450-
> **no `replace` directive** — both are ordinary pseudo-version `require`s, so
1451-
> this module builds from its own tag for anyone.
1456+
> **no `replace` directive** — every one is an ordinary `require` of a released
1457+
> tag, so this module builds from its own tag for anyone.
14521458
>
1453-
> Both pins are **released** now: `core v0.9.0` and `sdk-go v0.3.0`, with the
1454-
> `sdk-go/workcontext` leaf at a `main` pseudo-version because that leaf module
1455-
> has no tag by design. This said they were unreleased branches that "must move
1456-
> to the releases at merge"; they moved. Every capability's format changed in
1457-
> that cutover, so verifiers upgrade before minters or calls fail closed in the
1459+
> The pins are `core v0.19.1`, `sdk-go v0.3.2` and the `sdk-go/workcontext` leaf
1460+
> at its first tag, `workcontext/v0.1.0` (it was a `main` pseudo-version until
1461+
> that leaf was tagged). Every capability's format changed in the v0.9.0
1462+
> cutover, so verifiers upgrade before minters or calls fail closed in the
14581463
> window — which is a statement about the rollout order, not about these pins.

‎broker_credential.go‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import (
2121
)
2222

2323
// executionCredential is private so the public SDK-based CredentialSource
24-
// remains source compatible. Core alone inspects capability bytes; this runtime
25-
// never decodes a signed message or treats inspection as verification.
24+
// remains source compatible. Core alone reads capability bytes, through its
25+
// structural Decode; this runtime never decodes a signed message itself and
26+
// never treats that read as verification — core's Inspector, which does check
27+
// a signature, is a verifier and is not reachable here.
2628
type executionCredential interface {
2729
Token() string
2830
Seal() *workcontext.SealedValues
@@ -31,13 +33,13 @@ type executionCredential interface {
3133
}
3234

3335
type brokerCredential struct {
34-
token string
35-
inspected *corework.Inspected
36-
expires time.Time
36+
token string
37+
decoded *corework.Decoded
38+
expires time.Time
3739
}
3840

3941
func (c brokerCredential) Token() string { return c.token }
40-
func (c brokerCredential) Seal() *workcontext.SealedValues { return c.inspected.Seal() }
42+
func (c brokerCredential) Seal() *workcontext.SealedValues { return c.decoded.Seal() }
4143
func (c brokerCredential) ExpiresAt() time.Time { return c.expires }
4244
func (c brokerCredential) Attach(r *http.Request) error { return workcontext.Attach(r, c.token) }
4345

@@ -155,22 +157,22 @@ func (c *brokerCredentialSource) obtain(ctx context.Context) (executionCredentia
155157
if err != nil || !expiry.After(time.Now()) {
156158
return fail()
157159
}
158-
inspected, err := corework.Inspect(issued.Token)
160+
decoded, err := corework.Decode(issued.Token)
159161
if err != nil {
160162
return fail()
161163
}
162164
audience, err := c.options.Authority.Value(c.options.Audience.Name, c.options.Audience.Key)
163-
if err != nil || inspected.Context().GetAudience() != audience {
165+
if err != nil || decoded.Context().GetAudience() != audience {
164166
return fail()
165167
}
166-
if time.Now().Before(inspected.NotBefore().Add(-corework.DefaultSkew)) {
168+
if time.Now().Before(decoded.NotBefore().Add(-corework.DefaultSkew)) {
167169
return fail()
168170
}
169171
// The broker's expiry may shorten, never extend, the signed lifetime.
170-
if inspected.ExpiresAt().Before(expiry) {
171-
expiry = inspected.ExpiresAt()
172+
if decoded.ExpiresAt().Before(expiry) {
173+
expiry = decoded.ExpiresAt()
172174
}
173-
credential := brokerCredential{token: issued.Token, inspected: inspected, expires: expiry}
175+
credential := brokerCredential{token: issued.Token, decoded: decoded, expires: expiry}
174176
if err := usableCredential(credential); err != nil {
175177
if errors.Is(err, workcontext.ErrMintRefused) {
176178
return workcontext.Credential{}, err
@@ -183,7 +185,7 @@ func (c *brokerCredentialSource) obtain(ctx context.Context) (executionCredentia
183185
if err := c.identity.holdSealedIdentity(credential.Seal()); err != nil {
184186
return workcontext.Credential{}, workcontext.ErrMintRefused
185187
}
186-
lead := min(30*time.Second, expiry.Sub(inspected.NotBefore())/5)
188+
lead := min(30*time.Second, expiry.Sub(decoded.NotBefore())/5)
187189
c.renewAt = expiry.Add(-lead)
188190
if !time.Now().Before(c.renewAt) {
189191
return fail()

‎conformance_test.go‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ func TestCoreConformanceFixturesThroughTheCarrier(t *testing.T) {
4949
if err != nil {
5050
t.Fatalf("core conformance fixtures: %v", err)
5151
}
52-
if len(fixtures) < 39 {
53-
t.Fatalf("core published %d fixtures; the kit is 39 at core v0.9.0, so this test is reading a trimmed set", len(fixtures))
52+
if len(fixtures) < 40 {
53+
t.Fatalf("core published %d fixtures; the kit is 40 at core v0.19.1, so this test is reading a trimmed set", len(fixtures))
5454
}
5555

5656
var carriedCount, refusedCount int
@@ -216,6 +216,12 @@ var boundary = map[string]carriage{
216216
"partial-operation-binding": refusedHere,
217217
"actor-without-epoch": refusedHere,
218218
"unknown-field": refusedHere,
219+
// Added after core v0.9.0 (present at v0.19.1): a capability addressed to
220+
// no audience. Core's schema refuses it inside the decode, before anything
221+
// compares it to a route, so like the rest of this group it is a property
222+
// of the bytes in hand — distinct from "another-audience", which names one
223+
// and needs the far end to say it is not theirs.
224+
"empty-audience": refusedHere,
219225

220226
// Judgements this runtime cannot make. The issuer's identity is a claim
221227
// and the key is not the trust decision; a validity window needs a clock

‎consumer_identity_test.go‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package solution
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"testing"
10+
11+
"github.com/codefly-dev/core/resources"
12+
codefly "github.com/codefly-dev/sdk-go"
13+
)
14+
15+
// testModule is the module every test in this package runs as, pinned once for
16+
// the process the way a Codefly flow pins a service's: the SDK refuses every
17+
// endpoint query until Init has pinned the consumer, and a pin cannot be moved
18+
// afterwards, so there is exactly one for the whole binary. Neutral for the
19+
// same reason testSolutionID is.
20+
const testModule = "widgets"
21+
22+
// TestMain identifies the test process to the SDK before anything resolves.
23+
//
24+
// Not a convenience: since sdk-go v0.3.1 an unpinned process resolves no
25+
// endpoint at all, so without this every test that reads an address would be
26+
// asserting on a refusal it never asked about. The boot pins it too — start()
27+
// calls Init — and that path is what TestABootThatNamesNoModuleIsRefused
28+
// drives.
29+
func TestMain(m *testing.M) {
30+
if err := os.Setenv(resources.ModulePrefix, testModule); err != nil {
31+
fmt.Fprintln(os.Stderr, err)
32+
os.Exit(1)
33+
}
34+
if _, err := codefly.Init(context.Background()); err != nil {
35+
fmt.Fprintf(os.Stderr, "codefly.Init for the test process: %v\n", err)
36+
os.Exit(1)
37+
}
38+
os.Exit(m.Run())
39+
}
40+
41+
// TestABootThatNamesNoModuleIsRefused: a process the SDK cannot identify does
42+
// not serve, and the refusal says why rather than naming the first endpoint
43+
// that came back empty.
44+
//
45+
// The boot used to call LoadEnvironmentVariables and nothing else. Against
46+
// sdk-go v0.3.1 and later that resolves no endpoint — the gateway, the
47+
// frontend and this service's own port all come back empty — and validate()
48+
// then sent the operator to inspect endpoint provisioning for a variable that
49+
// is not an endpoint at all.
50+
func TestABootThatNamesNoModuleIsRefused(t *testing.T) {
51+
for _, tc := range []struct {
52+
name, module string
53+
}{
54+
// The process is already pinned to testModule, so an unset carrier
55+
// is the SDK's drift refusal here; in a fresh process it is
56+
// ErrConsumerNotIdentified. Both are the same refusal to this boot.
57+
{"no module named", ""},
58+
{"a module other than the one this process was pinned to", "someone-else"},
59+
} {
60+
t.Run(tc.name, func(t *testing.T) {
61+
t.Setenv(resources.ModulePrefix, tc.module)
62+
// Every endpoint overridden, so nothing else can be the reason.
63+
t.Setenv("PORT", "8443")
64+
t.Setenv("GATEWAY_URL", "https://gateway.cell")
65+
t.Setenv(CredentialMintURLEnvironmentVariable, "https://gateway.cell"+credentialMintPath)
66+
t.Cleanup(func() { _ = codefly.LoadEnvironmentVariables() })
67+
68+
_, initErr := codefly.Init(context.Background())
69+
if initErr == nil {
70+
t.Fatal("codefly.Init accepted a process naming no module, or another one: the SDK's contract this boot relies on has changed")
71+
}
72+
cfg := loadConfig(context.Background(), testSolutionID, initErr)
73+
if cfg.environmentLoadErr != nil {
74+
t.Errorf("an identity refusal was filed as an environment load failure: %v", cfg.environmentLoadErr)
75+
}
76+
err := cfg.validate()
77+
if err == nil {
78+
t.Fatal("validate() accepted a boot the SDK could not identify")
79+
}
80+
if !strings.Contains(err.Error(), resources.ModulePrefix) {
81+
t.Errorf("the refusal does not name %s, the variable that fixes it: %v", resources.ModulePrefix, err)
82+
}
83+
if !errors.Is(err, resources.ErrConsumerNotIdentified) && !errors.Is(err, codefly.ErrAuthorityValueChanged) {
84+
t.Errorf("the refusal dropped the SDK's sentinel: %v", err)
85+
}
86+
})
87+
}
88+
}
89+
90+
// TestTheBootIdentifiesItselfToTheSDK drives the same refusal through start(),
91+
// with every other value a boot needs in place and every endpoint overridden.
92+
//
93+
// That is what makes it a test of the boot rather than of validate(): with the
94+
// addresses overridden, a boot that only loaded the carriers — what start()
95+
// did before it called Init — would come up, because nothing it reads in this
96+
// test goes through endpoint resolution. It must not: the SDK's contract is
97+
// that a process failing Init does not serve.
98+
func TestTheBootIdentifiesItselfToTheSDK(t *testing.T) {
99+
mint := newHostMint(t, &hostMint{})
100+
t.Setenv(resources.ModulePrefix, "")
101+
t.Cleanup(func() { _ = codefly.LoadEnvironmentVariables() })
102+
103+
err := bootFails(t, New(Manifest{ID: testSolutionID}), mint)
104+
if !strings.Contains(err.Error(), resources.ModulePrefix) {
105+
t.Errorf("the boot was refused, but not for naming no module: %v", err)
106+
}
107+
mint.mu.Lock()
108+
reached := len(mint.paths)
109+
mint.mu.Unlock()
110+
if got := reached; got != 0 {
111+
t.Errorf("the boot reached the mint %d time(s) before refusing: a process the SDK cannot identify must not present its projected token anywhere", got)
112+
}
113+
}

‎environment_boundary_test.go‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,8 +831,10 @@ func readsEnvironment(call *ast.CallExpr) bool {
831831
case "WorkspaceConfiguration", "Environment", "Endpoint", "Secret":
832832
return true
833833
}
834-
// Not LoadEnvironmentVariables: that is the loader, and start() calling it
835-
// before loadConfig is the documented shape rather than a read to govern.
834+
// Not Init, nor the LoadEnvironmentVariables it begins with: that is the
835+
// loader, and start() calling it before loadConfig is the documented shape
836+
// rather than a read to govern. What it fails with is handed to loadConfig
837+
// and refused by validate(), like every other boot value.
836838
// Not For() either — it returns the query; the read is the accessor called
837839
// on it, which the names above cover.
838840
return false

‎go.mod‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ go 1.27.0
44

55
require (
66
connectrpc.com/connect v1.21.0
7-
github.com/codefly-dev/core v0.9.0
8-
github.com/codefly-dev/sdk-go v0.3.0
9-
github.com/codefly-dev/sdk-go/workcontext v0.0.0-20261005062635-dd6ca019c685
7+
github.com/codefly-dev/core v0.19.1
8+
github.com/codefly-dev/sdk-go v0.3.2
9+
github.com/codefly-dev/sdk-go/workcontext v0.1.0
1010
github.com/google/uuid v1.6.0
1111
github.com/modelcontextprotocol/go-sdk v1.8.0
1212
golang.org/x/tools v0.51.0
@@ -39,9 +39,6 @@ require (
3939
github.com/go-openapi/swag/typeutils v0.29.1 // indirect
4040
github.com/go-openapi/swag/yamlutils v0.29.1 // indirect
4141
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
42-
github.com/gofrs/flock v0.13.1 // indirect
43-
github.com/google/go-github/v89 v89.0.0 // indirect
44-
github.com/google/go-querystring v1.2.0 // indirect
4542
github.com/google/jsonschema-go v0.4.3 // indirect
4643
github.com/hashicorp/errwrap v1.1.0 // indirect
4744
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -55,7 +52,7 @@ require (
5552
go.yaml.in/yaml/v3 v3.0.5 // indirect
5653
golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect
5754
golang.org/x/mod v0.41.0 // indirect
58-
golang.org/x/net v0.59.0 // indirect
55+
golang.org/x/net v0.60.0 // indirect
5956
golang.org/x/oauth2 v0.36.0 // indirect
6057
golang.org/x/sync v0.23.0 // indirect
6158
golang.org/x/sys v0.48.0 // indirect

0 commit comments

Comments
 (0)