Skip to content

122. Declaring managed repository configuration in repos.yaml ​

Date: 2026-09-18

Status ​

Accepted

Context ​

The fullsend repos manifest manages installations across many repositories, but repository configuration overrides are not yet declarative in that manifest. Operators need one repeatable source of desired configuration for a fleet while retaining the existing typed config.yaml schema and merge semantics. A repository entry should be able to specify only the values it needs to override.

This ADR covers management of .fullsend/config.yaml only. It builds on the manifest and command model in ADR 0057 and ADR 0074.

Options ​

  1. Special-case selected fields, such as agents. This keeps the initial manifest schema small, but every new configuration field would require a separate manifest feature and merge implementation.
  2. Treat manifest values as an unmanaged patch. This would preserve local edits, but ownership and drift would be ambiguous and convergence could not compare one complete desired file with the installed file.
  3. Use generic, schema-backed configuration and manage the complete output. This gives supported configuration fields the same behavior and makes drift and convergence deterministic.

Decision ​

repos.yaml will support managed configuration blocks using the same schema as .fullsend/config.yaml:

  • A manifest-wide defaults.config block applies to every repository in the manifest.
  • A repository entry's config block applies only to that repository.
  • The manifest layers are merged from defaults.config and the repository's config; the repository value is more specific and wins. Unspecified values remain absent from the generated file and retain the existing config.yaml read-time behavior.
  • Managed configuration is generic for fields not already represented by manifest shorthands. agents and other supported configuration fields use the normal schema and merge rules. Existing runtime and allowed_remote_resources manifest fields remain authoritative for those paths; a config block must not set either path, avoiding two competing manifest representations.

Managed configuration is opt-in per repository. defaults.config opts every repository in the manifest into management; a repository config block opts in only that repository. A repository with neither declaration is not managed, even when another repository has one: install and status leave its existing .fullsend/config.yaml untouched and skip drift checks for that file. For an opted-in repository, the generated managed file contains only the explicitly supplied manifest values (including authoritative manifest shorthands). Values from an existing config.yaml that are not represented in the manifest are not carried forward after adoption; unmanaged repositories are the only ones whose existing files remain untouched.

Every managed .fullsend/config.yaml begins with these stable ownership comments:

yaml
# This file is managed by 'fullsend repos': repos-config-v1
# Do not edit this file directly; changes will be overwritten.

The marker is metadata rather than a configuration field and must survive canonical rendering. If a repository is opted in and has no existing .fullsend/config.yaml, install evaluates the candidate against the effective configuration from an empty managed layer, then creates the file and writes the marker if the safety gate passes; no adoption acknowledgement is needed when there is no relaxation. If it has an existing config.yaml without the marker, it is an adoption case: status reports that adoption is required, and install refuses to replace the file until the operator explicitly acknowledges the handoff and the safety gate passes. The first successful adoption writes the marker. Once the marker is present, differences from the manifest are ordinary managed drift and follow the normal convergence rules.

The installer will merge the manifest layers using the existing per-field rules, then render the complete desired managed .fullsend/config.yaml from the explicitly supplied manifest values. Scalars use specific-value-wins; agents uses keyed merging by derived name; and replace-if-set fields such as roles replace the less-specific value when supplied, including an explicit empty value. These merge rules apply only while producing the desired file; comparison with the installed file and convergence are whole-file operations, not a second merge with local edits. Any difference in the managed file is drift, including a change to a single key. Convergence rewrites the file with the generated desired content; manual edits are therefore reported as drift rather than preserved as an unmanaged patch.

Before any write of a managed file—first creation, first adoption, or subsequent convergence—the operation must compare the candidate effective configuration with the current effective configuration through the full runtime accessor chain. The comparison covers IsKillSwitchActive(), AllowedResources(), ConfigRoles(), agent keyed entries including enabled: false suppressions, and IssueCreationConfig() including create_issues.allow_targets.

Generated omission is evaluated as fallthrough through the parent chain, not ignored because the sparse file lacks the key. The operation must reject a less-restrictive candidate unless the manifest explicitly declares that relaxation. This includes dropping kill_switch: true, roles: [], an explicit allowed_remote_resources: [] deny-all value, an agent suppression, or widening a previously narrowed effective create_issues.allow_targets list. An explicit empty allowed_remote_resources remains deny-all and must not be treated as equivalent to an omitted value. A blanket adoption acknowledgement is not sufficient, and status/adoption output must identify the affected keys.

Manifest configuration must be decoded strictly and validated against the typed config.yaml schema. Unknown or misspelled fields fail with a clear error instead of being silently ignored. Each sparse manifest layer is first validated for known fields and forbidden shorthand paths. Before rendering, the resulting managed configuration (merged manifest blocks plus authoritative manifest shorthands) must be validated through the existing full runtime accessor chain, including separately managed parent configuration and code defaults. This ADR does not define or manage those parent layers.

Consequences ​

  • A manifest can reproducibly install and converge configuration across a repository fleet, including future supported configuration fields.
  • Local changes to .fullsend/config.yaml become visible as whole-file drift and are overwritten by convergence, so operators must make intentional changes in the manifest.
  • The manifest and config.yaml share one schema, reducing duplicated configuration concepts but requiring manifest validation to track schema changes.
  • Generated output is canonical managed content; formatting or comments that are not represented by the schema may not survive convergence, except for the stable ownership marker, which is required for adoption detection.
  • Existing repositories require an explicit ownership handoff before a markerless config.yaml can be replaced, preventing first adoption from silently discarding locally maintained restrictions. Subsequent convergence also rejects less-restrictive generated security values unless the manifest explicitly declares them.
  • Installation and status flows need to expose validation errors and drift clearly so a fleet operator can correct the manifest before converging.