Documentation
¶
Overview ¶
Package teamsecrets composes a team's secret material from two `secrets.env` layers — a shared host-wide default at `~/.kuke/teams/secrets.env` and a per-team override at `<teamDir>/secrets.env` — and renders the merged non-empty entries as `kind: Secret` documents the daemon-side apply pipeline (#1029) consumes alongside the project's CellBlueprints and CellConfigs.
The two-layer model splits operator-wide tokens (e.g. an ANTHROPIC OAuth token used in every team) from per-team overrides (e.g. project `dezot` using a different OPENROUTER token than project `kukeon`). Per-key per-team values win over the shared default; per-team-only keys join the set; shared-only keys carry through.
On first run the package scaffolds both files in place with one empty `KEY=` line per secret name the team's roles reference, leaving values for the operator to fill in by hand — mode 0o600, never overwritten on re-run once populated. Values are never logged or echoed: the empty-value warning names keys only, and the rendered Secret documents themselves are the only channel through which the values reach the daemon.
Index ¶
- Constants
- func KebabCase(s string) string
- func LoadEnvFile(path string) (map[string]string, error)
- func Merge(shared, perTeam map[string]string) map[string]string
- func Render(merged map[string]string, needs []string, realm string) ([]*v1beta1.SecretDoc, []string)
- func ScaffoldEnvFile(path string, keys []string) (bool, error)
- func UnionSecretNames(roles map[string]*model.Role, roster []model.ProjectTeamRole, ...) []string
- type ComposeInputs
- type ComposeResult
Constants ¶
const ( // FileName is the secrets-env filename used at both layers: shared at // <base>/teams/secrets.env and per-team at <teamDir>/secrets.env. FileName = "secrets.env" // FilePerm is the mode applied to a scaffolded secrets.env — operator // read/write only. Values that land in the file are never echoed to // stdout/stderr by this package. FilePerm = 0o600 // DirPerm is the mode applied to a freshly-created parent directory of // a scaffolded secrets.env. Matches the teams/ root mode used by // teamhost so the two scaffolding passes converge on the same shape. DirPerm = 0o700 )
Variables ¶
This section is empty.
Functions ¶
func KebabCase ¶
KebabCase lowercases s and replaces every `_` with `-`. The operator convention is SCREAMING_SNAKE_CASE for env keys and kebab-case for rendered Secret metadata.name — a single key `ANTHROPIC_AUTH_TOKEN` surfaces as Secret `anthropic-auth-token`.
func LoadEnvFile ¶
LoadEnvFile reads path as a flat KEY=VALUE env file. Blank lines and lines whose first non-whitespace character is `#` are ignored; the key is whitespace-trimmed, the value is preserved verbatim past the first `=`. A line missing the `=` is silently skipped (it cannot be acted on as a secret). A missing file returns an empty map and no error so the caller can compose against a partially-scaffolded host.
func Merge ¶
Merge overlays perTeam onto shared with per-team precedence: a key present in both maps with a non-empty perTeam value takes the perTeam value; a key in only one map carries through.
An *empty* perTeam value is treated as a scaffold placeholder, not an override: the scaffold writes one empty `KEY=` line per role-declared secret name into every per-team `secrets.env` so the operator sees every key they can override, even before they have a value to override with. Treating that empty as an active override would null-out the shared default in a single re-init pass and force every per-team file to be hand-curated. The pragmatic reading — non-empty per-team wins, empty per-team falls back to shared — keeps the shared layer useful as a default while leaving the operator free to override per-key by writing a value. The result is a fresh map; neither input is mutated.
func Render ¶
func Render(merged map[string]string, needs []string, realm string) ([]*v1beta1.SecretDoc, []string)
Render builds one SecretDoc per non-empty entry in merged whose key is also a member of needs. Keys outside needs are dropped — a stray entry in a hand-edited override file doesn't bleed unrelated bytes into the apply bundle. Output is sorted by metadata.name so two runs against the same input marshal byte-identically.
emptyKeys carries the needs entries whose merged value is empty (or absent from merged) — the caller emits one operator-facing warning per entry. Casing is preserved at SCREAMING_SNAKE_CASE so the operator can grep the line in the secrets.env file.
func ScaffoldEnvFile ¶
ScaffoldEnvFile writes path with one `KEY=` line per entry in keys when path does not exist. Keys are sorted lexicographically and de-duplicated so re-runs against the same input produce byte-identical output. The parent directory is created (DirPerm) if absent; the file is written with mode FilePerm. An existing file at path is left untouched — the "re-run never overwrites a populated file" AC.
Returns created=true when the file was freshly written; created=false when an existing file was found and skipped. An empty keys slice is a no-op (created=false, err=nil) — Compose's empty-Needs short-circuit covers this for the high-level path, but the primitive itself is also guarded so direct callers don't trip an "empty file" surprise.
func UnionSecretNames ¶
func UnionSecretNames(roles map[string]*model.Role, roster []model.ProjectTeamRole, harnesses []string) []string
UnionSecretNames walks the project roster and returns the lexicographically-sorted set of secret names every referenced role declares, unioning each role's per-harness lists (role.spec.harnesses.<h>.secrets, the agents#750 location) for the selected harnesses with the role-level fallback (role.spec.needs.secrets, the pre-migration location). This mirrors teamrender.effectiveSecretNames so the secret-composition pipeline (which creates the kind: Secret objects) and the render pipeline (which emits the CellConfigs referencing them) draw from the same secret set — a role that declares a token only under harnesses.<h>.secrets gets a Secret materialized out of secrets.env, not just a dangling CellConfig fill (#1160).
The per-harness contribution is gated by harnesses (the team's selected set, ProjectTeam.spec.defaults.harnesses), so a role's Codex/OpenCode-only secret is not gathered when those harnesses aren't selected. The role-level needs.secrets fallback is harness-independent and always contributes. Roles missing from roles map are skipped silently — Render would surface the missing reference, but at the pre-compose secret-name discovery stage there's nothing to warn about (the parent renderer already surfaces missing roles).
Types ¶
type ComposeInputs ¶
ComposeInputs is the per-team request handed to Compose.
SharedPath is the host-wide defaults file (`<base>/teams/secrets.env`). TeamPath is the per-team override (`<teamDir>/secrets.env`); when teamDir is relocated via TeamEntry.spec.teamDir the caller passes the relocated path so the override lands beside the rest of the per-team state.
Needs is the union of secret names every role in the team references — see UnionSecretNames. The scaffold lines are written for every Needs entry; merged values for keys not in Needs are dropped so a hand-edited override file with extra junk doesn't leak unrelated bytes into the apply bundle.
Realm is the realm rendered Secrets bind to. Team workloads live in the `default` user realm — pass teamrender.DefaultRealm.
DryRun suppresses both file writes so the dry-run path stays "renders to stdout, touches no files on disk". The merged read still happens against whatever's already there.
type ComposeResult ¶
ComposeResult carries the per-invocation outcome.
Secrets is the set of `kind: Secret` documents to bundle into the apply payload (ordered alphabetically by metadata.name — deterministic so two runs against the same inputs marshal byte-identically). EmptyKeys lists the original env keys (SCREAMING_SNAKE_CASE — the casing in the file the operator opens to fix it) whose merged value was empty; the caller emits one warning per empty key.
SharedScaffolded / TeamScaffolded report whether the corresponding file was just freshly written by this Compose call — the caller may surface these as one-time announcements so the operator notices the new file to fill in.
func Compose ¶
func Compose(in ComposeInputs) (*ComposeResult, error)
Compose runs the four-step pipeline: scaffold (skipped under DryRun), load both layers, merge with per-team precedence, render every non-empty entry as a SecretDoc. An empty Needs short-circuits the whole pipeline: nothing is written, nothing is read, no warnings are surfaced — a team whose roles reference no secret names (neither per-harness harnesses.<h>.secrets nor the role-level needs.secrets fallback; see UnionSecretNames) has no secret pipeline.