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
613 changes: 381 additions & 232 deletions go/gen/compass/v1/compass.pb.go

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions go/internal/runner/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,11 +676,11 @@ func (h *agentHost) Status(_ context.Context, sessionID string) ([]*compassv1.Ag
if !ok {
return nil, errSessionUnknown
}
return []*compassv1.AgentSessionStatus{{SessionId: s.sessionID, State: s.state, AgentAccountId: s.agentAccountID}}, nil
return []*compassv1.AgentSessionStatus{h.statusOf(s)}, nil
}
out := make([]*compassv1.AgentSessionStatus, 0, len(h.sessions))
for _, s := range h.sessions {
out = append(out, &compassv1.AgentSessionStatus{SessionId: s.sessionID, State: s.state, AgentAccountId: s.agentAccountID})
out = append(out, h.statusOf(s))
}
return out, nil
}
Expand Down Expand Up @@ -837,6 +837,20 @@ func (h *agentHost) RefreshConfig(ctx context.Context) error {
return nil
}

// statusOf stamps a live session with the tier and egress posture of the
// backend this Runner resolved. Both are Runner-wide, not per-session: the
// Runner reports them because it is the component that picked the backend, so a
// client never has to infer containment from a tier name.
func (h *agentHost) statusOf(s *liveSession) *compassv1.AgentSessionStatus {
return &compassv1.AgentSessionStatus{
SessionId: s.sessionID,
State: s.state,
AgentAccountId: s.agentAccountID,
RuntimeTier: runtimeTierProto(runtime.TierOf(h.engine)),
EgressPosture: egressPostureProto(h.runtime.EgressPosture()),
}
}

// provisionVsockGateway is Provision's microVM leg: it launches the container
// with NO agent-socket mount and NO config mount (record §(c)/§(f)), resolves
// the per-session host-side gateway path the backend serves the AgentGateway on,
Expand Down
62 changes: 62 additions & 0 deletions go/internal/runner/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,68 @@ func TestCloseJoinsConcurrentTeardowns(t *testing.T) {
// started session appears with its live state, and a stopped one is gone. The
// Runner is authoritative for live truth. A bug that answered from a stale or
// external source would not reflect the live set.
// hostTierStubRuntime is the streaming stub that also answers the tier and
// unenforced-egress capabilities, standing in for the host backend so the
// Status stamp is exercisable without spawning host children.
type hostTierStubRuntime struct {
*stubStreamingRuntime
}

func (hostTierStubRuntime) Tier() runtime.WorkloadTier { return runtime.WorkloadTierHost }
func (hostTierStubRuntime) EgressUnenforced() bool { return true }

// TestStatusStampsTheTierAndEgressPosture: the Runner reports both, because it
// is the component that resolved the backend. BOTH arms must stamp them — the
// targeted one and the all-sessions one — mirroring the account stamp below; a
// client reading the all-arm would otherwise render an uncontained host session
// as unspecified.
func TestStatusStampsTheTierAndEgressPosture(t *testing.T) {
// The host tier refuses a configured policy, which is the point of the
// posture — so its spec carries none, as the Runner's own resolution does.
hostSpec := liveSpec()
hostSpec.Egress = runtime.EgressPolicy{}
specs := &fakeSpecBuilder{spec: hostSpec}
engine := hostTierStubRuntime{stubStreamingRuntime: newStubStreamingRuntime(t)}
registry := runtime.NewAgentRegistry()
rt := runtime.NewAgentRuntimeWithRegistry(engine, registry)
link := newLink(newRunnerServiceServer(t, newCapturePublish()))
var n int
host := NewSessionHost(link, rt, registry, engine, specs,
AgentHostConfig{RuntimeDir: t.TempDir()}, discardLoggerRunner(),
func() string { n++; return "sess-" + string(rune('0'+n)) })
ctx := context.Background()

if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil {
t.Fatalf("Provision = %v", err)
}
sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "")
if err != nil {
t.Fatalf("Start = %v", err)
}

one, err := host.Status(ctx, sessionID)
if err != nil || len(one) != 1 {
t.Fatalf("Status(one) = %+v, %v", one, err)
}
if got := one[0].GetRuntimeTier(); got != compassv1.RuntimeTier_RUNTIME_TIER_HOST {
t.Errorf("Status(one) tier = %v, want RUNTIME_TIER_HOST", got)
}
if got := one[0].GetEgressPosture(); got != compassv1.EgressPosture_EGRESS_POSTURE_UNENFORCED {
t.Errorf("Status(one) posture = %v, want EGRESS_POSTURE_UNENFORCED", got)
}

all, err := host.Status(ctx, "")
if err != nil || len(all) != 1 {
t.Fatalf("Status(all) = %+v, %v", all, err)
}
if got := all[0].GetRuntimeTier(); got != compassv1.RuntimeTier_RUNTIME_TIER_HOST {
t.Errorf("Status(all) tier = %v, want RUNTIME_TIER_HOST (the all-arm must stamp it too)", got)
}
if got := all[0].GetEgressPosture(); got != compassv1.EgressPosture_EGRESS_POSTURE_UNENFORCED {
t.Errorf("Status(all) posture = %v, want EGRESS_POSTURE_UNENFORCED (the all-arm must stamp it too)", got)
}
}

func TestStatusIsAnsweredFromLiveSet(t *testing.T) {
specs := &fakeSpecBuilder{spec: liveSpec()}
host, _, _ := newHostFixture(t, specs)
Expand Down
36 changes: 36 additions & 0 deletions go/internal/runner/tier_proto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Wire mapping for the runtime tier and egress posture a session reports.
// Both map an unknown value onto UNSPECIFIED rather than a plausible default: a
// security surface that guesses is worse than one that says it does not know.

package runner

import (
compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1"
"github.com/RigelBuild/compass/go/internal/runtime"
)

func runtimeTierProto(tier runtime.WorkloadTier) compassv1.RuntimeTier {
switch tier {
case runtime.WorkloadTierPodman:
return compassv1.RuntimeTier_RUNTIME_TIER_PODMAN
case runtime.WorkloadTierMicroVM:
return compassv1.RuntimeTier_RUNTIME_TIER_MICROVM
case runtime.WorkloadTierAppleContainer:
return compassv1.RuntimeTier_RUNTIME_TIER_APPLE_CONTAINER
case runtime.WorkloadTierHost:
return compassv1.RuntimeTier_RUNTIME_TIER_HOST
default:
return compassv1.RuntimeTier_RUNTIME_TIER_UNSPECIFIED
}
}

func egressPostureProto(posture runtime.EgressPosture) compassv1.EgressPosture {
switch posture {
case runtime.EgressPostureArmed:
return compassv1.EgressPosture_EGRESS_POSTURE_ARMED
case runtime.EgressPostureUnenforced:
return compassv1.EgressPosture_EGRESS_POSTURE_UNENFORCED
default:
return compassv1.EgressPosture_EGRESS_POSTURE_UNSPECIFIED
}
}
49 changes: 49 additions & 0 deletions go/internal/runtime/tier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Tier identity: what kind of isolation boundary a backend puts an agent
// behind. Reported outward per session so an operator can see which boundary
// their agent actually got, rather than inferring it from a green launch.

package runtime

// WorkloadTier names the isolation boundary a backend provides.
type WorkloadTier string

const (
// WorkloadTierPodman is a rootless container with its own network namespace.
WorkloadTierPodman WorkloadTier = "podman"
// WorkloadTierMicroVM is a hardware-virtualized guest.
WorkloadTierMicroVM WorkloadTier = "microvm"
// WorkloadTierAppleContainer is Apple's container runtime on macOS.
WorkloadTierAppleContainer WorkloadTier = "apple-container"
// WorkloadTierHost is a direct child process of the Runner — no boundary.
WorkloadTierHost WorkloadTier = "host"
)

// workloadTierNamer is a backend that names its own tier. Like the egress
// markers, it is deliberately NOT a verb on the frozen WorkloadRuntime
// interface (podman.go): callers probe for it. A WorkloadRuntime decorator must
// re-expose Tier, or the session surface loses the tier it reports.
type workloadTierNamer interface {
Tier() WorkloadTier
}

// TierOf names the tier a backend provides. An unrecognized backend reports the
// empty tier rather than guessing: a wrong tier on a security surface is worse
// than an absent one, and the wire's unspecified value renders as unknown.
func TierOf(engine WorkloadRuntime) WorkloadTier {
if namer, ok := engine.(workloadTierNamer); ok {
return namer.Tier()
}
return ""
}

// Tier reports the podman tier.
func (p *PodmanCLI) Tier() WorkloadTier { return WorkloadTierPodman }

// Tier reports the microVM tier.
func (m *MicroVMRuntime) Tier() WorkloadTier { return WorkloadTierMicroVM }

// Tier reports the Apple-container tier.
func (a *AppleContainerCLI) Tier() WorkloadTier { return WorkloadTierAppleContainer }

// Tier reports the host tier.
func (h *HostRuntime) Tier() WorkloadTier { return WorkloadTierHost }
56 changes: 56 additions & 0 deletions go/internal/runtime/tier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package runtime

import "testing"

// TestEveryBackendNamesItsOwnTier: the tier is reported on a security surface,
// so each production backend must name itself rather than fall through to the
// empty tier. A backend added without a Tier method would fail here.
func TestEveryBackendNamesItsOwnTier(t *testing.T) {
for _, tc := range []struct {
name string
engine WorkloadRuntime
want WorkloadTier
}{
{"podman", NewPodmanCLI(), WorkloadTierPodman},
{"microvm", NewMicroVMRuntime(MicroVMConfig{}), WorkloadTierMicroVM},
{"apple-container", NewAppleContainerCLI(AppleContainerConfig{}), WorkloadTierAppleContainer},
{"host", NewHostRuntime(t.TempDir()), WorkloadTierHost},
} {
if got := TierOf(tc.engine); got != tc.want {
t.Errorf("TierOf(%s) = %q, want %q", tc.name, got, tc.want)
}
}
}

// TestTierOfReportsUnknownRatherThanGuessing: a backend that does not name its
// tier reports the empty tier, which the wire renders as unspecified. Defaulting
// to a plausible tier would mislabel a session on a surface an operator trusts.
func TestTierOfReportsUnknownRatherThanGuessing(t *testing.T) {
if got := TierOf(newFakeRuntime(t)); got != "" {
t.Errorf("TierOf(a backend with no Tier method) = %q, want the empty tier", got)
}
}

// TestSelectBackendTierMatchesTheRequestedBackend closes the loop the two tests
// above leave open: each is correct in isolation, but a mis-wired SelectBackend
// arm would hand back a healthy backend of the WRONG tier and both would still
// pass. The empty string is podman's documented default.
func TestSelectBackendTierMatchesTheRequestedBackend(t *testing.T) {
for _, tc := range []struct {
backend string
want WorkloadTier
}{
{"", WorkloadTierPodman},
{"podman", WorkloadTierPodman},
{"microvm", WorkloadTierMicroVM},
{"host", WorkloadTierHost},
} {
engine, err := SelectBackend(BackendConfig{Backend: tc.backend})
if err != nil {
t.Fatalf("SelectBackend(%q) error = %v", tc.backend, err)
}
if got := TierOf(engine); got != tc.want {
t.Errorf("SelectBackend(%q) resolved a backend of tier %q, want %q", tc.backend, got, tc.want)
}
}
}
Loading
Loading