Skip to content
Open
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
107 changes: 107 additions & 0 deletions api/v1alpha1/taskspawner_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,102 @@ type GenericWebhookFilter struct {
Pattern string `json:"pattern,omitempty"`
}

// ContextSource declares an external HTTP endpoint to query before task
// creation. The response body (optionally filtered via JSONPath) is made
// available as a .Context.NAME template variable.
type ContextSource struct {
// Name identifies this context source. The fetched value is available
// as .Context.NAME in promptTemplate, branch, and metadata templates.
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=64
// +kubebuilder:validation:Pattern=`^[a-zA-Z][a-zA-Z0-9_]*$`
Name string `json:"name"`

// URL is the HTTP(S) endpoint to fetch. Supports Go text/template
// variables from the work item (e.g., "https://api.example.com/items/{{.Number}}").
// HTTPS is required unless AllowInsecure is set.
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
URL string `json:"url"`

// Method is the HTTP method to use. Defaults to GET.
// +kubebuilder:validation:Enum=GET;POST
// +kubebuilder:default=GET
// +optional
Method string `json:"method,omitempty"`

// Headers are static HTTP headers to include in the request.
// Values support Go text/template variables from the work item.
// +optional
Headers map[string]string `json:"headers,omitempty"`

// HeadersFrom references Secrets whose data keys map to HTTP header
// values. These are merged with inline Headers; HeadersFrom values
// take precedence on conflict.
// +optional
HeadersFrom []HeaderFromSecret `json:"headersFrom,omitempty"`

// Body is a Go text/template for POST request bodies.
// +optional
Body string `json:"body,omitempty"`

// JSONPathFilter is a JSONPath expression applied to the JSON response
// body (e.g., "$.data.value"). When set, only the extracted value is
// stored as the context variable. When empty, the entire response body
// is stored as a string. Uses the same JSONPath syntax as generic
// webhook fieldMapping.
// +optional
JSONPathFilter string `json:"jsonPathFilter,omitempty"`

// AllowInsecure permits plain HTTP (non-TLS) URLs. Defaults to false.
// +optional
AllowInsecure bool `json:"allowInsecure,omitempty"`

// TimeoutSeconds is the per-request timeout. Defaults to 10.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=60
// +kubebuilder:default=10
// +optional
TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"`

// MaxResponseBytes limits the response body size read from the
// endpoint. Prevents oversized responses from inflating prompts.
// Defaults to 32768 (32 KiB).
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=131072
// +kubebuilder:default=32768
// +optional
MaxResponseBytes *int32 `json:"maxResponseBytes,omitempty"`

// Required when true causes task creation to be skipped for this work
// item if the context source fetch fails. When false (default), a
// failed fetch produces an empty string for the context variable and
// logs a warning.
// +optional
Required bool `json:"required,omitempty"`
}

// HeaderFromSecret maps a single HTTP header to a value stored in a
// Kubernetes Secret.
type HeaderFromSecret struct {
// Header is the HTTP header name (e.g., "Authorization").
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Header string `json:"header"`

// SecretRef references the Secret containing the header value.
// The Secret must be in the same namespace as the TaskSpawner.
// +kubebuilder:validation:Required
SecretRef SecretReference `json:"secretRef"`

// Key is the data key within the Secret whose value is used as the
// header value.
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Key string `json:"key"`
}

// TaskTemplateMetadata holds optional labels and annotations for spawned Tasks.
type TaskTemplateMetadata struct {
// Labels are merged into the spawned Task's labels. Values support Go
Expand Down Expand Up @@ -593,6 +689,7 @@ type TaskTemplate struct {
// GitHub webhook sources: {{.Event}}, {{.Action}}, {{.Sender}}, {{.Ref}}, {{.Repository}}, {{.Payload}} (full payload access)
// Linear webhook sources: {{.Type}}, {{.Action}}, {{.State}}, {{.Labels}}, {{.IssueID}}, {{.Payload}}
// Cron sources: {{.Time}}, {{.Schedule}}
// When contextSources are configured: .Context.NAME for each source
// +optional
Branch string `json:"branch,omitempty"`

Expand All @@ -603,6 +700,7 @@ type TaskTemplate struct {
// GitHub webhook sources: {{.Event}}, {{.Action}}, {{.Sender}}, {{.Ref}}, {{.Repository}}, {{.Payload}} (full payload access)
// Linear webhook sources: {{.Type}}, {{.Action}}, {{.State}}, {{.Labels}}, {{.IssueID}}, {{.Payload}}
// Cron sources: {{.Time}}, {{.Schedule}}
// When contextSources are configured: .Context.NAME for each source
// +optional
PromptTemplate string `json:"promptTemplate,omitempty"`

Expand All @@ -625,6 +723,15 @@ type TaskTemplate struct {
// +optional
Metadata *TaskTemplateMetadata `json:"metadata,omitempty"`

// ContextSources declares external HTTP endpoints to query before task
// creation. Each source's response is available as .Context.NAME
// in promptTemplate, branch, and metadata templates. Sources are
// fetched in parallel during the discovery cycle.
// +optional
// +kubebuilder:validation:MaxItems=8
// +kubebuilder:validation:XValidation:rule="self.all(a, self.exists_one(b, b.name == a.name))",message="contextSources names must be unique"
ContextSources []ContextSource `json:"contextSources,omitempty"`

// UpstreamRepo is the upstream repository in "owner/repo" format.
// When set, spawned Tasks inherit this value and inject
// KELOS_UPSTREAM_REPO into the agent container. This is typically
Expand Down
60 changes: 60 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions cmd/kelos-spawner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

kelosv1alpha1 "github.com/kelos-dev/kelos/api/v1alpha1"
"github.com/kelos-dev/kelos/internal/contextfetch"
"github.com/kelos-dev/kelos/internal/githubapp"
"github.com/kelos-dev/kelos/internal/logging"
"github.com/kelos-dev/kelos/internal/reporting"
Expand Down Expand Up @@ -342,6 +343,16 @@ func runCycleWithSourceCore(ctx context.Context, cl client.Client, key types.Nam
maxTotalTasks = int(*ts.Spec.MaxTotalTasks)
}

var contextFetcher *contextfetch.Fetcher
if len(ts.Spec.TaskTemplate.ContextSources) > 0 {
contextFetcher = &contextfetch.Fetcher{
Client: cl,
HTTPClient: http.DefaultClient,
Namespace: ts.Namespace,
Logger: log,
}
}

newTasksCreated := 0
for _, item := range newItems {
// Enforce max concurrency limit
Expand All @@ -360,6 +371,16 @@ func runCycleWithSourceCore(ctx context.Context, cl client.Client, key types.Nam

templateVars := source.WorkItemToTemplateVars(item)

// Enrich with external context sources
if contextFetcher != nil {
contextData, err := contextFetcher.FetchAll(ctx, ts.Spec.TaskTemplate.ContextSources, templateVars)
if err != nil {
log.Error(err, "Fetching context sources", "item", item.ID)
continue
}
templateVars["Context"] = contextData
}

tb, err := taskbuilder.NewTaskBuilder(cl)
if err != nil {
log.Error(err, "creating task builder", "item", item.ID)
Expand Down
Loading
Loading