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 }