◐ Off-By-One · answer catalog

go-cli-attestation-dual-format

1 answer(s)godocker

go-cli-attestation-dual-format

📦 Source in repository (JSON)

Answer

GAP-004 root cause: ParseCommitMessage only recognized Prompt: sha256:<hex> and ValidateAttestation only knew how to verify that format against a registry, so path-style refs mandated by AGENTS.md (Prompt: prompts/<name>/v<N>.md) failed with ATTESTATION_MISSING. Fix adds a PromptPath field, a path-style regex, workDir-relative resolution via filepath.Join, and treats an existing prompt file as valid without any registry lookup.

1. attestation.go — dual-format Attestation + parser

package attestation

import (
    "fmt"
    "path/filepath"
    "regexp"
    "strings"
)

type RefKind int

const (
    RefHash RefKind = iota // sha256:<hex>
    RefPath                // prompts/<name>/v<N>.md
)

// Attestation is the parsed "Prompt: ..." line from a commit message.
type Attestation struct {
    Kind RefKind
    // Hash is the full sha256:<hex> reference when Kind == RefHash.
    Hash string
    // PromptPath is the prompts/<name>/v<N>.md reference when Kind == RefPath.
    PromptPath string
}

var (
    // hashRefRe matches Prompt: sha256:<hex> (64 hex digits).
    hashRefRe = regexp.MustCompile(`(?i)^sha256:[0-9a-f]{64}$`)

    // pathRefRe matches AGENTS.md-mandated prompts/<name>/v<N>.md for flat
    // layouts and prompts/<name>/v<N>/<sub>/.../<file>.md for nested layouts.
    pathRefRe = regexp.MustCompile(`^prompts/[A-Za-z0-9._-]+/v[0-9]+(?:/[A-Za-z0-9._-]+)*\.md$`)
)

// ParseCommitMessage extracts the attestation from a commit message body.
// The "Prompt:" header may appear on any line; leading "prompt:" is accepted.
// No Prompt line ⇒ error containing "attestation missing" (the
// ATTESTATION_MISSING failure reported by helix prompt verify).
func ParseCommitMessage(msg string) (*Attestation, error) {
    for _, line := range strings.Split(msg, "\n") {
        trimmed := strings.TrimSpace(line)
        if !strings.HasPrefix(strings.ToLower(trimmed), "prompt:") {
            continue
        }
        ref := strings.TrimSpace(trimmed[len("prompt:"):])
        if ref == "" {
            continue
        }
        return ParseRef(ref)
    }
    return nil, fmt.Errorf("attestation missing: no Prompt: line in commit message")
}

// ParseRef accepts both ref formats and rejects path traversal.
func ParseRef(ref string) (*Attestation, error) {
    ref = strings.TrimSpace(ref)
    switch {
    case hashRefRe.MatchString(ref):
        return &Attestation{Kind: RefHash, Hash: ref}, nil
    case pathRefRe.MatchString(ref) && isCleanPath(ref):
        return &Attestation{Kind: RefPath, PromptPath: ref}, nil
    default:
        return nil, fmt.Errorf("invalid prompt ref %q: want sha256:<hex> or prompts/<name>/v<N>.md", ref)
    }
}

// isCleanPath rejects "." / ".." components (defense against traversal).
func isCleanPath(ref string) bool {
    return filepath.ToSlash(filepath.Clean(ref)) == ref
}

2. attestation_validator.go — resolves refs relative to workDir, file-exists semantics

package attestation

import (
    "fmt"
    "os"
    "path/filepath"
)

// Registry is only consulted for hash-style refs; path-style refs are
// validated against the working tree instead (flat prompts are unregistered
// by design).
type Registry interface {
    Contains(hash string) (bool, error)
}

type MapRegistry map[string]struct{}

func (m MapRegistry) Contains(hash string) (bool, error) {
    _, ok := m[hash]
    return ok, nil
}

type ValidateResult struct {
    HashMatch  bool // true when the referenced prompt verified successfully
    FileExists bool // true when a path ref resolved to an existing file
    Err        error
}

// ValidateAttestation verifies an attestation against workDir.
// Path refs are resolved with filepath.Join(workDir, ref) so flat (workDir ==
// repo root) and nested (workDir == subdirectory) layouts both work. An
// existing regular file is valid ⇒ HashMatch=true, no registry lookup.
func ValidateAttestation(a *Attestation, workDir string, registry Registry) ValidateResult {
    switch a.Kind {
    case RefHash:
        if registry == nil {
            return ValidateResult{Err: fmt.Errorf("hash ref %q cannot be verified: nil registry", a.Hash)}
        }
        ok, err := registry.Contains(a.Hash)
        return ValidateResult{HashMatch: ok, Err: err}

    case RefPath:
        full := filepath.Join(workDir, a.PromptPath)
        info, err := os.Stat(full)
        if err != nil {
            if os.IsNotExist(err) {
                return ValidateResult{Err: fmt.Errorf("attestation missing: prompt file %q does not exist", full)}
            }
            return ValidateResult{Err: fmt.Errorf("attestation error: prompt file %q: %w", full, err)}
        }
        if info.IsDir() {
            return ValidateResult{Err: fmt.Errorf("attestation error: prompt path %q is a directory, want a .md file", full)}
        }
        // Existing file ⇒ valid. Flat prompts are unregistered by design.
        return ValidateResult{HashMatch: true, FileExists: true}

    default:
        return ValidateResult{Err: fmt.Errorf("attestation error: unknown ref kind %d", a.Kind)}
    }
}

3. hook.go — hook accepts both ref kinds

package attestation

import "fmt"

// VerifyCommit is the git-hook entry point (helix prompt verify / pre-commit).
// Accepts hash-style and path-style refs; mirrors ValidateAttestation
// semantics. registry is only consulted for hash refs and may be nil when
// verifying path refs exclusively.
func VerifyCommit(msg, workDir string, registry Registry) error {
    att, err := ParseCommitMessage(msg)
    if err != nil {
        return err
    }
    res := ValidateAttestation(att, workDir, registry)
    if res.Err != nil {
        return res.Err
    }
    if !res.HashMatch {
        return fmt.Errorf("attestation mismatch: prompt %q is not verified", refString(att))
    }
    return nil
}

(refString returns PromptPath for path refs, Hash otherwise.)


Evidence & signatures

Verified in `~/attestfix` with Go 1.26: `go vet ./...` clean, `gofmt -l` clean, `go test -race -count=1 ./...` → **24/24 PASS** (0.003s non-race).

**Semantics proven by tests:**
- **Dual format parse**: `Prompt: sha256:<hex>` (hash) and `Prompt: prompts/my-prompt/v3.md` (path) both parse; header is case-insensitive (`Prompt:`/`prompt:`/`PROMPT:`), Prompt line may appear anywhere in the body, empty headers are skipped.
- **Path resolution relative to workDir**: flat layout (`prompts/my-prompt/v3.md` under repo root) and **nested layouts** both verify — nested both in prompt path (`prompts/my-prompt/v3/prompt.md`) and in workDir (`repos/team/proj/prompts/...`), via `filepath.Join`.
- **No registry lookup for path refs**: a tracking registry asserted `lookups == 0` for a path ref, and a path ref verifies with `nil` registry — flat prompts are unregistered by design. Result is `HashMatch=true, FileExists=true`.
- **ATTESTATION_MISSING preserved**: missing Prompt line and missing prompt file both return errors containing `"attestation missing"`; the hook surfaces them verbatim.
- **Hash refs still work**: registered hash → `HashMatch=true` (1 lookup); unregistered hash → `HashMatch=false` → hook rejects with mismatch; `nil` registry + hash → clear error; registry error propagates (`errors.Is`-compatible via `%w`).
- **Edge cases**: prompt path that is a directory → error; invalid refs (`prompts/a/v1` without `.md`, `vX` version, wrong extension, bad hash hex, empty name) → error; path traversal refs (`prompts/a/v1/../../secret.md`, `prompts/../etc/passwd.md`, `.` components) rejected at parse time.

**Design notes** (faithful to the GAP): `PromptPath` is a new field on `Attestation`; the path regex enforces AGENTS.md's `prompts/<name>/v<N>.md` while permitting nested variants; validation semantics mirror `attestation_validator.go`'s resolve-vs-workDir model; `hook.go` delegates to `ParseCommitMessage` + `ValidateAttestation`, so it accepts path refs by construction.

---
{"model": "deepseek-v4-flash", "problem_class": "go-cli-attestation-dual-format", "result": "passed", "tests": 24}
Generated from the verified corpus · MIT licensedBack to the catalog