package registry import ( "fmt" "strings" ) // VerificationPolicy is what a plan's automated checks may execute in this // project. It exists because a plan command is agent-authored, while the // quality gate is operator-authored: the two must not share an execution // envelope just because they run in the same worktree. // // Absence is a refusal, never a default-allow. A project that declares no // policy runs no plan command, and the planner is told so at seal time. type VerificationPolicy struct { // Allowed is a list of argv patterns. A command runs only if some pattern // matches it positionally. Allowed [][]string `json:"allowed,omitempty"` } // Allows reports whether argv matches any pattern, and names the reason when // it does not. The reason reaches the planner, so "not allowed" alone would // send it guessing at the project's real reach. func (p VerificationPolicy) Allows(argv []string) (bool, string) { if len(argv) == 0 { return false, "the command is empty" } if len(p.Allowed) == 0 { return false, "this project declares no verification policy, so no plan command may run" } for _, pattern := range p.Allowed { if matchArgv(pattern, argv) { return true, "" } } return false, fmt.Sprintf("%q is not in this project's verification policy, which allows: %s", strings.Join(argv, " "), p.describe()) } func (p VerificationPolicy) describe() string { out := make([]string, 0, len(p.Allowed)) for _, pattern := range p.Allowed { out = append(out, strings.Join(pattern, " ")) } return strings.Join(out, "; ") } // matchArgv matches positionally, never by prefix. // // - a literal element matches that element exactly // - "*" matches exactly one element, any value // - an element ending in "/..." matches a path argument under that prefix, // so ./internal/... covers ./internal/store/... but never ./internalsecrets // - "*" as the last pattern element matches every remaining element, and is // the only way a pattern covers a longer command. An operator writes that // deliberately for a runner that takes free-form arguments. // // A shorter pattern otherwise fails, so ["go", "test"] never authorises // ["go", "test", "-exec", "curl"]. func matchArgv(pattern, argv []string) bool { if len(pattern) == 0 { return false } for i, want := range pattern { if want == "*" && i == len(pattern)-1 && len(argv) >= len(pattern) { // Trailing wildcard: everything from here on is covered. return true } if i >= len(argv) { return false } got := argv[i] switch { case want == "*": // One element, any value. An empty argument is still an argument. case strings.HasSuffix(want, "/..."): if !strings.HasPrefix(got, strings.TrimSuffix(want, "...")) && got != strings.TrimSuffix(want, "/...") { return false } case want != got: return false } } return len(pattern) == len(argv) }