cronfoundry

module
v0.7.19 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 6, 2026 License: MIT

README

CronFoundry

Self-hostable, GitOps-style scheduler for LLM skills. Runs a skill against OpenAI / Anthropic / Azure AI Foundry on a schedule, publishes the output to GitHub issues, Slack, Discord, or Teams, and commits learnings back to the skill repo.

Status: MVP shipped — deployable to Azure. Includes always-on scheduler, GitHub App sync, /internal HTTP API, subprocess + Container Apps Jobs runner dispatch, React operator UI with live-tail logs, push-webhook resync, audit log, persistent user table, and a one-command Bicep deploy. See docs/superpowers/specs/2026-04-19-cronfoundry-design.md for the design and docs/guides/quickstart-azure.md for the Azure runbook.

Requirements

  • Go 1.25 or later (required by github.com/openai/openai-go/v3).
  • Git working tree (the skill repo the runner operates on must be a git repo — writeback commits through go-git).
  • An API key for one of: OpenAI, Anthropic, or Azure AI Foundry.
  • An Incoming Webhook URL for at least one destination you want to publish to (Slack, Discord, Teams via Power Automate), or a GitHub PAT for the GitHub issue destination.

Build

go build -o cronfoundry-runner ./cmd/runner

Produces a single ~25 MB static binary.

Quick start (local dev)

# 1. Build.
make build

# 2. Generate a master key on first run, copy the env line it prints.
./cronfoundry admin init
export CRONFOUNDRY_MASTER_KEY='<paste>'

# 3. Start Postgres + cronfoundry (docker-compose).
cp .env.example .env.local   # edit with your values
# Place your GitHub App's private key at ./app.pem
make dev

# 4. Run migrations + seed the default organization.
export CRONFOUNDRY_DATABASE_URL='postgres://cronfoundry:cronfoundry@localhost:5432/cronfoundry?sslmode=disable'
make migrate

# 5. Connect a repo + set secrets via the CLI.
./cronfoundry admin connect-repo myorg/skills-repo --installation-id 12345
echo -n 'https://hooks.slack.com/...' | ./cronfoundry admin set-secret slack_webhook
echo -n 'sk-...' | ./cronfoundry admin set-secret openai_key

# 6. Watch logs.
cd deploy && docker compose logs -f cronfoundry

See docs/guides/smoke-test-p2.md for the full walkthrough with GitHub App registration.

After deploying and connecting a GitHub App, set up the push webhook — see docs/webhook-setup.md for step-by-step instructions.

Quick start (standalone runner)

A tiny end-to-end run with the bundled smoke fixture:

export OPENAI_API_KEY='sk-...'
export CRONFOUNDRY_SECRET_SLACK_URL='https://hooks.slack.com/...'

./cronfoundry-runner run \
  --repo ./testdata \
  --manifest cronfoundry.yaml \
  --skill-path skills/weekly-digest \
  --schedule-name monday-morning \
  --skip-push

The runner will:

  1. Parse testdata/cronfoundry.yaml and resolve the named schedule.
  2. Load testdata/skills/weekly-digest/SKILL.md, inline {{ include "..." }} directives, and build the final prompt.
  3. Stream a completion from OpenAI with your key.
  4. Strip any <memory>…</memory> block from the output.
  5. POST the remaining text to the Slack webhook (isolated retries).
  6. If a <memory> block was present, append it to memory.md and commit as cronfoundry[bot]. --skip-push keeps it local.
  7. Print a JSON run summary to stdout.

Flags

Flag Default Purpose
--repo . Repo root containing the manifest and skill files
--manifest cronfoundry.yaml Manifest path (relative to --repo)
--skill-path (required) Skill path as declared in the manifest
--schedule-name (required) Schedule name within the skill
--llm-key-env OPENAI_API_KEY Env var name holding the LLM API key
--llm-endpoint Azure AI Foundry endpoint URL
--llm-deployment Azure AI Foundry deployment name
--dry-run false Skip publish + writeback; print output only
--skip-push false Perform writeback commit locally but don't push

Configuration format

cronfoundry.yaml — the manifest
version: 1
skills:
  - path: skills/weekly-digest
    schedules:
      - name: monday-morning
        cron: "0 9 * * MON"
        timezone: America/Los_Angeles
        overlap_policy: skip       # skip | queue | concurrent
        timeout_sec: 600
        provider: openai           # openai | anthropic | azure-foundry
        model: gpt-4o-mini
        destinations:
          - github-issue:
              repo: myorg/reports
              title: "Digest — {{ run.date }}"
              labels: [digest]
          - slack:
              secret: slack_webhook
              text: "{{ output.truncated 35000 }}"
        writeback:
          enabled: true
          path: memory.md
          mode: append             # append | replace
        env:
          LOOKBACK_DAYS: "7"
          TEAM_NAME:
            secret: team_name      # resolved from CRONFOUNDRY_SECRET_TEAM_NAME
SKILL.md — per-skill prompt
---
name: weekly-digest
description: Aggregates last week's activity
max_tokens: 4000
---
You are writing a weekly digest.

Context:
{{ include "context/template.md" }}

Respond with a short summary, then a <memory>...</memory> block with one
short learning.

Only {{ include "relative/path.md" }} is supported inside the body — no conditionals or loops. Paths are relative to the skill directory and may not escape it.

Secret resolution

Secrets referenced by { secret: name } in the manifest resolve from environment variables with the prefix CRONFOUNDRY_SECRET_<UPPER(name)>.

Example: secret: slack_webhookCRONFOUNDRY_SECRET_SLACK_WEBHOOK.

The LLM API key itself comes from whatever env var --llm-key-env names (default OPENAI_API_KEY). The writeback push (if enabled) uses GITHUB_TOKEN.

All known secret values are scrubbed from stderr (slog attrs, errors, and direct prints) before emission.

Destination templates

A small, fixed set of variables is available in destination text/content/ title/body fields:

Variable Value
{{ output }} Full published output (memory block stripped)
{{ output.truncated N }} Same, limited to N runes with an ellipsis
{{ run.id }} Run UUID
{{ run.date }} Run start date (YYYY-MM-DD, schedule's timezone)
{{ run.started_at }} ISO-8601 timestamp
{{ schedule.name }} Schedule name from the manifest
{{ skill.name }} Skill name from SKILL.md frontmatter

Unknown variables render as their literal form and emit a warning.

Architecture

cronfoundry/
├── cmd/
│   ├── cronfoundry/              # server + admin CLI (cobra)
│   └── runner/                   # one-shot per-fire runner
└── internal/
    ├── api/                      # /internal HTTP endpoints (runner-facing)
    ├── cloud/                    # Azure Container Apps Jobs dispatcher
    ├── config/                   # cronfoundry.yaml + SKILL.md parsers
    ├── db/                       # pgx + goose migrations + sqlc queries
    ├── github/                   # App JWT, install tokens, clone/commit
    ├── githubtest/               # test fixtures for github/
    ├── llm/                      # OpenAI / Anthropic / Azure Foundry
    ├── memory/                   # <memory>...</memory> parser
    ├── publish/                  # github-issue / slack / discord / teams
    ├── redact/                   # secret-value scrubber for logs
    ├── runner/                   # orchestration (load → LLM → publish)
    ├── scheduler/                # cron tick loop + overlap + sweep
    ├── secrets/                  # env-based secret resolver (runner-local)
    ├── secretstore/              # Azure Key Vault wrapper (server-side)
    ├── sync/                     # GitHub repo → skill/schedule sync
    ├── template/                 # destination-template renderer
    ├── testdb/                   # testcontainers Postgres boot helper
    ├── token/                    # per-run bearer JWT signer/verifier
    ├── webapi/                   # /api handlers for the React UI
    └── writeback/                # go-git commit + push

A run's status is one of succeeded, partial_failure (publish or writeback failure), or failed (load/LLM error). Per-destination failures are isolated — one broken webhook does not prevent other destinations from publishing.

End-to-end tests

make e2e runs the TestE2E_* suite under the e2e build tag. It boots throwaway Postgres containers via testcontainers and stubs the LLM, Slack/Discord webhooks, and the git clone, so no external network or real credentials are required — but Docker must be running locally.

CI runs the same target on every PR and on pushes to main.

Design & spec

Roadmap

  • MVP (this release) — Core runner, scheduler, GitHub sync, Key Vault, React UI with live-tail logs, push webhook, audit log, user management, Azure Bicep deploy. ✅
  • Deferred — see the "Deferred" section of the design spec for the ordered backlog (MCP tool support, Copilot Enterprise provider, auto-pause on consecutive failures, etc.).
Operator endpoints
  • POST /webhook/github — GitHub App push webhook; requires CRONFOUNDRY_GITHUB_WEBHOOK_SECRET (see docs/webhook-setup.md)
  • GET /api/audit — admin-only audit log of every mutating API call
  • GET/POST/PATCH/DELETE /api/users — admin user management backed by the app_user table; env vars CRONFOUNDRY_ADMIN_LOGINS / CRONFOUNDRY_VIEWER_LOGINS seed the table on first startup, then UI edits win
  • Per-run manifest.set, secret.fetched, and secret.denied events emitted on the run timeline so operators can see exactly which KV entries each run touched
  • GET /api/runs/{id}/events/stream — SSE stream consumed by the LogTail component in the Runs detail drawer for in-flight runs
  • GET /metrics — Prometheus text-format scrape endpoint (see docs/guides/observability.md)
In-app assistant (chat)

The operator UI ships a floating chat dock that helps users understand the app, troubleshoot failed runs, and draft cronfoundry.yaml snippets. The assistant is read-only — it cannot mutate schedules, secrets, or repos — and is opt-in via env vars on the server:

  • CRONFOUNDRY_CHAT_PROVIDERopenai | anthropic | azure-foundry | openrouter | copilot-enterprise.
  • CRONFOUNDRY_CHAT_MODEL — provider-specific model id, e.g. claude-sonnet-4-5, gpt-4o, or a Copilot model like gpt-4o.
  • One of:
    • CRONFOUNDRY_CHAT_API_KEY_SECRET — name of a secret in the secret store that holds the API key (for openai / anthropic / azure-foundry / openrouter). Add it via Settings → Secrets first.
    • CRONFOUNDRY_CHAT_COPILOT_PREFIX — secret-store prefix for a Copilot Enterprise connection (for copilot-enterprise). Connect Copilot once via Settings → Providers; the chat handler reuses the same OAuth-mint path the runner uses, with transparent IDE-token caching, so re-auth is rare.
  • Optional: CRONFOUNDRY_CHAT_MAX_TURNS (default 6) and CRONFOUNDRY_CHAT_MAX_TOKENS (default 2048).

When all three core vars are set the dock appears in the bottom-right corner of every page; otherwise the SPA hides it. The wire endpoints are GET /api/chat/info and POST /api/chat/stream (SSE).

CSRF & origin allowlist

Set CRONFOUNDRY_PUBLIC_BASE_URL to the externally-reachable URL of the service (scheme+host, e.g. https://cronfoundry.example.com). The CSRF middleware uses this as the allowlist for the Origin/Referer check. In dev (no env var), the origin check is disabled; the cf_csrf cookie + X-CSRF-Token header double-submit check still runs.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
cronfoundry command
cmd/cronfoundry/admin_init.go
cmd/cronfoundry/admin_init.go
runner command
Package main is the CronFoundry runner CLI: executes a single skill- schedule invocation end-to-end (load manifest → LLM → publish → writeback).
Package main is the CronFoundry runner CLI: executes a single skill- schedule invocation end-to-end (load manifest → LLM → publish → writeback).
internal
api
Package api hosts the /internal HTTP surface used by runner subprocesses to fetch their context, request scoped secrets, stream events, and finalize.
Package api hosts the /internal HTTP surface used by runner subprocesses to fetch their context, request scoped secrets, stream events, and finalize.
audit
Package audit centralizes audit_log writes so mutating handlers can record their actions with a single call.
Package audit centralizes audit_log writes so mutating handlers can record their actions with a single call.
bootstrap/azure
internal/bootstrap/azure/admininit.go
internal/bootstrap/azure/admininit.go
chat
Package chat implements the in-app assistant: a stateless tool-using agent loop that helps operators set up jobs and troubleshoot failures.
Package chat implements the in-app assistant: a stateless tool-using agent loop that helps operators set up jobs and troubleshoot failures.
cloud
Package cloud defines pluggable interfaces for cloud-specific concerns — job dispatch, secret storage, and identity.
Package cloud defines pluggable interfaces for cloud-specific concerns — job dispatch, secret storage, and identity.
cloud/azure
Package azure provides an Azure Container Apps Jobs implementation of cloud.JobDispatcher.
Package azure provides an Azure Container Apps Jobs implementation of cloud.JobDispatcher.
config
Package config parses CronFoundry manifest and skill files and resolves single-level `{{ include "..." }}` directives inside skill bodies.
Package config parses CronFoundry manifest and skill files and resolves single-level `{{ include "..." }}` directives inside skill bodies.
db
Package db wraps the Postgres connection pool and migration runner used by the CronFoundry service process.
Package db wraps the Postgres connection pool and migration runner used by the CronFoundry service process.
github
Package github wraps the GitHub App authentication + REST API calls CronFoundry needs for repo sync and writeback.
Package github wraps the GitHub App authentication + REST API calls CronFoundry needs for repo sync and writeback.
githubapp
Package githubapp drives the GitHub App "create from manifest" flow used by the install script's step 5.
Package githubapp drives the GitHub App "create from manifest" flow used by the install script's step 5.
githubtest
Package githubtest provides test helpers for code that uses internal/github.
Package githubtest provides test helpers for code that uses internal/github.
jobdispatch/flymachines
Package flymachines implements cloud.JobDispatcher using the Fly Machines API.
Package flymachines implements cloud.JobDispatcher using the Fly Machines API.
jobdispatch/k8sjobs
Package k8sjobs implements cloud.JobDispatcher using Kubernetes batch/v1 Jobs.
Package k8sjobs implements cloud.JobDispatcher using Kubernetes batch/v1 Jobs.
llm
Package llm defines a provider-agnostic interface for streaming chat completions against OpenAI, Anthropic, and Azure AI Foundry.
Package llm defines a provider-agnostic interface for streaming chat completions against OpenAI, Anthropic, and Azure AI Foundry.
mcp
Package mcp implements a minimal MCP 2024-11-05 stdio client: just enough of the protocol to initialize a server, enumerate its tools, call tools, and cancel in-flight calls.
Package mcp implements a minimal MCP 2024-11-05 stdio client: just enough of the protocol to initialize a server, enumerate its tools, call tools, and cancel in-flight calls.
memory
Package memory extracts the reserved <memory>...</memory> writeback block from an LLM's output.
Package memory extracts the reserved <memory>...</memory> writeback block from an LLM's output.
metrics
Package metrics defines all Prometheus metrics emitted by cronfoundry and exposes a /metrics handler.
Package metrics defines all Prometheus metrics emitted by cronfoundry and exposes a /metrics handler.
publish
Package publish fans output to destinations (GitHub issue, Slack, Discord, Teams), isolating per-destination failures.
Package publish fans output to destinations (GitHub issue, Slack, Discord, Teams), isolating per-destination failures.
redact
Package redact scrubs known secret values from strings before logging.
Package redact scrubs known secret values from strings before logging.
runner
Package runner orchestrates a single end-to-end skill execution: load manifest + skill, call LLM, parse memory, publish, writeback.
Package runner orchestrates a single end-to-end skill execution: load manifest + skill, call LLM, parse memory, publish, writeback.
scheduler
Package scheduler implements the tick loop that turns due schedules into run rows and dispatches them via the JobDispatcher.
Package scheduler implements the tick loop that turns due schedules into run rows and dispatches them via the JobDispatcher.
secrets
Package secrets is the umbrella for cronfoundry's secret subsystem.
Package secrets is the umbrella for cronfoundry's secret subsystem.
secrets/runner
Package runner resolves skill-declared secrets from environment variables in the form CRONFOUNDRY_SECRET_<UPPER(name)>.
Package runner resolves skill-declared secrets from environment variables in the form CRONFOUNDRY_SECRET_<UPPER(name)>.
secrets/server
Package server persists and retrieves secrets under envelope encryption.
Package server persists and retrieves secrets under envelope encryption.
secrets/server/azurekv
Package azurekv provides an Azure Key Vault implementation of server.SecretStore.
Package azurekv provides an Azure Key Vault implementation of server.SecretStore.
skillrepo
Package skillrepo composes go-github calls into the specific "open a PR with a single-file change" pipeline used by POST /api/skill-repo/jobs.
Package skillrepo composes go-github calls into the specific "open a PR with a single-file change" pipeline used by POST /api/skill-repo/jobs.
sync
Package sync implements the repo sync poller (Loop 1 in the P2 design): periodically HEAD-check each connected repo, shallow-clone on SHA change, parse cronfoundry.yaml + referenced SKILL.md files, upsert skill + schedule rows.
Package sync implements the repo sync poller (Loop 1 in the P2 design): periodically HEAD-check each connected repo, shallow-clone on SHA change, parse cronfoundry.yaml + referenced SKILL.md files, upsert skill + schedule rows.
template
Package template renders destination templates with a fixed, safe variable set — no logic, no loops.
Package template renders destination templates with a fixed, safe variable set — no logic, no loops.
testdb
Package testdb provides shared test helpers for booting a throwaway Postgres container and running migrations.
Package testdb provides shared test helpers for booting a throwaway Postgres container and running migrations.
token
Package token mints and verifies per-run bearer JWTs.
Package token mints and verifies per-run bearer JWTs.
webapi
Package webapi hosts the /api/* and /oauth/* browser-facing HTTP surface.
Package webapi hosts the /api/* and /oauth/* browser-facing HTTP surface.
writeback
Package writeback commits a <memory> block's content back to the skill repository using go-git.
Package writeback commits a <memory> block's content back to the skill repository using go-git.
yamledit
Package yamledit edits cronfoundry.yaml manifests with comment- and formatting-preserving precision via yaml.v3's Node API.
Package yamledit edits cronfoundry.yaml manifests with comment- and formatting-preserving precision via yaml.v3's Node API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL