carlos / platform Public

svcrole: Allow refuses the run role, so no Allow set can carry Admin through a run door (PRO-6214)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 151c946bb79225e46efe1a5cecaad20492c5d964 parent 1dad168
2 files changed, +45 −3
  • internal/svcrole/svcrole.go +8 −2
  • internal/svcrole/svcrole_test.go +37 −1
diff --git a/internal/svcrole/svcrole.go b/internal/svcrole/svcrole.go
index a6f4da0b7..6c212162b 100644
--- a/internal/svcrole/svcrole.go
+++ b/internal/svcrole/svcrole.go
@@ -57,8 +57,8 @@ const (
Service Role = "service"
// Run is a credential that starts `carlos run` runs: one-off commands on
// the platform's own hosts. It is deliberately outside the union Admin
- // stands for. Allow never adds it to a set, and nothing that lists
- // Admin reaches it; a door admits it only by being registered through
+ // stands for. Allow refuses it (it panics), because every Allow set
+ // carries Admin; a door admits it only by being registered through
// Exactly (spec 2026-10-24-service-runs-design.md § 1).
Run Role = "run"
// Admin is the union of every role except Run, plus ledgers, store and
@@ -129,6 +129,12 @@ func Allow(roles ...Role) Set {
// compile error, which is why this costs nothing in practice.
panic("svcrole: Allow called with an unknown role " + strconv.Quote(string(r)))
}
+ if r == Run {
+ // Every Allow set carries Admin, so Allow(Run) would let an
+ // administrator through a run door: the one thing Run exists
+ // to prevent. Fail loud at registration, like a typo.
+ panic("svcrole: Allow called with the run role, which Admin must not reach; use Exactly(Run)")
+ }
s.roles[r] = true
}
s.roles[Admin] = true
diff --git a/internal/svcrole/svcrole_test.go b/internal/svcrole/svcrole_test.go
index 180566e06..8b5412d48 100644
--- a/internal/svcrole/svcrole_test.go
+++ b/internal/svcrole/svcrole_test.go
@@ -2,6 +2,7 @@ package svcrole
import (
"reflect"
+ "strings"
"testing"
)
@@ -37,8 +38,11 @@ func TestAdminPermitsEveryNonEmptySet(t *testing.T) {
if Allow(Publish).Permits(Run) {
t.Error("Allow(Publish).Permits(Run) = true; want false")
}
+}
+
+func TestExactlySetsAdmitOnlyTheirRoles(t *testing.T) {
// An Exactly set admits only its listed roles, Admin included.
- for _, s := range []Set{Exactly(Run), Exactly(Run, Publish), Exactly(Publish)} {
+ for _, s := range []Set{Exactly(Run), Exactly(Run, Publish), Exactly(Publish), Exactly(Admin)} {
for _, r := range All() {
listed := false
for _, l := range s.Roles() {
@@ -51,6 +55,38 @@ func TestAdminPermitsEveryNonEmptySet(t *testing.T) {
}
}
+func TestExactlyWithNoRolesIsEmptyAndClosed(t *testing.T) {
+ s := Exactly()
+ if !s.Empty() {
+ t.Error("Exactly().Empty() = false; want true")
+ }
+ for _, r := range append(All(), Role("")) {
+ if s.Permits(r) {
+ t.Errorf("Exactly().Permits(%q) = true; want false", r)
+ }
+ }
+}
+
+func TestAllowRefusesRun(t *testing.T) {
+ // Every Allow set carries Admin, so Allow(Run) would hand Admin a run
+ // door. Both the bare and the mixed form must panic, and name Exactly.
+ for _, roles := range [][]Role{{Run}, {Publish, Run}, {Run, Operate}} {
+ func() {
+ defer func() {
+ got := recover()
+ if got == nil {
+ t.Errorf("Allow(%v) did not panic", roles)
+ return
+ }
+ if msg, _ := got.(string); !strings.Contains(msg, "Exactly(Run)") {
+ t.Errorf("Allow(%v) panic = %v; want it to say to use Exactly(Run)", roles, got)
+ }
+ }()
+ Allow(roles...)
+ }()
+ }
+}
+
func TestRunIsInTheCatalogue(t *testing.T) {
got, ok := Parse("run")
if !ok || got != Run {