Documentation
¶
Overview ¶
Package webapi hosts the /api/* and /oauth/* browser-facing HTTP surface. Runner-facing endpoints live in internal/api.
internal/webapi/skill_repo_jobs.go
Index ¶
- Constants
- Variables
- func CSRF(cfg CSRFConfig) func(http.Handler) http.Handler
- func ClearCSRFCookie(w http.ResponseWriter)
- func NewCSRFToken() (string, error)
- func NewTestSessionCookie(masterKey []byte, login, role string) (*http.Cookie, error)
- func RegisterRoutes(mux *http.ServeMux, deps Deps)
- func RequireRole(masterKey []byte, role string, next http.Handler) http.Handler
- func RequireSession(masterKey []byte, next http.Handler) http.Handler
- func ResolveCopilotToken(ctx context.Context, store server.SecretStore, prefix string, ...) (string, time.Time, error)
- func SetCSRFCookie(w http.ResponseWriter, token string, maxAge int, host string)
- func SignOAuthState(key []byte, ttl time.Duration) (string, error)
- func SignSession(claims SessionClaims, key []byte, ttl time.Duration) (string, error)
- type CSRFConfig
- type ChatConfig
- type CopilotTokenRefsJSON
- type Deps
- type RateLimiter
- type RateLimiterConfig
- type RepoSyncer
- type SessionClaims
- type SkillRepoClient
- type UIOverrides
- type YamlAppendScheduleFunc
Constants ¶
const CSRFCookieName = "cf_csrf"
CSRFCookieName is the cookie that carries the per-session CSRF token. It is non-HttpOnly so the SPA can read it via document.cookie and echo it in X-CSRF-Token. The cookie's presence on the cronfoundry origin is the security primitive — an attacker on a foreign origin cannot read it.
const CSRFHeaderName = "X-CSRF-Token"
CSRFHeaderName is the header the SPA must set on every state-changing request. The middleware compares its value to the cookie with a constant-time compare.
const CopilotClientID = "Iv1.b507a08c87ecfe98"
CopilotClientID is the public OAuth client_id of the GitHub Copilot platform — used for device-flow against GitHub's /login/device/code and /login/oauth/access_token endpoints to mint a Copilot seat token. This value is published by GitHub for any consumer doing Copilot OAuth (including the official VS Code and JetBrains Copilot extensions).
It is NOT the operator's CronFoundry GitHub App client ID; that App authenticates webhooks and per-installation API calls and has nothing to do with the user's Copilot seat.
Variables ¶
var ErrCopilotReauthRequired = errors.New("copilot: oauth re-auth required")
ErrCopilotReauthRequired indicates the operator must re-authenticate the Copilot integration (the OAuth token is missing/empty or GitHub rejected it as revoked). Surfaced as 401 by the HTTP handler so the runner — and any future SPA "Copilot status" widget — can prompt for re-auth instead of treating it as a transient outage.
Functions ¶
func CSRF ¶
func CSRF(cfg CSRFConfig) func(http.Handler) http.Handler
CSRF returns a middleware that enforces double-submit cookie + Origin check on all non-safe HTTP methods. GET, HEAD, and OPTIONS pass through unchanged (these are the IETF "safe methods"); every other method — including POST, PATCH, PUT, DELETE, and any unknown verb — must present a matching cf_csrf cookie and X-CSRF-Token header.
func ClearCSRFCookie ¶
func ClearCSRFCookie(w http.ResponseWriter)
ClearCSRFCookie deletes the cf_csrf cookie. Called from logout.
func NewCSRFToken ¶
NewCSRFToken returns a 32-byte random token, base64url-encoded without padding (43 chars). Suitable for use as a per-session CSRF token.
func NewTestSessionCookie ¶
NewTestSessionCookie creates a signed session cookie for use in handler tests.
func RegisterRoutes ¶
RegisterRoutes registers /oauth/*, /api/*, and /* (SPA catch-all) on mux.
func RequireRole ¶
RequireRole wraps a handler requiring a valid session with the given role. "admin" role may access "viewer" routes; "viewer" may not access "admin" routes.
func RequireSession ¶
RequireSession is middleware that validates the cf_session cookie. Attaches SessionClaims to the request context on success; returns 401 otherwise.
func ResolveCopilotToken ¶
func ResolveCopilotToken(ctx context.Context, store server.SecretStore, prefix string, githubOverrideURL *string) (string, time.Time, error)
ResolveCopilotToken returns a fresh Copilot IDE token (the API key sent to api.githubcopilot.com), minting a new one when the cached one is near expiry.
GitHub Copilot uses a two-token system:
OAuth access token ("gho_..."): obtained once via device flow, stored under <prefix>-access-token. Long-lived; doesn't expire on its own. The empty <prefix>-refresh-token slot is a vestige of an earlier (incorrect) assumption that Copilot's device flow returned an OAuth refresh token — it doesn't, and we don't need one.
IDE token / API key: short-lived (~25-30 min), minted by GET-ing https://api.github.com/copilot_internal/v2/token with the OAuth token in `Authorization: token <oauth>`. Returns {"token": "<api-key>", "expires_at": <unix>}. This is what internal/llm/copilot.go sends as the Bearer token to api.githubcopilot.com. We cache it under <prefix>-ide-token / <prefix>-ide-expiry to avoid hitting GitHub on every run.
On a fresh install the IDE-token cache is empty, so we mint unconditionally on the first call. After that we mint only when the cached token is within 60s of expiry.
githubOverrideURL overrides the api.github.com base for tests; pass nil to use the real endpoint. The override applies to the IDE-token URL only; the path /copilot_internal/v2/token is appended.
func SetCSRFCookie ¶
func SetCSRFCookie(w http.ResponseWriter, token string, maxAge int, host string)
SetCSRFCookie writes the cf_csrf cookie. Called from the OAuth callback alongside SetCookie for cf_session. HttpOnly is intentionally false.
func SignOAuthState ¶
SignOAuthState generates a signed random state token for CSRF protection.
func SignSession ¶
SignSession encodes claims as a signed cookie value:
base64url(JSON) + "." + base64url(HMAC-SHA256(payload, key))
Types ¶
type CSRFConfig ¶
type CSRFConfig struct {
// AllowedOrigin is the scheme+host of the trusted public origin
// (e.g. "https://cronfoundry.example.com"). When empty, the
// Origin/Referer check is skipped — intended for local dev only.
AllowedOrigin string
}
CSRFConfig configures the CSRF middleware.
type ChatConfig ¶ added in v0.7.16
type ChatConfig struct {
// Enabled gates the entire chat surface. When false, /api/chat/* return
// 503 and the SPA hides the dock. Defaults to false; the operator must
// opt in by setting the assistant env vars.
Enabled bool
// Provider is the llm.NewProvider name ("openai", "anthropic",
// "copilot-enterprise", ...).
Provider string
// Model is the provider-specific model id (e.g. "claude-sonnet-4-5").
Model string
// APIKeySecret is the secret store NAME holding the API key. Used by
// providers that authenticate with a long-lived API key (openai,
// anthropic, azure-foundry, openrouter). Ignored when Provider is
// "copilot-enterprise" — see CopilotPrefix.
APIKeySecret string
// CopilotPrefix is the secret-store prefix that holds the Copilot
// Enterprise token bundle (e.g. "copilot" maps to
// copilot-access-token + cached copilot-ide-token / -ide-expiry).
// Used only when Provider == "copilot-enterprise". The operator
// connects Copilot once via the OAuth device flow (Settings →
// Providers); ResolveCopilotToken handles IDE-token caching on the
// way to api.githubcopilot.com.
CopilotPrefix string
// MaxTurns caps the tool-using loop. 0 ⇒ chat package default.
MaxTurns int
// MaxTokens caps each turn's output. 0 ⇒ chat package default.
MaxTokens int
}
ChatConfig configures the in-app assistant. The config is operator-set at startup (env vars in cmd/cronfoundry/serve.go); it is intentionally separate from per-schedule provider config so jobs and the assistant can use different models / API keys / budgets.
type CopilotTokenRefsJSON ¶
type CopilotTokenRefsJSON struct {
Prefix string `json:"prefix"`
}
CopilotTokenRefsJSON is stored on the schedule row and identifies which KV secrets hold the token pair for a copilot-enterprise schedule.
type Deps ¶
type Deps struct {
MasterKey []byte
OAuthClientID string
OAuthClientSecret string
AdminLogins []string
ViewerLogins []string
// GitHubAPIBase overrides the GitHub API base URL in tests. Empty = real GitHub.
GitHubAPIBase string
// Queries provides DB access for /api/* handlers.
Queries *dbgen.Queries
// Secrets provides secret store access for /api/secrets handlers.
Secrets server.SecretStore
// APIBaseURL is the base URL for the internal API (used by run-now).
APIBaseURL string
// WebhookSecret is the shared HMAC secret registered with the GitHub App.
// When empty, POST /webhook/github responds 503 Service Unavailable.
WebhookSecret []byte
// Syncer triggers a one-off repo sync. Injected from cmd/cronfoundry/serve.go
// as a thin wrapper around sync.Poller.SyncOne.
Syncer RepoSyncer
// SkillRepoClient handles GitHub round-trips for `POST /api/skill-repo/jobs`.
// Injected from cmd/cronfoundry/serve.go as a *skillrepo.Client.
// In tests we substitute a fake satisfying the SkillRepoClient interface
// declared in skill_repo_jobs.go.
SkillRepoClient SkillRepoClient
// YamlEditAppendSchedule is the YAML editor used by the proposeJob handler.
// Wired from internal/yamledit.AppendScheduleToSkill in production; tests
// inject a function-typed fake.
YamlEditAppendSchedule YamlAppendScheduleFunc
// GitHubAppSlug is the GitHub App slug (e.g. "cronfoundry-tng") used to
// construct the permissions-review URL surfaced in the 412 response from
// proposeJob when the App lacks pull_requests:write. Empty falls back to
// "cronfoundry".
GitHubAppSlug string
// RateLimit configures per-IP rate limiting on public routes.
RateLimit RateLimiterConfig
// PublicBaseURL is the externally-reachable base URL of the service
// (scheme+host, e.g. "https://cronfoundry.example.com"). Used by the CSRF
// middleware as the Origin/Referer allowlist. Empty disables the Origin
// check (dev mode); the cookie+header double-submit check still runs.
PublicBaseURL string
// Clock provides the most recent scheduler tick time. May be nil in
// test environments — handlers degrade gracefully (status="down").
Clock *scheduler.TickClock
// SweepInterval is the configured cadence between scheduler ticks.
// Used to classify scheduler health: <2x healthy, <5x degraded, else down.
SweepInterval time.Duration
// Chat configures the in-app assistant. Disabled by default; the
// operator opts in by setting the assistant env vars.
Chat ChatConfig
// contains filtered or unexported fields
}
Deps holds everything webapi handlers need.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter holds per-group token-bucket state plus an SSE concurrency counter. Construct with NewRateLimiter.
func NewRateLimiter ¶
func NewRateLimiter(cfg RateLimiterConfig) (*RateLimiter, error)
NewRateLimiter returns a RateLimiter. LRUSize defaults to 4096 if < 1.
type RateLimiterConfig ¶
type RateLimiterConfig struct {
TrustProxy bool
Disabled bool
APIRPM int
OAuthRPM int
WebhookRPM int
SSEMaxConcurrent int
LRUSize int
}
RateLimiterConfig holds the operator-tunable rate-limit knobs. Zero RPM for a group disables that group; Disabled=true disables the entire middleware (kill switch).
type RepoSyncer ¶
RepoSyncer resolves a repo_connection by ID and triggers a single sync pass. The concrete implementation in serve.go wraps sync.Poller.SyncOne.
type SessionClaims ¶
type SessionClaims struct {
Login string `json:"login"`
Role string `json:"role"`
Exp int64 `json:"exp"` // Unix seconds
}
SessionClaims is the payload embedded in the cf_session cookie.
func SessionClaimsFromContext ¶
func SessionClaimsFromContext(ctx context.Context) SessionClaims
SessionClaimsFromContext returns the claims attached by RequireSession. Returns zero value if the middleware was not applied.
func VerifySession ¶
func VerifySession(cookie string, key []byte) (SessionClaims, error)
VerifySession parses and validates a signed cookie value produced by SignSession.
type SkillRepoClient ¶ added in v0.7.16
type SkillRepoClient interface {
GetFile(ctx context.Context, installID int64, owner, repo, path, ref string) (*skillrepo.FileContents, error)
CreateBranch(ctx context.Context, installID int64, owner, repo, branch, fromSHA string) error
PutFile(ctx context.Context, installID int64, owner, repo, branch, path, fileSHA, message string, content []byte) error
CreatePR(ctx context.Context, installID int64, req skillrepo.PRRequest) (*skillrepo.PRResult, error)
}
SkillRepoClient is the subset of *skillrepo.Client that proposeJob uses. Declared as an interface so tests can inject a fake.
type UIOverrides ¶
type UIOverrides struct {
Cron *string `json:"cron,omitempty"`
Timezone *string `json:"timezone,omitempty"`
TimeoutSec *int32 `json:"timeout_sec,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
UIOverrides is the subset of schedule fields editable via the UI. Pointer fields: nil means "not overridden".
Source Files
¶
- alerts.go
- apierr.go
- audit.go
- audit_helper.go
- auth.go
- chat.go
- copilot_connect.go
- copilot_github.go
- copilot_token.go
- csrf.go
- dto.go
- embed.go
- events.go
- me.go
- oauth.go
- ratelimit.go
- repos.go
- run_notifications.go
- runs.go
- schedule_overrides.go
- schedules.go
- secrets.go
- server.go
- session.go
- skill_repo_jobs.go
- skills.go
- static.go
- system.go
- users.go
- webhook.go