config

package
v0.3.4 Latest Latest
Warning

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

Go to latest
Published: Jun 3, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ActionSourceFetch fires once per layer fetch attempt that contacts (or
	// would have contacted) a source. Target is the source id; Fields carry
	// resolved_sha and cache_hit.
	ActionSourceFetch = "config.source.fetch"
	// ActionLayerResolve fires once per imported layer successfully resolved
	// and validated. Target is the "source_id:layer_path" ref; Fields carry
	// field_count and sha.
	ActionLayerResolve = "config.layer.resolve"
	// ActionFieldOverridden fires once per effective field that more than one
	// layer set (the higher-precedence layer overrides the lower). Target is the
	// field path; Fields carry from_layer, to_layer, value_summary.
	ActionFieldOverridden = "config.field.overridden"
	// ActionFieldProtectionViolation fires when a lower-precedence (imported /
	// user-local) layer attempts to set a repo-protected field and is dropped.
	// Target is the field path; Fields carry attempted_by_layer; Outcome=dropped.
	ActionFieldProtectionViolation = "config.field.protection_violation"
	// ActionImportFailed fires when an extends import cannot be resolved. Target
	// is the failing ref; Fields carry reason (transport|auth|content|schema|
	// not_found) and whether the entry was optional (and thus skipped).
	ActionImportFailed = "config.import.failed"
	// ActionEffectiveProduced fires once at the end of a successful resolve.
	// Target is the repo_id (or "" when unset); Fields carry layer_count.
	ActionEffectiveProduced = "config.effective.produced"
)

Config-tier audit action identifiers. These are the canonical `action` strings of the base event schema, matching the taxonomy table in the spec.

View Source
const (
	// OutcomeSuccess marks an event for an operation that completed normally.
	OutcomeSuccess = "success"
	// OutcomeDropped marks a protected-field override that was discarded.
	OutcomeDropped = "dropped"
	// OutcomeFailure marks an import that failed (non-optional).
	OutcomeFailure = "failure"
	// OutcomeSkipped marks an optional import that failed and was skipped.
	OutcomeSkipped = "skipped"
)

Audit outcome values used in the base event schema.

View Source
const (
	UnitKindLayer    = "layer"
	UnitKindArtifact = "artifact"
)

Unit kinds (§7A.1): a resolvable unit is either a config `layer` (merges into effective config, may declare more units) or an executable `artifact` (installed discretely, invoked under trust/signing). The kind governs merge/trust only, not sourcing.

View Source
const (
	LockSectionConfig   = "config"
	LockSectionPackages = "packages"
)

LockSectionConfig is the agentslock section name owned by the config resolver (spec §7). The package resolver (pass 2) owns "packages" and the graph adapter owns "adapters"; this resolver writes config + an empty packages stub.

View Source
const (
	// LayerProductDefaults is the built-in defaults layer shipped by dot-agents.
	LayerProductDefaults = "product-defaults"
	// LayerUserLocal is ~/.agents/.agentsrc.json (machine-local preferences).
	LayerUserLocal = "user-local"
	// LayerRepoLocal is the committed repo-local <project>/.agentsrc.json.
	LayerRepoLocal = "repo-local"
)

Layer identifiers, lowest precedence first. These mirror the layer model in org-config-resolution §4. The FlatResolver only produces the three FLAT-scope layers below; imported org/team/repo extends layers (config-v2 p1b) slot in between LayerUserLocal and LayerRepoLocal with the same provenance surface.

They are the canonical identifiers that `da config explain` (config-v2 p4) renders.

View Source
const AgentsLockFile = ".agentsrc.lock"

AgentsLockFile is the lockfile name — the resolved-state companion to AgentsRCFile (.agentsrc.json), committed alongside it (spec §7).

View Source
const AgentsRCFile = ".agentsrc.json"
View Source
const AgentsRCLocalFile = ".agentsrc.local.json"

AgentsRCLocalFile is the project-local overlay manifest (§7A.1): the user's personal, machine-local, per-project layer (the `.git/config` analog), stored gitignored alongside the committed manifest. It is one of the local scopes folded into inputs_digest.

View Source
const LockSectionUnits = "units"

LockSectionUnits is the agentslock section name for the unified units model (config-distribution-model §7A.3). It replaces the legacy per-tier "config" and "packages" sections: one map keyed by "source:path@version" carrying a `kind`. The "adapters" section stays separate (graph lifecycle owns it).

Variables

View Source
var (
	ErrProposalNotFound      = errors.New("proposal not found")
	ErrInvalidProposalTarget = errors.New("invalid proposal target")
)
View Source
var ErrLockWouldChange = errors.New("config: lock would change (--locked assertion failed)")

ErrLockWouldChange is the sentinel returned by EnsureResolved in Locked mode when the committed lock is stale (re-resolving WOULD rewrite it). It is the `--locked` CI assertion: the caller maps it to a non-zero exit. When this is returned, EnsureResolved has written nothing.

View Source
var ProtectedFields = []string{"repo_id", "project"}

ProtectedFields are repo-owned scalars that an imported (non-repo-local) layer must not override (org-config-resolution §7.4). An attempt to set one from a lower-precedence layer is dropped and recorded as a non-fatal ProvenanceWarning. In FLAT scope there are no imported layers, so a protected field simply resolves from whichever local layer set it; the guard becomes load-bearing in p1b when extends layers can carry these keys.

Functions

func AgentsCacheDir

func AgentsCacheDir() string

AgentsCacheDir returns the root directory for cached remote sources.

func AgentsContextDir

func AgentsContextDir() string

AgentsContextDir returns the local workflow context directory under ~/.agents.

func AgentsHome

func AgentsHome() string

AgentsHome returns the path to the ~/.agents directory.

func AgentsLockPath

func AgentsLockPath(projectPath string) string

AgentsLockPath returns the canonical .agentsrc.lock path for a project: the sibling of the repo-local .agentsrc.json (spec §7). This is the single shared definition of the lockfile location. Every section writer — the config resolver here, the package resolver (pass 2), the graph-adapter lifecycle (#178, "adapters" section), and `da doctor`/`da status` (config-v2 p2) — MUST resolve the lockfile through this helper rather than re-deriving the path, so the canonical location can never drift between writers.

func AgentsStateDir

func AgentsStateDir() string

AgentsStateDir returns the XDG state directory for dot-agents.

func AppendUnique

func AppendUnique(slice []string, s string) []string

AppendUnique appends s to slice only if not already present.

func ApplyProposal

func ApplyProposal(proposal *Proposal) error

func ArchiveProposal

func ArchiveProposal(proposal *Proposal) error

func ArchivedProposalPath

func ArchivedProposalPath(id string) string

func ArchivedProposalsDir

func ArchivedProposalsDir() string

func ComputeInputsDigest

func ComputeInputsDigest(projectPath, userLocalPath string) (string, error)

ComputeInputsDigest hashes all local config scopes whole-normalized (§7A.3), returning a "sha256:…" digest. Each scope's manifest is re-marshaled through canonical JSON so cosmetic edits (key order, whitespace) do not register as a content change; a missing scope hashes as empty. The result is the value compared against the lock's recorded inputs_digest to detect scope drift.

userLocalPath is the resolver's user-local manifest seam (empty ⇒ default <AgentsHome>/.agentsrc.json), so staleness honors the same test override the resolver uses.

func DeriveRepoIDFromGit

func DeriveRepoIDFromGit(repoPath string) string

DeriveRepoIDFromGit returns the canonical repo_id for the project at repoPath, derived from its `origin` git remote. The canonical form is `<host>/<path>` with the `.git` suffix stripped and the host lowercased, e.g. `github.com/acme/po-core-api-se` (per org-config-resolution §5.2).

Accepted remote forms:

  • SSH: git@github.com:acme/repo.git → github.com/acme/repo
  • SCP-style with user: ssh://git@github.com/acme/repo.git → github.com/acme/repo
  • HTTPS: https://github.com/acme/repo.git → github.com/acme/repo
  • HTTP: http://gitlab.acme.internal/g/r → gitlab.acme.internal/g/r
  • git://: git://github.com/acme/repo.git → github.com/acme/repo

Returns "" (no error) when:

  • the directory is not a git checkout
  • the repo has no `origin` remote (e.g. `git init` only)
  • the remote URL cannot be parsed into a host+path pair

Per spec §5.3 git derivation is a FALLBACK — callers must not overwrite an explicit repo_id set in the manifest. See MergeGenerateAgentsRC.

func DisplayPath

func DisplayPath(path string) string

DisplayPath converts an absolute path to a ~ prefixed display path.

func ExpandPath

func ExpandPath(path string) string

ExpandPath expands a path with ~ to the full absolute path.

func GitSourceCacheDir

func GitSourceCacheDir(url string) string

GitSourceCacheDir returns the cache directory for a given git URL.

func HooksScopeDir

func HooksScopeDir(scope string) string

HooksScopeDir returns the canonical hooks directory for a scope rooted at the resolved AgentsHome(): ~/.agents/hooks/<scope>. scope is either "global" or a managed project name. `da remove --clean`'s canonical-dir cleanup resolves the hooks subtree through this helper instead of independently concatenating "hooks/<scope>".

func HooksScopeDirIn

func HooksScopeDirIn(agentsHome, scope string) string

HooksScopeDirIn returns the canonical hooks directory for a scope under an explicit agents-home root: <agentsHome>/hooks/<scope>. This is the single definition of the canonical hooks-scope path model. Callers that already hold a resolved agents-home (the `da hooks` scope-tree guard, which must honor a test-injected root rather than the process environment) use this form so the path model can never drift between entrypoints.

func MarkProposalReviewed

func MarkProposalReviewed(proposal *Proposal, status, reason string)

func ProjectContextDir

func ProjectContextDir(project string) string

ProjectContextDir returns the local workflow context directory for a project.

func ProposalPath

func ProposalPath(id string) string

func ProposalTargetPath

func ProposalTargetPath(target string) (string, error)

func ProposalsDir

func ProposalsDir() string

func ReadLockedUnits

func ReadLockedUnits(projectPath string) (map[string]LockedUnit, error)

ReadLockedUnits loads the §7A "units" section of a project's .agentsrc.lock (migrating a legacy config/packages lockfile in memory when needed), returning an empty map when the file or section is absent. It is the exported, read-only companion doctor and config explain call to surface resolved unit digests / last-checked state without invoking any fetch or resolve. The map key is the resolved unit ref ("source:path@version").

func SaveProposal

func SaveProposal(proposal *Proposal, path string) error

func SetWindowsMirrorContext

func SetWindowsMirrorContext(repoPath string)

SetWindowsMirrorContext checks if the repo path is under a WSL Windows mount and sets the relevant env vars.

func UserHome

func UserHome() string

UserHome returns the current user's home directory.

func UserHomeDir

func UserHomeDir() (string, error)

UserHomeDir resolves the user's home directory, honoring $HOME when set before falling back to os.UserHomeDir(). This is the convention for CLI tools (git, gh, …): on Windows os.UserHomeDir() reads %USERPROFILE% and ignores $HOME, which (a) breaks per-test isolation that sets HOME to a temp dir — causing cross-test pollution and writes into the real runner profile — and (b) ignores explicit shell/WSL HOME overrides. Honoring $HOME first fixes both with zero behavior change when $HOME is unset (the normal Windows desktop case). Mirrors the os.UserHomeDir signature for drop-in use.

func UserHomeRoots

func UserHomeRoots() []string

UserHomeRoots returns the applicable user home directories. When AGENTS_WINDOWS_MIRROR is set for WSL, includes the Windows home too.

func ValidateProposal

func ValidateProposal(proposal *Proposal) error

func ValidateProposalTarget

func ValidateProposalTarget(target string) error

func WriteConfigLock

func WriteConfigLock(projectPath string, layers map[string]LockedLayer) error

WriteConfigLock writes the resolved config-layer state to .agentsrc.lock via the shared agentslock writer, preserving any sibling sections (packages, adapters) another writer already populated. It also stages an empty packages stub when none exists yet, so a fresh lockfile carries both tier-1 sections (spec §7); a pre-existing packages section written by pass 2 is left intact.

func WriteUnitsLock

func WriteUnitsLock(projectPath string, lock UnitsLock) error

WriteUnitsLock writes the resolved units state and inputs_digest to .agentsrc.lock via the shared agentslock writer, preserving any sibling sections (e.g. "adapters") another writer populated (§7A.3). It is the §7A successor to WriteConfigLock; a later resolver task wires it into the two-pass engine.

Types

type Agent

type Agent struct {
	Enabled bool   `json:"enabled"`
	Version string `json:"version,omitempty"`
}

type AgentsRC

type AgentsRC struct {
	Schema   string           `json:"$schema,omitempty"`
	Version  int              `json:"version"`
	Project  string           `json:"project,omitempty"`
	Skills   []string         `json:"skills,omitempty"`
	Rules    []string         `json:"rules,omitempty"`
	Agents   []string         `json:"agents,omitempty"`
	Hooks    StringsOrBool    `json:"hooks"`
	MCP      StringsOrBool    `json:"mcp"`
	Settings bool             `json:"settings"`
	Sources  []Source         `json:"sources"`
	KG       *AgentsRCKG      `json:"kg,omitempty"`
	Refresh  *RefreshMetadata `json:"refresh,omitempty"`

	// RepoID is the canonical repository identity (e.g. "github.com/acme/manager-ui").
	// Protected: imported layers cannot override it. See org-config-resolution §5.
	RepoID string `json:"repo_id,omitempty"`
	// Extends references config layers in the form "source-id:layer-path[@version]".
	// Each entry may be a plain string or an object form `{"ref": "...", "optional": true}`.
	// Tier constraint (enforced at schema validation): extends entries must reference
	// git|http|local sources — see config-distribution-model §4.
	Extends []LayerRef `json:"extends,omitempty"`
	// Packages references executable OCI/HTTP packages in the form
	// "source-id:artifact-path@version-spec". Tier constraint: oci|http sources only.
	Packages []PackageRef `json:"packages,omitempty"`
	// Features overrides feature-flag defaults (config-distribution-model §3.6).
	Features map[string]string `json:"features,omitempty"`

	// ExtraFields captures unknown JSON keys so Save() can round-trip them
	// instead of silently dropping legacy or custom fields.
	ExtraFields map[string]json.RawMessage `json:"-"`
}

AgentsRC represents the .agentsrc.json manifest committed to a project repo.

Schema versions:

  • version=1 (legacy): only the original field surface (project, sources, …) is meaningful. The v2 additive fields below remain absent/empty on a v1 file.
  • version=2: the v2 additive fields (RepoID, Extends, Packages, Features and the extended Source fields ID/CacheTTL/Auth, plus the http+oci source types) become first-class. All v2 fields use `omitempty` so a v1 manifest round-trips byte-for-byte when these fields are absent.

See specs config-distribution-model §3-§5 + org-config-resolution §15.2.

func GenerateAgentsRC

func GenerateAgentsRC(projectName, projectPath string) (*AgentsRC, error)

GenerateAgentsRC inspects ~/.agents/ and builds a manifest for the given project.

func LoadAgentsRC

func LoadAgentsRC(projectPath string) (*AgentsRC, error)

LoadAgentsRC reads .agentsrc.json from the given project directory.

func MergeGenerateAgentsRC

func MergeGenerateAgentsRC(existing, generated *AgentsRC) *AgentsRC

MergeGenerateAgentsRC overlays a freshly generated manifest onto an existing on-disk manifest. Scan-derived lists (skills, rules, agents, hooks, mcp, settings) come from generated; an existing non-empty project name, unknown JSON keys (ExtraFields), and supplemental sources (e.g. git remotes not produced by GenerateAgentsRC) are preserved. Source entries are unioned with deduplication so the default local source is not duplicated when merging.

func (AgentsRC) MarshalJSON

func (a AgentsRC) MarshalJSON() ([]byte, error)

func (*AgentsRC) Save

func (a *AgentsRC) Save(projectPath string) error

Save writes the manifest to .agentsrc.json in projectPath.

func (*AgentsRC) SetRefreshMetadata

func (a *AgentsRC) SetRefreshMetadata(version, commit, describe string, refreshedAt time.Time)

SetRefreshMetadata stores the latest refresh details in the manifest.

func (*AgentsRC) UnmarshalJSON

func (a *AgentsRC) UnmarshalJSON(data []byte) error

type AgentsRCKG

type AgentsRCKG struct {
	// GraphHome overrides KG_HOME env var for this project. Defaults to ~/.knowledge-graph.
	GraphHome string `json:"graph_home,omitempty"`
	// Backend selects the storage backend: "sqlite" (default) or "postgres".
	// Postgres requires KG_POSTGRES_URL.
	Backend string `json:"backend,omitempty"`
	// Bridge configures workflow/kg bridge query behaviour for this project.
	Bridge AgentsRCKGBridge `json:"bridge"`
}

AgentsRCKG is the knowledge-graph configuration block in agentsrc.json.

type AgentsRCKGBridge

type AgentsRCKGBridge struct {
	Enabled        bool     `json:"enabled"`
	AllowedIntents []string `json:"allowed_intents,omitempty"`
}

AgentsRCKGBridge is the bridge sub-config within the KG section.

type AuditEmitter

type AuditEmitter interface {
	// Emit records one audit event. Implementations must not block resolution.
	Emit(AuditEvent)
}

AuditEmitter receives config-tier audit events. It is the injectable seam the LayeredResolver emits through (WithEmitter). Implementations must be safe for concurrent use: the resolver fetches extends layers in parallel, so Emit may be called from multiple goroutines. The default emitter is noopEmitter.

func NoopEmitter

func NoopEmitter() AuditEmitter

NoopEmitter returns the shared no-op AuditEmitter. Callers that want auditing off use it explicitly; the resolver also falls back to it when none is set.

type AuditEvent

type AuditEvent struct {
	// Timestamp is when the event was produced (UTC).
	Timestamp time.Time `json:"timestamp"`
	// Actor is the emitting component (always auditActor for this package).
	Actor string `json:"actor"`
	// Principal is the human/service identity on whose behalf resolution ran,
	// or "" when not attributed.
	Principal string `json:"principal,omitempty"`
	// Action is one of the config.* action constants.
	Action string `json:"action"`
	// Target is the subject of the action (source id, layer ref, field path, or
	// repo id depending on Action).
	Target string `json:"target"`
	// Outcome is the disposition (success, dropped, failure, skipped).
	Outcome string `json:"outcome"`
	// TraceID correlates every event emitted within a single Resolve call.
	TraceID string `json:"trace_id"`
	// Fields holds the action-specific structured attributes.
	Fields map[string]any `json:"fields,omitempty"`
}

AuditEvent is a single config-tier audit record. It is the Go form of the base event schema shared by every config.* action (spec §9). Fields holds the action-specific structured attributes named in the taxonomy table.

type Checker

type Checker struct {
	// Authorizer is the pluggable policy implementation. When nil, no backend
	// is wired and governed scopes fall back to the safe default.
	Authorizer WriteAuthorizer
}

Checker decides editability. It owns the scope-derivation rules and delegates the governed (team/org) tiers to an optional WriteAuthorizer. A zero Checker (nil Authorizer) is valid and SAFE: governed sources resolve to DecisionPrompt (deny-then-confirm), never a silent allow.

func NewChecker

func NewChecker(authorizer WriteAuthorizer) *Checker

NewChecker returns a Checker bound to authorizer. Passing nil yields the safe default-deny-or-prompt behavior for governed scopes.

func (*Checker) CanWrite

func (c *Checker) CanWrite(p Principal, s WriteTarget) Verdict

CanWrite answers "can principal p write source s?".

Rules (proposal §8 / §9):

  • local → always allow (personal).
  • project with no Owner → derives to local: allow (personal project).
  • project with an Owner → derives to that owner's governance: treated as a governed write (team/org) and delegated to the backend.
  • team/org → governed: delegate to the backend; with no backend wired (or a backend that errors), return the SAFE default DecisionPrompt.

An unknown/empty scope is denied — callers must route through a known scope.

type Config

type Config struct {
	Version  int                `json:"version"`
	Defaults Defaults           `json:"defaults,omitempty"`
	Projects map[string]Project `json:"projects"`
	Agents   map[string]Agent   `json:"agents,omitempty"`
	Features Features           `json:"features,omitempty"`
}

Config represents the ~/.agents/config.json structure.

func Load

func Load() (*Config, error)

Load reads config.json from AgentsHome.

func (*Config) AddProject

func (c *Config) AddProject(name, path string)

AddProject registers a project in the config.

func (*Config) GetProjectPath

func (c *Config) GetProjectPath(name string) string

GetProjectPath returns the path for a registered project, or empty string.

func (*Config) IsPlatformEnabled

func (c *Config) IsPlatformEnabled(platform string) bool

IsPlatformEnabled checks if a platform is enabled. Defaults to true if not set.

func (*Config) ListProjects

func (c *Config) ListProjects() []string

ListProjects returns all registered project names.

func (*Config) RemoveProject

func (c *Config) RemoveProject(name string)

RemoveProject unregisters a project from the config.

func (*Config) Save

func (c *Config) Save() error

Save writes config.json to AgentsHome.

func (*Config) SetPlatformState

func (c *Config) SetPlatformState(platform string, enabled bool, version string)

SetPlatformState updates enabled/version for a platform in config.

type Decision

type Decision string

Decision is the outcome of an editability check.

const (
	// DecisionAllow means the write is permitted.
	DecisionAllow Decision = "allow"
	// DecisionDeny means the write is refused.
	DecisionDeny Decision = "deny"
	// DecisionPrompt means the write requires explicit confirmation before it
	// proceeds — the safe default when a source is governed but no policy
	// backend is wired to decide.
	DecisionPrompt Decision = "prompt"
)

type Defaults

type Defaults struct {
	Agent string `json:"agent,omitempty"`
}

type EditScope

type EditScope string

EditScope is the ownership scope of a source, as it bears on who may write it. It is the editability-relevant projection of the precedence-scope axis in §7A.1; it is deliberately NOT the full precedence ladder (product/user/ runtime never receive CRUD writes through this seam).

const (
	// ScopeLocal is the personal, machine-local asset store. Always writable
	// by its owner — the `.git/config`/project-local-overlay analog.
	ScopeLocal EditScope = "local"
	// ScopeTeam is a team-owned source. Writes are governed by the team's
	// policy backend.
	ScopeTeam EditScope = "team"
	// ScopeOrg is an org-owned source. Writes are governed by the org's
	// policy backend.
	ScopeOrg EditScope = "org"
	// ScopeProject is a project-owned source. Its editability DERIVES: a
	// project owned by a team/org defers to that governance; otherwise it is
	// personal and local-writable.
	ScopeProject EditScope = "project"
)

func (EditScope) Valid

func (s EditScope) Valid() bool

Valid reports whether s is a known editability scope.

type EnsureOpts

type EnsureOpts struct {
	// Locked asserts the lock is fresh: a stale lock yields ErrLockWouldChange
	// and no write (CI gate).
	Locked bool
	// Frozen uses the lock as-is and skips the staleness check entirely.
	Frozen bool
	// NoSync skips the caller's outputs/projection step. It does not affect the
	// lock decision here; it is recorded on the result for the caller.
	NoSync bool
	// Offline resolves from lock/cache only and never contacts the network.
	Offline bool

	// UserLocalPath is the resolver's user-local manifest seam, threaded into the
	// staleness inputs_digest computation so it honors the same override the
	// resolver uses. Empty ⇒ default <AgentsHome>/.agentsrc.json.
	UserLocalPath string
	// UnitDigest is the staleness per-unit digest seam (staleness.go). Nil skips
	// the per-unit digest driver event (inputs_digest + declared-set only).
	UnitDigest UnitDigestFunc
	// Resolver overrides the resolver seam (test injection of a fake). Nil ⇒ a
	// default NewLayeredResolver(). Both Resolve (rewrites the lock) and
	// ResolveLocked (read-only) stay behind this interface so tests never touch
	// the network.
	Resolver EnsureResolverSeam
}

EnsureOpts selects the resolution mode for EnsureResolved. The four bools map 1:1 to the §7A.5 flags (--locked / --frozen / --no-sync / --offline). The remaining fields are the existing test seams threaded through so the seam stays hermetic: no network, no real clock.

type EnsureResolverSeam

type EnsureResolverSeam interface {
	// Resolve rebuilds the layer stack and REWRITES the lock.
	Resolve(projectPath string) (*Snapshot, error)
	// ResolveLocked rebuilds the snapshot from the lock/cache WITHOUT any fetch
	// or write.
	ResolveLocked(projectPath string) (*Snapshot, error)
}

EnsureResolverSeam is the resolution surface EnsureResolved drives: the rewriting Resolve path and the read-only ResolveLocked path. *LayeredResolver satisfies it; tests inject a fake so no resolution touches the network.

type EnsureResult

type EnsureResult struct {
	// Snapshot is the resolved effective config. Always non-nil on a nil error.
	Snapshot *Snapshot
	// ReResolved is true when the default-mode stale path ran Resolve and
	// rewrote the lock. It is false for the Frozen, Offline, fresh, and (error)
	// Locked paths — none of which write.
	ReResolved bool
	// Fresh is true when the staleness check reported no driver event. It is
	// false in Frozen mode (the check is skipped) and on any stale path.
	Fresh bool
	// NoSync echoes EnsureOpts.NoSync so the caller can gate its outputs step
	// without re-deriving it.
	NoSync bool
	// Reasons lists the driver events that made the lock stale (empty when fresh
	// or Frozen). Surfaced so the caller can explain why a re-resolve happened.
	Reasons []StalenessReason
}

EnsureResult is the outcome of EnsureResolved: the effective-config Snapshot plus the metadata the caller needs to decide what to do next (whether the lock was rewritten, whether the lock was already fresh, and the carried-over NoSync flag for the outputs step).

func EnsureResolved

func EnsureResolved(projectPath string, opts EnsureOpts) (*EnsureResult, error)

EnsureResolved is the §7A.5 auto-sync seam. It computes staleness once (unless Frozen) and dispatches to exactly one resolution path. It owns the lock half of §7A.5 only; the caller owns the outputs/projection half (gated by NoSync, echoed on the result).

It is hermetic: every resolution goes through opts.Resolver (default LayeredResolver) and staleness through the injected UnitDigest seam, so no path here contacts the network or a clock directly.

type Features

type Features struct {
	Tasks   bool `json:"tasks,omitempty"`
	History bool `json:"history,omitempty"`
	Sync    bool `json:"sync,omitempty"`
}

type FetchedArtifact

type FetchedArtifact struct {
	// Data is the raw artifact blob.
	Data []byte
	// Digest is the canonical "sha256:<hex>" content digest (spec §7.2).
	Digest string
	// CacheHit reports whether Data came from the local package cache.
	CacheHit bool
	// Posture is the signing posture applied to this pull.
	Posture SigningPosture
}

FetchedArtifact is the result of a tier-2 package pull: the raw artifact bytes, the content digest they were fetched at (the cache key and lockfile digest, spec §7), whether the bytes came from cache, and the signing posture that governed the pull.

type FetchedLayer

type FetchedLayer struct {
	// Data is the raw layer.json bytes.
	Data []byte
	// ResolvedSHA is the git commit SHA (git) or content hash (http/local) the
	// layer was fetched at. It is the cache key and the lockfile resolved_sha.
	ResolvedSHA string
	// CacheHit reports whether Data came from the local cache.
	CacheHit bool
}

FetchedLayer is the result of a successful layer fetch: the raw layer.json bytes, the resolved SHA/content hash they were fetched at, and whether the content came from cache (vs. a fresh network fetch).

type Fetcher

type Fetcher interface {
	// Fetch returns the layer bytes for parts.LayerPath from src. cacheDir is
	// the content-addressed cache root for this source+layer
	// (~/.agents/cache/config/<source-id>/<layer-path>); the fetcher writes its
	// resolved <sha>/layer.json beneath it and returns the resolved SHA.
	Fetch(src Source, parts LayerRefParts, cacheDir string) (FetchedLayer, error)
}

Fetcher fetches a config layer's bytes from a resolved source. One impl per source type (git, http, local); the resolver selects by Source.Type. The interface is the test seam: a fakeFetcher stands in so no test touches the network or a git binary.

func SelectFetcher

func SelectFetcher(sourceType string) (Fetcher, error)

SelectFetcher returns the Fetcher for a source type, or an error for an unsupported or tier-invalid type. oci is valid only for packages (pass 2), never for extends, so it is rejected here as a schema violation.

type FieldProvenance

type FieldProvenance struct {
	// ActiveLayer is the winning layer id, or "" when the field is unset
	// everywhere.
	ActiveLayer string `json:"active_layer"`
	// Layers is the ordered (lowest precedence first) per-layer stack. Always
	// a non-nil slice so JSON marshals to [] not null.
	Layers []LayerValue `json:"layers"`
}

FieldProvenance is the full layer stack for a single field path, ordered by precedence (lowest first). ActiveLayer is the identifier of the winning layer, or "" when no layer set the field.

type FlatResolver

type FlatResolver struct {
	// ProductDefaults is the lowest-precedence layer. When nil, an empty object
	// is used so the layer is always present in the stack (Present=true) even
	// when it carries no fields, which keeps explain output stable.
	ProductDefaults map[string]any
	// contains filtered or unexported fields
}

FlatResolver resolves effective config from the FLAT layer set only: built-in product defaults, the user-local manifest (~/.agents/.agentsrc.json), and the repo-local manifest. It performs no network or git fetch — `extends` entries are recorded on the effective config but not followed (that is config-v2 p1b).

func NewFlatResolver

func NewFlatResolver() *FlatResolver

NewFlatResolver returns a FlatResolver with empty product defaults and the default user-local manifest path.

func (*FlatResolver) Resolve

func (r *FlatResolver) Resolve(projectPath string) (*Snapshot, error)

Resolve implements Resolver for the FLAT layer set.

func (*FlatResolver) WithUserLocalPath

func (r *FlatResolver) WithUserLocalPath(path string) *FlatResolver

WithUserLocalPath sets an explicit user-local manifest path (test seam) and returns the receiver for chaining.

type GitRepo

type GitRepo interface {
	// IsRepo reports whether dir is a git work tree.
	IsRepo(dir string) bool
	// Init initializes a new git repository rooted at dir.
	Init(dir string) error
	// Resolve returns the HEAD commit hash and working-tree dirtiness for the
	// repo at dir. An unborn branch yields ("", dirty, nil).
	Resolve(dir string) (commit string, dirty bool, err error)
}

GitRepo is the seam over the git operations the local source needs. The production implementation is in-process via go-git (no `git` subprocess, no PATH lookup — the repo-wide policy since the config fetcher rewrite); tests inject a fake so bootstrap and ref resolution run with no real repo or network.

IsRepo reports whether dir is a git work tree. Init initializes a repo at dir. Resolve returns the HEAD commit hash plus whether the working tree is dirty; an unborn branch (freshly-init'd, no commits) is reported as an empty commit with no error so the caller can fall back to a deterministic empty-tree ref.

func NewGoGitRepo

func NewGoGitRepo() GitRepo

NewGoGitRepo returns the production GitRepo backed by the in-process go-git library.

type ImportError

type ImportError struct {
	// Ref is the failing extends/packages entry, e.g. "acme:org/base".
	Ref string
	// SourceID is the source the ref was expected from.
	SourceID string
	// Reason is the failure category.
	Reason ImportFailReason
	// Err is the underlying cause, if any.
	Err error
}

ImportError carries the structured failure contract for an extends/packages import (spec §11): which ref failed, which source it came from, and the failure category. It is the Go form of a config.import.failed event.

func (*ImportError) Error

func (e *ImportError) Error() string

func (*ImportError) Unwrap

func (e *ImportError) Unwrap() error

Unwrap exposes the underlying cause for errors.Is/errors.As.

type ImportFailReason

type ImportFailReason string

ImportFailReason classifies a config-layer import failure, mapping 1:1 to the `reason` field of the config.import.failed audit event (spec §9, §11).

const (
	// ReasonTransport: the source could not be reached / the fetch I/O failed.
	ReasonTransport ImportFailReason = "transport"
	// ReasonAuth: authentication or authorization was rejected.
	ReasonAuth ImportFailReason = "auth"
	// ReasonContent: the layer was fetched but its content was unreadable.
	ReasonContent ImportFailReason = "content"
	// ReasonSchema: a tier-constraint or layer-schema violation (surfaces
	// before any network call for tier constraints — spec §4).
	ReasonSchema ImportFailReason = "schema"
	// ReasonNotFound: the referenced source id or layer path does not exist.
	ReasonNotFound ImportFailReason = "not_found"
)

type LayerLockStatus

type LayerLockStatus struct {
	Ref        string `json:"ref"`
	SourceID   string `json:"source_id,omitempty"`
	SourceType string `json:"source_type,omitempty"` // local | git | http | oci
	Optional   bool   `json:"optional,omitempty"`
	// Locked is true when the lockfile has an entry for Ref with a non-empty SHA.
	Locked bool   `json:"locked"`
	SHA    string `json:"sha,omitempty"`
	// Cached is true when the cached layer.json exists at the locked SHA. Applies
	// to every source type — git/http/local layers are all content-hashed and
	// written to the same content-addressed cache.
	Cached    bool   `json:"cached"`
	CachePath string `json:"cache_path,omitempty"`
	// Problem is empty when the layer verifies; otherwise a short, actionable
	// reason (missing lock entry, cache miss, bad ref, undeclared source).
	Problem string `json:"problem,omitempty"`
}

LayerLockStatus is the offline verification result for one declared `extends` layer: whether the lockfile pins a resolved SHA for it and whether the downloaded layer bytes are present in the on-disk cache at that SHA. Every source type (git/http/local) is content-hashed and cached identically, so the same check applies to all. It is what `da config verify` (config-v2 p4c) cross-checks so a layer can be confirmed offline — present and consistent with the lockfile — without ever re-fetching.

func VerifyLayerLocks

func VerifyLayerLocks(projectPath string) ([]LayerLockStatus, error)

VerifyLayerLocks cross-checks every declared `extends` layer in the project's manifest against the lockfile and the on-disk layer cache, WITHOUT any fetch or lockfile mutation. For each layer it reports whether the lockfile pins a SHA and whether the downloaded bytes for that SHA are present in the cache — the same check for git, http, and local sources (all are content-hashed and cached identically).

Returns an empty slice (no error) when the project declares no `extends`, or when the manifest is absent (the caller's manifest check owns that failure).

func (LayerLockStatus) OK

func (s LayerLockStatus) OK() bool

OK reports whether this layer verified cleanly offline.

type LayerRef

type LayerRef struct {
	// Ref is the layer reference string "source-id:layer-path[@version]".
	Ref string `json:"ref"`
	// Optional marks the layer as non-fatal on fetch failure.
	Optional bool `json:"optional,omitempty"`
}

LayerRef is a single entry in AgentsRC.Extends. It accepts either a bare reference string ("acme:org/base") or an object form with an optional flag:

{"ref": "acme:team/experimental", "optional": true}

Per config-distribution-model §11.

func (LayerRef) MarshalJSON

func (l LayerRef) MarshalJSON() ([]byte, error)

MarshalJSON emits the compact string form when Optional is false, otherwise emits the object form. Round-trip is stable under repeated marshal/unmarshal.

func (*LayerRef) UnmarshalJSON

func (l *LayerRef) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts either a plain string or the object form.

type LayerRefParts

type LayerRefParts struct {
	SourceID  string
	LayerPath string
	Version   string
}

LayerRefParts is the parsed form of a "source-id:layer-path[@version]" ref (spec §5). Version is the optional pin; for extends it overrides the source's declared ref (git SHA, tag, or branch).

func ParseLayerRef

func ParseLayerRef(ref string) (LayerRefParts, error)

ParseLayerRef splits "source-id:layer-path[@version]" into its parts (spec §5). The source-id is everything before the first ':'; the version (if present) is everything after the last '@' in the remainder. A missing ':' or an empty source-id / layer-path is a parse error.

type LayerValue

type LayerValue struct {
	// Layer is the layer identifier (LayerProductDefaults, …).
	Layer string `json:"layer"`
	// Value is the JSON-decoded value this layer contributed, or nil if the
	// layer did not set this field.
	Value any `json:"value"`
	// Active marks the winning layer for the field.
	Active bool `json:"active"`
}

LayerValue is one slot in a single field's provenance stack: the value (if any) that a given layer contributed for that field path.

Active is true on exactly one entry per field — the winning (highest precedence) layer that set a value. When no layer sets the field, every entry has Active=false and Value=nil.

type LayeredResolver

type LayeredResolver struct {
	// contains filtered or unexported fields
}

LayeredResolver extends the FLAT layer set with tier-1 `extends` imports (spec §6 pass 1): product defaults → user-local → extends[] (left-to-right, fetched over git/http/local) → repo-local. It resolves each extends ref to a source, enforces the tier constraint (oci in extends is a schema error), fetches + caches the layer content-addressed by SHA, validates it against the layer schema, and records the resolved SHAs to .agentsrc.lock.

func NewLayeredResolver

func NewLayeredResolver() *LayeredResolver

NewLayeredResolver returns a LayeredResolver wrapping a default FlatResolver.

func (*LayeredResolver) Resolve

func (r *LayeredResolver) Resolve(projectPath string) (*Snapshot, error)

Resolve implements Resolver. It builds the full layer stack (FLAT + imported extends), merges it into a Snapshot, and writes the resolved-layer SHAs to .agentsrc.lock. Layer fetch/validation errors surface as *ImportError for non-optional entries; optional entries that fail are skipped with a warning.

func (*LayeredResolver) ResolveLocked

func (r *LayeredResolver) ResolveLocked(projectPath string) (*Snapshot, error)

ResolveLocked produces an effective-config Snapshot WITHOUT any network or git fetch and WITHOUT mutating .agentsrc.lock or the layer cache. It is the read-only seam `da config explain` (config-v2 p4e) parses the locked state through, so explain can be inspected offline and as a pure observer.

Resolution model (mirrors Resolve, minus all writes/fetches):

  • The repo-local manifest is read. If it declares no `extends`, resolution degrades to the FLAT layer set (product-defaults → user-local → repo-local) via the embedded FlatResolver, so explain still works on a flat project.
  • Otherwise the imported `extends` layers are reconstructed by reading each layer's bytes from the on-disk cache at its LOCKED SHA (from .agentsrc.lock). No fetcher is ever invoked: a layer that is absent from the lockfile, or whose cached bytes are missing, is a hard error (explain surfaces it) rather than a fetch trigger.
  • The assembled stack (product-defaults → user-local → imported → repo-local) is merged through resolveSnapshot (the §7.2 merge shared with Resolve).

ResolveLocked NEVER calls WriteConfigLock and NEVER calls a Fetcher.

func (*LayeredResolver) WithClock

func (r *LayeredResolver) WithClock(now func() time.Time) *LayeredResolver

WithClock sets the TTL clock seam and returns the receiver.

func (*LayeredResolver) WithEmitter

func (r *LayeredResolver) WithEmitter(e AuditEmitter) *LayeredResolver

WithEmitter registers the AuditEmitter that receives config.* events emitted during resolution (spec §9). A nil emitter disables auditing. Returns the receiver for chaining.

func (*LayeredResolver) WithFetcher

func (r *LayeredResolver) WithFetcher(sourceType string, f Fetcher) *LayeredResolver

WithFetcher registers a Fetcher for a source type (test seam: inject a fakeFetcher for "git" so no test touches the network). Returns the receiver.

func (*LayeredResolver) WithOffline

func (r *LayeredResolver) WithOffline(offline bool) *LayeredResolver

WithOffline toggles offline mode (use last resolved SHA from the lockfile).

func (*LayeredResolver) WithProductDefaults

func (r *LayeredResolver) WithProductDefaults(d map[string]any) *LayeredResolver

WithProductDefaults sets the product-defaults layer and returns the receiver.

func (*LayeredResolver) WithUserLocalPath

func (r *LayeredResolver) WithUserLocalPath(path string) *LayeredResolver

WithUserLocalPath sets the user-local manifest path (test seam) and returns the receiver.

type LocalSource

type LocalSource struct {
	// Root is the absolute path of the local source repo (`~/.agents`).
	Root string
	// Git is the seam over git operations; never nil after NewLocalSource.
	Git GitRepo
}

LocalSource is the git-backed `local` source rooted at Root (the `~/.agents` repo). All git access goes through Git so the type is hermetic in tests.

func NewLocalSource

func NewLocalSource(root string, repo GitRepo) *LocalSource

NewLocalSource builds a LocalSource for the repo rooted at root. A nil repo defaults to the production in-process go-git implementation, so callers that do not need a fake can pass nil.

func (*LocalSource) EnsureBootstrapped

func (s *LocalSource) EnsureBootstrapped() (initialized bool, err error)

EnsureBootstrapped makes the local source resolvable on first resolve / init (§7A.1): if Root is not yet a git work tree, it is git-initialized so any local-authored unit can version by the repo's ref. It is idempotent — an already-initialized repo is left untouched — and reports whether it performed an init, so a caller can log first-time setup.

func (*LocalSource) EnsureProvenanceGitignore

func (s *LocalSource) EnsureProvenanceGitignore(remotePaths []string) error

EnsureProvenanceGitignore writes the idempotent da-owned block into the local source's `.gitignore` so the given remote-materialized asset paths are excluded from the local repo's git tracking (§7A.5: "one asset dir, provenance-gitignored"). After this, `da sync` never commits fetched assets, and provenance is readable from git state alone: a tracked unit is local-authored, a gitignored one is remote-materialized.

It preserves any user-authored content outside the managed markers, sorts and de-duplicates the managed paths for a stable diff, and is a no-op-stable rewrite (calling it twice with the same inputs yields the same bytes). An empty path set removes the managed block entirely.

func (*LocalSource) LockKey

func (s *LocalSource) LockKey(unitPath string) (string, error)

LockKey returns the uniform unit ref / lock key for a unit at unitPath within the local source: "local:<rel-path>@<resolved-ref>" (§7A.3). unitPath may be absolute (it is made relative to Root) or already repo-relative. The path is always slash-normalized so the key is stable across OSes. The local source must be bootstrapped first.

func (*LocalSource) ResolvedRef

func (s *LocalSource) ResolvedRef() (string, error)

ResolvedRef returns the local source's resolved version: the current commit, suffixed with dirtySuffix when the working tree has uncommitted changes (§7A.4). A repo with no commits yet resolves to emptyTreeRef (still deterministic), optionally dirty when the tree already has staged/working content. The repo must be bootstrapped first; a non-repo Root is an error.

type LockDriftResult

type LockDriftResult struct {
	// LockPresent is true when a .agentsrc.lock file exists for the project.
	LockPresent bool
	// HasDeclaredUnits is true when the manifest declares at least one `extends`
	// or `packages` unit (lock drift is only applicable to such manifests).
	HasDeclaredUnits bool
	// Units holds one record per ref in the union of declared and locked units,
	// sorted by Ref. Records with LockStatusOK are included so callers can render
	// a healthy summary count.
	Units []LockUnitDrift
}

LockDriftResult is the renderable outcome of comparing a project's .agentsrc.lock against its declared `extends`/`packages` units. Callers branch on a few booleans and iterate a single sorted slice:

  • LockPresent false → no lockfile at all (only meaningful when units are declared; a project with no extends/packages and no lock is simply local).
  • HasDeclaredUnits false → the manifest declares no units, so lock drift is not applicable.
  • Units → per-unit drift records, sorted by Ref, covering every declared and every locked ref (the union).

IsClean reports the common "nothing to surface" case.

func LockDrift

func LockDrift(projectPath string) (LockDriftResult, error)

LockDrift compares a project's committed .agentsrc.lock against the `extends`/`packages` units declared in its .agentsrc.json and reports the per-unit *driver-event* drift. It is strictly read-only: it never writes or repairs the lockfile, and it never consults a clock (§7A.3 moves staleness off the TTL axis entirely).

Drift dimensions reported:

  • a declared unit absent from the lock (missing-from-lock),
  • a locked unit no longer declared (extra-in-lock).

A manifest with no declared units yields HasDeclaredUnits=false and an empty Units slice. A missing manifest surfaces as the LoadAgentsRC error; a missing lockfile is not an error (LockPresent=false), since the absence is itself the drift to report against declared units.

func (LockDriftResult) IsClean

func (r LockDriftResult) IsClean() bool

IsClean reports whether the result has no drift to surface: either the manifest declares no units, or every declared unit is locked and the lock carries no extra entries.

func (LockDriftResult) Problems

func (r LockDriftResult) Problems() []LockUnitDrift

Problems returns the subset of Units whose status is not OK, preserving the sorted order. Convenience for doctor / config explain drift-only rendering.

type LockDriftStatus

type LockDriftStatus string

LockDriftStatus classifies a single declared/locked unit's drift state.

const (
	// LockStatusOK means the declared unit has a matching lock entry. (Content
	// staleness is a separate, clock-free axis — see staleness.go — not a drift
	// status, because it does not mean the lock is structurally wrong.)
	LockStatusOK LockDriftStatus = "ok"
	// LockStatusMissingFromLock means the unit is declared in .agentsrc.json
	// (`extends` or `packages`) but has no entry in the lockfile — the project
	// was never resolved, or the lock predates the declaration (run
	// `da install` / `da config sync`).
	LockStatusMissingFromLock LockDriftStatus = "missing-from-lock"
	// LockStatusExtraInLock means the lockfile carries an entry for a unit that
	// is no longer declared — a stale leftover after the declaration was removed
	// from the manifest.
	LockStatusExtraInLock LockDriftStatus = "extra-in-lock"
)

type LockUnitDrift

type LockUnitDrift struct {
	// Ref is the declared unit reference ("source-id:path[@version]").
	Ref string
	// Status classifies this unit's drift.
	Status LockDriftStatus
	// Digest is the locked content digest, empty for missing-from-lock entries.
	Digest string
	// Kind is the locked unit kind (layer|artifact), empty for
	// missing-from-lock entries.
	Kind string
}

LockUnitDrift is one unit's drift record: its declared ref, the classification, and (when present) the locked digest / kind so doctor and config explain can render a one-line diagnostic without re-reading the lock.

type LockedLayer

type LockedLayer struct {
	// ResolvedSHA is the git commit SHA or content hash at fetch time.
	ResolvedSHA string `json:"resolved_sha"`
	// FetchedAt is the RFC3339 timestamp the layer was fetched.
	FetchedAt string `json:"fetched_at"`
	// TTLExpiresAt is when the SHA should be re-checked, derived from the
	// source cache_ttl. Empty means never re-check automatically (requires an
	// explicit `da config sync`).
	TTLExpiresAt string `json:"ttl_expires_at,omitempty"`
}

LockedLayer is one entry in the lockfile's "config" section: the resolved SHA a config layer was fetched at, plus its cache TTL window (spec §7). The map key is the layer ref ("acme:org/base").

type LockedUnit

type LockedUnit struct {
	// Kind is UnitKindLayer or UnitKindArtifact.
	Kind string `json:"kind"`
	// Digest is the content hash recorded at fetch time ("sha256:…").
	Digest string `json:"digest"`
	// FetchedAt is the RFC3339 timestamp the unit was fetched.
	FetchedAt string `json:"fetched_at"`
	// LastCheckedAt is the RFC3339 timestamp of the last explicit upstream
	// re-check; it powers a doctor/explain review-nudge and never drives
	// auto-invalidation (§7A.3). Empty when never re-checked since fetch.
	LastCheckedAt string `json:"last_checked_at,omitempty"`
}

LockedUnit is one entry in the lockfile's "units" section (§7A.3). The map key is the fully-resolved ref "source:path@resolved-version"; the entry records the kind plus content-hash and timestamps. Staleness is content-hash driven (digest mismatch), never clock-driven — LastCheckedAt is a review-nudge basis only and never auto-invalidates.

type MergeCategory

type MergeCategory int

MergeCategory classifies how a field combines across layers, per org-config-resolution §7.2. The default for any field not explicitly categorized is CategoryScalar (last writer wins).

const (
	// CategoryScalar: last writer in precedence order wins (the whole value is
	// replaced). Applies to scalars and, by default, to any uncategorized field.
	CategoryScalar MergeCategory = iota
	// CategorySetUnion: arrays representing sets — union with stable order,
	// dedup by value. Applies to skills, agents, rules.
	CategorySetUnion
	// CategoryMapMerge: object maps — merge by key, recursing into nested maps;
	// per-key value uses CategoryScalar semantics. Applies to verifier_profiles,
	// features, kg.
	CategoryMapMerge
	// CategoryOrderedReplace: arrays representing ordered execution — replaced
	// wholesale by the highest-precedence writer (never merged). Applies to
	// sources and to each app_type_verifier_map entry's sequence.
	CategoryOrderedReplace
)

type PackageFetcher

type PackageFetcher interface {
	// FetchArtifact returns the artifact blob for parts.ArtifactPath@VersionSpec
	// from src, content-addressed and cached under the packages cache root.
	FetchArtifact(src Source, parts PackageRefParts) (FetchedArtifact, error)
}

PackageFetcher pulls a tier-2 package artifact from a resolved source. One impl per packages-valid source type (oci, http). The interface is the test seam: a fake stands in so no test touches a real registry or the network.

func SelectPackageFetcher

func SelectPackageFetcher(sourceType string) (PackageFetcher, error)

SelectPackageFetcher returns the PackageFetcher for a source type, or an error for a source type that is not valid for packages. This is the pass-2 (p6) counterpart to SelectFetcher: packages accept oci and http; git and local are rejected as a tier/schema violation (spec §4).

type PackageRef

type PackageRef struct {
	// Ref is the package reference string "source-id:artifact-path@version-spec".
	Ref string `json:"ref"`
}

PackageRef is a single entry in AgentsRC.Packages. The string form is the canonical wire form per config-distribution-model §5; the object form is accepted for forward compatibility with future per-entry options.

func (PackageRef) MarshalJSON

func (p PackageRef) MarshalJSON() ([]byte, error)

func (*PackageRef) UnmarshalJSON

func (p *PackageRef) UnmarshalJSON(data []byte) error

type PackageRefParts

type PackageRefParts struct {
	SourceID     string
	ArtifactPath string
	VersionSpec  string
}

PackageRefParts is the parsed form of a "source-id:artifact-path@version-spec" packages ref (config-distribution-model §5). Unlike extends refs, the version spec is required for packages.

func ParsePackageRef

func ParsePackageRef(ref string) (PackageRefParts, error)

ParsePackageRef splits "source-id:artifact-path@version-spec" into its parts. The source-id is everything before the first ':'; the version spec (required) is everything after the last '@'. A missing ':' / '@', or an empty component, is a parse error (spec §5: @version-spec is required for packages).

type Principal

type Principal struct {
	// ID is the stable identifier of the actor (e.g. a user handle).
	ID string
	// Groups are the team/org memberships the principal holds. A governed
	// source whose Owner is in Groups is one the principal may be authorized
	// to write — the policy backend has the final say.
	Groups []string
}

Principal is the actor requesting a write. It is intentionally minimal: a stable id plus the groups/teams it belongs to, which is all a policy backend needs to answer ownership/membership questions. Backends may carry richer identity out of band; this seam does not model auth.

type Project

type Project struct {
	Path  string    `json:"path"`
	Added time.Time `json:"added"`
}

type Proposal

type Proposal struct {
	SchemaVersion int    `yaml:"schema_version"`
	ID            string `yaml:"id"`
	Status        string `yaml:"status"`
	Type          string `yaml:"type"`
	Action        string `yaml:"action"`
	Target        string `yaml:"target"`
	Rationale     string `yaml:"rationale"`
	Content       string `yaml:"content"`
	CreatedAt     string `yaml:"created_at"`
	CreatedBy     string `yaml:"created_by"`
	ReviewedAt    string `yaml:"reviewed_at"`
	ReviewReason  string `yaml:"review_reason"`
}

func ListPendingProposals

func ListPendingProposals() ([]Proposal, error)

func LoadProposal

func LoadProposal(id string) (*Proposal, error)

type ProvenanceWarning

type ProvenanceWarning struct {
	// FieldPath is the dot-separated path of the offending field.
	FieldPath string `json:"field_path"`
	// AttemptedByLayer is the layer that tried to set a protected field.
	AttemptedByLayer string `json:"attempted_by_layer"`
	// Outcome is the disposition of the attempt; always "dropped" today.
	Outcome string `json:"outcome"`
}

ProvenanceWarning is a non-fatal event emitted during resolution — currently only protected-field override attempts (org-config-resolution §7.4). It maps to the config.field.protection_violation audit event landed in config-v2 p3.

type RefreshMetadata

type RefreshMetadata struct {
	Version     string `json:"version,omitempty"`
	Commit      string `json:"commit,omitempty"`
	Describe    string `json:"describe,omitempty"`
	RefreshedAt string `json:"refreshedAt,omitempty"`
}

RefreshMetadata records the latest da install/refresh that updated a project.

type ResolvedLayer

type ResolvedLayer struct {
	// ID is the layer identifier (LayerProductDefaults, …).
	ID string `json:"id"`
	// Present reports whether the layer existed (file on disk / built-in stub).
	Present bool `json:"present"`
	// Raw is the layer's decoded top-level object, or nil when absent.
	Raw map[string]any `json:"raw,omitempty"`
}

ResolvedLayer is one input layer that participated in resolution, in precedence order. Raw holds the layer's decoded top-level JSON object (nil when the layer was absent, e.g. no user-local file), so explain surfaces can distinguish "layer present but empty" from "layer absent".

type Resolver

type Resolver interface {
	// Resolve produces the effective Snapshot for the project at projectPath.
	// A fatal error (e.g. repo-local manifest fails to parse) is returned;
	// non-fatal events (protected-field violations) surface in Snapshot.Warnings.
	Resolve(projectPath string) (*Snapshot, error)
}

Resolver produces an effective-config Snapshot from a set of layers. The FLAT implementation (FlatResolver) walks only the local layers (product defaults, user-local, repo-local). The layered implementation (config-v2 p1b) extends the same interface to fetch declared `extends` layers over git/http/local before the repo-local layer. The interface is the seam both share.

type ReviewNudge

type ReviewNudge struct {
	// Ref is the resolved unit ref ("source:path@version").
	Ref string
	// LastCheckedAt is the RFC3339 timestamp of the last upstream re-check
	// (falls back to fetched_at when never re-checked since fetch). Empty when
	// the unit records neither.
	LastCheckedAt string
	// SinceLastCheck is how long ago the last re-check was, relative to the
	// injected `now`. Zero when LastCheckedAt is empty or unparseable.
	SinceLastCheck time.Duration
}

ReviewNudge is the demoted-TTL advisory for one unit (§7A.3): how long since its last upstream re-check. It NEVER invalidates the lock — it only drives a "last re-checked N ago — da config sync" reminder in doctor / config explain.

func ReviewNudges

func ReviewNudges(projectPath string, now time.Time) ([]ReviewNudge, error)

ReviewNudges returns the per-unit review-nudge advisories for a project's locked units, sorted by ref. Each reports how long since the unit was last re-checked upstream (last_checked_at, or fetched_at as the basis when never re-checked). This is the demoted-TTL surface: it is advisory only and never affects staleness or the lock. Units with no timestamp basis are still returned (with a zero duration) so the surface lists every locked unit.

The reference instant `now` is injected by the caller (production passes time.Now(); tests pass a fixed instant) — the per-TEST_SEAMS.md DI shape that parallels Staleness's injected UnitDigestFunc, not a package-level clock var.

type SigningPosture

type SigningPosture string

SigningPosture is the declared verification stance for a fetched package artifact (config-distribution-model §12 scope boundary; signing brought in earlier per spec Q3). It is a stub in p5: the posture is recorded and the verify hook is wired, but real signature material (cosign/sigstore, spec external-agent-sources §6 roadmap) is not yet checked. The posture governs whether an unverifiable artifact is allowed to resolve.

const (
	// PostureUnsigned: signatures are not expected; the artifact resolves on a
	// successful digest match alone. Default for the p5 stub.
	PostureUnsigned SigningPosture = "unsigned"
	// PostureOptional: a signature is verified when present but its absence is
	// not fatal (warn-and-continue once real verification lands).
	PostureOptional SigningPosture = "optional"
	// PostureRequired: a verified signature is mandatory; an unsigned or
	// unverifiable artifact must fail to resolve.
	PostureRequired SigningPosture = "required"
)

func PostureFromSource

func PostureFromSource(src Source) SigningPosture

PostureFromSource derives the signing posture for a source. The posture is read from the opaque, pass-through auth block (the only source field whose schema is not owned by the config layer — external-agent-sources spec), under the well-known "signing" key, so no new typed Source field (and thus no agentsrc.go / struct-schema change) is required for the p5 stub. An absent, empty, or unrecognized value defaults to PostureUnsigned.

func (SigningPosture) Valid

func (p SigningPosture) Valid() bool

Valid reports whether p is a recognized posture.

type Snapshot

type Snapshot struct {
	// Effective is the merged manifest after applying every layer per the
	// category merge rules (org-config-resolution §7.2).
	Effective AgentsRC `json:"effective"`
	// Provenance maps a dot-separated field path to its full layer stack. Only
	// top-level manifest keys are pre-populated; explain surfaces may request
	// deeper paths via FieldAt.
	Provenance map[string]FieldProvenance `json:"provenance"`
	// Layers is the ordered (lowest precedence first) set of input layers.
	Layers []ResolvedLayer `json:"layers"`
	// Warnings holds non-fatal resolution events (protected-field violations).
	// Always non-nil ([]ProvenanceWarning{}) so JSON marshals to [].
	Warnings []ProvenanceWarning `json:"warnings"`
}

Snapshot is the resolved effective-config view produced by a Resolver. It is the canonical surface consumed by `da config explain`, `workflow app-types`, bundle materialization, and config validation (config-distribution-model §10).

func (*Snapshot) EffectiveRaw

func (s *Snapshot) EffectiveRaw() (map[string]any, error)

EffectiveRaw returns the effective config as a decoded top-level JSON object. This is the shape explain surfaces walk for arbitrary dot-paths; it round-trips through the AgentsRC marshaler so ExtraFields (verifier_profiles, app_type_verifier_map, …) are included.

func (*Snapshot) FieldAt

func (s *Snapshot) FieldAt(path string) FieldProvenance

FieldAt returns the FieldProvenance for an arbitrary dot-separated path (e.g. "kg.backend", "app_type_verifier_map.go-cli"), computing it on demand by walking each layer's raw object. Top-level paths are also available directly in s.Provenance; FieldAt is the general accessor that also handles nested keys.

func (*Snapshot) FieldNames

func (s *Snapshot) FieldNames() []string

FieldNames returns the sorted union of top-level field names that any layer sets — the keys explain's --all view iterates.

type Source

type Source struct {
	Type string `json:"type"`           // "local" | "git" | "http" | "oci"
	Path string `json:"path,omitempty"` // override path for "local"
	URL  string `json:"url,omitempty"`  // repository URL for "git" / "http" / "oci"
	Ref  string `json:"ref,omitempty"`  // branch/tag for "git", or OCI tag

	// ID is the stable local identifier used in extends/packages refs.
	// Required for v2 sources referenced by extends or packages; optional for
	// bare v1-style sources that exist only for legacy compatibility.
	ID string `json:"id,omitempty"`
	// CacheTTL is a duration string (e.g. "4h") governing tier-1 layer TTL.
	// Ignored for oci sources (which are strictly content-addressed per spec §8).
	CacheTTL string `json:"cache_ttl,omitempty"`
	// Auth is an opaque pass-through block whose schema is owned by the
	// external-agent-sources spec. The config layer treats it as an arbitrary
	// JSON object and does not introspect it.
	Auth json.RawMessage `json:"auth,omitempty"`
}

Source describes where to find agent resources. The v1 surface accepts `local` and `git` types; v2 adds `http` and `oci` per config-distribution-model §4. The v2 additive fields (ID, CacheTTL, Auth) all use omitempty so a v1 Source round-trips byte-for-byte when those fields are absent.

type StalenessReason

type StalenessReason string

StalenessReason classifies why a lock is considered stale. A fresh lock has no reasons; a stale lock carries one or more.

const (
	// ReasonInputsDigest means the whole-normalized hash of the local config
	// scopes no longer matches the lock's recorded inputs_digest (§7A.3).
	ReasonInputsDigest StalenessReason = "inputs-digest-mismatch"
	// ReasonDeclaredSet means the declared `extends`/`packages` unit set changed
	// since the lock was written (a ref was added or removed).
	ReasonDeclaredSet StalenessReason = "declared-set-changed"
	// ReasonUnitDigest means a recorded unit's digest no longer matches the
	// digest recomputed for its current content.
	ReasonUnitDigest StalenessReason = "unit-digest-mismatch"
)

type StalenessResult

type StalenessResult struct {
	// Fresh is true when no driver event fired — the lock matches every local
	// input and may be used as-is (the `--frozen`/no-op path of EnsureResolved).
	Fresh bool
	// Reasons lists the distinct driver events that made the lock stale, in a
	// stable order. Empty when Fresh.
	Reasons []StalenessReason
	// ExpectedInputsDigest is the inputs_digest computed from the current local
	// scopes; doctor/explain surface it next to the lock's recorded value.
	ExpectedInputsDigest string
}

StalenessResult is the renderable outcome of a clock-free staleness check. Fresh reports the common "nothing changed" case; Reasons enumerates every driver event that fired so doctor/explain can show precisely what drifted.

func Staleness

func Staleness(projectPath, userLocalPath string, recompute UnitDigestFunc) (StalenessResult, error)

Staleness performs the full clock-free staleness check (§7A.3) for a project against its committed lock. It compares the recorded inputs_digest, the declared `extends`/`packages` set, and (when recompute is non-nil) each recorded unit's digest, returning every driver event that fired.

It is strictly read-only and never touches the network or a clock. recompute may be nil to skip the per-unit digest check (inputs_digest + declared-set only); userLocalPath threads the resolver's user-local seam into the digest computation.

func (StalenessResult) IsStale

func (r StalenessResult) IsStale() bool

IsStale reports whether any driver event fired (the inverse of Fresh).

type StringsOrBool

type StringsOrBool struct {
	All   bool
	Names []string
}

StringsOrBool holds either a boolean flag (all/none) or a named list. It marshals/unmarshals as either a JSON bool or a JSON string array:

true             → All resources of this type
false            → No resources
["name1","name2"] → Only the named resources

func (*StringsOrBool) Add

func (s *StringsOrBool) Add(name string)

Add appends name to Names unless All is true (already covers everything).

func (StringsOrBool) Contains

func (s StringsOrBool) Contains(name string) bool

Contains returns true if name is covered (either All=true or name is in Names).

func (StringsOrBool) IsEnabled

func (s StringsOrBool) IsEnabled() bool

IsEnabled returns true if any resources are enabled (All or at least one name).

func (StringsOrBool) MarshalJSON

func (s StringsOrBool) MarshalJSON() ([]byte, error)

func (*StringsOrBool) Remove

func (s *StringsOrBool) Remove(name string)

Remove removes name from Names. No-op if All is true.

func (*StringsOrBool) UnmarshalJSON

func (s *StringsOrBool) UnmarshalJSON(data []byte) error

type UnitDigestFunc

type UnitDigestFunc func(ref string) (digest string, ok bool)

UnitDigestFunc recomputes the current content digest for a resolved unit ref ("source:path@version"), reporting whether the unit's content is locally available to hash. It is the seam through which staleness checks the third driver event (recorded digest no longer matches) without this file reaching into the resolver, the cache, or the network: callers that have a cheap local digest source (e.g. the local source's working-tree hash) supply one; callers that only want the inputs_digest + declared-set checks pass nil.

type UnitsLock

type UnitsLock struct {
	// Units is keyed by "source:path@resolved-version".
	Units map[string]LockedUnit
	// InputsDigest is the top-level whole-normalized local-scope hash. Empty
	// when no local scope has been hashed yet.
	InputsDigest string
}

UnitsLock is the config-owned view of the lockfile under the §7A model: the resolved units map plus the top-level inputs_digest (the whole-normalized hash of all local config scopes). Staleness is an inputs_digest mismatch OR a changed declared set OR a per-unit digest mismatch — all cheap, local, and clock-free.

func ReadUnits

func ReadUnits(projectPath string) (UnitsLock, error)

ReadUnits loads the §7A units view of an existing lockfile. When the file already carries a "units" section it is read directly; when it does not but a legacy "config"/"packages" pair is present, the legacy shape is migrated in memory (v1 → v2) so callers always see the unified model. A wholly absent or empty lockfile yields an empty (non-nil) units map (§7A.3).

type Verdict

type Verdict struct {
	Decision Decision
	// Reason explains the decision in operator-facing terms.
	Reason string
	// Scope is the editability scope that produced the decision. For a derived
	// project source this is the scope it derived TO (ScopeLocal for a personal
	// project, the governing tier for an owned one).
	Scope EditScope
}

Verdict is the full result of an editability check: a Decision plus a human-readable Reason and the Scope that drove it, so callers (and `config explain`/`doctor`) can render WHY a write was allowed, denied, or gated.

func (Verdict) Allowed

func (v Verdict) Allowed() bool

Allowed reports whether the verdict permits the write outright (Decision is DecisionAllow). A DecisionPrompt is NOT allowed without confirmation.

type WriteAuthorizer

type WriteAuthorizer interface {
	// Authorize returns the verdict for a governed source. Implementations
	// should return DecisionAllow/DecisionDeny per their policy; returning a
	// non-nil error signals the backend could not reach a decision (e.g. the
	// policy service was unreachable), which the Checker treats as a safe
	// fail-closed prompt rather than an allow.
	Authorize(p Principal, s WriteTarget) (Verdict, error)
}

WriteAuthorizer is the policy-backend-AGNOSTIC seam org/team governance plugs into (the adapter-contract pattern). A backend answers, for a governed (team/org) source, whether the principal may write it. The default Checker calls a backend only for governed scopes; local and personal-project writes never reach a backend.

Backends are intentionally NOT registered here — registration/selection is a downstream concern. This file ships only the contract and a nil-safe default.

type WriteTarget

type WriteTarget struct {
	// ID is the stable local source identifier (the `id` in the `sources`
	// array, §3) used by `--source`.
	ID string
	// Scope is the source's editability scope (ownership tier).
	Scope EditScope
	// Owner names the team/org that owns a governed (team/org) source, or the
	// owner of a project source when project ownership is a team/org. Empty
	// Owner on a project scope means a personal/unowned project, which derives
	// to local-writable.
	Owner string
}

WriteTarget identifies the destination of a write through the editability seam. It is distinct from the wire-format Source (agentsrc.go): a Source declares transport/versioning; a WriteTarget carries the ownership SCOPE and owner that govern who may write it.

Jump to

Keyboard shortcuts

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