Documentation
¶
Overview ¶
Package source loads and represents the canonical agentsync repo layout (~/.agentsync/). Structs in this file are TOML-tagged and serve as the canonical model — there is no separate IR layer; adapters consume these types directly.
Package-level write-back helpers for the canonical source (~/.agentsync/). These are called by the reconcile command when the user selects [w]rite-back, and by the import command to capture native edits.
SECRET-SAFETY INVARIANT (read before adding a caller): these Write* helpers take only the TEMPLATED source types (source.Canonical / its sub-structs) and perform NO secret re-referencing. apply substitutes ${secret:…} to cleartext into destinations, so any canonical reconstructed from a destination holds live credentials. The ONLY sanctioned dest->source write path is capture.Capture, which calls secrets.ReReferenceCanonical first. Do NOT pass these helpers a value obtained by unwrapping secrets.Resolved.Canonical() — it is the resolved (cleartext) apply model and would leak the secret into source. This API is intentionally not lint-fenced (it is the legitimate templated writer Capture is built on), so this discipline is the guard; see CLAUDE.md "Secret-handling invariants" and internal/secrets/resolved.go.
v1 trade-off: TOML comments in the original file are not preserved on write-back. Comment-preserving mutation is deferred to v1.x.
Index ¶
- func ExpandMemoryImports(body string, fragments map[string]string) string
- func MemoryHasFragments(home string) bool
- func ParseFrontmatter(data []byte) (map[string]any, string, error)
- func ParseFrontmatterWithReport(data []byte) (fm map[string]any, body string, lenient bool, err error)
- func ValidateComponentID(kind, id string) error
- func WriteCommand(home string, cm Command) error
- func WriteHooks(home, event string, hooks []Hook) error
- func WriteLSP(home string, ls LSPServer) error
- func WriteMCP(home, id string, m MCPServer) error
- func WriteMarketplace(home, name string, m Marketplace) error
- func WriteMemory(home string, m Memory) error
- func WritePlugin(home, id string, p Plugin) error
- func WriteSkill(home string, sk Skill) error
- func WriteSubagent(home string, sa Subagent) error
- type Agent
- type Canonical
- type Command
- type Config
- type Hook
- type LSPServer
- type LSPServerSpec
- type MCPServer
- type MCPServerSpec
- type Marketplace
- type MarketplaceSpec
- type Memory
- type Plugin
- type PluginSpec
- type SecretsConfig
- type Skill
- type SkillFile
- type Subagent
- type UpdateDefaults
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExpandMemoryImports ¶
ExpandMemoryImports replaces `@import ./fragments/<name>` directives in body with the named fragment's content, RECURSIVELY — a fragment may itself `@import` another. A single non-recursive pass (the previous behavior) left nested directives as literal `@import` lines in the rendered CLAUDE.md / AGENTS.md. Cycles are broken (a fragment already being expanded is left as a literal directive), recursion is depth-bounded, and unknown fragments are left as literal directives so the user notices.
Each inlined fragment is wrapped in boundary markers (see fragmentMarkerToken) so CollapseMemoryMarkers can reverse the expansion. The one exception is a MARKER COLLISION: if the body or any fragment already contains the marker token, markers are omitted entirely (plain expansion) so they can't corrupt the reverse parse — the write-back guard then keeps that memory apply-only.
func MemoryHasFragments ¶
MemoryHasFragments reports whether the canonical memory at home is composed of fragment files (memory/fragments/*). Write-back of memory (import / reconcile) is UNSAFE when it is: the destination CLAUDE.md/AGENTS.md is the fully EXPANDED memory, so persisting it back into memory/AGENTS.md would inline every `@import` and strand the fragment files. Ingest cannot de-resolve the expansion (which inlined text came from which fragment is unrecoverable), so the only safe action is to refuse/skip the write-back — callers consult this.
func ParseFrontmatter ¶
ParseFrontmatter extracts YAML frontmatter and body from a markdown file. If the input doesn't begin with "---\n", returns an empty map and the entire input as body. Returns an error on any YAML decode failure; callers that want to accept the kind of relaxed "key: value-with-colons" frontmatter that Claude Code itself reads should use ParseFrontmatterWithReport instead.
func ParseFrontmatterWithReport ¶
func ParseFrontmatterWithReport(data []byte) (fm map[string]any, body string, lenient bool, err error)
ParseFrontmatterWithReport is the lenient-fallback variant: it first tries strict YAML; on a YAML decode failure it falls back to a line-oriented "key: rest-of-line" parser that accepts bare colons inside values. lenient is true iff the strict parse failed but the lenient one succeeded — callers (typically Ingest paths) can then surface a warning so the user knows their SKILL.md is not strict YAML.
The lenient parser exists because Claude Code itself reads frontmatter that way: a SKILL.md whose description contains a bare "Triggers on: X, Y" parses fine in claude.ai but breaks sigs.k8s.io/yaml. Without this fallback, `import` silently dropped any skill with such a description (the call site swallowed the parse error with `continue`).
func ValidateComponentID ¶
ValidateComponentID rejects a component id/name/event that would not produce a clean file path under its source subdirectory. This is the single dest->source write boundary, reached by `import` and `reconcile` write-back with ids/keys taken from a NATIVE config — which may be foreign, synced, or project-supplied. Without this, an id like "../../../tmp/x" joined into the path is an arbitrary-file-write (on write) / read (on read) primitive. Mirrors the CLI's validateMCPID; here it guards every Write*/Read* so no caller can bypass it.
func WriteCommand ¶
WriteCommand writes commands/<name>.md from cm into home. Overwrites atomically.
func WriteHooks ¶
WriteHooks writes hooks/<event>.toml for the given event. Overwrites atomically.
func WriteMarketplace ¶
func WriteMarketplace(home, name string, m Marketplace) error
WriteMarketplace writes marketplaces/<name>.toml from m into home. Overwrites atomically.
func WriteMemory ¶
WriteMemory writes memory/AGENTS.md (m.Body) plus every fragment in m.Fragments (memory/fragments/<name>, traversal-checked) into home. It is the faithful inverse of loadMemory — a reverse-collapse (CollapseMemoryMarkers) populates Fragments, so import/reconcile can restore a fragmented memory.
It still REFUSES the one dangerous shape: a flattened body (no Fragments) over a source that IS fragment-composed. That only happens when a native memory edit could not be reversed (markers absent/disabled), where writing the expanded body would inline every @import and orphan the fragment files; the caller surfaces it rather than flatten silently.
func WritePlugin ¶
WritePlugin writes plugins/<id>.toml from p into home. Overwrites atomically.
func WriteSkill ¶
WriteSkill writes skills/<name>/SKILL.md plus every bundled file (scripts/, references/, assets/, …) from sk into home. Each file is written atomically with its preserved permission bits; bundled content is written verbatim (never frontmatter-encoded), so binary assets round-trip byte-for-byte.
func WriteSubagent ¶
WriteSubagent writes agents/<name>.md from sa into home. Overwrites atomically.
Types ¶
type Canonical ¶
type Canonical struct {
Config Config
MCPServers []MCPServer
Skills []Skill
Subagents []Subagent
Commands []Command
Hooks []Hook
LSPServers []LSPServer
Plugins []Plugin
Marketplaces []Marketplace
Memory Memory
// Project holds the project-only canonical set by project.Merge. It is nil
// for a user-scope canonical (one loaded directly from ~/.agentsync/ without
// a project overlay). Scope-aware render paths (apply/status/diff/reconcile
// --scope project) render from Project instead of the merged canonical so
// user-scope items are not duplicated into the project directory.
Project *Canonical
}
Canonical is the in-memory image of a fully-loaded ~/.agentsync/ tree.
func Load ¶
Load reads a canonical model from <home>. Missing home or missing subdirectories return an empty Canonical (not an error). Malformed files return an error with a path prefix for actionability.
Load is plugin-UNAWARE: it does not project plugin manifests. Callers that need plugin components expanded into the model use marketplace.LoadProjected (which owns the single plugin projector). This keeps source free of any dependency on plugin/marketplace concepts.
type Command ¶
type Command struct {
Name string // filename without .md extension
Frontmatter map[string]any // YAML frontmatter
Body string // markdown body
}
Command mirrors commands/<name>.md (frontmatter + body). Slash commands in Claude live at ~/.claude/commands/<name>.md.
type Config ¶
type Config struct {
Agents map[string]Agent `toml:"agents"`
Updates UpdateDefaults `toml:"updates"`
Secrets SecretsConfig `toml:"secrets"`
}
Config mirrors agentsync.toml at the root of ~/.agentsync/.
type Hook ¶
type Hook struct {
Event string // e.g. "PreToolUse"
Matcher string // glob/regex string
Type string // "command"
Command string // shell command
}
Hook represents a single hook entry for an event.
type LSPServer ¶
type LSPServer struct {
ID string
Spec LSPServerSpec
}
LSPServer mirrors lsp/<id>.toml.
type LSPServerSpec ¶
type LSPServerSpec struct {
Command string `toml:"command,omitempty"`
Args []string `toml:"args,omitempty"`
Env map[string]string `toml:"env,omitempty"`
URL string `toml:"url,omitempty"`
Headers map[string]string `toml:"headers,omitempty"`
// Agents / Enabled mirror MCPServerSpec: they are source-only targeting
// fields that the rendered destination never carries, so capture must
// preserve them (via source.ReadLSP) rather than reset them from the dest.
Agents []string `toml:"agents,omitempty"` // ["*"] or ["claude",...]; empty = all
Enabled *bool `toml:"enabled,omitempty"` // nil means default-on
// Extra carries native LSP-server fields agentsync does not model, captured
// on ingest and rendered back verbatim. Passthrough only — see the note on
// MCPServerSpec.Extra (never secret-resolved or walked; the capture leak
// backstop scans it).
Extra map[string]any `toml:"extra,omitempty"`
}
LSPServerSpec holds the server configuration for an LSP server.
type MCPServer ¶
type MCPServer struct {
ID string `toml:"-"` // filename minus .toml
Server MCPServerSpec `toml:"server"`
}
MCPServer mirrors mcp/<id>.toml.
type MCPServerSpec ¶
type MCPServerSpec struct {
Type string `toml:"type"` // stdio | http | sse
Command string `toml:"command,omitempty"`
Args []string `toml:"args,omitempty"`
URL string `toml:"url,omitempty"`
Headers map[string]string `toml:"headers,omitempty"`
Env map[string]string `toml:"env,omitempty"`
Agents []string `toml:"agents,omitempty"` // ["*"] or ["claude","opencode"]
Enabled *bool `toml:"enabled,omitempty"` // nil means default-on
// Extra carries native MCP-server fields agentsync does not model (e.g.
// timeout, disabled, cwd), captured on ingest and rendered back verbatim so
// import/reconcile/apply are not lossy for them. It is PASSTHROUGH ONLY:
// values are never secret-resolved (a ${secret:…} here is written literally,
// not substituted) and never visited by walkSecretFields — so it stays out of
// the secret machinery. The capture leak backstop (secrets.ResidualSecretCleartext)
// scans it instead, refusing a write that would persist a live secret value.
Extra map[string]any `toml:"extra,omitempty"`
}
type Marketplace ¶
type Marketplace struct {
Name string `toml:"-"`
Marketplace MarketplaceSpec `toml:"marketplace"`
}
Marketplace mirrors marketplaces/<name>.toml.
type MarketplaceSpec ¶
type Memory ¶
type Memory struct {
Body string // resolved AGENTS.md after @import expansion
Fragments map[string]string // body, keyed by slash path under memory/fragments/ (loaded recursively)
}
Memory mirrors memory/AGENTS.md and memory/fragments/.
func CollapseMemoryMarkers ¶
CollapseMemoryMarkers reverses ExpandMemoryImports' marker emission: it parses a rendered memory file back into memory/AGENTS.md (with `@import` directives restored where each fragment block was) plus the fragment files. It is how import/reconcile capture a native memory edit back into the fragment structure instead of flattening it.
Returns (mem, true, nil) when balanced markers were found and collapsed; (zero, false, nil) when the input carries no markers (caller takes the plain path); (zero, true, err) when markers are present but malformed, unbalanced, reference a traversing path, or the same fragment appears twice with differing content — the caller must then refuse rather than guess.
type Plugin ¶
type Plugin struct {
ID string `toml:"-"`
Plugin PluginSpec `toml:"plugin"`
}
Plugin mirrors plugins/<id>.toml.
NOTE: a per-agent `[plugin.overrides.<agent>]` table is NOT wired in v1 — the projector does not consult it. It was previously a struct field that never parsed and was read nowhere, so it has been removed rather than left as a silent no-op. (Per-agent fan-out is still controllable via a component's `agents` allowlist.)
type PluginSpec ¶
type PluginSpec struct {
ID string `toml:"id"`
Version string `toml:"version,omitempty"`
ManifestSHA string `toml:"manifest_sha,omitempty"`
Update string `toml:"update,omitempty"` // pinned | track | manual
Agents []string `toml:"agents,omitempty"`
// Disabled, when true, suppresses the plugin's projection during
// marketplace.LoadProjected. `agentsync plugin disable <id>` sets this.
// Without honouring it there, the CLI's TOML write would be a no-op:
// projection would still surface the plugin's MCP servers / skills / etc.
// into the canonical model and apply would ship them.
Disabled bool `toml:"disabled,omitempty"`
}
type SecretsConfig ¶
type Skill ¶
type Skill struct {
Name string `toml:"-"` // dirname
Frontmatter map[string]any `toml:"-"` // YAML frontmatter parsed
Body string `toml:"-"` // markdown body
Files []SkillFile `toml:"-"` // bundled files other than SKILL.md
}
Skill mirrors a skill directory skills/<name>/. Per the Agent Skills spec a skill is a DIRECTORY whose only required member is SKILL.md (frontmatter + body); it may also bundle scripts/, references/, assets/, and arbitrary nested files. Files captures everything in the directory other than SKILL.md so apply/import/reconcile are not lossy for those resources.
type SkillFile ¶
SkillFile is one bundled resource inside a skill directory (e.g. scripts/extract.py, references/REFERENCE.md, assets/logo.png). Content is captured verbatim — never secret-substituted, never frontmatter-parsed — so binary assets round-trip byte-for-byte. Path is relative to the skill directory and slash-separated; Mode preserves the file's permission bits so executable scripts keep their +x bit.
func ReadSkillFiles ¶
ReadSkillFiles walks skillDir and returns every regular file other than SKILL.md as a SkillFile, with a slash-separated path relative to skillDir and the file's permission bits preserved. It is the single bundled-file capture implementation shared by the canonical loader and every adapter's Ingest, so the "a skill is a directory, not just SKILL.md" rule cannot drift between them. Non-regular files (symlinks, devices) are skipped — only real bundled resources are captured. Results are sorted by path for deterministic ordering.
type Subagent ¶
type Subagent struct {
Name string // filename without .md extension
Frontmatter map[string]any // YAML frontmatter (description, tools, model, color, etc.)
Body string // markdown body
}
Subagent mirrors agents/<name>.md (frontmatter + body). Subagents in Claude live at ~/.claude/agents/<name>.md.