Go 180 lines
package cockpit
import (
"errors"
"fmt"
"path/filepath"
"strings"
)
// Renderer translates a neutral Playbook into one harness target's config
// bytes. A Renderer is the only component that knows a target's file layout
// (RelPath) and permission spelling (Render). Render must be deterministic:
// the same Playbook yields byte-identical output, so re-emit is idempotent
// and drift detection is a sha comparison.
//
// A per-playbook target (Claude, Cursor) emits one file per Playbook. An
// aggregate target (AGENTS.md, GEMINI.md) emits one shared file for the whole
// selected set and additionally implements AggregateRenderer; the per-playbook
// Render/RelPath on an aggregate renderer are defined for interface
// completeness but are not on the emit path (Generate rejects aggregate
// targets — see emit.go).
type Renderer interface {
Target() string // the target name, e.g. "claude"
RelPath(p Playbook) string // artifact path relative to the user dir
Render(p Playbook) ([]byte, error) // deterministic config bytes
}
// AggregateRenderer is the optional contract for a target whose config is a
// single shared file describing the whole selected playbook set (AGENTS.md,
// GEMINI.md) rather than one file per playbook. It is discovered by
// type-assertion (isAggregate); a renderer that does not implement it is
// per-playbook. RenderAll must be deterministic for a given set regardless of
// input order (it sorts by Name internally) so re-emit stays sha-idempotent.
type AggregateRenderer interface {
Renderer
AggRelPath() string // single shared artifact path, playbook-independent
RenderAll(ps []Playbook) ([]byte, error) // deterministic bytes for the whole set
}
// isAggregate reports whether r emits one shared file for a set rather than
// one file per playbook, returning the aggregate view when it does.
func isAggregate(r Renderer) (AggregateRenderer, bool) {
a, ok := r.(AggregateRenderer)
return a, ok
}
// Enforcement describes the harness-runtime side of a target's safety
// guarantee: whether the harness itself enforces the playbook's allowlist
// (Claude's allowed-tools) or the emitted artifact is only advisory text the
// AI is asked to honor (AGENTS.md, GEMINI.md, Cursor rules). It says nothing
// about eeco's own generate-time gate, which is always enforced for every
// target — "advisory" never relaxes the safety invariant, it only describes
// what the downstream harness does with the file.
type Enforcement int
const (
// EnforcementEnforced means the harness enforces the allowlist at runtime.
EnforcementEnforced Enforcement = iota
// EnforcementAdvisory means the artifact is advisory text only — the
// harness does not enforce tool permissions from it.
EnforcementAdvisory
)
// String renders the enforcement level for banners, fidelity lines, and
// status output.
func (e Enforcement) String() string {
if e == EnforcementEnforced {
return "enforced"
}
return "advisory"
}
// Fidelity is the optional contract a Renderer implements to declare its
// harness-runtime enforcement. A renderer that does not implement it is
// treated as advisory (fail-honest default — never claim enforcement that
// isn't there).
type Fidelity interface {
Enforcement() Enforcement
}
// fidelityOf returns r's declared enforcement, defaulting to advisory for a
// renderer that does not declare one (fail-honest).
func fidelityOf(r Renderer) Enforcement {
if f, ok := r.(Fidelity); ok {
return f.Enforcement()
}
return EnforcementAdvisory
}
// relUnder validates that a renderer-supplied artifact path stays inside the
// user dir before it is joined to cfg.UserDir — the write-scope-floor guard.
// It rejects an absolute path or one that escapes via "..", mirroring the
// path guards in internal/config, and returns the cleaned relative path.
func relUnder(rel string) (string, error) {
if strings.TrimSpace(rel) == "" {
return "", errors.New("empty artifact path")
}
// A leading "/" or "\" is not absolute on Windows (Go needs a volume
// name), so guard the rooted forms explicitly alongside filepath.IsAbs.
if filepath.IsAbs(rel) || strings.HasPrefix(rel, "/") || strings.HasPrefix(rel, `\`) {
return "", fmt.Errorf("artifact path %q must be relative to the user dir", rel)
}
clean := filepath.Clean(rel)
if slash := filepath.ToSlash(clean); slash == ".." || strings.HasPrefix(slash, "../") {
return "", fmt.Errorf("artifact path %q escapes the user dir", rel)
}
return clean, nil
}
// rendererFor returns the renderer for a target name. Claude is enforced and
// per-playbook; Cursor is advisory and per-playbook; AGENTS.md and GEMINI.md
// are advisory and aggregate.
func rendererFor(target string) (Renderer, bool) {
switch target {
case "claude":
return claudeRenderer{}, true
case "cursor":
return cursorRenderer{}, true
case "agents":
return agentsRenderer{}, true
case "gemini":
return geminiRenderer{}, true
default:
return nil, false
}
}
// Targets lists the renderer target names eeco can emit, for usage and error
// messages. Order is stable (Claude first as the enforced reference target).
func Targets() []string {
return []string{"claude", "cursor", "agents", "gemini"}
}
// unknownTargetErr is the shared "unknown target" error naming the known set,
// so every entry point words it identically.
func unknownTargetErr(target string) error {
return fmt.Errorf("unknown target %q (known: %s)", target, strings.Join(Targets(), ", "))
}
// IsAggregateTarget reports whether target emits one shared file for the whole
// set (AGENTS.md, GEMINI.md) rather than one file per playbook. Unknown
// targets report false. The cmd layer uses it to route generate/verify/off
// down the per-playbook or aggregate path.
func IsAggregateTarget(target string) bool {
r, ok := rendererFor(target)
if !ok {
return false
}
_, agg := isAggregate(r)
return agg
}
// TargetFidelity returns target's harness-runtime enforcement and whether the
// target is known, for `eeco cockpit target list` and status.
func TargetFidelity(target string) (Enforcement, bool) {
r, ok := rendererFor(target)
if !ok {
return EnforcementAdvisory, false
}
return fidelityOf(r), true
}
// MachineryFidelity reports whether target can host the auto-firing
// deterministic machinery — the PreToolUse git-write guard and the other
// runtime hook events the cockpit emits. It is EnforcementEnforced only for a
// harness with a real runtime hook channel (claude), EnforcementAdvisory
// otherwise; the second return is false for an unknown target. It is the
// machinery analog of TargetFidelity (which describes the emitted playbook
// allowlist), letting the cmd layer print honest fidelity without the hooks
// package importing target logic.
func MachineryFidelity(target string) (Enforcement, bool) {
if _, ok := rendererFor(target); !ok {
return EnforcementAdvisory, false
}
if target == "claude" {
return EnforcementEnforced, true
}
return EnforcementAdvisory, true
}