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
1,230 changes: 1,230 additions & 0 deletions gitprovider/conformance/conformance.go

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions gitprovider/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Package gitprovider is the Git Provider SPI (version "gitprovider.v1"): the
// contract a Paca plugin implements to adapt one source-control platform
// (GitHub, GitLab, Bitbucket, Gitea, …) to Paca's platform-neutral Git
// integration.
//
// Paca core owns everything that is not platform-specific — the webhook
// endpoint and inbox, trigger routing, actor policy, agent runs, status
// comments, audit, the MCP tools and the UI. A provider only answers the
// platform-specific questions: how to verify and parse a webhook, how to mint a
// credential, how to read and write change requests, reviews, comments and
// checks.
//
// # Writing a provider
//
// 1. Implement [Provider] (required operations) and any of the optional
// interfaces ([ThreadResolver], [FileReader], …) the platform supports.
// 2. Return an honest [Info]: every optional interface you implement must have
// its capability flag set, and every flag you set must be backed by an
// implementation. The conformance suite checks both directions.
// 3. Mount it from your plugin's Init with serve.Register(ctx, provider)
// (package gitprovider/serve).
// 4. Pass the conformance suite (package gitprovider/conformance) using
// recorded webhook fixtures and a fake HTTP backend. The reference
// implementation in gitprovider/fakeprovider shows the expected behaviour.
//
// # Conventions
//
// - Operations that do not apply return [ErrUnsupported] (and the matching
// capability is false); core never calls them.
// - Map platform failures onto the [ErrorCode] set so core can retry and back
// off uniformly. Never include credentials or webhook secrets in an error.
// - Writes accept an IdempotencyKey; honour it where the platform allows and
// say so with [Capabilities.IdempotentWrites].
// - Text that originates from users or repository content (comment bodies,
// diffs, logs, file contents) is untrusted. Return it verbatim; core fences
// it before it reaches a model.
package gitprovider
115 changes: 115 additions & 0 deletions gitprovider/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package gitprovider

import (
"encoding/json"
"errors"
"fmt"
)

// ErrorCode is the closed set of failure classes core understands. Providers
// map every platform failure onto one of them so retry and back-off behave the
// same everywhere.
type ErrorCode string

const (
CodeNotFound ErrorCode = "not_found"
CodeForbidden ErrorCode = "forbidden" // the identity lacks access
CodeInvalidSignature ErrorCode = "invalid_signature" // webhook failed verification
CodeConflict ErrorCode = "conflict" // e.g. branch exists, head moved
CodeRateLimited ErrorCode = "rate_limited"
CodeValidation ErrorCode = "validation" // bad request from core or the platform
CodeUnsupported ErrorCode = "unsupported"
CodeTransient ErrorCode = "transient" // retryable (5xx, timeouts)
CodeInternal ErrorCode = "internal"
)

// AllErrorCodes lists every defined code.
var AllErrorCodes = []ErrorCode{
CodeNotFound, CodeForbidden, CodeInvalidSignature, CodeConflict,
CodeRateLimited, CodeValidation, CodeUnsupported, CodeTransient, CodeInternal,
}

// Error is a provider failure. Message must never contain credentials, webhook
// secrets or raw payloads.
type Error struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
RetryAfterSeconds int `json:"retry_after_seconds,omitempty"`
}

func (e *Error) Error() string { return fmt.Sprintf("gitprovider: %s: %s", e.Code, e.Message) }

// Retryable reports whether the same call may succeed later.
func (e *Error) Retryable() bool { return e.Code == CodeRateLimited || e.Code == CodeTransient }

// Is makes errors.Is(err, ErrUnsupported) match any *Error with that code.
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
return ok && t.Code == e.Code
}

// Sentinels for errors.Is.
var (
ErrNotFound = &Error{Code: CodeNotFound}
ErrForbidden = &Error{Code: CodeForbidden}
ErrInvalidSignature = &Error{Code: CodeInvalidSignature}
ErrConflict = &Error{Code: CodeConflict}
ErrRateLimited = &Error{Code: CodeRateLimited}
ErrValidation = &Error{Code: CodeValidation}
ErrUnsupported = &Error{Code: CodeUnsupported, Message: "operation not supported by this provider"}
ErrTransient = &Error{Code: CodeTransient}
)

// Errorf builds an *Error with a formatted message.
func Errorf(code ErrorCode, format string, args ...any) *Error {
return &Error{Code: code, Message: fmt.Sprintf(format, args...)}
}

// RateLimited builds a rate-limit error with a retry hint.
func RateLimited(retryAfterSeconds int, msg string) *Error {
return &Error{Code: CodeRateLimited, Message: msg, RetryAfterSeconds: retryAfterSeconds}
}

// AsError converts any error to an *Error, wrapping unknown errors as
// [CodeInternal] with a generic message so internals never leak.
func AsError(err error) *Error {
if err == nil {
return nil
}
var e *Error
if errors.As(err, &e) {
return e
}
return &Error{Code: CodeInternal, Message: "internal provider error"}
}

// HTTPStatus maps a code to the status the serve adapter returns.
func (c ErrorCode) HTTPStatus() int {
switch c {
case CodeNotFound:
return 404
case CodeForbidden:
return 403
case CodeInvalidSignature:
return 401
case CodeConflict:
return 409
case CodeRateLimited:
return 429
case CodeValidation:
return 400
case CodeUnsupported:
return 501
case CodeTransient:
return 503
default:
return 500
}
}

// Envelope is the JSON every operation answers with.
type Envelope struct {
OK bool `json:"ok"`
Result json.RawMessage `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
}
Loading
Loading