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
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ jobs:
- name: Build Runtime
working-directory: .ci-runtime
run: go build -trimpath -ldflags "-X github.com/Judgment-Pack/judgment-pack-runtime/internal/result.CLIVersion=0.33.0-dev.09206e7" -o "$RUNNER_TEMP/jpack" ./cmd/jpack
# Test-only old companion: its current-store refusal is part of the
# explicit manual-store preparation contract, not a release dependency.
- name: Check out ownership-era Runner
uses: actions/checkout@v4
with:
repository: Judgment-Pack/judgment-pack-runner
ref: 1cc6b29885b344716a970cd21fefeaca0a4edaeb
path: .ci-runner-old
persist-credentials: false
- name: Build ownership-era Runner
working-directory: .ci-runner-old
run: go build -buildvcs=false -trimpath -o "$RUNNER_TEMP/jpack-runner-old" ./cmd/jpack-runner
# A Runtime that signs and does not know the expected key, as every
# release from v0.26.0 to v0.29.x: Runner must name the key a replaced
# seed signed under (TestAnEarlierRuntimeSigningWithAReplacedSeedIsNamed,
Expand Down Expand Up @@ -92,12 +104,58 @@ jobs:
go build -trimpath -o "$RUNNER_TEMP/gateway" .
cd ../adapters
go build -trimpath -o "$RUNNER_TEMP/adapter-mcp" ./cmd/adapter-mcp
- name: Manual admission helper and real Runtime gate tests cannot skip
run: |
out="$(JPACK_TEST_BIN="$RUNNER_TEMP/jpack" \
JPACK_OLD_RUNNER_TEST_BIN="$RUNNER_TEMP/jpack-runner-old" \
go test -count=1 -v \
-run '^(TestManualGovernedRealRuntimeGate|TestManualGovernedActualRuntimeHasNoAuthorityFD|TestManualStorePrepareActualChildRetainsOriginalCustody|TestManualStorePreparationDurableAndNoDowngrade|TestManualStorePreparationSubstitutedStateRefuses|TestManualStoreSQLiteTargetUsesHeldDirectory|TestManualStoreLibraryNeverChangesCWD|TestManualInheritedAuthoritySocketClosesBeforeRuntimeExec|TestManualAuthorityDescriptorObserverDetectsSocket|TestManualStorePreparationPendingAndUnsupported|TestManualExternalDemandCannotBeOmitted|TestManualQueuedRunListsKeepPrivatePreimages)$' \
./internal/runner)" || { printf '%s\n' "$out"; exit 1; }
printf '%s\n' "$out"
for name in \
TestManualGovernedRealRuntimeGate \
TestManualGovernedRealRuntimeGate/success \
TestManualGovernedRealRuntimeGate/blocked \
TestManualGovernedRealRuntimeGate/lost-reply \
TestManualGovernedRealRuntimeGate/changed-facts \
TestManualGovernedRealRuntimeGate/changed-config \
TestManualGovernedRealRuntimeGate/changed-lock \
TestManualGovernedRealRuntimeGate/changed-signing \
TestManualGovernedRealRuntimeGate/changed-epoch \
TestManualGovernedRealRuntimeGate/changed-demand \
TestManualGovernedRealRuntimeGate/journal-failure \
TestManualGovernedRealRuntimeGate/start-journal-failure \
TestManualGovernedRealRuntimeGate/recovered-reserve \
TestManualStorePrepareActualChildRetainsOriginalCustody \
TestManualStorePrepareActualChildRetainsOriginalCustody/committed \
TestManualStorePrepareActualChildRetainsOriginalCustody/refused \
TestManualStorePrepareActualChildRetainsOriginalCustody/lose-reply \
TestManualStorePreparationDurableAndNoDowngrade \
TestManualStorePreparationSubstitutedStateRefuses \
TestManualStorePreparationSubstitutedStateRefuses/before-adapter \
TestManualStorePreparationSubstitutedStateRefuses/after-adapter \
TestManualStoreSQLiteTargetUsesHeldDirectory \
TestManualStoreLibraryNeverChangesCWD \
TestManualInheritedAuthoritySocketClosesBeforeRuntimeExec \
TestManualAuthorityDescriptorObserverDetectsSocket \
TestManualStorePreparationPendingAndUnsupported \
TestManualExternalDemandCannotBeOmitted \
TestManualQueuedRunListsKeepPrivatePreimages; do
printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; }
done
# This observer identifies the actual Runtime through Linux /proc.
# Every other named helper/gate test is mandatory on both hosts.
if [ "$RUNNER_OS" = Linux ]; then
name=TestManualGovernedActualRuntimeHasNoAuthorityFD
printf '%s\n' "$out" | grep -q -- "^[[:space:]]*--- PASS: $name " || { echo "$name did not run and pass"; exit 1; }
fi
- name: Unit, conformance and cross-process recovery tests
run: |
JPACK_TEST_BIN="$RUNNER_TEMP/jpack" \
JPACK_EARLIER_TEST_BIN="$RUNNER_TEMP/jpack-earlier" \
JPACK_GATEWAY_TEST_BIN="$RUNNER_TEMP/gateway" \
JPACK_MCP_ADAPTER_TEST_BIN="$RUNNER_TEMP/adapter-mcp" \
JPACK_OLD_RUNNER_TEST_BIN="$RUNNER_TEMP/jpack-runner-old" \
go test -race ./...
# A skipped test passes `go test`; these must run (#35, #54, #55).
- name: Chain, signature, binding and ownership tests run under the pinned Runtimes
Expand Down
27 changes: 26 additions & 1 deletion cmd/jpack-runner/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"fmt"
"io"
"strings"

"github.com/Judgment-Pack/judgment-pack-runner/internal/runner"
)

const (
jobsCapability = "jobs/1"
requiredBootCapability = "required-boot-capabilities/1"
executorOwnershipCapability = "executor-ownership/1"
manualAdmissionCapability = "policy-manual-pack-run/1"
)

// This is a private, closed versioned contract; see docs/design/runner-capabilities.md.
Expand All @@ -20,13 +23,25 @@ func printCapabilities(args []string, stdout io.Writer) error {
if len(args) != 2 || args[0] != "--format" || args[1] != "json" {
return fmt.Errorf("usage: jpack-runner capabilities --format json")
}
if runner.ManualAdmissionAvailable() {
return printManualCapabilities(stdout)
}
return json.NewEncoder(stdout).Encode(struct {
Version int `json:"version"`
Capabilities []string `json:"capabilities"`
GovernanceAdmission bool `json:"governanceAdmission"`
}{2, []string{jobsCapability, requiredBootCapability, executorOwnershipCapability}, false})
}

func printManualCapabilities(stdout io.Writer) error {
return json.NewEncoder(stdout).Encode(struct {
Version int `json:"version"`
Capabilities []string `json:"capabilities"`
GovernanceAdmission bool `json:"governanceAdmission"`
GovernanceProfiles []string `json:"governanceProfiles"`
}{3, []string{jobsCapability, requiredBootCapability, executorOwnershipCapability, manualAdmissionCapability}, true, []string{runner.ManualAdmissionProfile}})
}

// checkBootCapabilities runs before runner.Open: even a rejected requirement
// must not create state, pin a Runtime, recover a queue, or start workers.
func checkBootCapabilities(line []byte) error {
Expand All @@ -47,6 +62,9 @@ func checkBootCapabilities(line []byte) error {
return invalidBoot
}
for name := range boot {
if name != "executorAdmission" && strings.EqualFold(name, "executorAdmission") {
return fmt.Errorf("invalid manual admission binding")
}
// encoding/json accepts case-insensitive struct field names. Do not let
// a misspelled requirement disappear or replace a stronger requirement.
if name != "executorOwnership" && strings.EqualFold(name, "executorOwnership") {
Expand All @@ -58,6 +76,9 @@ func checkBootCapabilities(line []byte) error {
}
raw, required := boot["requiredCapabilities"]
if !required {
if _, present := boot["executorAdmission"]; present {
return fmt.Errorf("manual admission requires its boot capability")
}
if _, present := boot["executorOwnership"]; present {
return fmt.Errorf("executor ownership requires its boot capability")
}
Expand All @@ -82,14 +103,18 @@ func checkBootCapabilities(line []byte) error {
return invalidRequirements
}
seen[capability] = true
if capability != jobsCapability && capability != requiredBootCapability && capability != executorOwnershipCapability {
if capability != jobsCapability && capability != requiredBootCapability && capability != executorOwnershipCapability && capability != manualAdmissionCapability {
return fmt.Errorf("unsupported required boot capability")
}
}
_, owned := boot["executorOwnership"]
if owned != seen[executorOwnershipCapability] {
return fmt.Errorf("executor ownership and its boot capability must agree")
}
_, admission := boot["executorAdmission"]
if admission != seen[manualAdmissionCapability] || admission && !owned {
return fmt.Errorf("manual admission and its ownership/capability must agree")
}
return nil
}

Expand Down
9 changes: 8 additions & 1 deletion cmd/jpack-runner/capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"reflect"
"strings"
"testing"

"github.com/Judgment-Pack/judgment-pack-runner/internal/runner"
)

type unreadableBoot struct{}
Expand All @@ -25,6 +27,11 @@ func TestCapabilitiesCommandDoesNotReadBootOrInitializeRunner(t *testing.T) {
var stdout, stderr bytes.Buffer
status := runnerCommand([]string{"capabilities", "--format", "json"}, unreadableBoot{}, &stdout, &stderr)
want := "{\"version\":2,\"capabilities\":[\"jobs/1\",\"required-boot-capabilities/1\",\"executor-ownership/1\"],\"governanceAdmission\":false}\n"
members := 3
if runner.ManualAdmissionAvailable() {
want = "{\"version\":3,\"capabilities\":[\"jobs/1\",\"required-boot-capabilities/1\",\"executor-ownership/1\",\"policy-manual-pack-run/1\"],\"governanceAdmission\":true,\"governanceProfiles\":[\"local-admin-manual-pack/1\"]}\n"
members = 4
}
if status != 0 || stdout.String() != want || stderr.Len() != 0 || stdout.Len() > 512 {
t.Fatalf("status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
}
Expand All @@ -33,7 +40,7 @@ func TestCapabilitiesCommandDoesNotReadBootOrInitializeRunner(t *testing.T) {
}
decoder := json.NewDecoder(&stdout)
var report map[string]json.RawMessage
if err := decoder.Decode(&report); err != nil || len(report) != 3 {
if err := decoder.Decode(&report); err != nil || len(report) != members {
t.Fatalf("not one closed capability object: %v %v", report, err)
}
if err := decoder.Decode(&report); err != io.EOF {
Expand Down
23 changes: 18 additions & 5 deletions cmd/jpack-runner/disposition_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,34 @@ func TestVerificationCoverageInHelpAndGuide(t *testing.T) {
}
}
for name, check := range map[string]struct {
typeOf reflect.Type
want []string
typeOf reflect.Type
want []string
private []string
}{
"verification bundle": {reflect.TypeOf(runner.VerificationBundle{}), []string{"version", "releaseDigest", "release", "run", "inputs", "chain"}},
"run": {reflect.TypeOf(runner.Run{}), []string{"trigger", "schemaVersion", "id", "jobId", "releaseId", "revision", "state", "input", "createdAt", "startedAt", "finishedAt", "requestedBy", "attempt", "result", "audit", "auditBytes", "auditSignatures", "problem"}},
"verification bundle": {reflect.TypeOf(runner.VerificationBundle{}), []string{"version", "releaseDigest", "release", "run", "inputs", "chain"}, nil},
"run": {reflect.TypeOf(runner.Run{}), []string{"trigger", "schemaVersion", "id", "jobId", "releaseId", "revision", "state", "input", "createdAt", "startedAt", "finishedAt", "requestedBy", "attempt", "result", "audit", "auditBytes", "auditSignatures", "problem"}, []string{"manual"}},
} {
var got []string
var private []string
for i := 0; i < check.typeOf.NumField(); i++ {
tag := strings.Split(check.typeOf.Field(i).Tag.Get("json"), ",")[0]
field := check.typeOf.Field(i)
tag := strings.Split(field.Tag.Get("json"), ",")[0]
if !field.IsExported() {
if tag != "-" {
t.Fatalf("%s private field %s lacks explicit JSON exclusion", name, field.Name)
}
private = append(private, field.Name)
continue
}
if tag != "-" {
got = append(got, tag)
}
}
if !reflect.DeepEqual(got, check.want) {
t.Fatalf("%s export members changed: got %v; update verification coverage and this list together", name, got)
}
if !reflect.DeepEqual(private, check.private) {
t.Fatalf("%s private storage members changed: %v", name, private)
}
}
}
31 changes: 30 additions & 1 deletion cmd/jpack-runner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package main

import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -38,6 +39,12 @@ func runnerCommand(args []string, stdin io.Reader, stdout, stderr io.Writer) int
} else {
_, err = fmt.Fprintln(stdout, "jpack-runner", buildinfo.Version())
}
case args[0] == "prepare-manual-store":
if len(args) != 1 {
err = fmt.Errorf("prepare-manual-store takes one trusted JSON request on stdin")
} else {
return prepareManualStore(stdin, stdout)
}
case args[0] == "relocate-workspace":
if len(args) == 2 && args[1] == "--capabilities" {
_, err = fmt.Fprintln(stdout, `{"version":1}`)
Expand All @@ -62,6 +69,19 @@ func runnerCommand(args []string, stdin io.Reader, stdout, stderr io.Writer) int
return 0
}

func prepareManualStore(stdin io.Reader, stdout io.Writer) int {
raw, err := io.ReadAll(io.LimitReader(stdin, 16385))
result := runner.ManualStorePrepareResult{Version: 1, Status: "refused", Code: "invalid-request"}
if err == nil && len(raw) > 1 && len(raw) <= 16384 && raw[len(raw)-1] == '\n' && bytes.Count(raw, []byte{'\n'}) == 1 {
runner.RunManualStorePreparationProcess(raw[:len(raw)-1], stdout)
return 1 // the process-only adapter always exits
}
if json.NewEncoder(stdout).Encode(result) != nil || result.Status != "ok" {
return 1
}
return 0
}

// bootConfig reads the boot line: the service's configuration and the
// bearer.
func bootConfig(line []byte) (runner.Config, string, error) {
Expand Down Expand Up @@ -89,7 +109,16 @@ func bootConfig(line []byte) (runner.Config, string, error) {
if err != nil {
return runner.Config{}, "", err
}
return runner.Config{ExecutorOwnership: ownership, Dir: boot.Dir, Runtime: boot.Runtime, Workspace: boot.Workspace, Owner: boot.Owner, InputProfiles: boot.InputProfiles, InputRoot: boot.InputRoot, CloudConnections: boot.CloudConnections, GatewayConnections: boot.GatewayConnections, RequireTestedReleases: boot.RequireTested, SigningKey: boot.SigningKey, SigningPublicKey: boot.SigningPublicKey}, boot.Token, nil
var envelope map[string]json.RawMessage
_ = json.Unmarshal(line, &envelope)
var admission *runner.ManualAdmissionBinding
if raw, present := envelope["executorAdmission"]; present {
admission, err = runner.ParseManualAdmissionBinding(raw, ownership, boot.Workspace)
if err != nil {
return runner.Config{}, "", fmt.Errorf("invalid manual admission binding")
}
}
return runner.Config{ExecutorAdmission: admission, ExecutorOwnership: ownership, Dir: boot.Dir, Runtime: boot.Runtime, Workspace: boot.Workspace, Owner: boot.Owner, InputProfiles: boot.InputProfiles, InputRoot: boot.InputRoot, CloudConnections: boot.CloudConnections, GatewayConnections: boot.GatewayConnections, RequireTestedReleases: boot.RequireTested, SigningKey: boot.SigningKey, SigningPublicKey: boot.SigningPublicKey}, boot.Token, nil
}

func serve(stdin io.Reader, stdout, stderr io.Writer) error {
Expand Down
108 changes: 108 additions & 0 deletions cmd/jpack-runner/manual_admission_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/Judgment-Pack/judgment-pack-runner/internal/runner"
)

func manualBootFixture(t *testing.T, state string) map[string]any {
t.Helper()
var wire struct {
Synthetic struct {
Boot map[string]any `json:"boot"`
} `json:"synthetic"`
}
raw, err := os.ReadFile("../../docs/design/manual-admission-wire.json")
if err != nil || json.Unmarshal(raw, &wire) != nil {
t.Fatal("wire fixture", err)
}
b := wire.Synthetic.Boot
return map[string]any{"dir": state, "runtime": "/missing-runtime", "workspace": b["runnerWorkspace"], "owner": "owner", "token": strings.Repeat("s", 32), "executorOwnership": map[string]any{"version": 1, "root": "/never-opened", "machineWorkspace": b["machineWorkspace"], "runnerWorkspace": b["runnerWorkspace"], "rootDevice": "1", "rootInode": "1", "fileDevice": "1", "fileInode": "1"}, "requiredCapabilities": map[string]any{"version": 1, "capabilities": []string{"jobs/1", "required-boot-capabilities/1", "executor-ownership/1", "policy-manual-pack-run/1"}}, "executorAdmission": b}
}
func TestManualAdmissionBootSnapshotRefusesUnsafeOwnership(t *testing.T) {
state := filepath.Join(t.TempDir(), "absent")
boot := manualBootFixture(t, state)
line, _ := json.Marshal(boot)
cfg, _, err := bootConfig(line)
if err != nil || cfg.ExecutorAdmission == nil {
t.Fatal("strict snapshot", err)
}
if cfg.ExecutorAdmission.FD != 3 || cfg.ExecutorAdmission.Profile != "local-admin-manual-pack/1" {
t.Fatal("binding changed")
}
boot["executorAdmission"].(map[string]any)["challenge"] = "mutated"
if cfg.ExecutorAdmission.Challenge == "mutated" {
t.Fatal("mutable binding")
}
var stdout, stderr bytes.Buffer
if status := runnerCommand(nil, bytes.NewReader(append(line, '\n')), &stdout, &stderr); status == 0 || stdout.Len() != 0 || stderr.Len() == 0 {
t.Fatalf("unsafe ownership escaped: %d %q %q", status, stdout.String(), stderr.String())
}
if runner.ManualAdmissionAvailable() && strings.Contains(stderr.String(), "not activated") {
t.Fatal("enabled candidate retained inactive hold")
}
if _, err := os.Lstat(state); !os.IsNotExist(err) {
t.Fatal("unsafe ownership touched state", err)
}
}
func TestManualAdmissionBootClosedShapes(t *testing.T) {
edits := map[string]func(map[string]any){
"missing requirement": func(b map[string]any) { delete(b, "requiredCapabilities") },
"missing binding": func(b map[string]any) { delete(b, "executorAdmission") },
"missing ownership": func(b map[string]any) { delete(b, "executorOwnership") },
"unknown member": func(b map[string]any) { b["executorAdmission"].(map[string]any)["url"] = "http://never" },
"case aliased member": func(b map[string]any) {
a := b["executorAdmission"].(map[string]any)
a["Challenge"] = a["challenge"]
delete(a, "challenge")
},
"case aliased binding": func(b map[string]any) {
b["ExecutorAdmission"] = b["executorAdmission"]
delete(b, "executorAdmission")
},
"null": func(b map[string]any) { b["executorAdmission"] = nil },
"null fd": func(b map[string]any) { b["executorAdmission"].(map[string]any)["fd"] = nil },
"wrong fd": func(b map[string]any) { b["executorAdmission"].(map[string]any)["fd"] = 4 },
"string fd": func(b map[string]any) { b["executorAdmission"].(map[string]any)["fd"] = "3" },
"unsupported profile": func(b map[string]any) { b["executorAdmission"].(map[string]any)["profile"] = "other/1" },
"runner mismatch": func(b map[string]any) { b["executorAdmission"].(map[string]any)["runnerWorkspace"] = "other" },
"machine mismatch": func(b map[string]any) {
b["executorAdmission"].(map[string]any)["machineWorkspace"] = strings.Repeat("e", 64)
},
"nonhex executor": func(b map[string]any) { b["executorAdmission"].(map[string]any)["executorId"] = "executor_browser" },
}
for name, edit := range edits {
t.Run(name, func(t *testing.T) {
state := filepath.Join(t.TempDir(), "absent")
boot := manualBootFixture(t, state)
edit(boot)
line, _ := json.Marshal(boot)
if _, _, err := bootConfig(line); err == nil {
t.Fatal("malformed binding passed parser")
}
var stdout, stderr bytes.Buffer
if runnerCommand(nil, bytes.NewReader(append(line, '\n')), &stdout, &stderr) == 0 || stdout.Len() != 0 {
t.Fatal("malformed binding started")
}
if _, err := os.Stat(state); !os.IsNotExist(err) {
t.Fatal("state created", err)
}
})
}
state := filepath.Join(t.TempDir(), "absent")
raw, _ := json.Marshal(manualBootFixture(t, state))
duplicate := bytes.Replace(raw, []byte(`"fd":3`), []byte(`"fd":3,"f\u0064":3`), 1)
if _, _, err := bootConfig(duplicate); err == nil {
t.Fatal("escaped duplicate accepted")
}
fractional := bytes.Replace(raw, []byte(`"fd":3`), []byte(`"fd":3.0`), 1)
if _, _, err := bootConfig(fractional); err == nil {
t.Fatal("fractional fd accepted")
}
}
Loading
Loading