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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Gitleaks compares the full pushed/PR commit range. A depth-1
# checkout cannot resolve the parent once the branch has >1 commit.
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
Expand Down Expand Up @@ -71,4 +75,3 @@ jobs:
with:
subject-name: ghcr.io/bayleafwalker/bindery-core
subject-digest: ${{ steps.build.outputs.digest }}

3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
FROM golang:1.23-bookworm AS build
ARG SOURCE_COMMIT=unknown
WORKDIR /src
COPY go.mod ./
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/bindery-external-runtime ./cmd/bindery-external-runtime \
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w -X main.buildRevision=${SOURCE_COMMIT}" -o /out/bindery-external-runtime ./cmd/bindery-external-runtime \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/bindery-udp-relay ./cmd/bindery-udp-relay

FROM gcr.io/distroless/static-debian12:nonroot
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@ research reference service. The research pack under
is immutable input. Runtime code imports only the promoted contracts under
[`contracts/externalruntime/v1`](contracts/externalruntime/v1).

The first implementation slice is deliberately in-memory and synthetic. It
proves the public/authenticated DTO split, token-backed identity, session and
enrollment lifecycle, idempotent mutations, and public redaction before
PostgreSQL, object storage, Windows clients, or live UDP are introduced.
The RA2 external-runtime path has now been demonstrated end to end once. The
reference control plane persists identity, session, placement, execution,
enrollment, idempotency, and reconciled evidence records through a crash-safe
single-writer state store. The current file-backed mode is intentionally not a
multi-replica database.

The evidence and gate boundary is described in
[`docs/architecture/evidence-and-gates.md`](docs/architecture/evidence-and-gates.md).
The dated RA2 result and its limits are recorded in
[`docs/assessments/2026-08-25-ra2-vertical-slice.md`](docs/assessments/2026-08-25-ra2-vertical-slice.md).

## Local verification

```sh
go test ./...
go vet ./...
BINDERY_RELAY_ENDPOINT=127.0.0.1:50001 \
BINDERY_BUILD_REVISION="$(git rev-parse HEAD)" \
BINDERY_STATE_PATH=/tmp/bindery-control-state.json \
go run ./cmd/bindery-external-runtime
```

`GET /v1/sessions` is intentionally not implemented. Known session IDs are
public; discovery is not.
The `/tmp` path above is disposable local development state. Deployments mount
a persistent volume and run exactly one replica. Multiple replicas require a
shared relational store; changing the count alone is rejected by the chart.

`GET /v1/sessions` is intentionally not implemented. Known session IDs are
public; discovery is not. The same known-ID rule applies to placements,
executions, enrollments, and evidence sets.
7 changes: 6 additions & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ attached to its Sprintctl item:
provenance attestation;
- OCI Helm chart version and digest;
- `go test -race ./...`, `go vet ./...`, Helm lint, and redaction receipts;
- a restart drill resolving identity, session, placement, execution, and
evidence-set IDs from the persisted state file;
- positive and negative calibration receipts for every consequential gate,
including gate version and implementation hash;
- an allocator implementation revision and configuration digest in the
placement fixture;
- research-pack provenance and wave acceptance references;
- operator approval for the local-inference exception, public data terms,
network boundary, and baseline tunnel revision.

4 changes: 2 additions & 2 deletions charts/bindery-external-runtime/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: bindery-external-runtime
description: Bindery external-runtime control-plane reference service
type: application
version: 0.1.0
appVersion: 0.1.0
version: 0.2.0
appVersion: 0.2.0
home: https://github.com/bayleafwalker/bindery-core
sources:
- https://github.com/bayleafwalker/bindery-core
Expand Down
23 changes: 23 additions & 0 deletions charts/bindery-external-runtime/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ metadata:
labels:
{{- include "bindery-external-runtime.labels" . | nindent 4 }}
spec:
{{- if ne (int .Values.replicaCount) 1 }}
{{- fail "file-backed control state supports exactly one replica; configure a shared relational store before scaling" }}
{{- end }}
{{- if not .Values.persistence.enabled }}
{{- fail "durable control state is mandatory for the external-runtime service" }}
{{- end }}
replicas: {{ .Values.replicaCount }}
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: {{ include "bindery-external-runtime.name" . }}
Expand All @@ -29,6 +37,10 @@ spec:
containerPort: 8080
protocol: TCP
env:
- name: BINDERY_BUILD_REVISION
value: {{ .Values.buildRevision | quote }}
- name: BINDERY_STATE_PATH
value: {{ printf "%s/control-state.json" .Values.persistence.mountPath | quote }}
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
Expand All @@ -46,3 +58,14 @@ spec:
httpGet: {path: /healthz, port: http}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.persistence.enabled }}
volumeMounts:
- name: control-state
mountPath: {{ .Values.persistence.mountPath | quote }}
{{- end }}
{{- if .Values.persistence.enabled }}
volumes:
- name: control-state
persistentVolumeClaim:
claimName: {{ default (printf "%s-state" (include "bindery-external-runtime.fullname" .)) .Values.persistence.existingClaim }}
{{- end }}
17 changes: 17 additions & 0 deletions charts/bindery-external-runtime/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ printf "%s-state" (include "bindery-external-runtime.fullname" .) }}
labels:
{{- include "bindery-external-runtime.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}
20 changes: 19 additions & 1 deletion charts/bindery-external-runtime/values.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
replicaCount: 2
# The file-backed v1 store is single-writer. Use a shared relational store
# before making this greater than one.
replicaCount: 1

# Full bindery-core Git commit embedded in every placement decision. The
# service refuses to start when this is not an exact 40-character revision.
buildRevision: ""

image:
repository: ghcr.io/bayleafwalker/bindery-core
Expand All @@ -18,6 +24,10 @@ resources: {}

podSecurityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault

Expand All @@ -30,6 +40,14 @@ securityContext:
env:
BINDERY_EXTERNAL_RUNTIME_ADDR: ":8080"

persistence:
enabled: true
existingClaim: ""
mountPath: /var/lib/bindery
size: 1Gi
accessModes: [ReadWriteOnce]
storageClass: ""

secretEnv: []

serviceMonitor:
Expand Down
42 changes: 42 additions & 0 deletions cmd/bindery-external-runtime/allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package main

import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"regexp"
"slices"
"strings"

Expand All @@ -17,11 +21,22 @@ import (
// allocator refuses to serve anything else rather than falling back.
const cncnetPrivateProviderID = "cncnet-private"

const allocatorRepository = "https://github.com/bayleafwalker/bindery-core"

var revisionPattern = regexp.MustCompile(`^[0-9a-f]{40}$`)

// buildRevision is set from the image build. go run/local tests may supply
// BINDERY_BUILD_REVISION instead; a supplied value may not contradict an exact
// revision already embedded in the binary.
var buildRevision = "unknown"

type allocatorConfig struct {
Provider string
Endpoint string
Region string
PolicyVersion string
Revision string
ConfigDigest string
}

// allocatorConfigFromEnv reads the deployment-owned placement policy. The
Expand All @@ -34,6 +49,10 @@ func allocatorConfigFromEnv() (allocatorConfig, error) {
Endpoint: os.Getenv("BINDERY_RELAY_ENDPOINT"),
Region: envOrDefault("BINDERY_RELAY_REGION", "eu-north"),
PolicyVersion: envOrDefault("BINDERY_PLACEMENT_POLICY_VERSION", "cncnet-private-lab-v1"),
Revision: strings.TrimSpace(os.Getenv("BINDERY_BUILD_REVISION")),
}
if config.Revision == "" {
config.Revision = buildRevision
}
if config.Provider != cncnetPrivateProviderID {
return allocatorConfig{}, fmt.Errorf("this service serves only %q, not %q", cncnetPrivateProviderID, config.Provider)
Expand All @@ -45,6 +64,23 @@ func allocatorConfigFromEnv() (allocatorConfig, error) {
if err != nil || host == "" || port == "" {
return allocatorConfig{}, fmt.Errorf("BINDERY_RELAY_ENDPOINT %q is not host:port", config.Endpoint)
}
if !revisionPattern.MatchString(config.Revision) {
return allocatorConfig{}, errors.New("BINDERY_BUILD_REVISION must be the full 40-character Git commit")
}
if revisionPattern.MatchString(buildRevision) && config.Revision != buildRevision {
return allocatorConfig{}, errors.New("BINDERY_BUILD_REVISION does not match the revision embedded in the binary")
}
encoded, err := json.Marshal(struct {
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
Region string `json:"region"`
PolicyVersion string `json:"policy_version"`
}{config.Provider, config.Endpoint, config.Region, config.PolicyVersion})
if err != nil {
return allocatorConfig{}, fmt.Errorf("encode allocator config: %w", err)
}
digest := sha256.Sum256(encoded)
config.ConfigDigest = "sha256:" + hex.EncodeToString(digest[:])
return config, nil
}

Expand All @@ -69,6 +105,12 @@ func newCncNetPrivateAllocator(config allocatorConfig) externalruntime.Placement
RelayEndpoint: config.Endpoint,
PolicyVersion: config.PolicyVersion,
DecisionSummary: fmt.Sprintf("private CnCNet tunnel in %s; p95 intent %dms", config.Region, intent.LatencyP95MS),
Allocator: externalruntime.ImplementationIdentity{
Implementation: cncnetPrivateProviderID,
Repository: allocatorRepository,
Revision: config.Revision,
ConfigDigest: config.ConfigDigest,
},
}, nil
}
}
Expand Down
22 changes: 19 additions & 3 deletions cmd/bindery-external-runtime/allocator_test.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package main

import (
"os"
"testing"

"github.com/bayleafwalker/bindery-core/internal/externalruntime"
"github.com/bayleafwalker/bindery-core/pkg/relayv1"
)

func TestAllocatorConfigRequiresAnEndpoint(t *testing.T) {
t.Setenv("BINDERY_BUILD_REVISION", testBuildRevision)
t.Setenv("BINDERY_RELAY_ENDPOINT", "")
if _, err := allocatorConfigFromEnv(); err == nil {
t.Fatal("expected a missing endpoint to be refused")
}
}

func TestAllocatorConfigRejectsOtherProviders(t *testing.T) {
t.Setenv("BINDERY_BUILD_REVISION", testBuildRevision)
t.Setenv("BINDERY_RELAY_ENDPOINT", "192.168.122.1:50001")
t.Setenv("BINDERY_RELAY_PROVIDER", "cncnet-public")
if _, err := allocatorConfigFromEnv(); err == nil {
Expand All @@ -24,15 +25,16 @@ func TestAllocatorConfigRejectsOtherProviders(t *testing.T) {
}

func TestAllocatorConfigRejectsMalformedEndpoints(t *testing.T) {
t.Setenv("BINDERY_BUILD_REVISION", testBuildRevision)
t.Setenv("BINDERY_RELAY_ENDPOINT", "192.168.122.1")
if _, err := allocatorConfigFromEnv(); err == nil {
t.Fatal("expected an endpoint without a port to be refused")
}
}

func TestBothClientsReceiveTheSameEndpointWithDistinctAllocations(t *testing.T) {
os.Setenv("BINDERY_RELAY_ENDPOINT", "192.168.122.1:50001")
defer os.Unsetenv("BINDERY_RELAY_ENDPOINT")
t.Setenv("BINDERY_RELAY_ENDPOINT", "192.168.122.1:50001")
t.Setenv("BINDERY_BUILD_REVISION", testBuildRevision)
config, err := allocatorConfigFromEnv()
if err != nil {
t.Fatalf("config: %v", err)
Expand Down Expand Up @@ -61,10 +63,14 @@ func TestBothClientsReceiveTheSameEndpointWithDistinctAllocations(t *testing.T)
if _, err := relayv1.PeekMustUUID(first.RelayAllocationID); err != nil {
t.Fatalf("allocation id must be a canonical UUID: %v", err)
}
if first.Allocator.Revision != testBuildRevision || first.Allocator.ConfigDigest == "" {
t.Fatalf("allocator identity is incomplete: %+v", first.Allocator)
}
}

func TestIntentWithoutTheServedRegionIsRefused(t *testing.T) {
t.Setenv("BINDERY_RELAY_ENDPOINT", "192.168.122.1:50001")
t.Setenv("BINDERY_BUILD_REVISION", testBuildRevision)
config, err := allocatorConfigFromEnv()
if err != nil {
t.Fatalf("config: %v", err)
Expand All @@ -75,3 +81,13 @@ func TestIntentWithoutTheServedRegionIsRefused(t *testing.T) {
t.Fatal("expected an unservable region to be refused rather than silently reassigned")
}
}

func TestAllocatorConfigRequiresAnExactImplementationRevision(t *testing.T) {
t.Setenv("BINDERY_RELAY_ENDPOINT", "192.168.122.1:50001")
t.Setenv("BINDERY_BUILD_REVISION", "unknown")
if _, err := allocatorConfigFromEnv(); err == nil {
t.Fatal("expected an unresolved allocator revision to be refused")
}
}

const testBuildRevision = "738e9f752ad1d892bdad8852cd4bd4e29182c16a"
17 changes: 15 additions & 2 deletions cmd/bindery-external-runtime/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,27 @@ func main() {
logger.Error("placement allocator is not configured", "error", err)
os.Exit(2)
}
service := externalruntime.NewServiceWithPlacementAllocator(newCncNetPrivateAllocator(config))
statePath := os.Getenv("BINDERY_STATE_PATH")
store, err := externalruntime.NewFileStateStore(statePath)
if err != nil {
logger.Error("durable control state is not configured", "error", err)
os.Exit(2)
}
service, err := externalruntime.OpenPersistentService(newCncNetPrivateAllocator(config), store)
if err != nil {
logger.Error("durable control state could not be restored", "error", err)
os.Exit(2)
}
server := &http.Server{Addr: addr, Handler: externalruntime.NewHandler(service)}
logger.Info("bindery external-runtime reference service listening",
"addr", addr,
"relay_provider", config.Provider,
"relay_endpoint", config.Endpoint,
"region", config.Region,
"policy_version", config.PolicyVersion)
"policy_version", config.PolicyVersion,
"allocator_revision", config.Revision,
"allocator_config_digest", config.ConfigDigest,
"state_path", statePath)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("server stopped", "error", err)
os.Exit(1)
Expand Down
11 changes: 10 additions & 1 deletion contracts/externalruntime/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ Invariants:
lease tokens, and transport credentials are never public fields or logs;
- identifiers are UUIDv7 values and wire timestamps are RFC 3339 UTC;
- client classes in v1 are `player` and `observer`.

- identity, session, placement and execution records survive a reference-service
restart and resolve by stable identifier;
- every placement names the allocator implementation repository, exact revision
and configuration digest that produced it;
- observations refer to an execution; evidence sets retain every compared
stream and record a reconciliation method and outcome;
- exact event-count equality is reconciliation policy #1. It establishes stream
consistency at that level only, not semantic truth;
- gate results use `PASS`, `FAIL`, `NOT_APPLICABLE`, `UNRESOLVED`, or `ERROR`;
consequential gates require known-pass and known-fail calibration evidence.
Loading
Loading