Skip to content

Add an external render-adapter protocol for community agents #94

Description

@kelos-bot

🤖 Kelos Strategist Agent @gjkim42

Area: Integration Opportunities

Summary

Kanon's adapter backlog is now broad enough that adding every new agent directly to core will become a product bottleneck. There are open proposals for Cursor/opencode/Copilot CLI, Continue, Roo Code, Cascade/Windsurf, VS Code Copilot, Junie, Goose, Kiro, and Zed (#23, #62, #66, #72, #73, #74, #79, #91, #92). Those are valuable, but each one currently requires a core code change, a release, and ongoing maintenance as that agent's native config format changes.

Proposal: add an opt-in external render-adapter protocol. Kanon would keep ownership of source loading, validation, diff, merge policy, conflict handling, backups, and apply. A community or internal adapter executable would only receive a normalized render request on stdin and return a list of target files plus warnings on stdout.

This creates a stable integration surface for experimental or niche agents without weakening Kanon's source/target/destination model.

Evidence from the current codebase

  • The only agent constants are codex, claude, and all (internal/core/types.go).
  • adaptersFor is a hard-coded switch that can return only codexAdapter{} and claudeAdapter{} (internal/core/render.go).
  • The CLI rejects any --agent outside all|codex|claude (internal/cli/root.go).
  • validateTargets rejects every targets: value except all, codex, and claude (internal/core/config.go).
  • The existing Adapter interface is clean but compile-time only: Name(), Render(), Import() (internal/core/types.go).
  • RenderedFile already has the wire shape an external renderer needs to return: agent, destination path, content, mode, and merge strategy (internal/core/types.go).

Net: Kanon has a good internal adapter seam, but no runtime extension seam.

Existing issue overlap check

This is intentionally distinct from:

I searched open and closed issues for external adapter/plugin/protocol wording and did not find a focused proposal.

External signal

This is a proven pattern in mature CLI/config tools:

The common lesson: keep the host's lifecycle and state model stable, and let explicit external executables handle extension-specific behavior.

Proposed config

Keep this explicit and local-only in v1. No auto-discovery and no remote adapter execution from a shared kanon.yaml.

version: 1

# Existing configs stay unchanged. Declared external agents become valid targets.
external_adapters:
  zed-local:
    command: kanon-adapter-zed
    protocol: render.v1
    targets: [zed]
    allow_paths:
      - ~/.config/zed
      - ${PROJECT}/.zed

instructions:
  files:
    - instructions/shared.md

mcp:
  servers:
    github:
      command: github-mcp-server
      targets: [codex, claude, zed]

Notes:

  • external_adapters.<name>.targets declares target names that become valid in targets: and --agent.
  • command is resolved from PATH or an absolute path on the local machine.
  • allow_paths constrains returned destination paths. If omitted, default to paths under UserHome and, when --project is set, under Project.
  • protocol lets Kanon fail fast when an adapter speaks an incompatible wire contract.

Wire protocol sketch

Kanon invokes the adapter only during render:

kanon-adapter-zed render

Request on stdin:

{
  "protocol": "render.v1",
  "operation": "render",
  "agent": "zed",
  "config": { "version": 1, "instructions": {}, "skills": [], "mcp": {}, "hooks": [] },
  "target": {
    "kanonHome": "/home/alice/.config/kanon",
    "userHome": "/home/alice",
    "project": "/home/alice/src/repo"
  }
}

Response on stdout:

{
  "protocol": "render.v1",
  "files": [
    {
      "agent": "zed",
      "path": "/home/alice/src/repo/.zed/settings.json",
      "mode": "0644",
      "contentBase64": "eyJjb250ZXh0U2VydmVycyI6e319Cg==",
      "merge": { "strategy": "replace" }
    }
  ],
  "warnings": []
}

Kanon then validates paths, decodes content, sorts/deduplicates, and feeds the resulting RenderedFiles into the existing diff / apply path.

Merge policy

Do not let external adapters mutate destination files directly. Kanon should remain the only writer.

For v1, allow:

  • replace: adapter owns the whole returned file.
  • json_top_level, toml_top_level, yaml_top_level: adapter owns named top-level keys, and Kanon performs the structured merge while preserving unmanaged keys.

Unknown merge strategies should fail validation. Richer merge drivers can come later, but the key point is that external adapters describe ownership and Kanon executes the merge.

CLI surface

Suggested minimal additions:

kanon adapter list
kanon adapter inspect zed-local
kanon render --agent zed
kanon diff --agent zed --project .
kanon apply --agent zed --project . --yes

adapter inspect calls the executable with an inspect operation and prints content-free capabilities: protocol version, supported target names, render/import support, merge strategies used, and native files it may emit.

Import support should be deferred. Rendering is the growth bottleneck; import requires each adapter to normalize destination state back into Kanon source, which is much riskier.

Guardrails

  • External adapters are never auto-discovered in v1. They must be declared locally and explicitly.
  • Do not fetch adapter binaries from URLs in kanon.yaml; Declare agent runtime dependencies for MCP servers and hooks #52-style runtime/dependency metadata can describe installation later.
  • Do not pass secret values beyond what already exists in the resolved config/environment. Redact adapter stderr in the same places Kanon redacts git URLs.
  • Adapter stderr should be included only on failure and should be capped to avoid dumping large prompts or secrets.
  • Returned paths must normalize under allowed roots and must not escape via symlinks or .. segments.
  • Same-path collisions across built-in and external adapters should fail unless content and merge ownership are identical.
  • External adapters should be opt-in targets; existing --agent all behavior should not start executing external commands unless the config explicitly enables them.

Use cases unlocked

  1. Internal enterprise agents: a company can maintain kanon-adapter-internal for a private IDE/agent without upstreaming proprietary native config logic.
  2. Fast-moving public agents: community adapters for Zed/Kiro/Goose/etc. can iterate at the agent's release pace while Kanon core stays stable.
  3. Experimental targets: users can prototype render mappings before asking Kanon to accept a built-in adapter.
  4. Platform-team specialization: orgs can enforce local destination policies and merge ownership while reusing Kanon's neutral source schema and apply planner.

Suggested MVP scope

  1. Add ExternalAdapters map[string]ExternalAdapter to Config.
  2. Extend target validation so names declared by external adapters are valid in targets: and --agent.
  3. Add an externalAdapter implementation that invokes command render with JSON stdin/stdout.
  4. Support replace plus one generic structured top-level merge strategy, preferably JSON first.
  5. Add path-root validation and same-path collision detection before planning.
  6. Add kanon adapter list and kanon adapter inspect.
  7. Add tests with a small fake adapter executable/script covering successful render, protocol mismatch, invalid path escape, unsupported merge, target filtering, and adapter stderr redaction.

Backward compatibility

  • Existing kanon.yaml files are unchanged.
  • Built-in Codex and Claude behavior stays the default.
  • External adapters are opt-in and local-only.
  • Existing render/diff/apply/import commands keep their current behavior when no external adapter is declared.

Non-goals

  • Do not create a remote adapter registry in v1.
  • Do not manage agent-native plugin marketplaces or install state.
  • Do not require existing built-in adapters to move out of core.
  • Do not implement import protocol in the first increment.
  • Do not let adapters write destination files themselves; Kanon remains the applier.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions