Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e67863f
fix(environments): hold scoped names to the selected environment too
antoinetoussaint-byte Oct 28, 2026
4673070
test(environments): pin that the narrowing does not excuse the select…
antoinetoussaint-byte Oct 28, 2026
f38c821
fix(environments): reach the real NewFlow seam, and retire a renamed …
antoinetoussaint-byte Oct 28, 2026
1c22469
docs(environment): say when an import retires a re-keyed service key
antoinetoussaint-byte Oct 28, 2026
0d5d8e8
revert(environment): withdraw the import retired-key change
antoinetoussaint-byte Oct 29, 2026
38f57ff
feat(environment): retire a re-keyed service key on explicit operator…
antoinetoussaint-byte Oct 29, 2026
436e15e
refactor(environments): drop the unused ServicesByName export
antoinetoussaint-byte Oct 29, 2026
abb5fcf
docs(environment): state the real no-write boundary for retirement
antoinetoussaint-byte Oct 29, 2026
612e81f
fix(environment): keep import flags and its inspection to one invocation
antoinetoussaint-byte Oct 29, 2026
683ce1e
fix(gitops): tell an identity workload where its projected token is
antoinetoussaint-byte Oct 29, 2026
87658ff
feat(gitops): carry the installation declaration on the Core signed c…
antoinetoussaint-byte Oct 29, 2026
22c1ff9
feat(gitops): carry the installation declaration on BOTH signed carriers
antoinetoussaint-byte Oct 29, 2026
8c81d3c
test(gitops): prove the produced carriers agree under Authority's own…
antoinetoussaint-byte Oct 29, 2026
99d36d2
test(gitops),docs: close the review's wire regression and documentati…
antoinetoussaint-byte Oct 29, 2026
f74d6b4
build: pin the released Core v0.19.17
antoinetoussaint-byte Oct 29, 2026
6088bf3
test(gitops): an installation-only change must advance the generation
antoinetoussaint-byte Oct 29, 2026
0e5f28e
feat(gitops): the cell names the delivery endpoint, so no consumer gu…
antoinetoussaint-byte Oct 29, 2026
3c41a3a
feat(delivery): prepare and send the signed pair, with the credential…
antoinetoussaint-byte Oct 29, 2026
d08e327
chore: preserve incomplete installation delivery job wiring for handoff
antoinetoussaint-byte Oct 29, 2026
683af61
feat(gitops): a publish toward a composition host renders the paired …
antoinetoussaint-byte Oct 29, 2026
881da1e
Merge ghhttps/main into handoff/wiki-builder-install-20261029
antoinetoussaint-byte Oct 29, 2026
03d7de9
fix(gitops): the paired delivery Job runs codefly companion 0.0.7, th…
antoinetoussaint-byte Oct 29, 2026
1450b46
Merge ghhttps/main into handoff/wiki-builder-install-20261029
antoinetoussaint-byte Oct 29, 2026
d5cff65
refactor(environment): read the retirement inputs in one helper
antoinetoussaint-byte Oct 29, 2026
caf06e4
fix(gitops): a paired document is signed once and carried to the exec…
antoinetoussaint-byte Oct 29, 2026
5d77510
fix(gitops): a manifest entering paired settlement advances once, eve…
antoinetoussaint-byte Oct 29, 2026
09f3b30
fix(gitops): a prepared binding addressed to the installer itself is …
antoinetoussaint-byte Oct 29, 2026
4632b98
chore(audit): re-review the three suppressed findings at 2026-10-30
antoinetoussaint-byte Oct 30, 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
14 changes: 11 additions & 3 deletions .govulncheck.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ suppressions:
Follow-up unchanged and still not landed: core migrates
releaseupdate/go-selfupdate off x/crypto/openpgp. Drop this entry
once that ships.
reviewed: 2026-09-14
2026-10-30 re-review: govulncheck 1.9.0 (db 2026-10-27) still reports
one module-level trace, no symbol reached, Fixed still N/A.
reviewed: 2026-10-30

- id: GO-2026-4887
module: github.com/docker/docker
Expand All @@ -51,7 +53,11 @@ suppressions:
Upstream fix: only in github.com/moby/moby/v2 >= 2.0.0-beta.8. That
is a renamed module and still pre-release; github.com/docker/docker
publishes no v29 tag, so no dependency bump makes this actionable.
reviewed: 2026-09-14
2026-10-30 re-review: docker/docker v28.5.2 is the newest v28 release;
the fix exists only in moby/moby/v2 (2.0.0-beta.8). 95 traces, all
client/* or api/types/* frames and package-init chains; none reaches
pkg/authorization or the daemon.
reviewed: 2026-10-30

- id: GO-2026-4883
module: github.com/docker/docker
Expand All @@ -63,4 +69,6 @@ suppressions:
pkg/authorization.
Upstream fix: only in github.com/moby/moby/v2 >= 2.0.0-beta.8, the
renamed pre-release module; unreachable from docker/docker v28.
reviewed: 2026-09-14
2026-10-30 re-review: same trace set and verdict as GO-2026-4887;
docker/docker v28.5.2 is the newest v28, fix only in moby/moby/v2.
reviewed: 2026-10-30
8 changes: 6 additions & 2 deletions cmd/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"github.com/spf13/cobra"
)

var DeliveryCmd = &cobra.Command{Use: "delivery", Short: "Manage delivery signing keys", Long: "Manage the local keys used to sign delivery documents for a local cluster.", Example: " codefly delivery key"}
var DeliveryCmd = &cobra.Command{Use: "delivery", Short: "Manage delivery signing keys and send prepared delivery documents", Long: "Manage the local keys used to sign delivery documents for a local cluster, and send the prepared paired documents a render wrote to the host's composition interface.", Example: " codefly delivery key\n codefly delivery apply --documents ./prepared --address localhost:8081 --identity-file ./token"}

func init() { DeliveryCmd.AddCommand(delivery.KeyCmd) }
func init() {
DeliveryCmd.AddCommand(delivery.KeyCmd)
DeliveryCmd.AddCommand(delivery.PrepareCmd)
DeliveryCmd.AddCommand(delivery.ApplyCmd)
}
128 changes: 128 additions & 0 deletions cmd/delivery/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package delivery

import (
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/codefly-dev/cli/cmd/common"
"github.com/codefly-dev/cli/pkg/gitops"
"github.com/codefly-dev/cli/pkg/orchestration"

compositionv1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/composition/v1"
)

// applyOptions are the flags, named so the Job that runs this reads as the
// thing it does.
var applyOptions struct {
documents, address, identityFile string
secure bool
budget, request, maxPause time.Duration
}

// ApplyCmd sends the prepared paired delivery documents a render wrote to the
// host's composition interface. It is what a cell's delivery Job runs: the
// documents are mounted, the credential is projected, and neither is baked
// into anything.
//
// NOT exposed over MCP, deliberately. Every input it needs exists only inside
// the pod that runs it — a mounted document set, a projected credential and a
// cluster-internal address — so an assistant calling it from outside has
// nothing to pass, and the one thing it could pass is a path to a credential.
// The verbs that produce what this sends (render and publish) are the ones
// worth driving.
var ApplyCmd = &cobra.Command{
Use: "apply", Short: "Send prepared paired delivery documents to the host's composition interface",
Long: `Send the prepared delivery documents under --documents to the host's composition
interface (host.composition.v1.CompositionService/ApplyManifest) at --address.

Each document is one ApplyManifestRequest carrying both signed halves — core's
module manifest in its DSSE carrier, and the authority declaration signed
against that carrier's exact bytes: method policies, scope ceilings, roles,
owner-awareness, the installation selector and its person-context edges. Only
this interface takes the pair. The host's legacy HTTP manifest route takes the
Core half alone, so a module delivered that way reaches the host with no roles.

A prepared document carries NO credential: a delivery credential is a
projected ServiceAccount token with a lifetime of minutes, and one written into
a rendered manifest would be a published secret and expired by the time the
manifest synced. The credential is read from --identity-file at execution,
once per request, so a token rotated between rounds is the one the next round
sends. It is sent RAW, in the request's credential field, because the host adds
the bearer scheme itself. It is never printed and never passed as an argument.

Documents are sent in ROUNDS: every pending one once per round, then a pause
that doubles up to --max-pause, until each is answered or --budget is spent, so
a document the host keeps failing never keeps a later one — a withdrawal among
them — from its first attempt. A refusal the host means (an invalid document, a
refused carrier, a stale or rewritten generation, a withdrawal it keeps
terminal) refuses that document and is not retried; only "not serving yet"
(unavailable, out of time, out of capacity) is. The command fails when any
document was not accepted, so a delivery that did not land fails the Job that
ran it.

The address is the host's delivery endpoint. A Connect endpoint is one: a
Connect server answers the gRPC protocol on the same listener (h2c in
plaintext), so --address is <host>:<port> of the endpoint the environment's
host.delivery names, and --secure only when that endpoint serves TLS itself.

A GitOps publish toward a host whose delivery endpoint declares
codefly.dev/composition writes these documents into the delivery overlay, one
per manifest, and renders the manifest's delivery Job to run this command.`,
Example: ` codefly delivery apply --documents /etc/codefly/delivery --address accounts.platform.svc.cluster.local:8081 --identity-file /var/run/secrets/codefly/delivery-token
codefly delivery apply --documents ./prepared --address localhost:8081 --identity-file ./token --budget 2m`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx, cancel := common.NewContext()
defer cancel()
if applyOptions.documents == "" {
return errors.New("--documents is the directory holding the prepared delivery documents")
}
if applyOptions.address == "" {
return errors.New("--address is <host>:<port> of the endpoint the environment's host.delivery names")
}
if applyOptions.identityFile == "" {
return errors.New("--identity-file is the projected delivery credential, read at execution")
}
connection, err := orchestration.GRPCTarget{Address: applyOptions.address, Secure: applyOptions.secure}.Dial()
if err != nil {
return fmt.Errorf("dial the host's composition interface at %s: %w", applyOptions.address, err)
}
defer func() { _ = connection.Close() }()
sender := &gitops.PairedApply{
Client: compositionv1.NewCompositionServiceClient(connection),
Credential: func() (string, error) { return readCredential(applyOptions.identityFile) },
Budget: applyOptions.budget, Request: applyOptions.request, MaxPause: applyOptions.maxPause,
Report: func(line string) { fmt.Fprintln(cmd.OutOrStdout(), line) },
}
_, err = sender.Apply(ctx, applyOptions.documents)
return err
},
}

// readCredential reads the projected token and returns it as the host takes
// it: raw, with no bearer scheme and no surrounding whitespace, which a
// projection's trailing newline would otherwise carry into the request. The
// value is returned and nothing more — it is not logged, and the error names
// the path rather than the content.
func readCredential(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read the projected delivery credential at %s: %w", path, err)
}
return strings.TrimSpace(string(data)), nil
}

func init() {
ApplyCmd.Flags().StringVar(&applyOptions.documents, "documents", "", "Directory holding the prepared delivery documents (*.json)")
ApplyCmd.Flags().StringVar(&applyOptions.address, "address", "", "<host>:<port> of the host's delivery endpoint")
ApplyCmd.Flags().StringVar(&applyOptions.identityFile, "identity-file", "", "Path of the projected delivery credential, read at execution")
ApplyCmd.Flags().BoolVar(&applyOptions.secure, "secure", false, "Dial with TLS, for a delivery endpoint that serves it itself")
ApplyCmd.Flags().DurationVar(&applyOptions.budget, "budget", 25*time.Minute, "Bound on the whole run")
ApplyCmd.Flags().DurationVar(&applyOptions.request, "request-timeout", 30*time.Second, "Bound on one call")
ApplyCmd.Flags().DurationVar(&applyOptions.maxPause, "max-pause", time.Minute, "Longest pause between rounds")
}
146 changes: 146 additions & 0 deletions cmd/delivery/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package delivery

import (
"bytes"
"context"
"crypto/ed25519"
"net"
"os"
"path/filepath"
"strings"
"testing"

"github.com/codefly-dev/core/solutionhost"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"

"github.com/codefly-dev/cli/pkg/gitops"

compositionv1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/composition/v1"
)

// qualifyingHost is a real composition host on a real TCP listener: the command
// dials it exactly as a delivery Job dials the cell's, so what this exercises
// is the whole path — flags, dial, the credential read from a file on disk,
// the generated client and the wire.
type qualifyingHost struct {
compositionv1.UnimplementedCompositionServiceServer
seen []*compositionv1.ApplyManifestRequest
}

func (h *qualifyingHost) ApplyManifest(_ context.Context, request *compositionv1.ApplyManifestRequest) (*compositionv1.ApplyManifestResponse, error) {
h.seen = append(h.seen, proto.Clone(request).(*compositionv1.ApplyManifestRequest))
manifest, err := solutionhost.ManifestFromVerified(request.GetCarrier().GetDocument())
if err != nil {
return nil, err
}
return &compositionv1.ApplyManifestResponse{Principal: manifest.Alias, Generation: manifest.Generation}, nil
}

// The command delivers both halves to a host it dials over TCP, with the
// credential read from the projected file at execution. This is the local
// qualification of the verb a cell's delivery Job runs.
func TestApplyDeliversBothHalvesToAHostItDials(t *testing.T) {
host := &qualifyingHost{}
server := grpc.NewServer()
compositionv1.RegisterCompositionServiceServer(server, host)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() { _ = server.Serve(listener) }()
t.Cleanup(server.Stop)

signer, err := solutionhost.NewInMemorySigner("dev-0123456789abcdef", ed25519.NewKeyFromSeed(make([]byte, 32)))
require.NoError(t, err)
manifest := &solutionhost.ModuleManifest{
Schema: solutionhost.SchemaModuleManifestV1, Binding: "wiki", Alias: "wiki", Kind: solutionhost.KindModule, Generation: 4,
Host: solutionhost.HostTarget{Coordinate: "local", Domain: "local"},
Release: solutionhost.Release{Publisher: "test", Name: "wiki", Version: "0.1.0"},
Workloads: []solutionhost.Workload{{Service: "api", Container: "app"}},
Authority: &solutionhost.Authority{Principal: "wiki", InstallationRole: "installer"},
}
payload, err := solutionhost.SignedPayload(manifest)
require.NoError(t, err)
envelope, err := solutionhost.SignEnvelope(payload, signer)
require.NoError(t, err)
signed, err := solutionhost.Carrier(payload, envelope)
require.NoError(t, err)
carrier, err := solutionhost.MarshalSigned(signed)
require.NoError(t, err)
declaration, err := proto.Marshal(&compositionv1.AuthorityManifestV1{
InstallationRole: "installer",
Roles: []*compositionv1.RoleV1{{Name: "installer"}, {Name: "editor"}, {Name: "viewer"}},
})
require.NoError(t, err)
document, err := gitops.PreparedPairedDocument(carrier, declaration, signer)
require.NoError(t, err)

documents := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(documents, "wiki.json"), document, 0o600))
token := filepath.Join(t.TempDir(), "delivery-token")
require.NoError(t, os.WriteFile(token, []byte("projected-delivery-token\n"), 0o600))

for flag, value := range map[string]string{
"documents": documents, "address": listener.Addr().String(), "identity-file": token,
} {
require.NoError(t, ApplyCmd.Flags().Set(flag, value))
}
var output bytes.Buffer
ApplyCmd.SetOut(&output)
t.Cleanup(func() { ApplyCmd.SetOut(nil) })
require.NoError(t, ApplyCmd.RunE(ApplyCmd, nil))

require.Len(t, host.seen, 1)
require.NotNil(t, host.seen[0].GetCarrier(), "the Core half")
require.NotNil(t, host.seen[0].GetAuthority(), "and the authority half the legacy route cannot carry")
require.Equal(t, "projected-delivery-token", host.seen[0].GetCredential(),
"raw and trimmed: the projection's trailing newline is not part of the token, and the host adds the bearer scheme")
var policy compositionv1.AuthorityManifestV1
require.NoError(t, proto.Unmarshal(host.seen[0].GetAuthority().GetDocument(), &policy))
require.Equal(t, []string{"installer", "editor", "viewer"},
[]string{policy.GetRoles()[0].GetName(), policy.GetRoles()[1].GetName(), policy.GetRoles()[2].GetName()})
require.Equal(t, "installer", policy.GetInstallationRole())
require.Contains(t, output.String(), "delivered wiki.json")
require.NotContains(t, output.String(), "projected-delivery-token", "a Job's log is a published record")
}

// A credential file the projection has not written yet refuses, and nothing is
// sent with an empty credential.
func TestApplyRefusesWhenTheProjectionIsAbsent(t *testing.T) {
host := &qualifyingHost{}
server := grpc.NewServer()
compositionv1.RegisterCompositionServiceServer(server, host)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() { _ = server.Serve(listener) }()
t.Cleanup(server.Stop)

documents := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(documents, "one.json"), []byte(`{"carrier":{"document":"e30=","envelope":"e30="}}`), 0o600))
for flag, value := range map[string]string{
"documents": documents, "address": listener.Addr().String(),
"identity-file": filepath.Join(t.TempDir(), "not-written-yet"),
} {
require.NoError(t, ApplyCmd.Flags().Set(flag, value))
}
var output bytes.Buffer
ApplyCmd.SetOut(&output)
t.Cleanup(func() { ApplyCmd.SetOut(nil) })
err = ApplyCmd.RunE(ApplyCmd, nil)
require.Error(t, err)
require.Empty(t, host.seen)
}

// readCredential returns the token as the host takes it, with no scheme and no
// surrounding whitespace a projection's trailing newline would carry in.
func TestTheCredentialIsReadRawAndTrimmed(t *testing.T) {
path := filepath.Join(t.TempDir(), "token")
require.NoError(t, os.WriteFile(path, []byte(" eyJhbGciOiJSUzI1NiJ9.payload.signature\n\n"), 0o600))
credential, err := readCredential(path)
require.NoError(t, err)
require.Equal(t, "eyJhbGciOiJSUzI1NiJ9.payload.signature", credential)
require.False(t, strings.HasPrefix(credential, "Bearer "), "the host adds the scheme itself")

_, err = readCredential(filepath.Join(t.TempDir(), "absent"))
require.ErrorContains(t, err, "read the projected delivery credential at")
}
Loading
Loading