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
29 changes: 29 additions & 0 deletions config/profiles/default/profile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# profiles/default/profile.yml — the committed fleet config IS the `default`
# profile (RIG-2968 T1). Every Compass Manager without an explicit profile
# resolves to this one, so it must always exist and pass the store door.
#
# v1 consumes ONLY the `models` axis; corpus/extensions/settings are schema'd
# but their consumption is deferred (their shapes are provisional until first
# consumption — see the design's Resolved decisions). They are declared here as
# empty/null so `default` documents the full superset.
models:
# The Manager's own model selector. Mirrors the committed fleet manager model
# (the seeded root supervisor's model): the fleet runs on litellm/claude-opus,
# and the design's own superset example pins the manager at :high.
manager: litellm/claude-opus:high
# Per-subagent-role models, keyed by the FRONTMATTER name: of a shipped
# agents/*.md def. The fleet ships config/agents/implementer.md
# (frontmatter name: implementer); left empty here so `default` pins no
# per-role override — subagents inherit the Manager's model.
agents: {}
corpus:
# System-prompt corpus selection. SCHEMA'd, consumption DEFERRED.
prompts: null
skills: []
rules: []
extensions:
# Extension/MCP tool-set selection. SCHEMA'd, consumption DEFERRED.
mcp: null
settings:
# Session-settings overlay. SCHEMA'd, consumption DEFERRED.
{}
25 changes: 22 additions & 3 deletions go/cmd/compass/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
topDirRules = "rules"
topDirAgents = "agents"
topDirPrompts = "prompts"
topDirProfiles = "profiles"
)

// Top-level regular-file members admitted by exact filename, not under a top dir
Expand All @@ -46,6 +47,9 @@ const (
// memberSystemMD is the only filename admitted under prompts/<role>/
// (store door: RIG-3075 T2).
memberSystemMD = "SYSTEM.md"
// memberProfileYML is the only filename admitted under profiles/<name>/
// (store door: RIG-2968 T1).
memberProfileYML = "profile.yml"
)

// maxBundleFileCount and maxBundleContentBytes are a fail-fast client-side check
Expand All @@ -72,6 +76,7 @@ var bundleTopDirs = map[string]bool{
topDirRules: true,
topDirAgents: true,
topDirPrompts: true,
topDirProfiles: true,
}

// bundleNamePattern is the grammar for a member's <name> segment (store door:
Expand Down Expand Up @@ -133,7 +138,7 @@ func buildBundle(dir string) ([]byte, error) {
return nil, walkErr
}
if fileCount == 0 {
return nil, fmt.Errorf("bundle directory %q contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/ and no top-level %s or %s; use `agent-config delete` to clear the fleet config", dir, memberAgentsMD, memberModels)
return nil, fmt.Errorf("bundle directory %q contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, prompts/, or profiles/ and no top-level %s or %s; use `agent-config delete` to clear the fleet config", dir, memberAgentsMD, memberModels)
}
if err := tw.Close(); err != nil {
return nil, fmt.Errorf("finalizing tar: %w", err)
Expand Down Expand Up @@ -170,7 +175,7 @@ func validateDirMember(name string) error {
if !bundleTopDirs[parts[0]] {
return errNotWhitelisted(name)
}
if (parts[0] == topDirSkills || parts[0] == topDirExtensions || parts[0] == topDirPrompts) && len(parts) >= 2 && !bundleNamePattern.MatchString(parts[1]) {
if (parts[0] == topDirSkills || parts[0] == topDirExtensions || parts[0] == topDirPrompts || parts[0] == topDirProfiles) && len(parts) >= 2 && !bundleNamePattern.MatchString(parts[1]) {
return fmt.Errorf("bundle member name %q must match %s", parts[1], bundleNamePattern.String())
}
return nil
Expand All @@ -180,7 +185,7 @@ func validateDirMember(name string) error {
// neither under a whitelisted top dir nor one of the two top-level singletons
// (store door: configMemberParts).
func errNotWhitelisted(name string) error {
return fmt.Errorf("bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/ and is not a top-level %s or %s", name, memberAgentsMD, memberModels)
return fmt.Errorf("bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, agents/, prompts/, or profiles/ and is not a top-level %s or %s", name, memberAgentsMD, memberModels)
}

// addRegularMember validates a regular file against the door grammar and writes
Expand Down Expand Up @@ -292,6 +297,20 @@ func validateMemberGrammar(parts []string, name string, content []byte) error {
return fmt.Errorf("prompts role name %q must match %s", parts[1], bundleNamePattern.String())
}
return nil
case topDirProfiles:
// Exactly profiles/<name>/profile.yml — three components, grammar-valid
// <name>, filename exactly profile.yml, and a YAML-mapping body (the
// cheap shape check, twin of settings/models). The superset-key closure
// and cross-member models.agents frontmatter-name lint are the store
// door's authoritative checks, NOT replicated client-side (store door:
// RIG-2968 T1).
if len(parts) != 3 || parts[2] != memberProfileYML {
return fmt.Errorf("profiles member %q must be profiles/<name>/%s", name, memberProfileYML)
}
if !bundleNamePattern.MatchString(parts[1]) {
return fmt.Errorf("profiles name %q must match %s", parts[1], bundleNamePattern.String())
}
return validateYAMLMapping(name, content)
default:
// skills/ or extensions/: the <name> second component must match.
if len(parts) < 2 {
Expand Down
11 changes: 9 additions & 2 deletions go/cmd/compass/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func TestBuildBundleRejects(t *testing.T) {
{"prompts too deep", map[string]string{"prompts/supervisor/sub/SYSTEM.md": "x"}, "must be prompts/<role>/SYSTEM.md"},
{"prompts too shallow", map[string]string{"prompts/SYSTEM.md": "x"}, "must be prompts/<role>/SYSTEM.md"},
{"prompts bad role name", map[string]string{"prompts/bad name/SYSTEM.md": "x"}, "must match"},
{"profiles wrong filename", map[string]string{"profiles/candidate/other.yml": "models: {}\n"}, "must be profiles/<name>/profile.yml"},
{"profiles too deep", map[string]string{"profiles/candidate/sub/profile.yml": "models: {}\n"}, "must be profiles/<name>/profile.yml"},
{"profiles too shallow", map[string]string{"profiles/profile.yml": "models: {}\n"}, "must be profiles/<name>/profile.yml"},
{"profiles bad name", map[string]string{"profiles/bad name/profile.yml": "models: {}\n"}, "must match"},
{"profiles non-mapping", map[string]string{"profiles/candidate/profile.yml": "- a\n- b\n"}, "must be a YAML mapping"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -192,7 +197,7 @@ func TestBuildBundleEmpty(t *testing.T) {
if err == nil {
t.Fatal("buildBundle(empty dir) = nil error, want rejection")
}
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/") {
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, prompts/, or profiles/") {
t.Errorf("buildBundle(empty) error %q does not name the no-members condition", err)
}
})
Expand All @@ -206,7 +211,7 @@ func TestBuildBundleEmpty(t *testing.T) {
if err == nil {
t.Fatal("buildBundle(only empty subdir) = nil error, want rejection")
}
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/") {
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, prompts/, or profiles/") {
t.Errorf("buildBundle(empty subdir) error %q does not name the no-members condition", err)
}
})
Expand Down Expand Up @@ -248,6 +253,7 @@ func TestBuildBundleDoorParity(t *testing.T) {
"agents/zeta.md": "# zeta agent",
"AGENTS.md": "# fleet context",
"prompts/supervisor/SYSTEM.md": "# supervisor prompt",
"profiles/default/profile.yml": "models:\n manager: litellm/claude-opus:high\n agents: {}\n",
"models.yml": "models:\n main: anthropic/claude\n",
})
bundle, err := buildBundle(root)
Expand All @@ -274,6 +280,7 @@ func TestBuildBundleDoorParity(t *testing.T) {
"extensions/beta/main.go",
"mcp/gamma.json",
"models.yml",
"profiles/default/profile.yml",
"prompts/supervisor/SYSTEM.md",
"rules/delta.md",
"rules/epsilon.mdc",
Expand Down
117 changes: 73 additions & 44 deletions go/internal/runner/config_materialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const (
topDirRules = "rules"
topDirAgents = "agents"
topDirPrompts = "prompts"
topDirProfiles = "profiles"
)

// configTopDirs are the only permitted top-level directories in a config bundle.
Expand All @@ -77,6 +78,7 @@ var configTopDirs = map[string]struct{}{
topDirRules: {},
topDirAgents: {},
topDirPrompts: {},
topDirProfiles: {},
}

// Top-level regular-file members admitted by exact filename (RIG-1678 T2), and
Expand All @@ -90,6 +92,9 @@ const (
// memberSystemMD is the only filename admitted under prompts/<role>/
// (RIG-3075 T2); structural twin of the store door.
memberSystemMD = "SYSTEM.md"
// memberProfileYML is the only filename admitted under profiles/<name>/
// (RIG-2968 T1); structural twin of the store door.
memberProfileYML = "profile.yml"
)

// configFetcher is the T3 fetch seam the materializer pulls through. *ServerLink
Expand Down Expand Up @@ -486,7 +491,7 @@ func validateMemberPath(name string, typeflag byte) (string, error) {
return validateTopLevelMember(name, clean, top, typeflag)
}
if _, ok := configTopDirs[top]; !ok {
return "", fmt.Errorf("config bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/", name)
return "", fmt.Errorf("config bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, agents/, prompts/, or profiles/", name)
}
return validateNestedMember(name, clean, top, parts, typeflag)
}
Expand Down Expand Up @@ -515,51 +520,75 @@ func validateTopLevelMember(name, clean, top string, typeflag byte) (string, err
// carry per-dir grammar.
func validateNestedMember(name, clean, top string, parts []string, typeflag byte) (string, error) {
if typeflag == tar.TypeReg {
switch top {
case topDirSettings:
// Exactly settings/config.yml — yml-only (OQ-1).
if clean != settingsMember {
return "", fmt.Errorf("config bundle settings member %q must be exactly %s", name, settingsMember)
}
return clean, nil
case topDirRules:
if err := validateFlatRunnerMember(topDirRules, name, parts, ".md", ".mdc"); err != nil {
return "", err
}
return clean, nil
case topDirAgents:
if err := validateFlatRunnerMember(topDirAgents, name, parts, ".md"); err != nil {
return "", err
}
return clean, nil
case topDirMCP:
// mcp/<name>.json — safe base name, required .json suffix.
entry := parts[1]
base, ok := strings.CutSuffix(entry, ".json")
if !ok {
return "", fmt.Errorf("config bundle mcp member %q must have a .json suffix", name)
}
if !configTopLevelName.MatchString(base) {
return "", fmt.Errorf("config bundle mcp member name %q is not a safe name", entry)
}
return clean, nil
case topDirPrompts:
// Exactly prompts/<role>/SYSTEM.md — three components, safe <role>,
// filename exactly SYSTEM.md (RIG-3075 T2). Stricter than
// skills/extensions, so it needs its own arm, not the fall-through.
if len(parts) != 3 || parts[2] != memberSystemMD {
return "", fmt.Errorf("config bundle prompts member %q must be prompts/<role>/%s", name, memberSystemMD)
}
if !configTopLevelName.MatchString(parts[1]) {
return "", fmt.Errorf("config bundle prompts role name %q is not a safe name", parts[1])
}
return clean, nil
}
return validateNestedRegularMember(name, clean, top, parts)
}
// Directory entries under any top dir: only the first-level name must be
// safe; deeper segments rely on the traversal + containment guards.
if !configTopLevelName.MatchString(parts[1]) {
return "", fmt.Errorf("config bundle member name %q is not a safe name", parts[1])
}
return clean, nil
}

// skills/ or extensions/ (and directory entries under any top dir): the
// first-level name must be safe; deeper segments rely on the traversal +
// containment guards.
// validateNestedRegularMember validates a regular-file member (two or more path
// components) under a whitelisted top dir. settings/rules/agents/mcp/prompts/
// profiles carry per-dir grammar; a regular file under skills/ or extensions/
// requires only a safe first-level name.
func validateNestedRegularMember(name, clean, top string, parts []string) (string, error) {
switch top {
case topDirSettings:
// Exactly settings/config.yml — yml-only (OQ-1).
if clean != settingsMember {
return "", fmt.Errorf("config bundle settings member %q must be exactly %s", name, settingsMember)
}
return clean, nil
case topDirRules:
if err := validateFlatRunnerMember(topDirRules, name, parts, ".md", ".mdc"); err != nil {
return "", err
}
return clean, nil
case topDirAgents:
if err := validateFlatRunnerMember(topDirAgents, name, parts, ".md"); err != nil {
return "", err
}
return clean, nil
case topDirMCP:
// mcp/<name>.json — safe base name, required .json suffix.
entry := parts[1]
base, ok := strings.CutSuffix(entry, ".json")
if !ok {
return "", fmt.Errorf("config bundle mcp member %q must have a .json suffix", name)
}
if !configTopLevelName.MatchString(base) {
return "", fmt.Errorf("config bundle mcp member name %q is not a safe name", entry)
}
return clean, nil
case topDirPrompts:
// Exactly prompts/<role>/SYSTEM.md — three components, safe <role>,
// filename exactly SYSTEM.md (RIG-3075 T2).
if len(parts) != 3 || parts[2] != memberSystemMD {
return "", fmt.Errorf("config bundle prompts member %q must be prompts/<role>/%s", name, memberSystemMD)
}
if !configTopLevelName.MatchString(parts[1]) {
return "", fmt.Errorf("config bundle prompts role name %q is not a safe name", parts[1])
}
return clean, nil
case topDirProfiles:
// Exactly profiles/<name>/profile.yml — three components, safe <name>,
// filename exactly profile.yml (RIG-2968 T1). The store door carries the
// profile SCHEMA validation (superset-key closure + models.agents
// frontmatter-name lint); the runner is the structural twin — layout
// only, no content schema (mirrors the settings/models asymmetry).
if len(parts) != 3 || parts[2] != memberProfileYML {
return "", fmt.Errorf("config bundle profiles member %q must be profiles/<name>/%s", name, memberProfileYML)
}
if !configTopLevelName.MatchString(parts[1]) {
return "", fmt.Errorf("config bundle profiles name %q is not a safe name", parts[1])
}
return clean, nil
}
// skills/ or extensions/: first-level name must be safe; deeper segments
// rely on the traversal + containment guards.
if !configTopLevelName.MatchString(parts[1]) {
return "", fmt.Errorf("config bundle member name %q is not a safe name", parts[1])
}
Expand Down
Loading
Loading