diff --git a/Makefile b/Makefile index 4eeb75e..12e820e 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ KORA_CRDS_CHART ?= $(KORA_CHARTS)/kora-crds --version $(KORA_VERSION) SCHEMAS := .schemas # The kustomizations make validate builds, one per example repository. -KUSTOMIZATIONS := single-tenant/platform single-tenant/tenant multi-tenant/platform multi-tenant/engineering multi-tenant/sre factories/autofix/platform factories/autofix/factory factories/sre/platform factories/sre/factory factories/sre/demo +KUSTOMIZATIONS := single-tenant/platform single-tenant/tenant multi-tenant/platform multi-tenant/engineering multi-tenant/sre factories/autofix/platform factories/autofix/factory factories/sre/platform factories/sre/factory factories/sre/demo factories/feature/platform factories/feature/factory KUBECONFORM := kubeconform -strict -summary \ -schema-location default \ diff --git a/README.md b/README.md index 8949b69..0649449 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ An example is a set of Kubernetes manifests that declare tenants, models, agents * [`multi-tenant`](multi-tenant): An engineering team and an SRE team, each in a separate tenant and repository. * [`factories/autofix`](factories/autofix): A factory of four agents that finds, verifies and fixes problems in a GitHub repository and hands a person the pull request. * [`factories/sre`](factories/sre): A factory that triages alerts in the cluster, sweeps its costs with OpenCost and proposes fixes as pull requests. +* [`factories/feature`](factories/feature): A factory that turns a feature request into an approved spec, splits it into tasks and builds each task in a pull request. Each example splits its manifests by owner: diff --git a/factories/autofix/README.md b/factories/autofix/README.md index 2a64831..fc12857 100644 --- a/factories/autofix/README.md +++ b/factories/autofix/README.md @@ -53,7 +53,7 @@ Gate A is on in the example. To switch to gate B, remove `triggers/fix-verified. `make example` checks the key file, the owner and the placeholders first, and lists anything missing. 5. Run `make tunnel` and add a webhook to your repository with the URL and secret it prints. Select the issues, issue comments, pull requests, pull request reviews and workflow runs events. See [Receive webhooks locally](../../quickstart/README.md#receive-webhooks-locally). -Sign in as `admin@autofix.local.runkora.io` to watch the factory's runs. `maintainer@autofix.local.runkora.io` can see the agents, triggers and runs. +Sign in as `autofix-admin@local.runkora.io` to watch the factory's runs. `autofix-maintainer@local.runkora.io` can see the agents, triggers and runs. ## Run it on your own cluster diff --git a/factories/autofix/platform/tenants/autofix.yaml b/factories/autofix/platform/tenants/autofix.yaml index b789818..c66a830 100644 --- a/factories/autofix/platform/tenants/autofix.yaml +++ b/factories/autofix/platform/tenants/autofix.yaml @@ -12,12 +12,17 @@ spec: displayName: Autofix active: true oidc: - issuerURL: https://dex.local.runkora.io:5557/dex + issuerURL: http://dex.localhost:5557/dex clientID: kora-quickstart scopes: [openid, profile, email, groups] - # The console routes a person to this tenant by the domain of their email. + # Every example tenant claims local.runkora.io. kora signs a person in to + # the tenant whose group is in their groups claim. emailDomains: - - autofix.local.runkora.io + - local.runkora.io + claimBinding: + claim: groups + values: + - tenant-autofix apiAuthoring: Enabled # The tenant's spending limit across every agent and person. budget: diff --git a/factories/feature/README.md b/factories/feature/README.md new file mode 100644 index 0000000..ae6bde4 --- /dev/null +++ b/factories/feature/README.md @@ -0,0 +1,60 @@ +# Feature factory + +The feature factory is an example of a lights-off feature delivery loop in runkora. A person labels a feature request, and `spec-writer` opens the spec as a pull request. A person approves the spec by merging it. `captain` splits the spec into tasks and starts an `implementer` run for each one. Each task pull request goes through `reviewer`, CI and `verifier` until it is ready for a person to merge. When every task has merged, `captain` marks the feature done. + +## How the factory works + +1. **Spec:** A person labels an issue `ready-to-spec`. `spec-writer` writes `specs/.md` with the tasks and acceptance criteria, and opens a pull request on `spec/`. +2. **Approve:** A person reviews the spec and merges it. +3. **Plan:** `captain` posts a checklist of the tasks on the issue and starts an `implementer` run for each task that is ready. +4. **Build:** `implementer` builds its task with tests and opens a pull request on `feature/-`. +5. **Check:** `reviewer` reviews each push. When CI passes, `verifier` checks the acceptance criteria. Failed CI, reviews and verifications go back to `implementer`, which joins its open run. +6. **Merge:** A person merges each task. `captain` ticks it off and starts the tasks that were waiting on it. + +The implementer can push to the repository, and pushing also allows merging. Protect your default branch with a required approving review. The factory cannot approve its own pull requests. + +## How the captain starts other agents + +`captain` starts runs with the `runs:start` tool on the kora MCP server. The server checks the captain's own grant, and the captain's `scopes` allow it to start and read `implementer` runs and no other agent's. The checklist on the issue holds its progress. A merged task and a task labelled `needs-human` both wake it again. + +## What each agent can reach + +Each agent is set up with an example configuration: + +| Agent | Model | kora | GitHub actions | Monthly budget | +| --- | --- | --- | --- | --- | +| `spec-writer` | `anthropic/claude-haiku-5-5` | None | `read`, `contents:write`, `pulls:write`, `issues:write` | $10 | +| `captain` | `anthropic/claude-haiku-5-5` | `agents:list`, `runs:start`, `runs:get`, `runs:list` on `implementer` | `read`, `issues:write` | $10 | +| `implementer` | `anthropic/claude-sonnet-5-5` | None | `read`, `contents:write`, `pulls:write` | $40 | +| `reviewer` | `anthropic/claude-haiku-5-5` | None | `read`, `pulls:write` | $20 | +| `verifier` | `anthropic/claude-haiku-5-5` | None | `read`, `pulls:write` | $20 | + +## Adapt the agents to your repository + +The prompts are general. Teach each agent about your repository: how to build and test it, the conventions you follow and what a good spec looks like for your team. + +## Set it up + +1. Create a GitHub App with read and write access to contents, issues and pull requests, install it on your repository and generate a private key. +2. In `factory/shared/github-connection.yaml`, set `app_id`, `installation_id` and `owner`. +3. In every file in `factory/triggers/`, replace `example-org/example-repo` with your repository and `example-feature[bot]` with your App's bot user `[bot]`. +4. On the quickstart, set `GITHUB_APP_PRIVATE_KEY_FILE` in `quickstart/.env`, uncomment `quickstart/providers/openrouter.yaml` and run: + + ```bash + make credentials providers example EXAMPLE=factories/feature + ``` + + `make example` lists anything missing first. +5. Run `make tunnel` and add a webhook to your repository with the URL and secret it prints. Select the issues, pull requests, pull request reviews and workflow runs events. See [Receive webhooks locally](../../quickstart/README.md#receive-webhooks-locally). +6. Open an issue for a feature and add the label `ready-to-spec`. + +Sign in as `feature-admin@local.runkora.io` to watch the runs. `feature-maintainer@local.runkora.io` can see the agents, triggers and runs. + +## Run it on your own cluster + +1. Copy `platform/` and `factory/` into the repositories your GitOps tool reads. +2. In `platform/tenants/feature.yaml`, replace the `oidc` block with your identity provider's client. +3. In `platform/models/`, point each route at an `InferenceProvider` you registered. +4. Create a `ClusterSecretStore` named `example-secret-store`. Store the App's private key under `feature-github-app` and the webhook secret under `feature-github-webhook`. + +See [Declarative resources](https://github.com/runkora/kora/blob/main/docs/guides/declarative-resources.md) and [Create a trigger](https://github.com/runkora/kora/blob/main/docs/guides/create-a-trigger.md) for the resources the factory uses. diff --git a/factories/feature/factory/agents/captain.yaml b/factories/feature/factory/agents/captain.yaml new file mode 100644 index 0000000..e219351 --- /dev/null +++ b/factories/feature/factory/agents/captain.yaml @@ -0,0 +1,60 @@ +apiVersion: runkora.io/v1alpha1 +kind: Agent +metadata: + name: captain +spec: + displayName: Captain + description: Splits an approved spec into tasks, starts an implementer run for each task and tracks the feature to done on its issue. + identityMode: autonomous + model: + family: anthropic + name: claude-haiku-5-5 + systemPrompt: | + You deliver one feature from its approved spec. You split it into tasks, + start an implementer run for each task and see every task through until + a person merges it. + + Triggers already send every review and every failed CI run on a task pull + request to the implementer that owns it. You do not relay these. You step + in when a task stalls. + + When a person merges the spec: + 1. Read specs/.md and its tasks. + 2. Post one checklist comment on the feature issue. List each task, what + it depends on and its state. + 3. Find the implementer's agent_type_id with agents.list. Start an + implementer run with runs.start for each task with no open dependency. + In the initial message, give the repository, the issue, the spec path, + the task and the branch feature/-. + 4. Add each run ID to the checklist. + + Each time you are woken, read the checklist first. It is your record. + + When a person merges a task pull request, tick the task. Start every task + whose dependencies are now merged, and add their run IDs. + + When a task needs help, check its run with runs.get: + * If the run failed or ended before a pull request opened, start a new + implementer run for the task and record the new run ID. + * If the pull request is labelled needs-human, comment on the feature + issue with what remains and mark the task blocked on the checklist. + + When a person has merged every task, comment on the feature issue that + the feature is done. + environments: + - name: build + default: true + # The captain starts and reads implementer runs, and no other agent's. + mcpTools: + - server: kora + tools: [agents:list, runs:start, runs:get, runs:list] + scopes: + - agents:types:implementer:read + - agents:runs:implementer:read + - agents:runs:implementer:write + connectors: + - connection: github + actions: [read, issues:write] + budget: + limitUSD: 10 + resetCadence: monthly diff --git a/factories/feature/factory/agents/implementer.yaml b/factories/feature/factory/agents/implementer.yaml new file mode 100644 index 0000000..6ef0273 --- /dev/null +++ b/factories/feature/factory/agents/implementer.yaml @@ -0,0 +1,37 @@ +apiVersion: runkora.io/v1alpha1 +kind: Agent +metadata: + name: implementer +spec: + displayName: Implementer + description: Builds one task of an approved spec in a pull request and answers review, verification and failed CI on its own pull requests. + identityMode: autonomous + model: + family: anthropic + name: claude-sonnet-5-5 + systemPrompt: | + You build one task of an approved spec. + + For a task from the captain: + 1. Read the spec and the task. Clone the repository. + 2. Write tests for the task's acceptance criteria, then build it, run + the tests and push the branch the captain gave you. + 3. Open a pull request whose body starts with "Refs #", names the + task and lists the acceptance criteria it meets. + + For a review, a verification or a failed CI run on your pull request: + 1. Read every review, every comment and the latest CI run. + 2. Fix what they ask for, run the tests and push to the same branch. + + Count your pushes after the first. When review, verification or CI still + fails after your third, comment what remains, add the label needs-human + and stop. Do not merge. + environments: + - name: build + default: true + connectors: + - connection: github + actions: [read, contents:write, pulls:write] + budget: + limitUSD: 40 + resetCadence: monthly diff --git a/factories/feature/factory/agents/reviewer.yaml b/factories/feature/factory/agents/reviewer.yaml new file mode 100644 index 0000000..dea12b5 --- /dev/null +++ b/factories/feature/factory/agents/reviewer.yaml @@ -0,0 +1,31 @@ +apiVersion: runkora.io/v1alpha1 +kind: Agent +metadata: + name: reviewer +spec: + displayName: Reviewer + description: Reviews each task pull request against its spec and states a verdict. + identityMode: autonomous + model: + family: anthropic + name: claude-haiku-5-5 + systemPrompt: | + You review the task pull request you are given. + + 1. Read the spec it refers to, the diff and the latest CI run. + 2. Report correctness problems first, then gaps against the spec, then + missing tests. Comment on the lines each problem is on. + 3. Submit your review with event COMMENT. The implementer opens pull + requests as the same GitHub App you post as, and GitHub rejects + APPROVE and REQUEST_CHANGES from a pull request's own author. + 4. End the review body with one verdict line: "Verdict: would approve" or + "Verdict: would request changes". + environments: + - name: build + default: true + connectors: + - connection: github + actions: [read, pulls:write] + budget: + limitUSD: 20 + resetCadence: monthly diff --git a/factories/feature/factory/agents/spec-writer.yaml b/factories/feature/factory/agents/spec-writer.yaml new file mode 100644 index 0000000..5bd031c --- /dev/null +++ b/factories/feature/factory/agents/spec-writer.yaml @@ -0,0 +1,33 @@ +apiVersion: runkora.io/v1alpha1 +kind: Agent +metadata: + name: spec-writer +spec: + displayName: Spec writer + description: Turns a feature request issue into a spec with acceptance criteria and opens it as a pull request for a person to approve. + identityMode: autonomous + model: + family: anthropic + name: claude-haiku-5-5 + systemPrompt: | + You write the spec for the feature request issue you are given. + + 1. Read the issue and the code it touches. + 2. Write specs/.md with the goal, the design, the tasks in the + order they can merge and numbered acceptance criteria. Each criterion + is a check someone can run. + 3. Push a branch named spec/ and open a pull request whose body + starts with "Refs #". + 4. Comment on the issue with a link to the pull request. + + For a review or a comment on your pull request, update the spec and push + to the same branch. Do not merge. + environments: + - name: build + default: true + connectors: + - connection: github + actions: [read, contents:write, pulls:write, issues:write] + budget: + limitUSD: 10 + resetCadence: monthly diff --git a/factories/feature/factory/agents/verifier.yaml b/factories/feature/factory/agents/verifier.yaml new file mode 100644 index 0000000..f6b6615 --- /dev/null +++ b/factories/feature/factory/agents/verifier.yaml @@ -0,0 +1,29 @@ +apiVersion: runkora.io/v1alpha1 +kind: Agent +metadata: + name: verifier +spec: + displayName: Verifier + description: Checks a task pull request with passing CI against each acceptance criterion in its spec. + identityMode: autonomous + model: + family: anthropic + name: claude-haiku-5-5 + systemPrompt: | + You verify the task pull request you are given against its spec. + + 1. Clone the repository and check out the pull request's branch. + 2. Run each acceptance criterion the task claims, and record what you + ran and what you saw. + 3. Submit a review with event COMMENT. Start the body with + "Verification passed" or "Verification failed", then list each + criterion with its result. + environments: + - name: build + default: true + connectors: + - connection: github + actions: [read, pulls:write] + budget: + limitUSD: 20 + resetCadence: monthly diff --git a/factories/feature/factory/kustomization.yaml b/factories/feature/factory/kustomization.yaml new file mode 100644 index 0000000..96dd340 --- /dev/null +++ b/factories/feature/factory/kustomization.yaml @@ -0,0 +1,32 @@ +# The feature factory: a tenant team's repository that turns a feature request +# into a spec, splits it into tasks and builds each task in a pull request. +# Point your GitOps tool at this folder, or apply it with kubectl apply -k. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# The factory's tenant namespace. The resource files leave namespace out. +namespace: kora-tenant-feature +commonAnnotations: + # The console shows this value as "Managed by" on each declared item. + runkora.io/repository: github.com/runkora/examples +# Applies the resources in the order below. The captain's scopes name the +# implementer, which must exist first. +sortOptions: + order: fifo +resources: + - shared/github-connection.yaml + - shared/github-source.yaml + - shared/build-environment.yaml + - shared/maintainer-role.yaml + - agents/implementer.yaml + - agents/spec-writer.yaml + - agents/reviewer.yaml + - agents/verifier.yaml + - agents/captain.yaml + - triggers/spec-on-ready.yaml + - triggers/plan-on-spec-merged.yaml + - triggers/next-on-task-merged.yaml + - triggers/captain-on-needs-human.yaml + - triggers/review-task-pr.yaml + - triggers/verify-on-ci-pass.yaml + - triggers/implementer-on-ci.yaml + - triggers/implementer-on-review.yaml diff --git a/factories/feature/factory/shared/build-environment.yaml b/factories/feature/factory/shared/build-environment.yaml new file mode 100644 index 0000000..f501eb1 --- /dev/null +++ b/factories/feature/factory/shared/build-environment.yaml @@ -0,0 +1,16 @@ +# The sandbox the agents clone, build and test the target repository in. +# Set sandboxImage to the bundled image for your repository's language: +# kora-base, kora-go, kora-python or kora-node. +apiVersion: runkora.io/v1alpha1 +kind: Environment +metadata: + name: build +spec: + displayName: Build + description: Clones, builds and tests the target repository. + sandboxImage: kora-base + # Each run gets its own sandbox of this size. Raise it for a large repository. + size: + cpu: 500m + memory: 1Gi + disk: 5Gi diff --git a/factories/feature/factory/shared/github-connection.yaml b/factories/feature/factory/shared/github-connection.yaml new file mode 100644 index 0000000..a01c6fc --- /dev/null +++ b/factories/feature/factory/shared/github-connection.yaml @@ -0,0 +1,37 @@ +# The factory's GitHub App installation. Store the App's private key under the +# remote key below and set the config values from the App's settings. See +# https://github.com/runkora/kora/blob/main/docs/guides/register-connection.md. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: github-app +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: example-secret-store + target: + name: github-app + data: + - secretKey: key.pem + remoteRef: + key: feature-github-app + property: key.pem +--- +apiVersion: runkora.io/v1alpha1 +kind: Connection +metadata: + name: github +spec: + adapter: github_app + baseHost: github.com + secretRef: github-app + secretKey: key.pem + config: + # The App ID from the App's settings page. + app_id: "123456" + # The number at the end of the installation's settings URL. + installation_id: 12345678 + # The organisation the App is installed on. kora denies requests for any + # other owner. + owner: example-org diff --git a/factories/feature/factory/shared/github-source.yaml b/factories/feature/factory/shared/github-source.yaml new file mode 100644 index 0000000..d05bfe6 --- /dev/null +++ b/factories/feature/factory/shared/github-source.yaml @@ -0,0 +1,34 @@ +# The GitHub webhook the factory's triggers read events from. Point a webhook +# on the target repository at the API host plus status.webhookPath, with the +# signing secret stored under the remote key below. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: github-webhook +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: example-secret-store + target: + name: github-webhook + template: + metadata: + labels: + # kora reads only Secrets that carry this label. + runkora.io/trigger-source: "true" + data: + - secretKey: secret + remoteRef: + key: feature-github-webhook + property: secret +--- +apiVersion: runkora.io/v1alpha1 +kind: TriggerSource +metadata: + name: github +spec: + provider: github + signingSecretRef: + name: github-webhook + key: secret diff --git a/factories/feature/factory/shared/maintainer-role.yaml b/factories/feature/factory/shared/maintainer-role.yaml new file mode 100644 index 0000000..8290765 --- /dev/null +++ b/factories/feature/factory/shared/maintainer-role.yaml @@ -0,0 +1,16 @@ +# The maintainer role. People whose groups claim includes maintainer can see +# the factory's agents and triggers, and read every agent's runs. +apiVersion: runkora.io/v1alpha1 +kind: PolicyRole +metadata: + name: maintainer +spec: + role: maintainer + scopes: + - agents:types:read + - triggers:read + - agents:runs:spec-writer:read + - agents:runs:captain:read + - agents:runs:implementer:read + - agents:runs:reviewer:read + - agents:runs:verifier:read diff --git a/factories/feature/factory/triggers/captain-on-needs-human.yaml b/factories/feature/factory/triggers/captain-on-needs-human.yaml new file mode 100644 index 0000000..c7f369d --- /dev/null +++ b/factories/feature/factory/triggers/captain-on-needs-human.yaml @@ -0,0 +1,27 @@ +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: captain-on-needs-human +spec: + source: github + eventType: pull_request + match: + action: [labeled] + label.name: [needs-human] + repository.full_name: [example-org/example-repo] + matchContains: + pull_request.head.ref: [feature/] + routing: + candidates: [captain] + model: + family: typesafe + name: jev-1-14 + fallback: captain + joinRuns: true + promptTemplate: | + A task pull request needs a person. Check the task and update the feature. + + Repository: {{ repository.full_name }} + Task pull request: #{{ pull_request.number }} + Branch: {{ pull_request.head.ref }} + Link: {{ pull_request.html_url }} diff --git a/factories/feature/factory/triggers/implementer-on-ci.yaml b/factories/feature/factory/triggers/implementer-on-ci.yaml new file mode 100644 index 0000000..c0a5186 --- /dev/null +++ b/factories/feature/factory/triggers/implementer-on-ci.yaml @@ -0,0 +1,32 @@ +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: implementer-on-ci +spec: + source: github + eventType: workflow_run + match: + action: [completed] + workflow_run.conclusion: [failure] + workflow_run.actor.login: ["example-feature[bot]"] + repository.full_name: [example-org/example-repo] + matchContains: + workflow_run.head_branch: [feature/] + coalesce: + key: "{{ repository.full_name }}@{{ workflow_run.head_sha }}" + windowSeconds: 60 + routing: + candidates: [implementer] + model: + family: typesafe + name: jev-1-14 + fallback: implementer + joinRuns: true + promptTemplate: | + CI failed on your pull request. Fix it. + + Repository: {{ repository.full_name }} + Pull request: #{{ workflow_run.pull_requests.0.number }} + Branch: {{ workflow_run.head_branch }} + Failed workflow: {{ workflow_run.name }} + Run: {{ workflow_run.html_url }} diff --git a/factories/feature/factory/triggers/implementer-on-review.yaml b/factories/feature/factory/triggers/implementer-on-review.yaml new file mode 100644 index 0000000..2ced733 --- /dev/null +++ b/factories/feature/factory/triggers/implementer-on-review.yaml @@ -0,0 +1,30 @@ +# The reviewer's review, the verifier's verification and a person's review all +# reach the implementer here. +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: implementer-on-review +spec: + source: github + eventType: pull_request_review + match: + action: [submitted] + pull_request.user.login: ["example-feature[bot]"] + repository.full_name: [example-org/example-repo] + matchContains: + pull_request.head.ref: [feature/] + routing: + candidates: [implementer] + model: + family: typesafe + name: jev-1-14 + fallback: implementer + joinRuns: true + promptTemplate: | + {{ review.user.login }} reviewed your pull request. Address the review. + + Repository: {{ repository.full_name }} + Pull request: #{{ pull_request.number }} + Branch: {{ pull_request.head.ref }} + Link: {{ pull_request.html_url }} + Review: {{ review.body }} diff --git a/factories/feature/factory/triggers/next-on-task-merged.yaml b/factories/feature/factory/triggers/next-on-task-merged.yaml new file mode 100644 index 0000000..93582fb --- /dev/null +++ b/factories/feature/factory/triggers/next-on-task-merged.yaml @@ -0,0 +1,27 @@ +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: next-on-task-merged +spec: + source: github + eventType: pull_request + match: + action: [closed] + pull_request.merged: ["true"] + repository.full_name: [example-org/example-repo] + matchContains: + pull_request.head.ref: [feature/] + routing: + candidates: [captain] + model: + family: typesafe + name: jev-1-14 + fallback: captain + joinRuns: true + promptTemplate: | + A task pull request merged. Update the checklist and start the next tasks. + + Repository: {{ repository.full_name }} + Task pull request: #{{ pull_request.number }} + Branch: {{ pull_request.head.ref }} + Body: {{ pull_request.body }} diff --git a/factories/feature/factory/triggers/plan-on-spec-merged.yaml b/factories/feature/factory/triggers/plan-on-spec-merged.yaml new file mode 100644 index 0000000..b64768a --- /dev/null +++ b/factories/feature/factory/triggers/plan-on-spec-merged.yaml @@ -0,0 +1,22 @@ +# A person approves the spec by merging its pull request. +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: plan-on-spec-merged +spec: + source: github + eventType: pull_request + match: + action: [closed] + pull_request.merged: ["true"] + repository.full_name: [example-org/example-repo] + matchContains: + pull_request.head.ref: [spec/] + agent: captain + promptTemplate: | + A spec merged. Plan the feature and start its first tasks. + + Repository: {{ repository.full_name }} + Spec pull request: #{{ pull_request.number }} + Branch: {{ pull_request.head.ref }} + Body: {{ pull_request.body }} diff --git a/factories/feature/factory/triggers/review-task-pr.yaml b/factories/feature/factory/triggers/review-task-pr.yaml new file mode 100644 index 0000000..271e4f9 --- /dev/null +++ b/factories/feature/factory/triggers/review-task-pr.yaml @@ -0,0 +1,26 @@ +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: review-task-pr +spec: + source: github + eventType: pull_request + match: + action: [opened, synchronize] + pull_request.user.login: ["example-feature[bot]"] + repository.full_name: [example-org/example-repo] + matchContains: + pull_request.head.ref: [feature/] + coalesce: + key: "{{ repository.full_name }}#{{ pull_request.number }}" + windowSeconds: 120 + agent: reviewer + promptTemplate: | + Review this task pull request. + + Repository: {{ repository.full_name }} + Pull request: #{{ pull_request.number }} + Title: {{ pull_request.title }} + Head commit: {{ pull_request.head.sha }} + Link: {{ pull_request.html_url }} + Body: {{ pull_request.body }} diff --git a/factories/feature/factory/triggers/spec-on-ready.yaml b/factories/feature/factory/triggers/spec-on-ready.yaml new file mode 100644 index 0000000..1d8617b --- /dev/null +++ b/factories/feature/factory/triggers/spec-on-ready.yaml @@ -0,0 +1,20 @@ +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: spec-on-ready +spec: + source: github + eventType: issues + match: + action: [labeled] + label.name: [ready-to-spec] + repository.full_name: [example-org/example-repo] + agent: spec-writer + promptTemplate: | + Write the spec for this feature request. + + Repository: {{ repository.full_name }} + Issue: #{{ issue.number }} + Title: {{ issue.title }} + Link: {{ issue.html_url }} + Body: {{ issue.body }} diff --git a/factories/feature/factory/triggers/verify-on-ci-pass.yaml b/factories/feature/factory/triggers/verify-on-ci-pass.yaml new file mode 100644 index 0000000..72fd477 --- /dev/null +++ b/factories/feature/factory/triggers/verify-on-ci-pass.yaml @@ -0,0 +1,25 @@ +apiVersion: runkora.io/v1alpha1 +kind: Trigger +metadata: + name: verify-on-ci-pass +spec: + source: github + eventType: workflow_run + match: + action: [completed] + workflow_run.conclusion: [success] + workflow_run.actor.login: ["example-feature[bot]"] + repository.full_name: [example-org/example-repo] + matchContains: + workflow_run.head_branch: [feature/] + coalesce: + key: "{{ repository.full_name }}@{{ workflow_run.head_sha }}" + windowSeconds: 60 + agent: verifier + promptTemplate: | + CI passed on this task pull request. Verify it against its spec. + + Repository: {{ repository.full_name }} + Pull request: #{{ workflow_run.pull_requests.0.number }} + Branch: {{ workflow_run.head_branch }} + Head commit: {{ workflow_run.head_sha }} diff --git a/factories/feature/platform/kustomization.yaml b/factories/feature/platform/kustomization.yaml new file mode 100644 index 0000000..4926093 --- /dev/null +++ b/factories/feature/platform/kustomization.yaml @@ -0,0 +1,19 @@ +# The platform team's repository for the feature factory: the tenant and the +# models its agents run on. Point your GitOps tool at this folder, or apply it +# with kubectl apply -k. +# +# These resources span kora-system and the cluster scope. This file sets no +# namespace. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +commonAnnotations: + # The console shows this value as "Managed by" on each declared item. + runkora.io/repository: github.com/runkora/examples +resources: + - tenants/feature.yaml + - models/claude-sonnet-5-5.yaml + - models/claude-haiku-5-5.yaml + - models/deepseek-v4-1-flash.yaml + - models/gpt-6-luna.yaml + - models/gpt-oss-20b.yaml + - models/jev-1-14.yaml diff --git a/factories/feature/platform/models/claude-haiku-5-5.yaml b/factories/feature/platform/models/claude-haiku-5-5.yaml new file mode 100644 index 0000000..c63a18d --- /dev/null +++ b/factories/feature/platform/models/claude-haiku-5-5.yaml @@ -0,0 +1,18 @@ +# anthropic/claude-haiku-5-5 is the model the spec writer, captain, reviewer and +# verifier run on. It routes to the OpenRouter provider in quickstart/providers. On your own +# cluster, point the route at a provider you registered. +apiVersion: runkora.io/v1alpha1 +kind: VirtualModel +metadata: + name: anthropic-claude-haiku-5-5 + namespace: kora-system +spec: + family: anthropic + name: claude-haiku-5-5 + displayName: Claude Haiku 5.5 + description: Claude Haiku 5.5 through OpenRouter. + strategy: priority + routes: + - provider: compatible:openrouter + modelID: anthropic/claude-haiku-5.5 + priority: 1 diff --git a/factories/feature/platform/models/claude-sonnet-5-5.yaml b/factories/feature/platform/models/claude-sonnet-5-5.yaml new file mode 100644 index 0000000..33ba8ef --- /dev/null +++ b/factories/feature/platform/models/claude-sonnet-5-5.yaml @@ -0,0 +1,18 @@ +# anthropic/claude-sonnet-5-5 is the model the implementer builds features +# with. It routes to the OpenRouter provider in quickstart/providers. On your own +# cluster, point the route at a provider you registered. +apiVersion: runkora.io/v1alpha1 +kind: VirtualModel +metadata: + name: anthropic-claude-sonnet-5-5 + namespace: kora-system +spec: + family: anthropic + name: claude-sonnet-5-5 + displayName: Claude Sonnet 5.5 + description: Claude Sonnet 5.5 through OpenRouter. + strategy: priority + routes: + - provider: compatible:openrouter + modelID: anthropic/claude-sonnet-5.5 + priority: 1 diff --git a/factories/feature/platform/models/deepseek-v4-1-flash.yaml b/factories/feature/platform/models/deepseek-v4-1-flash.yaml new file mode 100644 index 0000000..13ec540 --- /dev/null +++ b/factories/feature/platform/models/deepseek-v4-1-flash.yaml @@ -0,0 +1,17 @@ +# deepseek/deepseek-v4-1-flash is a low-cost chat model. It routes to the +# OpenRouter provider in quickstart/providers. +apiVersion: runkora.io/v1alpha1 +kind: VirtualModel +metadata: + name: deepseek-deepseek-v4-1-flash + namespace: kora-system +spec: + family: deepseek + name: deepseek-v4-1-flash + displayName: DeepSeek V4.1 Flash + description: DeepSeek V4.1 Flash through OpenRouter. + strategy: priority + routes: + - provider: compatible:openrouter + modelID: deepseek/deepseek-v4.1-flash + priority: 1 diff --git a/factories/feature/platform/models/gpt-6-luna.yaml b/factories/feature/platform/models/gpt-6-luna.yaml new file mode 100644 index 0000000..d5801dc --- /dev/null +++ b/factories/feature/platform/models/gpt-6-luna.yaml @@ -0,0 +1,17 @@ +# openai/gpt-6-luna is a low-cost chat model. It routes to the OpenRouter +# provider in quickstart/providers. +apiVersion: runkora.io/v1alpha1 +kind: VirtualModel +metadata: + name: openai-gpt-6-luna + namespace: kora-system +spec: + family: openai + name: gpt-6-luna + displayName: GPT-6 Luna + description: GPT-6 Luna through OpenRouter. + strategy: priority + routes: + - provider: compatible:openrouter + modelID: openai/gpt-6-luna + priority: 1 diff --git a/factories/feature/platform/models/gpt-oss-20b.yaml b/factories/feature/platform/models/gpt-oss-20b.yaml new file mode 100644 index 0000000..2e68970 --- /dev/null +++ b/factories/feature/platform/models/gpt-oss-20b.yaml @@ -0,0 +1,18 @@ +# openai/gpt-oss-20b runs gpt-oss 20B in Ollama on your machine. It routes to +# the quickstart's Ollama provider in quickstart/providers. To run an agent on +# it, set the agent's model to family openai and name gpt-oss-20b. +apiVersion: runkora.io/v1alpha1 +kind: VirtualModel +metadata: + name: openai-gpt-oss-20b + namespace: kora-system +spec: + family: openai + name: gpt-oss-20b + displayName: gpt-oss 20B (Ollama) + description: gpt-oss 20B running in Ollama on your machine. + strategy: priority + routes: + - provider: compatible:ollama + modelID: gpt-oss:20b + priority: 1 diff --git a/factories/feature/platform/models/jev-1-14.yaml b/factories/feature/platform/models/jev-1-14.yaml new file mode 100644 index 0000000..19f8253 --- /dev/null +++ b/factories/feature/platform/models/jev-1-14.yaml @@ -0,0 +1,17 @@ +# The decision model that routed triggers use to pick an agent or an open run. +# Routing needs a decision model. A chat model cannot route. +apiVersion: runkora.io/v1alpha1 +kind: VirtualModel +metadata: + name: typesafe-jev-1-14 + namespace: kora-system +spec: + family: typesafe + name: jev-1-14 + displayName: Jev 1.14 + description: TypeSafe Jev 1.14 decision model through OpenRouter. + strategy: priority + routes: + - provider: compatible:openrouter + modelID: typesafe/jev-1.14 + priority: 1 diff --git a/factories/feature/platform/tenants/feature.yaml b/factories/feature/platform/tenants/feature.yaml new file mode 100644 index 0000000..8b44eab --- /dev/null +++ b/factories/feature/platform/tenants/feature.yaml @@ -0,0 +1,30 @@ +# The feature tenant. The operator creates its namespace, kora-tenant-feature. +# ../factory applies the factory's agents, triggers and roles there. +# +# The oidc block points at the quickstart's Dex. On your own cluster, replace +# it with the client your identity provider gives you. See +# https://github.com/runkora/kora/blob/main/docs/guides/tenants.md. +apiVersion: runkora.io/v1alpha1 +kind: KoraTenant +metadata: + name: feature +spec: + displayName: Feature + active: true + oidc: + issuerURL: http://dex.localhost:5557/dex + clientID: kora-quickstart + scopes: [openid, profile, email, groups] + # Every example tenant claims local.runkora.io. kora signs a person in to + # the tenant whose group is in their groups claim. + emailDomains: + - local.runkora.io + claimBinding: + claim: groups + values: + - tenant-feature + apiAuthoring: Enabled + # The tenant's spending limit across every agent and person. + budget: + limitUSD: 100 + resetCadence: monthly diff --git a/factories/sre/README.md b/factories/sre/README.md index e9013f5..010cb41 100644 --- a/factories/sre/README.md +++ b/factories/sre/README.md @@ -44,7 +44,7 @@ The prompts are general. Teach each agent about your cluster: your runbooks, how `make example` lists anything missing first. It installs OpenCost, connects the factory to the quickstart's API server and deploys the broken `checkout` service from `demo/`. 6. Run `make tunnel` and add a webhook to your repository with the URL and secret it prints. Select the issues, issue comments and pull request reviews events. See [Receive webhooks locally](../../quickstart/README.md#receive-webhooks-locally). -Sign in as `admin@oncall.local.runkora.io` to watch the runs. `sre@oncall.local.runkora.io` can see the agents, triggers and runs. +Sign in as `oncall-admin@local.runkora.io` to watch the runs. `oncall-sre@local.runkora.io` can see the agents, triggers and runs. ## Fire an alert on the quickstart diff --git a/factories/sre/platform/tenants/oncall.yaml b/factories/sre/platform/tenants/oncall.yaml index 0272033..c4c3c41 100644 --- a/factories/sre/platform/tenants/oncall.yaml +++ b/factories/sre/platform/tenants/oncall.yaml @@ -12,12 +12,17 @@ spec: displayName: On-call active: true oidc: - issuerURL: https://dex.local.runkora.io:5557/dex + issuerURL: http://dex.localhost:5557/dex clientID: kora-quickstart scopes: [openid, profile, email, groups] - # The console routes a person to this tenant by the domain of their email. + # Every example tenant claims local.runkora.io. kora signs a person in to + # the tenant whose group is in their groups claim. emailDomains: - - oncall.local.runkora.io + - local.runkora.io + claimBinding: + claim: groups + values: + - tenant-oncall apiAuthoring: Enabled # The tenant's spending limit across every agent and person. budget: diff --git a/multi-tenant/README.md b/multi-tenant/README.md index 231c2b8..b4de81e 100644 --- a/multi-tenant/README.md +++ b/multi-tenant/README.md @@ -41,7 +41,7 @@ kubectl apply -k multi-tenant/engineering kubectl apply -k multi-tenant/sre ``` -Sign in as `admin@engineering.local.runkora.io` or `admin@sre.local.runkora.io`. Each admin sees only their own tenant. +Sign in as `engineering-admin@local.runkora.io` or `sre-admin@local.runkora.io`. Both tenants share the `local.runkora.io` domain, and each binds to its own tenant group. Each admin sees only their own tenant. ## How the agents reach a model @@ -57,7 +57,7 @@ Sign in as `admin@engineering.local.runkora.io` or `admin@sre.local.runkora.io`. ## Add a team -1. Add a `KoraTenant` for the team under `platform/tenants/` with the team's email domain. +1. Add a `KoraTenant` for the team under `platform/tenants/` with your email domain and a `claimBinding` on the team's group. 2. Create a repository for the team with a `kustomization.yaml` that sets `namespace: kora-tenant-`. 3. Point your GitOps tool at the new repository. diff --git a/multi-tenant/platform/tenants/engineering.yaml b/multi-tenant/platform/tenants/engineering.yaml index e51a2ec..3592027 100644 --- a/multi-tenant/platform/tenants/engineering.yaml +++ b/multi-tenant/platform/tenants/engineering.yaml @@ -13,12 +13,17 @@ spec: displayName: Engineering active: true oidc: - issuerURL: https://dex.local.runkora.io:5557/dex + issuerURL: http://dex.localhost:5557/dex clientID: kora-quickstart scopes: [openid, profile, email, groups] - # The console routes a person to this tenant by the domain of their email. + # Every example tenant claims local.runkora.io. kora signs a person in to + # the tenant whose group is in their groups claim. emailDomains: - - engineering.local.runkora.io + - local.runkora.io + claimBinding: + claim: groups + values: + - tenant-engineering # Enabled lets the team build agents in the console next to the declared # ones. Set Disabled to make ../engineering the only way to change them. apiAuthoring: Enabled diff --git a/multi-tenant/platform/tenants/sre.yaml b/multi-tenant/platform/tenants/sre.yaml index d28e721..d6ac676 100644 --- a/multi-tenant/platform/tenants/sre.yaml +++ b/multi-tenant/platform/tenants/sre.yaml @@ -13,12 +13,17 @@ spec: displayName: SRE active: true oidc: - issuerURL: https://dex.local.runkora.io:5557/dex + issuerURL: http://dex.localhost:5557/dex clientID: kora-quickstart scopes: [openid, profile, email, groups] - # The console routes a person to this tenant by the domain of their email. + # Every example tenant claims local.runkora.io. kora signs a person in to + # the tenant whose group is in their groups claim. emailDomains: - - sre.local.runkora.io + - local.runkora.io + claimBinding: + claim: groups + values: + - tenant-sre # Enabled lets the team build agents in the console next to the declared # ones. Set Disabled to make ../sre the only way to change them. apiAuthoring: Enabled diff --git a/quickstart/.env.example b/quickstart/.env.example index 9749c6c..65f216f 100644 --- a/quickstart/.env.example +++ b/quickstart/.env.example @@ -13,7 +13,6 @@ OPENROUTER_API_KEY= EXA_API_KEY= # The absolute path to your GitHub App's private key file, for the github -# connections in ../multi-tenant/engineering, ../factories/autofix and -# ../factories/sre. GitHub gives you the .pem file when you generate a private -# key in the App's settings. +# connections in ../multi-tenant/engineering and ../factories. GitHub gives you +# the .pem file when you generate a private key in the App's settings. GITHUB_APP_PRIVATE_KEY_FILE= diff --git a/quickstart/Makefile b/quickstart/Makefile index 57c6466..c1a2c3e 100644 --- a/quickstart/Makefile +++ b/quickstart/Makefile @@ -12,7 +12,7 @@ CONTEXT := k3d-$(CLUSTER) # is an identity migration. TRUST_DOMAIN := quickstart.runkora.test -# Keys, passwords and certificates generated on your machine. Git ignores them. +# Keys and passwords generated on your machine. Git ignores them. SECRETS := .secrets # The webhook tunnel's process ID, URL and log. Git ignores the folder. @@ -21,23 +21,26 @@ TUNNEL_DIR := .tunnel # The examples make example applies. For each example: the tenants its # platform repository declares, its tenant repositories and the user to sign in # as. -EXAMPLES := single-tenant multi-tenant factories/autofix factories/sre +EXAMPLES := single-tenant multi-tenant factories/autofix factories/sre factories/feature TENANTS_single-tenant := example REPOS_single-tenant := tenant SIGN_IN_single-tenant := admin@local.runkora.io TENANTS_multi-tenant := engineering sre REPOS_multi-tenant := engineering sre -SIGN_IN_multi-tenant := admin@engineering.local.runkora.io or admin@sre.local.runkora.io +SIGN_IN_multi-tenant := engineering-admin@local.runkora.io or sre-admin@local.runkora.io TENANTS_factories/autofix := autofix REPOS_factories/autofix := factory -SIGN_IN_factories/autofix := admin@autofix.local.runkora.io +SIGN_IN_factories/autofix := autofix-admin@local.runkora.io TENANTS_factories/sre := oncall REPOS_factories/sre := factory -SIGN_IN_factories/sre := admin@oncall.local.runkora.io +SIGN_IN_factories/sre := oncall-admin@local.runkora.io +TENANTS_factories/feature := feature +REPOS_factories/feature := factory +SIGN_IN_factories/feature := feature-admin@local.runkora.io # The webhook signing secrets the examples' trigger sources read, one per # tenant and provider. make credentials generates each one. -WEBHOOK_SECRETS := example-github-webhook engineering-github-webhook engineering-slack-webhook sre-alertmanager-webhook autofix-github-webhook oncall-github-webhook oncall-alertmanager-webhook +WEBHOOK_SECRETS := example-github-webhook engineering-github-webhook engineering-slack-webhook sre-alertmanager-webhook autofix-github-webhook oncall-github-webhook oncall-alertmanager-webhook feature-github-webhook # The credentials you fill in from .env.example. The install runs without the # file, and make credentials skips each value that is not set. @@ -59,7 +62,7 @@ up: tools cluster namespaces secrets istio spire data dex eso kora credentials p # Checks the tools the install uses. tools: - @for tool in docker k3d kubectl helm mkcert openssl; do \ + @for tool in docker k3d kubectl helm openssl; do \ command -v $$tool >/dev/null || { echo "Missing $$tool. See README.md for the prerequisites."; exit 1; }; \ done @@ -77,14 +80,6 @@ cluster: namespaces: $(KUBECTL) apply -f manifests/namespaces.yaml -# A certificate for *.local.runkora.io from a local CA. mkcert -install adds the -# CA to your system trust store for your browser to trust the console and Dex. -# It asks for your password the first time. -$(SECRETS)/tls.crt: - @mkdir -p $(SECRETS) - mkcert -install - mkcert -cert-file $(SECRETS)/tls.crt -key-file $(SECRETS)/tls.key local.runkora.io '*.local.runkora.io' - # The RSA key kora signs its access tokens with. $(SECRETS)/signing.key: @mkdir -p $(SECRETS) @@ -109,7 +104,7 @@ $(SECRETS)/%-webhook: # Stores the generated values in the cluster. kora reads kora-secrets and # kora-frontend-secrets. The chart does not create them. -secrets: $(SECRETS)/tls.crt $(SECRETS)/signing.key $(SECRETS)/db-password $(SECRETS)/redis-password $(SECRETS)/session-secret $(SECRETS)/triggers-key +secrets: $(SECRETS)/signing.key $(SECRETS)/db-password $(SECRETS)/redis-password $(SECRETS)/session-secret $(SECRETS)/triggers-key $(call apply,kora-data,secret generic postgres --from-file=password=$(SECRETS)/db-password) $(call apply,kora-data,secret generic redis --from-file=password=$(SECRETS)/redis-password) $(call apply,kora-system,secret generic kora-secrets \ @@ -118,9 +113,6 @@ secrets: $(SECRETS)/tls.crt $(SECRETS)/signing.key $(SECRETS)/db-password $(SECR --from-file=KORA_TOKEN_SIGNING_KEY=$(SECRETS)/signing.key \ --from-file=KORA_TRIGGERS_ENCRYPTION_KEY=$(SECRETS)/triggers-key) $(call apply,kora-system,secret generic kora-frontend-secrets --from-file=BFF_SESSION_SECRET=$(SECRETS)/session-secret) - $(call apply,kora-system,secret tls kora-gateway-tls --cert=$(SECRETS)/tls.crt --key=$(SECRETS)/tls.key) - $(call apply,dex,secret tls dex-tls --cert=$(SECRETS)/tls.crt --key=$(SECRETS)/tls.key) - $(call apply,kora-system,configmap local-ca --from-file=ca.crt="$$(mkcert -CAROOT)/rootCA.pem") # Istio in sidecar mode with the CNI plugin. kora's services and agents run in # the mesh. @@ -187,10 +179,11 @@ credentials: $(addprefix $(SECRETS)/,$(WEBHOOK_SECRETS)) $(call apply,kora-credentials,secret generic sre-exa --from-literal=apiKey="$$EXA_API_KEY"); \ fi @if [ -n "$$GITHUB_APP_PRIVATE_KEY_FILE" ]; then \ - echo "GITHUB_APP_PRIVATE_KEY_FILE -> Secrets engineering-github-app, autofix-github-app and oncall-github-app in kora-credentials"; \ + echo "GITHUB_APP_PRIVATE_KEY_FILE -> Secrets engineering-github-app, autofix-github-app, oncall-github-app and feature-github-app in kora-credentials"; \ $(call apply,kora-credentials,secret generic engineering-github-app --from-file=key.pem="$$GITHUB_APP_PRIVATE_KEY_FILE"); \ $(call apply,kora-credentials,secret generic autofix-github-app --from-file=key.pem="$$GITHUB_APP_PRIVATE_KEY_FILE"); \ $(call apply,kora-credentials,secret generic oncall-github-app --from-file=key.pem="$$GITHUB_APP_PRIVATE_KEY_FILE"); \ + $(call apply,kora-credentials,secret generic feature-github-app --from-file=key.pem="$$GITHUB_APP_PRIVATE_KEY_FILE"); \ fi @for name in $(WEBHOOK_SECRETS); do \ echo "$(SECRETS)/$$name -> Secret $$name in kora-credentials"; \ @@ -218,9 +211,10 @@ example: echo " 2) multi-tenant Two teams with four use cases. Also needs an Exa key and a GitHub App."; \ echo " 3) autofix A factory that finds, verifies and fixes bugs. Needs a GitHub App and a repository."; \ echo " 4) sre A factory that triages alerts and proposes fixes. Needs a GitHub App and a repository."; \ - echo " 5) none Apply one later with make example."; \ - printf "Choose 1, 2, 3, 4 or 5 [1]: "; read choice; \ - case "$$choice" in 2) example=multi-tenant;; 3) example=factories/autofix;; 4) example=factories/sre;; 5) example=none;; *) example=single-tenant;; esac; \ + echo " 5) feature A factory that specs, plans and builds features. Needs a GitHub App and a repository."; \ + echo " 6) none Apply one later with make example."; \ + printf "Choose 1, 2, 3, 4, 5 or 6 [1]: "; read choice; \ + case "$$choice" in 2) example=multi-tenant;; 3) example=factories/autofix;; 4) example=factories/sre;; 5) example=factories/feature;; 6) example=none;; *) example=single-tenant;; esac; \ fi; \ [ "$${example:-single-tenant}" = none ] || $(MAKE) --no-print-directory apply-example EXAMPLE=$${example:-single-tenant} @@ -273,6 +267,21 @@ ifeq ($(EXAMPLE),factories/sre) echo "See factories/sre/README.md. Then run make credentials example EXAMPLE=factories/sre."; exit 1; \ fi endif +ifeq ($(EXAMPLE),factories/feature) + @missing=0; \ + if [ ! -f "$$GITHUB_APP_PRIVATE_KEY_FILE" ]; then \ + echo "feature: set GITHUB_APP_PRIVATE_KEY_FILE in quickstart/.env to your GitHub App's private key file."; missing=1; \ + fi; \ + if grep -q 'owner: example-org' ../factories/feature/factory/shared/github-connection.yaml; then \ + echo "feature: set app_id, installation_id and owner in factories/feature/factory/shared/github-connection.yaml."; missing=1; \ + fi; \ + if grep -rqF -e 'example-org/example-repo' -e 'example-feature[bot]' ../factories/feature/factory/triggers; then \ + echo "feature: replace example-org/example-repo and example-feature[bot] in factories/feature/factory/triggers with your repository and your App's bot user."; missing=1; \ + fi; \ + if [ $$missing = 1 ]; then \ + echo "See factories/feature/README.md. Then run make credentials example EXAMPLE=factories/feature."; exit 1; \ + fi +endif # Applies an example: its platform repository first, then each tenant # repository once the operator has created the tenant's namespace. @@ -302,7 +311,7 @@ ifeq ($(EXAMPLE),factories/sre) endif @echo @echo "$(EXAMPLE) is running." - @echo " Console: https://local.runkora.io:9443" + @echo " Console: http://kora.localhost:9880" @echo " Sign in: $(SIGN_IN_$(EXAMPLE)), password \"password\"" @echo @@ -330,15 +339,14 @@ ask-tunnel: # for every trigger source in the cluster. Paste each URL and its secret into # the provider's webhook settings. The URL changes each time the tunnel starts. # -# The tunnel forwards to the gateway on localhost:9443 as api.local.runkora.io. +# The tunnel forwards to the gateway on localhost:9880 as api.kora.localhost. # The webhook path accepts only events signed with the source's secret. The # console and the API still need a sign-in. tunnel: tunnel-stop @command -v cloudflared >/dev/null || { echo "Missing cloudflared. Install it with brew install cloudflared."; exit 1; } @mkdir -p $(TUNNEL_DIR) - @nohup cloudflared tunnel --no-autoupdate --url https://localhost:9443 \ - --http-host-header api.local.runkora.io --origin-server-name api.local.runkora.io \ - --no-tls-verify > $(TUNNEL_DIR)/cloudflared.log 2>&1 & echo $$! > $(TUNNEL_DIR)/pid + @nohup cloudflared tunnel --no-autoupdate --url http://localhost:9880 \ + --http-host-header api.kora.localhost > $(TUNNEL_DIR)/cloudflared.log 2>&1 & echo $$! > $(TUNNEL_DIR)/pid @for i in $$(seq 30); do \ url=$$(grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' $(TUNNEL_DIR)/cloudflared.log | head -1); \ [ -n "$$url" ] && break; sleep 1; \ @@ -368,9 +376,9 @@ alert: [ -n "$$path" ] || { echo "The sre example's alertmanager source is not ready. Run make example EXAMPLE=factories/sre."; exit 1; }; \ now=$$(date -u +%Y-%m-%dT%H:%M:%SZ); \ printf '%s' '{"version":"4","status":"$(STATUS)","receiver":"kora","groupKey":"{}:{alertname=\"KubePodCrashLooping\", namespace=\"shop\"}","truncatedAlerts":0,"groupLabels":{"alertname":"KubePodCrashLooping","namespace":"shop"},"commonLabels":{"alertname":"KubePodCrashLooping","namespace":"shop","deployment":"checkout","severity":"critical"},"commonAnnotations":{"summary":"checkout is crash-looping in shop","description":"A checkout pod in shop restarted more than three times in the last ten minutes."},"externalURL":"http://alertmanager.local","alerts":[{"status":"$(STATUS)","labels":{"alertname":"KubePodCrashLooping","namespace":"shop","deployment":"checkout","severity":"critical"},"annotations":{"summary":"checkout is crash-looping in shop"},"startsAt":"'"$$now"'","endsAt":"0001-01-01T00:00:00Z","generatorURL":"http://prometheus.local/graph","fingerprint":"5f1c0a6e2b7d9c31"}]}' | \ - curl -sS --fail-with-body --resolve api.local.runkora.io:9443:127.0.0.1 --cacert "$$(mkcert -CAROOT)/rootCA.pem" \ + curl -sS --fail-with-body \ -H "Authorization: Bearer $$(cat $(SECRETS)/oncall-alertmanager-webhook)" -H 'Content-Type: application/json' \ - --data-binary @- "https://api.local.runkora.io:9443$$path" && echo && echo "Posted a $(STATUS) KubePodCrashLooping alert for shop/checkout." + --data-binary @- "http://api.kora.localhost:9880$$path" && echo && echo "Posted a $(STATUS) KubePodCrashLooping alert for shop/checkout." # Deletes the cluster. The next make up reuses the files in .secrets. down: tunnel-stop diff --git a/quickstart/README.md b/quickstart/README.md index 20a8143..2f47005 100644 --- a/quickstart/README.md +++ b/quickstart/README.md @@ -5,8 +5,8 @@ The quickstart installs runkora on a local k3d cluster. It installs Istio, SPIRE ## Prerequisites * Docker Desktop or Colima with at least 4 CPUs and 8 GiB of memory allocated to the VM. -* `k3d`, `kubectl`, `helm`, `mkcert`, `openssl` and `make`. On macOS, install them with `brew install k3d kubectl helm mkcert`. -* Free ports 9443, 9880 and 5557 on your machine. +* `k3d`, `kubectl`, `helm`, `openssl` and `make`. On macOS, install them with `brew install k3d kubectl helm`. +* Free ports 9880 and 5557 on your machine. * An OpenRouter account, or Ollama on your machine. ## Files @@ -28,12 +28,13 @@ The quickstart installs runkora on a local k3d cluster. It installs Istio, SPIRE * **[`multi-tenant`](../multi-tenant):** Two tenants, engineering and SRE, with four use cases. It also needs an Exa API key and a GitHub App. * **[`autofix`](../factories/autofix):** A factory that finds, verifies and fixes bugs in a GitHub repository. It needs a GitHub App and a repository. * **[`sre`](../factories/sre):** A factory that triages alerts, sweeps the cluster's costs with OpenCost and proposes fixes. It needs a GitHub App and a repository. + * **[`feature`](../factories/feature):** A factory that specs, plans and builds features from issues. It needs a GitHub App and a repository. * **`none`:** Installs kora with no tenants. You can apply an example later with `make example`. Set `EXAMPLE` to skip the question, such as `make up EXAMPLE=multi-tenant`. -4. **Sign in:** Open `https://local.runkora.io:9443` and sign in with a user from [Users](#users) for the example you applied. +4. **Sign in:** Open `http://kora.localhost:9880` and sign in with a user from [Users](#users) for the example you applied. -`*.local.runkora.io` is a public DNS record that resolves to `127.0.0.1`. It needs no local DNS setup. +Every `*.localhost` name resolves to your machine, and browsers treat it as a secure origin over plain HTTP. The quickstart needs no certificate and no local DNS setup. ## Choose a model provider @@ -43,7 +44,7 @@ Each example declares four models in its `platform/models/`: * `deepseek/deepseek-v4-1-flash` and `openai/gpt-6-luna` route to OpenRouter. * `openai/gpt-oss-20b` routes to Ollama. -`multi-tenant`, `autofix` and `sre` also declare `typesafe/jev-1-14`, a decision model on OpenRouter that their routed triggers use to pick an agent. +`multi-tenant`, `autofix`, `sre` and `feature` also declare `typesafe/jev-1-14`, a decision model on OpenRouter that their routed triggers use to pick an agent. ### OpenRouter @@ -77,7 +78,7 @@ Each stage is a `make` target. Run one on its own to watch what it installs. 1. **`make cluster`:** Creates the `kora-quickstart` cluster from `k3d.yaml` and raises the Docker VM's inotify limit for kora's file watchers. 2. **`make namespaces`:** Creates `kora-data`, `dex`, `kora-credentials` and `kora-system`. The `kora-system` labels turn on Istio sidecar injection and the restricted Pod Security level. -3. **`make secrets`:** Generates the keys, passwords and the `*.local.runkora.io` certificate into `.secrets/`, then stores them in the cluster. `mkcert -install` adds the local CA to your system trust store and asks for your password the first time. +3. **`make secrets`:** Generates the keys and passwords into `.secrets/`, then stores them in the cluster. 4. **`make istio`:** Installs Istio in sidecar mode with the CNI plugin. 5. **`make spire`:** Installs SPIRE. SPIRE issues a SPIFFE identity to every kora workload and agent. 6. **`make data`:** Starts Postgres and Redis in `kora-data`. @@ -93,24 +94,27 @@ Inspect the cluster between stages with `kubectl --context k3d-kora-quickstart g ## Users -Every user signs in with the password `password`. kora routes each person to a tenant by the domain of their email. A user can sign in once the example that declares their tenant is applied. +Every user signs in with the password `password`. Every tenant claims the `local.runkora.io` domain and binds to one tenant group with `claimBinding`. kora signs each person in to the tenant whose group they are in. A user can sign in once the example that declares their tenant is applied. * **`single-tenant`:** - * `admin@local.runkora.io`: Groups `platform-admin` and `engineer`. - * `engineer@local.runkora.io`: Groups `engineer` and `kora-viewer`. + * `admin@local.runkora.io`: Tenant `example`, groups `platform-admin` and `engineer`. + * `engineer@local.runkora.io`: Tenant `example`, groups `engineer` and `kora-viewer`. * **`multi-tenant`:** - * `admin@engineering.local.runkora.io`: Tenant `engineering`, group `platform-admin`. - * `dev@engineering.local.runkora.io`: Tenant `engineering`, groups `reviewer` and `kora-viewer`. - * `admin@sre.local.runkora.io`: Tenant `sre`, group `platform-admin`. - * `oncall@sre.local.runkora.io`: Tenant `sre`, groups `sre` and `kora-viewer`. + * `engineering-admin@local.runkora.io`: Tenant `engineering`, group `platform-admin`. + * `engineering-dev@local.runkora.io`: Tenant `engineering`, groups `reviewer` and `kora-viewer`. + * `sre-admin@local.runkora.io`: Tenant `sre`, group `platform-admin`. + * `sre-oncall@local.runkora.io`: Tenant `sre`, groups `sre` and `kora-viewer`. * **`autofix`:** - * `admin@autofix.local.runkora.io`: Group `platform-admin`. - * `maintainer@autofix.local.runkora.io`: Group `maintainer`. + * `autofix-admin@local.runkora.io`: Tenant `autofix`, group `platform-admin`. + * `autofix-maintainer@local.runkora.io`: Tenant `autofix`, group `maintainer`. * **`sre`:** - * `admin@oncall.local.runkora.io`: Tenant `oncall`, group `platform-admin`. - * `sre@oncall.local.runkora.io`: Tenant `oncall`, group `sre`. + * `oncall-admin@local.runkora.io`: Tenant `oncall`, group `platform-admin`. + * `oncall-sre@local.runkora.io`: Tenant `oncall`, group `sre`. +* **`feature`:** + * `feature-admin@local.runkora.io`: Tenant `feature`, group `platform-admin`. + * `feature-maintainer@local.runkora.io`: Tenant `feature`, group `maintainer`. -The users and their groups live in `manifests/dex/glauth.yaml`. kora reads a person's roles from the `groups` claim in their ID token. `platform-admin` holds every scope in kora, and `kora-viewer` reads everything in the tenant. Both are built-in roles that kora maps from the group name. Each example grants the other groups their scopes with `PolicyRole` resources. +The users and their groups live in `manifests/dex/glauth.yaml`. Each user is also in one `tenant-` group, which picks their tenant. kora reads a person's roles from the `groups` claim in their ID token. `platform-admin` holds every scope in kora, and `kora-viewer` reads everything in the tenant. Both are built-in roles that kora maps from the group name. Each example grants the other groups their scopes with `PolicyRole` resources. ## Run another example @@ -127,7 +131,7 @@ A webhook trigger fires only when the provider can reach the cluster. A tunnel g The tunnel makes the console and the API reachable from the internet while it runs. The webhook path accepts only events signed with the source's secret, and the console and the API still need a sign-in. -A quick tunnel gets a new URL each time it starts. Update the provider's webhook URL after each `make tunnel`. For a URL that stays the same, create a [named Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) or use ngrok with a reserved domain. Point either one at `https://localhost:9443` with the host header `api.local.runkora.io`. +A quick tunnel gets a new URL each time it starts. Update the provider's webhook URL after each `make tunnel`. For a URL that stays the same, create a [named Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) or use ngrok with a reserved domain. Point either one at `http://localhost:9880` with the host header `api.kora.localhost`. ## How an agent reaches a model @@ -147,7 +151,7 @@ On your own cluster, keep the agents and the tenants, and point each model's rou ## Use your own sign-in provider -Replace `issuerURL` and `clientID` in a tenant under the example's `platform/tenants/` with the values from your provider, set `emailDomains` to your domain, and run `make example` again. The provider must allow the redirect URI `https://local.runkora.io:9443/auth/callback`. +Replace `issuerURL` and `clientID` in a tenant under the example's `platform/tenants/` with the values from your provider, set `emailDomains` to your domain, and run `make example` again. The provider must allow the redirect URI `http://kora.localhost:9880/auth/callback`. ## Check and remove the install diff --git a/quickstart/k3d.yaml b/quickstart/k3d.yaml index 43458d0..0efd271 100644 --- a/quickstart/k3d.yaml +++ b/quickstart/k3d.yaml @@ -8,15 +8,12 @@ servers: 1 agents: 2 ports: - # The kora gateway. The console is https://local.runkora.io:9443. - - port: 9443:443 - nodeFilters: - - loadbalancer + # The kora gateway. The console is http://kora.localhost:9880. - port: 9880:80 nodeFilters: - loadbalancer # Dex, the sign-in provider. The browser and the identity service both use - # https://dex.local.runkora.io:5557/dex. + # http://dex.localhost:5557/dex. - port: 5557:5557 nodeFilters: - loadbalancer diff --git a/quickstart/manifests/dex/dex.yaml b/quickstart/manifests/dex/dex.yaml index a3f5821..891e3c5 100644 --- a/quickstart/manifests/dex/dex.yaml +++ b/quickstart/manifests/dex/dex.yaml @@ -2,9 +2,9 @@ # each tenant points at your own provider, such as Okta or Entra ID. See the # tenants folder in each example's platform repository. # -# The issuer https://dex.local.runkora.io:5557/dex must be the same URL for the +# The issuer http://dex.localhost:5557/dex must be the same URL for the # browser and for the identity service: -# - The browser resolves dex.local.runkora.io to 127.0.0.1, and k3d forwards +# - The browser resolves dex.localhost to your machine, and k3d forwards # port 5557 to this Service. # - The identity service resolves it to this Service's fixed ClusterIP through # identity.hostAliases in values/kora.yaml. @@ -15,20 +15,18 @@ metadata: namespace: dex data: config.yaml: | - issuer: https://dex.local.runkora.io:5557/dex + issuer: http://dex.localhost:5557/dex # Sessions are lost when Dex restarts. Sign in again if that happens. storage: type: memory web: - https: 0.0.0.0:5557 - tlsCert: /etc/dex/tls/tls.crt - tlsKey: /etc/dex/tls/tls.key + http: 0.0.0.0:5557 # The console fetches Dex's discovery document and token endpoint from # the browser. allowedOrigins: - - https://local.runkora.io:9443 + - http://kora.localhost:9880 oauth2: deviceCodeGrant: true @@ -42,7 +40,7 @@ data: name: kora quickstart public: true redirectURIs: - - https://local.runkora.io:9443/auth/callback + - http://kora.localhost:9880/auth/callback - /device/callback connectors: @@ -85,7 +83,7 @@ spec: selector: app: dex ports: - - name: https + - name: http port: 5557 --- apiVersion: apps/v1 @@ -113,19 +111,13 @@ spec: httpGet: path: /dex/healthz port: 5557 - scheme: HTTPS + scheme: HTTP periodSeconds: 5 volumeMounts: - name: config mountPath: /etc/dex/config.yaml subPath: config.yaml - - name: tls - mountPath: /etc/dex/tls volumes: - name: config configMap: name: dex - # make secrets creates this from the mkcert certificate for *.local.runkora.io. - - name: tls - secret: - secretName: dex-tls diff --git a/quickstart/manifests/dex/glauth.yaml b/quickstart/manifests/dex/glauth.yaml index 52e863e..ae941f2 100644 --- a/quickstart/manifests/dex/glauth.yaml +++ b/quickstart/manifests/dex/glauth.yaml @@ -2,21 +2,23 @@ # kora reads a person's roles from the groups claim in their ID token. Dex's # built-in password database has no groups. Dex reads its users from glauth. # -# kora routes a person to a tenant by the domain of their email. Each example -# in ../single-tenant, ../multi-tenant and ../factories declares the tenants -# these domains belong to. +# Every tenant in the examples claims the local.runkora.io domain and binds to +# one tenant group in the groups claim. kora signs a person in to the tenant +# whose group they are in. Each user is in exactly one tenant group. # # Every user has the password "password" (passsha256 is its SHA-256). -# admin@local.runkora.io single-tenant platform-admin, engineer -# engineer@local.runkora.io single-tenant engineer, kora-viewer -# admin@engineering.local.runkora.io multi-tenant platform-admin -# dev@engineering.local.runkora.io multi-tenant reviewer, kora-viewer -# admin@sre.local.runkora.io multi-tenant platform-admin -# oncall@sre.local.runkora.io multi-tenant sre, kora-viewer -# admin@autofix.local.runkora.io autofix platform-admin -# maintainer@autofix.local.runkora.io autofix maintainer -# admin@oncall.local.runkora.io sre platform-admin -# sre@oncall.local.runkora.io sre sre +# admin@local.runkora.io example platform-admin, engineer +# engineer@local.runkora.io example engineer, kora-viewer +# engineering-admin@local.runkora.io engineering platform-admin +# engineering-dev@local.runkora.io engineering reviewer, kora-viewer +# sre-admin@local.runkora.io sre platform-admin +# sre-oncall@local.runkora.io sre sre, kora-viewer +# autofix-admin@local.runkora.io autofix platform-admin +# autofix-maintainer@local.runkora.io autofix maintainer +# oncall-admin@local.runkora.io oncall platform-admin +# oncall-sre@local.runkora.io oncall sre +# feature-admin@local.runkora.io feature platform-admin +# feature-maintainer@local.runkora.io feature maintainer apiVersion: v1 kind: ConfigMap metadata: @@ -51,7 +53,7 @@ data: mail = "admin@local.runkora.io" uidnumber = 1001 primarygroup = 1100 - othergroups = [5001, 5003] + othergroups = [5101, 5001, 5003] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] @@ -61,87 +63,107 @@ data: mail = "engineer@local.runkora.io" uidnumber = 1002 primarygroup = 1100 - othergroups = [5003, 5002] + othergroups = [5101, 5003, 5002] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "engineering-admin" givenname = "Admin" sn = "User" - mail = "admin@engineering.local.runkora.io" + mail = "engineering-admin@local.runkora.io" uidnumber = 1003 primarygroup = 1100 - othergroups = [5001] + othergroups = [5102, 5001] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "engineering-dev" givenname = "Dev" sn = "User" - mail = "dev@engineering.local.runkora.io" + mail = "engineering-dev@local.runkora.io" uidnumber = 1004 primarygroup = 1100 - othergroups = [5004, 5002] + othergroups = [5102, 5004, 5002] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "sre-admin" givenname = "Admin" sn = "User" - mail = "admin@sre.local.runkora.io" + mail = "sre-admin@local.runkora.io" uidnumber = 1005 primarygroup = 1100 - othergroups = [5001] + othergroups = [5103, 5001] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "sre-oncall" givenname = "Oncall" sn = "User" - mail = "oncall@sre.local.runkora.io" + mail = "sre-oncall@local.runkora.io" uidnumber = 1006 primarygroup = 1100 - othergroups = [5005, 5002] + othergroups = [5103, 5005, 5002] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "autofix-admin" givenname = "Admin" sn = "User" - mail = "admin@autofix.local.runkora.io" + mail = "autofix-admin@local.runkora.io" uidnumber = 1007 primarygroup = 1100 - othergroups = [5001] + othergroups = [5104, 5001] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "autofix-maintainer" givenname = "Maintainer" sn = "User" - mail = "maintainer@autofix.local.runkora.io" + mail = "autofix-maintainer@local.runkora.io" uidnumber = 1008 primarygroup = 1100 - othergroups = [5006] + othergroups = [5104, 5006] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "oncall-admin" givenname = "Admin" sn = "User" - mail = "admin@oncall.local.runkora.io" + mail = "oncall-admin@local.runkora.io" uidnumber = 1009 primarygroup = 1100 - othergroups = [5001] + othergroups = [5105, 5001] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users]] name = "oncall-sre" givenname = "SRE" sn = "User" - mail = "sre@oncall.local.runkora.io" + mail = "oncall-sre@local.runkora.io" uidnumber = 1010 primarygroup = 1100 - othergroups = [5005] + othergroups = [5105, 5005] + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + + [[users]] + name = "feature-admin" + givenname = "Admin" + sn = "User" + mail = "feature-admin@local.runkora.io" + uidnumber = 1011 + primarygroup = 1100 + othergroups = [5106, 5001] + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + + [[users]] + name = "feature-maintainer" + givenname = "Maintainer" + sn = "User" + mail = "feature-maintainer@local.runkora.io" + uidnumber = 1012 + primarygroup = 1100 + othergroups = [5106, 5006] passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[groups]] @@ -174,6 +196,25 @@ data: [[groups]] name = "maintainer" gidnumber = 5006 + # The tenant groups. Each tenant's claimBinding names one of them. + [[groups]] + name = "tenant-example" + gidnumber = 5101 + [[groups]] + name = "tenant-engineering" + gidnumber = 5102 + [[groups]] + name = "tenant-sre" + gidnumber = 5103 + [[groups]] + name = "tenant-autofix" + gidnumber = 5104 + [[groups]] + name = "tenant-oncall" + gidnumber = 5105 + [[groups]] + name = "tenant-feature" + gidnumber = 5106 --- apiVersion: v1 kind: Service diff --git a/quickstart/values/kora.yaml b/quickstart/values/kora.yaml index 9c52340..a566273 100644 --- a/quickstart/values/kora.yaml +++ b/quickstart/values/kora.yaml @@ -7,13 +7,16 @@ global: # The in-cluster Postgres in manifests/data serves plaintext. sslMode: disable -# *.local.runkora.io is a public DNS record that resolves to 127.0.0.1. The -# console is https://local.runkora.io:9443 and the API is api.local.runkora.io. -frontendHost: local.runkora.io +# Every *.localhost name resolves to your machine, and browsers treat it as a +# secure origin over plain HTTP. The console is http://kora.localhost:9880 and +# the API is api.kora.localhost. No certificate is needed. +frontendHost: kora.localhost frontend: config: - nextPublicAppUrl: https://local.runkora.io:9443 + # The chart builds this URL from frontendHost on the standard HTTPS port. + # The quickstart serves plain HTTP on port 9880, so it sets the URL itself. + nextPublicAppUrl: http://kora.localhost:9880 config: db: @@ -42,32 +45,13 @@ egressGateway: upstreamPorts: - 6443 -gateway: - tls: - enabled: true - # make secrets creates this from the mkcert certificate for *.local.runkora.io. - credentialName: kora-gateway-tls - identity: - # Inside the cluster dex.local.runkora.io resolves to 127.0.0.1. This alias - # points it at the fixed ClusterIP of the Dex Service in manifests/dex/dex.yaml. + # Inside the cluster this alias points dex.localhost at the fixed ClusterIP + # of the Dex Service in manifests/dex/dex.yaml. hostAliases: - ip: 10.43.200.10 hostnames: - - dex.local.runkora.io - # Dex serves a certificate from the mkcert CA. The identity service trusts - # that CA alongside the system roots to verify Dex's tokens over TLS. - extraEnv: - - name: SSL_CERT_DIR - value: /etc/ssl/certs:/etc/kora/local-ca - extraVolumes: - - name: local-ca - configMap: - name: local-ca - extraVolumeMounts: - - name: local-ca - mountPath: /etc/kora/local-ca - readOnly: true + - dex.localhost # The quickstart runs one operator replica, with no leader election and no # disruption budget. diff --git a/single-tenant/README.md b/single-tenant/README.md index f2da0cd..4342872 100644 --- a/single-tenant/README.md +++ b/single-tenant/README.md @@ -49,7 +49,7 @@ On the quickstart, run `make tunnel` first and use the webhook URL it prints, as ## Run it on your own cluster 1. Copy `platform/` and `tenant/` into the repositories your GitOps tool reads. -2. In `platform/tenants/example.yaml`, replace the `oidc` block with the client your identity provider gives you, and set `emailDomains` to your domains. +2. In `platform/tenants/example.yaml`, replace the `oidc` block with the client your identity provider gives you, and set `emailDomains` to your domains. Drop `claimBinding` when no other tenant shares the domain. 3. In `platform/models/`, point each route at an `InferenceProvider` you registered. 4. Create a `ClusterSecretStore` named `example-secret-store` that points at your secret manager, and store the webhook signing secret under the key `example-github-webhook`. 5. Set `runkora.io/repository` in each `kustomization.yaml` to your repository. diff --git a/single-tenant/platform/tenants/example.yaml b/single-tenant/platform/tenants/example.yaml index e5b7d80..ef24047 100644 --- a/single-tenant/platform/tenants/example.yaml +++ b/single-tenant/platform/tenants/example.yaml @@ -12,12 +12,17 @@ spec: displayName: Example active: true oidc: - issuerURL: https://dex.local.runkora.io:5557/dex + issuerURL: http://dex.localhost:5557/dex clientID: kora-quickstart scopes: [openid, profile, email, groups] - # The console routes a person to this tenant by the domain of their email. + # Every example tenant claims local.runkora.io. kora signs a person in to + # the tenant whose group is in their groups claim. emailDomains: - local.runkora.io + claimBinding: + claim: groups + values: + - tenant-example # Enabled lets people build agents in the console next to the declared ones. # Set Disabled to make ../tenant the only way to change the tenant's agents, # environments, trigger sources, triggers and roles. diff --git a/versions.mk b/versions.mk index 6a50d1f..bbb61dd 100644 --- a/versions.mk +++ b/versions.mk @@ -2,7 +2,7 @@ # Override any of them on the command line, e.g. make up KORA_VERSION=0.11.0. # The kora release the examples install. Both charts share the version. -KORA_VERSION ?= 0.11.0-rc.6 +KORA_VERSION ?= 0.11.3 KORA_CHARTS ?= oci://ghcr.io/runkora/charts # kora supports Istio 1.30 and 1.31. The operator's preflight rejects any other version.