Teams¶
A team is a roster of agent roles a project runs, composed in-repo from a
pinned agents source. Kukeon's runtime owns the schemas of the
team-distribution contract; the contents (roles, harnesses, images) live in a
separate, version-pinned agents repository. This page describes the contract —
the six document kinds, where each lives, and the disciplines the parser
enforces. The CLI verb that renders and applies a team (kuke team init) is a
later step; this page covers the declarative surface only.
Where the files live¶
| File | Where | Owner | Holds | Committed |
|---|---|---|---|---|
kuke.yaml |
host | runtime | kukeon runtime only (realm/space/stack, daemon). No git/registry/teams. | n/a |
kuketeam.yaml |
project repo | project | roster: source, roles, harnesses, per-role needs |
yes |
~/.kuke/kuketeams.yaml |
host | operator | git identity + signing, registry, secret sources, source URLs. Operator-global facts only. | no |
~/.kuke/kuketeam.d/<project>.yaml |
host | operator | one file per composed project: name, path, source. Written by each kuke team init. |
no |
The agents source contributes three more kinds — role.yaml, harness.yaml,
and harnesses/images.yaml — authored in the agents repo but deserialized by
kukeon's parser.
The six kinds¶
All six are GVK objects under one API group, kuketeams.io/v1, matching
kukeon's parsed-document convention (every document carries apiVersion +
kind). An unknown or empty apiVersion/kind pair is a parse error.
ProjectTeam(kuketeam.yaml, committed in each project repo) — the per-project roster. Pins the agentssource, declares harness defaults, lists the roles the project runs.TeamsConfig(~/.kuke/kuketeams.yaml, host, operator-owned) — operator facts (git identity + signing, registry, source clone URLs, secret sources). Operator-global only — the per-project composition records moved out into thekuketeam.d/drop-in directory below.TeamEntry(~/.kuke/kuketeam.d/<project>.yaml, host, operator-owned) — one document per composed project, written by eachkuke team init. Carries the per-project init-time locatorspec.path, the structured agentsspec.sourcepin copied from the project'skuketeam.yaml, and an optionalspec.teamDirhost-state root. It replaces the formerTeamsConfig.spec.teams[]array: a drop-in directory keeps each project isolated, so a corrupt write touches one project and two concurrent inits never race on a shared array.Role(role.yaml, in agents) — a role's skills, per-harness native config, andneeds(image capabilities, repos to clone, mounts to bind, params, secrets).Harness(harness.yaml, in agents) — a harness's base image, in-container skill path, make target, and blueprint template.ImageCatalog(harnesses/images.yaml, in agents) — the prebuilt image → capability map, plus build provenance. The v1 image selector: a role's capability names pick a hand-built image; there is no dynamic build.
Rendering onto the runtime¶
The team-layer types are declarative sugar over the existing v1beta1 runtime
types:
TeamsConfig.gitis a strict superset ofv1beta1.ContainerGit— it carriesauthor,committer,signingKey,sign, andallowedSignersunchanged, and adds thesshKeyclone identity. (It embedsContainerGitdirectly, so any field added there is automatically carried.)Role.needs.reposrender to[]v1beta1.ContainerRepo(git clones).Role.needs.mountsrender to[]v1beta1.VolumeMount(bind mounts).
Disciplines the parser enforces¶
Three disciplines keep the contract honest:
Capabilities are names, not image references¶
A needs.image entry — and every ImageCatalog capability — is a bare
capability name (git, gh, go), never an image tag or digest. The
parser rejects any capability containing /, :, or @. Capabilities are the
selector input; the ImageCatalog maps them to a concrete registry-qualified
image.
Secrets declare a source, never a value¶
A TeamsConfig.secrets entry declares where its value is read from
(from: env or from: file, plus a key), never an inline value. Secret bytes
never live in a committed or operator file.
The source ref is the version, nothing else¶
Content versioning is carried solely by the structured source object: a
host-explicit repo plus exactly one of tag / branch / commit. The key
name is the intent — tag and commit pin to a reproducible ref, branch
floats (it is refetched and reset to the branch tip on every init) — so
pinned-vs-floating is unambiguous without interrogating git (a string @ref
cannot tell a branch from a same-named tag). Exactly one of the three must be
set; zero or multiple is a parse error, and the legacy
<owner>/<repo>@vX.Y.Z string form is rejected with a migration error (no
silent dual-parse). The agents kinds (Role, Harness, ImageCatalog) carry
no in-file version field: it would be redundant with the ref, drift-prone,
and per-release toil to bump across every role.
init prints whether the resolved source is pinned or floating, so a
non-reproducible roster is visible.
Transport: SSH by default, sources as an override¶
The default clone transport is SSH: repo: <host>/<owner>/<repo> expands to
git@<host>:<owner>/<repo>.git, cloned under TeamsConfig.spec.git.sshKey as
the identity. A bare <owner>/<repo> defaults its host to github.com, but any
host is expressible. TeamsConfig.spec.sources is optional — consult it only
to override the transport (HTTPS/token, an internal mirror, a non-standard port);
the common case needs no sources entry.
The project is cloned, not mounted¶
The per-project entry's path (in ~/.kuke/kuketeam.d/<project>.yaml) is an init-time locator, not a bind-mount source.
At init time kukeon reads the project's committed kuketeam.yaml from that
path and resolves the project's clone URL from its git remote; the cell then
clones the project fresh as a ContainerRepo. Consequences:
- The project's clone URL is not declared in
kuketeam.yaml— it is resolved from the localgit remoteat init time. - The project checks out floating
main(it is the operator's own repo). The agentssource, by contrast, declares its ref intent explicitly: a pinnedtag/commitfor a reproducible roster, or a floatingbranchwhen the project deliberately tracks a moving agents branch. - The operator's working tree is not mounted — uncommitted local work is invisible in the cell.
Overriding the in-cell clone directory¶
ProjectTeam.spec.projectDir (in the committed kuketeam.yaml) overrides the
in-cell project clone-dir basename, decoupling it from metadata.name (the team
label). It exists for self-referential teams — the agents repo managing
itself, where a role declares both the project and agents repo slots:
without the override both resolve to /home/<user>/agents and collide at clone
time. When unset, the clone dir falls back to metadata.name. The value is
validated as a safe path basename that does not collide with the reserved
agents slot directory.
Scoping the team's cells¶
ProjectTeam.spec.realm, spec.space, and spec.stack (in the committed
kuketeam.yaml) are the optional scope coordinates the team's rendered cells
bind to. Each defaults to default when omitted, and the renderer stamps
the defaulted value onto every rendered Blueprint/Config so the persisted
Config scope matches the live cell. Set them to place a project's team cells
under a non-default realm/space/stack.
Secrets: two secrets.env layers render to kind: Secret¶
Team secret material is composed from two secrets.env layers and rendered as
kind: Secret documents the daemon-side apply pipeline consumes alongside the
project's CellBlueprints and CellConfigs:
- a shared host-wide default at
~/.kuke/teams/secrets.env, and - a per-team override at
<teamDir>/secrets.env.
Per-key per-team values win over the shared default; per-team-only keys join the
set; shared-only keys carry through. The keys come from the union of secret
names every role in the team references (each role's needs.secrets). On first
run both files are scaffolded in place with one empty KEY= line per referenced
secret (mode 0600), leaving values for the operator to fill in by hand;
populated files are never overwritten on re-run. Values are never logged or
echoed — the empty-value warning names keys only, and the rendered Secret
documents are the sole channel through which the values reach the daemon.
Example¶
# kuketeam.yaml — committed in each project repo
apiVersion: kuketeams.io/v1
kind: ProjectTeam
metadata: { name: sbsh }
spec:
source: { repo: github.com/eminwux/agents, tag: v1.4.0 } # pinned (tag)
realm: default # optional scope coords; each defaults to `default` when omitted
space: default
stack: default
defaults: { harnesses: [claude, opencode] }
roles:
- { ref: dev, needs: { image: [go] } }
- { ref: pm }
- { ref: pr-reviewer }
---
# ~/.kuke/kuketeams.yaml — host, operator-owned
apiVersion: kuketeams.io/v1
kind: TeamsConfig
spec:
git:
author: { name: "...", email: "..." }
signingKey: ~/.ssh/id_ed25519.pub
sign: [commits, tags]
allowedSigners: ~/.ssh/allowed_signers
sshKey: ~/.ssh/id_ed25519
registry: registry.eminwux.com
# sources is an optional transport override (HTTPS mirror, token, custom port);
# the SSH default (git@github.com:eminwux/agents.git via git.sshKey) needs no entry.
sources: { eminwux/agents: git@github.com:eminwux/agents.git }
secrets:
claude-code-oauth-token: { from: env, key: CLAUDE_CODE_OAUTH_TOKEN }
---
# ~/.kuke/kuketeam.d/sbsh.yaml — host, one file per composed project
apiVersion: kuketeams.io/v1
kind: TeamEntry
metadata: { name: sbsh }
spec:
path: ~/src/sbsh # init-time locator (clone URL resolved from its git remote)
source: { repo: github.com/eminwux/agents, tag: v1.4.0 }
---
# role.yaml — in agents, per role
apiVersion: kuketeams.io/v1
kind: Role
metadata: { name: dev }
spec:
skills: [skills/, ../common/skills/]
harnesses:
claude: { settings: config/claude.settings.json }
codex: { sandbox: workspace-write, approval: on-request }
opencode: { permissions: skip }
needs:
image: [git, gh] # capability NAMES (selector input)
repos: [project, agents] # git clones → ContainerRepo
mounts: [ssh] # bind mounts → VolumeMount
params: [PROJECT_DIR, ANTHROPIC_MODEL]
secrets: [claude-code-oauth-token]
---
# harness.yaml — in agents, per harness
apiVersion: kuketeams.io/v1
kind: Harness
metadata: { name: claude }
spec:
{
baseImage: claude,
skillPath: /home/claude/.claude/skills,
makeTarget: claude,
template: blueprint.tmpl.yaml,
}
---
# harnesses/images.yaml — prebuilt image → capabilities
apiVersion: kuketeams.io/v1
kind: ImageCatalog
spec:
images:
- ref: claude
harness: claude
image: registry.eminwux.com/claude:latest # registry-qualified
build: { context: harnesses/claude, dockerfile: Dockerfile }
capabilities: [git, gh, go, node, make]