carlos / platform Public

caps: the closed capability vocabulary — strict names, tolerant fields, fail-closed versions

internal/caps.Set decodes a manifest's capabilities object through a strict
door (spec §3b): an unknown capability NAME (or a "v" this binary doesn't
speak) refuses the whole block so an unrecognised ask never materializes as
a silently-incomplete unit; an unknown FIELD inside a known capability is
ignored and recorded in Notes() so the vocabulary can grow additively.
ValidCapabilities is the semantic gate shared by ship (human reads the
refusal) and adopt (the box refuses the version) — same shape as
appconfig.ValidExecDelivery.

Encodes every rule in the plan's validation table: command/runtime_dir
charset and the runtime_dir reserved-name list (carlos is load-bearing —
owning it would own every tenant socket the edge dials), ports range
(1..1023), the 22/80/443 absolute reservation, duplicate/too-many-ports
bounds, data.replicate's closed pair, data.dbs charset/separator/"."/".."
refusals and its bound, dbs-with-replicate:"app" refusal (the
two-replicators hazard), and the host_role account-name length check via
AppUser.

Deviation: the plan's pinned TestRuntimeDirCharsetAndReservedNames included
"" in its bad-value list, but that contradicts its own rule ("non-empty and
not regex") and TestPortsReservedAndNonPrivilegedRefused's final assertion
(a Set with only Ports set — RuntimeDir at its zero value "" — must be
valid). Dropped "" from that test and pinned it separately in
TestRuntimeDirEmptyMeansNotDeclared: empty means not declared, not an
invalid name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell f17a64d86a6669379edce62b87684fe6e095ad66 parent aaa547e
2 files changed, +549 −0
  • internal/caps/caps.go +336 −0
  • internal/caps/caps_test.go +213 −0
diff --git a/internal/caps/caps.go b/internal/caps/caps.go
new file mode 100644
index 000000000..f6a00c05a
--- /dev/null
+++ b/internal/caps/caps.go
@@ -0,0 +1,336 @@
+// Package caps is the closed capability vocabulary: what an app may ask the
+// platform for, and what each name maps to. A manifest never names a path, a
+// user, a uid, a resource number, or a systemd directive — the directives are
+// Go constants in this package. There is no passthrough field and no
+// "extra" escape hatch; the escape hatch is 90-operator.conf, which this
+// package never writes and never reads.
+//
+// See docs/superpowers/specs/2026-08-03-app-capabilities-design.md §2 (the
+// model: self-service vs operator-consented), §3 (wire shape and which way
+// each failure falls), §7 (security posture — why each rail exists), and §8
+// (the testing shape this file's tests follow).
+package caps
+
+import (
+ "encoding/json"
+ "fmt"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+// Version is the capability block version THIS binary understands. A block
+// declaring a higher "v" refuses the version on this box (spec §3b) — fail
+// closed in the direction that changes nothing: the box keeps serving what
+// it has rather than materialize a unit missing something the app needs.
+const Version = 1
+
+// Marker is the first line of every drop-in this package generates — the
+// same marker `carlos ops litestream` uses. A file at a generated path
+// WITHOUT it is never overwritten and never deleted (spec §3c).
+const Marker = "# GENERATED by carlos adopt — do not hand-edit"
+
+// DataReplicatePlatform and DataReplicateApp are the only two values
+// Data.Replicate accepts (spec §2, §3a): the box-wide carlos-litestream
+// unit replicates the named dbs, or the app streams its own WAL and the
+// platform provisions the directory and replicates nothing. Two
+// replicators on one WAL fight over checkpointing and corrupt the replica,
+// so the two are mutually exclusive — see the "dbs with replicate:\"app\""
+// rule in ValidCapabilities.
+const (
+ DataReplicatePlatform = "platform"
+ DataReplicateApp = "app"
+)
+
+// tokenRe is the charset shared by command, runtime_dir, and ports[].name
+// (spec §7): one lowercase token, so "../etc", "carlos/apps", absolute
+// paths, flags, and spaces never parse.
+var tokenRe = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`)
+
+// dbNameRe is data.dbs's charset: a bare filename, used only as
+// filepath.Join(stateDir, name) and never a path (spec §7). "." and ".."
+// pass this character class and are rejected separately below.
+var dbNameRe = regexp.MustCompile(`^[A-Za-z0-9._-]{1,64}$`)
+
+// reservedRuntimeDir names a directory the platform or the OS already owns.
+// "carlos" is the load-bearing entry: a tenant owning /run/carlos would own
+// every tenant socket the edge dials — the single most valuable directory
+// on the box (spec §7). The "systemd-" prefix is checked separately.
+var reservedRuntimeDir = map[string]bool{
+ "carlos": true,
+ "systemd": true,
+ "lock": true,
+ "log": true,
+ "user": true,
+ "dbus": true,
+ "sshd": true,
+ "udev": true,
+}
+
+// reservedPort is bound absolutely and is never grantable, typo or not
+// (spec §7): the edge's own ports and the box's SSH.
+var reservedPort = map[int]bool{22: true, 80: true, 443: true}
+
+// knownCapability is the closed vocabulary's seven names. A manifest key
+// under "capabilities" that isn't one of these (or "v") names a capability
+// this binary does not know, and UnmarshalJSON refuses the whole block.
+var knownCapability = map[string]bool{
+ "command": true,
+ "daemon": true,
+ "children": true,
+ "runtime_dir": true,
+ "ports": true,
+ "data": true,
+ "host_role": true,
+}
+
+// Set is a manifest's whole "capabilities" object. It is decoded only
+// through UnmarshalJSON below — the strict door that tells an unknown
+// capability NAME (refuse) apart from an unknown FIELD inside a known one
+// (ignore and note).
+type Set struct {
+ V int `json:"v"`
+ Command string `json:"command,omitempty"`
+ Daemon bool `json:"daemon,omitempty"`
+ Children bool `json:"children,omitempty"`
+ RuntimeDir string `json:"runtime_dir,omitempty"`
+ Ports []Port `json:"ports,omitempty"`
+ Data *Data `json:"data,omitempty"`
+ HostRole bool `json:"host_role,omitempty"`
+
+ notes []string // unknown-field-inside-a-known-capability lines; see Notes
+}
+
+// Port is one privileged-port ask. Name is an operator/log label and
+// carries no behaviour (spec §3a, §4c) — it is never a systemd
+// FileDescriptorName.
+type Port struct {
+ Port int `json:"port"`
+ Name string `json:"name,omitempty"` // an operator/log label; carries NO behaviour
+}
+
+// Data is the persistent-data-dir ask: which files under the state dir are
+// databases, and who replicates them. The path itself is never in the
+// manifest — it is always StateDirectory=carlos-app/%i (spec §2).
+type Data struct {
+ Replicate string `json:"replicate"` // DataReplicatePlatform | DataReplicateApp
+ DBs []string `json:"dbs,omitempty"`
+}
+
+// UnmarshalJSON is the strict door (spec §3b), and the three outcomes are
+// deliberately different:
+// - an unknown CAPABILITY NAME, or a "v" greater than Version: error,
+// naming the capability and "update the platform binary". This
+// propagates through json.Unmarshal -> release.GetManifest ->
+// ensureStaged, which is exactly how the version comes to be refused on
+// this box.
+// - an unknown FIELD INSIDE a known capability: accepted, recorded in
+// notes.
+// - anything absent: the zero value, which means "not declared".
+//
+// It decodes into a map[string]json.RawMessage first so it can check every
+// key against the seven known names plus "v" before decoding anything
+// typed, then re-decodes each object-valued capability with a second
+// map[string]json.RawMessage pass to collect notes before decoding into the
+// typed value. json.Decoder.DisallowUnknownFields is deliberately NOT used
+// on the whole block — it cannot distinguish an unknown name (refuse) from
+// an unknown field (ignore), and that distinction is the entire
+// forward-compatibility contract (spec §3b).
+func (s *Set) UnmarshalJSON(b []byte) error {
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(b, &raw); err != nil {
+ return err
+ }
+
+ // "v" absent decodes as the zero value, which is never Version (today
+ // Version is 1) — an omitted "v" on a present block is refused exactly
+ // like an explicit mismatch, per the validation table.
+ var v int
+ if vRaw, ok := raw["v"]; ok {
+ if err := json.Unmarshal(vRaw, &v); err != nil {
+ return fmt.Errorf("capabilities: v: %w", err)
+ }
+ }
+ if v != Version {
+ return fmt.Errorf("capabilities: v %d is not supported by this binary (this binary speaks v%d) — update the platform binary", v, Version)
+ }
+
+ for name := range raw {
+ if name == "v" {
+ continue
+ }
+ if !knownCapability[name] {
+ return fmt.Errorf("capabilities: unknown capability %q — update the platform binary", name)
+ }
+ }
+
+ var notes []string
+
+ if err := decodeIfPresent(raw, "command", &s.Command); err != nil {
+ return fmt.Errorf("capabilities: command: %w", err)
+ }
+ if err := decodeIfPresent(raw, "daemon", &s.Daemon); err != nil {
+ return fmt.Errorf("capabilities: daemon: %w", err)
+ }
+ if err := decodeIfPresent(raw, "children", &s.Children); err != nil {
+ return fmt.Errorf("capabilities: children: %w", err)
+ }
+ if err := decodeIfPresent(raw, "runtime_dir", &s.RuntimeDir); err != nil {
+ return fmt.Errorf("capabilities: runtime_dir: %w", err)
+ }
+ if err := decodeIfPresent(raw, "host_role", &s.HostRole); err != nil {
+ return fmt.Errorf("capabilities: host_role: %w", err)
+ }
+
+ if portsRaw, ok := raw["ports"]; ok {
+ var items []json.RawMessage
+ if err := json.Unmarshal(portsRaw, &items); err != nil {
+ return fmt.Errorf("capabilities: ports: %w", err)
+ }
+ ports := make([]Port, len(items))
+ for i, item := range items {
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(item, &fields); err != nil {
+ return fmt.Errorf("capabilities: ports[%d]: %w", i, err)
+ }
+ for f := range fields {
+ if f != "port" && f != "name" {
+ notes = append(notes, fmt.Sprintf("capabilities: ports[%d]: unknown field %q ignored", i, f))
+ }
+ }
+ if err := json.Unmarshal(item, &ports[i]); err != nil {
+ return fmt.Errorf("capabilities: ports[%d]: %w", i, err)
+ }
+ }
+ s.Ports = ports
+ }
+
+ if dataRaw, ok := raw["data"]; ok {
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(dataRaw, &fields); err != nil {
+ return fmt.Errorf("capabilities: data: %w", err)
+ }
+ for f := range fields {
+ if f != "replicate" && f != "dbs" {
+ notes = append(notes, fmt.Sprintf("capabilities: data: unknown field %q ignored", f))
+ }
+ }
+ var d Data
+ if err := json.Unmarshal(dataRaw, &d); err != nil {
+ return fmt.Errorf("capabilities: data: %w", err)
+ }
+ s.Data = &d
+ }
+
+ sort.Strings(notes)
+ s.V = v
+ s.notes = notes
+ return nil
+}
+
+// decodeIfPresent unmarshals raw[key] into out when key is present, and
+// leaves out untouched (the zero value, meaning "not declared") otherwise.
+func decodeIfPresent(raw map[string]json.RawMessage, key string, out any) error {
+ if v, ok := raw[key]; ok {
+ return json.Unmarshal(v, out)
+ }
+ return nil
+}
+
+// Notes are the ignore-with-a-log lines the decoder collected, sorted.
+// Callers print them once per pass, never per host.
+func (s *Set) Notes() []string {
+ return s.notes
+}
+
+// ValidCapabilities is the semantic gate, shared by SHIP (a human is
+// present to read the refusal) and ADOPT (the box refuses the version),
+// exactly as appconfig.ValidExecDelivery is shared by set-time and
+// sync-time. A nil Set is valid and means no capabilities. app is needed
+// for the host_role user-name length check and nothing else.
+func ValidCapabilities(app string, s *Set) error {
+ if s == nil {
+ return nil
+ }
+ if s.V != Version {
+ return fmt.Errorf("capabilities: v %d is not supported by this binary (this binary speaks v%d) — update the platform binary", s.V, Version)
+ }
+ if s.Command != "" && !tokenRe.MatchString(s.Command) {
+ return fmt.Errorf("capabilities: command %q must be one lowercase token matching ^[a-z][a-z0-9-]{0,31}$ — one token, no flags, no spaces", s.Command)
+ }
+ if s.RuntimeDir != "" {
+ if !tokenRe.MatchString(s.RuntimeDir) {
+ return fmt.Errorf("capabilities: runtime_dir %q must match ^[a-z][a-z0-9-]{0,31}$", s.RuntimeDir)
+ }
+ if reservedRuntimeDir[s.RuntimeDir] || strings.HasPrefix(s.RuntimeDir, "systemd-") {
+ return fmt.Errorf("capabilities: runtime_dir %q is a reserved name", s.RuntimeDir)
+ }
+ }
+ if err := validPorts(s.Ports); err != nil {
+ return err
+ }
+ if s.Data != nil {
+ if err := validData(s.Data); err != nil {
+ return err
+ }
+ }
+ if s.HostRole {
+ if u := AppUser(app); len(u) > 32 {
+ return fmt.Errorf("capabilities: host_role: user %q is longer than the 32-character account-name limit — refused at ship, where a human can rename", u)
+ }
+ }
+ return nil
+}
+
+// validPorts encodes the ports rules: only privileged ports are a
+// capability, 22/80/443 are reserved absolutely, no duplicates, and a bound
+// of 4 — a bound, not a feature.
+func validPorts(ports []Port) error {
+ if len(ports) > 4 {
+ return fmt.Errorf("capabilities: ports: %d declared, want at most 4", len(ports))
+ }
+ seen := make(map[int]bool, len(ports))
+ for _, p := range ports {
+ if p.Port < 1 || p.Port > 1023 {
+ return fmt.Errorf("capabilities: ports: port %d is outside 1..1023 — only privileged ports are a capability; a high port needs no help from the platform", p.Port)
+ }
+ if reservedPort[p.Port] {
+ return fmt.Errorf("capabilities: ports: port %d is reserved and not grantable", p.Port)
+ }
+ if seen[p.Port] {
+ return fmt.Errorf("capabilities: ports: port %d declared more than once", p.Port)
+ }
+ seen[p.Port] = true
+ if p.Name != "" && !tokenRe.MatchString(p.Name) {
+ return fmt.Errorf("capabilities: ports: name %q must match ^[a-z][a-z0-9-]{0,31}$ — it lands in logs", p.Name)
+ }
+ }
+ return nil
+}
+
+// validData encodes the data rules: replicate must be one of the two
+// constants, dbs is a bound list of bare filenames, and dbs is meaningless
+// (and refused) when the app replicates its own WAL.
+func validData(d *Data) error {
+ if d.Replicate != DataReplicatePlatform && d.Replicate != DataReplicateApp {
+ return fmt.Errorf("capabilities: data.replicate %q must be %q or %q", d.Replicate, DataReplicatePlatform, DataReplicateApp)
+ }
+ if len(d.DBs) > 8 {
+ return fmt.Errorf("capabilities: data.dbs: %d entries, want at most 8", len(d.DBs))
+ }
+ if d.Replicate == DataReplicateApp && len(d.DBs) > 0 {
+ return fmt.Errorf("capabilities: data.dbs is meaningless when replicate is %q — the app streams its own WAL, and accepting it would invite the two-replicators hazard", DataReplicateApp)
+ }
+ for _, name := range d.DBs {
+ if name == "." || name == ".." || !dbNameRe.MatchString(name) {
+ return fmt.Errorf("capabilities: data.dbs: %q must be a bare filename matching ^[A-Za-z0-9._-]{1,64}$", name)
+ }
+ }
+ return nil
+}
+
+// AppUser is the static system user a host_role app runs as, DERIVED by the
+// platform and never taken from a manifest.
+func AppUser(app string) string {
+ return "carlos-" + app
+}
diff --git a/internal/caps/caps_test.go b/internal/caps/caps_test.go
new file mode 100644
index 000000000..0d09f1252
--- /dev/null
+++ b/internal/caps/caps_test.go
@@ -0,0 +1,213 @@
+package caps
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+)
+
+// TestUnknownCapabilityNameIsRefused pins the fail-closed half of spec §3b:
+// a capability name this binary does not know must refuse the whole block,
+// naming the capability and the fix — never silently drop it.
+func TestUnknownCapabilityNameIsRefused(t *testing.T) {
+ var s Set
+ err := json.Unmarshal([]byte(`{"v":1,"daemon":true,"gpu":true}`), &s)
+ if err == nil || !strings.Contains(err.Error(), "gpu") {
+ t.Fatalf("err = %v, want a refusal naming gpu", err)
+ }
+ if !strings.Contains(err.Error(), "update the platform binary") {
+ t.Fatalf("refusal %q does not name the fix", err)
+ }
+}
+
+// TestUnknownFieldInsideAKnownCapabilityIsIgnoredAndNoted pins the tolerant
+// half of spec §3b: room to grow a known capability additively without
+// breaking an older binary that already understands its other fields.
+func TestUnknownFieldInsideAKnownCapabilityIsIgnoredAndNoted(t *testing.T) {
+ var s Set
+ if err := json.Unmarshal([]byte(`{"v":1,"data":{"replicate":"platform","dbs":["console.db"],"future":7}}`), &s); err != nil {
+ t.Fatalf("unknown field inside a known capability must NOT fail: %v", err)
+ }
+ if s.Data == nil || s.Data.Replicate != DataReplicatePlatform || len(s.Data.DBs) != 1 {
+ t.Fatalf("known fields lost: %+v", s.Data)
+ }
+ if n := s.Notes(); len(n) != 1 || !strings.Contains(n[0], "future") {
+ t.Fatalf("notes = %v, want one line naming future", n)
+ }
+}
+
+// TestVersionBumpIsRefused pins fail-closed on "v": the box keeps serving
+// what it has rather than materialize a unit missing something new.
+func TestVersionBumpIsRefused(t *testing.T) {
+ var s Set
+ if err := json.Unmarshal([]byte(`{"v":2,"daemon":true}`), &s); err == nil {
+ t.Fatal("v:2 must be refused — fail closed, the box keeps what it serves")
+ }
+}
+
+// TestCapabilitiesEmptyObjectWithNoVIsRefused pins the reviewer's sharpest
+// edge case: "capabilities": {} must NOT be read as "no capabilities" — an
+// absent "v" on a PRESENT block decodes as the zero value, which is never
+// Version, so it is refused exactly like an explicit mismatch. Only an
+// absent capabilities KEY (a nil *Set, never constructed) means "none".
+func TestCapabilitiesEmptyObjectWithNoVIsRefused(t *testing.T) {
+ var s Set
+ if err := json.Unmarshal([]byte(`{}`), &s); err == nil {
+ t.Fatal("{} (no v) must be refused, not silently mean no capabilities")
+ }
+}
+
+// TestRuntimeDirCharsetAndReservedNames pins the charset and the reserved
+// list. NOTE: the plan's Step-1 listing included "" in this bad list, but
+// that contradicts both TestNilSetIsValidAndMeansNothing and
+// TestPortsReservedAndNonPrivilegedRefused's final assertion (a Set with
+// only Ports set, RuntimeDir at its zero value "", must be valid) — an
+// empty RuntimeDir means "not declared", not "declared as empty". "" is
+// dropped here and pinned separately as valid below.
+func TestRuntimeDirCharsetAndReservedNames(t *testing.T) {
+ for _, bad := range []string{"../etc", "carlos/apps", "/run/keymail", "Keymail", "carlos", "systemd", "systemd-foo", "lock", "log", "user", "dbus", "sshd", "udev", strings.Repeat("a", 33)} {
+ s := &Set{V: 1, RuntimeDir: bad}
+ if err := ValidCapabilities("keymail", s); err == nil {
+ t.Errorf("runtime_dir %q was accepted", bad)
+ }
+ }
+ if err := ValidCapabilities("keymail", &Set{V: 1, RuntimeDir: "keymail"}); err != nil {
+ t.Fatalf("keymail refused: %v", err)
+ }
+}
+
+// TestRuntimeDirEmptyMeansNotDeclared pins the deviation explained in the
+// comment above: the zero value is "not declared", not an invalid name.
+func TestRuntimeDirEmptyMeansNotDeclared(t *testing.T) {
+ if err := ValidCapabilities("keymail", &Set{V: 1, RuntimeDir: ""}); err != nil {
+ t.Fatalf("empty runtime_dir (not declared) must be valid: %v", err)
+ }
+}
+
+// TestPortsReservedAndNonPrivilegedRefused pins the port range and the
+// absolute reservation of 22/80/443 — an operator typo must not be able to
+// hand a tenant the edge's ports or the box's SSH (spec §7).
+func TestPortsReservedAndNonPrivilegedRefused(t *testing.T) {
+ for _, p := range []int{0, 22, 80, 443, 1024, 8080, 65535, -1} {
+ if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: p}}}); err == nil {
+ t.Errorf("port %d was accepted", p)
+ }
+ }
+ if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 25, Name: "mx"}}}); err != nil {
+ t.Fatalf("port 25 refused: %v", err)
+ }
+}
+
+// TestDuplicatePortIsRefused pins the plan's "Plus:" line item: the same
+// port twice in one ports list is refused rather than silently deduplicated.
+func TestDuplicatePortIsRefused(t *testing.T) {
+ if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 25}, {Port: 25}}}); err == nil {
+ t.Fatal("duplicate port 25 was accepted")
+ }
+}
+
+// TestTooManyPortsIsRefused pins the len(ports) > 4 bound — a bound, not a
+// feature.
+func TestTooManyPortsIsRefused(t *testing.T) {
+ ports := []Port{{Port: 1}, {Port: 2}, {Port: 3}, {Port: 4}, {Port: 5}}
+ if err := ValidCapabilities("keymail", &Set{V: 1, Ports: ports}); err == nil {
+ t.Fatal("5 ports (bound is 4) was accepted")
+ }
+ if err := ValidCapabilities("keymail", &Set{V: 1, Ports: ports[:4]}); err != nil {
+ t.Fatalf("4 ports (at the bound) refused: %v", err)
+ }
+}
+
+// TestPortNameCharsetIsRefused pins ports[].name's charset — it lands in
+// logs.
+func TestPortNameCharsetIsRefused(t *testing.T) {
+ if err := ValidCapabilities("keymail", &Set{V: 1, Ports: []Port{{Port: 25, Name: "Mail Exchange!"}}}); err == nil {
+ t.Fatal("port name with spaces and punctuation was accepted")
+ }
+}
+
+// TestHostRoleRefusesAnAppNameTooLongForAUser pins the account-name limit,
+// refused at ship where a human can rename.
+func TestHostRoleRefusesAnAppNameTooLongForAUser(t *testing.T) {
+ long := strings.Repeat("a", 30) // carlos-<30> = 37 > 32
+ if err := ValidCapabilities(long, &Set{V: 1, HostRole: true}); err == nil {
+ t.Fatal("host_role on an over-long app name must be refused at ship")
+ }
+}
+
+// TestNilSetIsValidAndMeansNothing pins today's every-manifest behaviour:
+// no capabilities key parses to a nil *Set, and a nil *Set is valid.
+func TestNilSetIsValidAndMeansNothing(t *testing.T) {
+ if err := ValidCapabilities("console", nil); err != nil {
+ t.Fatalf("nil (today's every manifest) must be valid: %v", err)
+ }
+}
+
+// TestCommandCharsetIsRefused pins the plan's "Plus:" line item: a command
+// is one lowercase token, never flags or a capitalized subcommand.
+func TestCommandCharsetIsRefused(t *testing.T) {
+ for _, bad := range []string{"relay --flag", "Relay", "1relay", "-relay", "a b", strings.Repeat("a", 33)} {
+ if err := ValidCapabilities("keymail", &Set{V: 1, Command: bad}); err == nil {
+ t.Errorf("command %q was accepted", bad)
+ }
+ }
+ if err := ValidCapabilities("keymail", &Set{V: 1, Command: "relay"}); err != nil {
+ t.Fatalf("command \"relay\" refused: %v", err)
+ }
+}
+
+// TestDataDBsCharsetAndSeparatorRefusals pins the "used only as
+// filepath.Join(stateDir, name)" rail: dbs entries must be bare filenames,
+// never ".", "..", or containing any separator.
+func TestDataDBsCharsetAndSeparatorRefusals(t *testing.T) {
+ for _, bad := range []string{".", "..", "console.db/../../etc/passwd", "a/b", "con*", strings.Repeat("a", 65)} {
+ s := &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{bad}}}
+ if err := ValidCapabilities("console", s); err == nil {
+ t.Errorf("dbs entry %q was accepted", bad)
+ }
+ }
+ if err := ValidCapabilities("console", &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}}); err != nil {
+ t.Fatalf("console.db refused: %v", err)
+ }
+}
+
+// TestTooManyDBsIsRefused pins the len(data.dbs) > 8 bound.
+func TestTooManyDBsIsRefused(t *testing.T) {
+ dbs := make([]string, 9)
+ for i := range dbs {
+ dbs[i] = strings.Repeat("a", i+1) + ".db"
+ }
+ if err := ValidCapabilities("console", &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: dbs}}); err == nil {
+ t.Fatal("9 dbs (bound is 8) was accepted")
+ }
+}
+
+// TestDataDBsWithAppReplicateIsRefused pins the two-replicators hazard: dbs
+// is meaningless when the app streams its own WAL, so it is refused rather
+// than silently ignored.
+func TestDataDBsWithAppReplicateIsRefused(t *testing.T) {
+ s := &Set{V: 1, Data: &Data{Replicate: DataReplicateApp, DBs: []string{"x.db"}}}
+ if err := ValidCapabilities("keymail", s); err == nil {
+ t.Fatal("dbs alongside replicate:\"app\" must be refused")
+ }
+}
+
+// TestDataReplicateUnknownValueIsRefused pins the closed pair of
+// replicate values.
+func TestDataReplicateUnknownValueIsRefused(t *testing.T) {
+ if err := ValidCapabilities("keymail", &Set{V: 1, Data: &Data{Replicate: "bogus"}}); err == nil {
+ t.Fatal("data.replicate \"bogus\" was accepted")
+ }
+}
+
+// TestGrantorReplicateValuesAreValidOnTheirOwn pins that replicate:"app"
+// with no dbs, and replicate:"platform" with dbs, are both accepted —
+// only replicate:"app" WITH dbs is refused.
+func TestGrantorReplicateValuesAreValidOnTheirOwn(t *testing.T) {
+ if err := ValidCapabilities("keymail", &Set{V: 1, Data: &Data{Replicate: DataReplicateApp}}); err != nil {
+ t.Fatalf("replicate:\"app\" with no dbs refused: %v", err)
+ }
+ if err := ValidCapabilities("console", &Set{V: 1, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}}); err != nil {
+ t.Fatalf("replicate:\"platform\" with dbs refused: %v", err)
+ }
+}