mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
The agentcontext resolver recognizes Agent Plugins (agent-plugins.org, spec 1.0.0) directories and emits them as KindPlugin resources with their skills attributed to the plugin. Discovery: a plugin is an immediate child of plugins/ or .agents/plugins/ under a scan root, or a scan root that itself holds a plugin.json. A plugin.json at a scan root, or in plugins/ (a name other tools also use), whose $schema is absent or not an https://agent-plugins.org/schemas/ URL may belong to another tool and is ignored after a debug log; at a scan root an unreadable or malformed file is ignored too. Every other problem is reported as an invalid plugin with the reason, and in .agents/plugins/ every plugin.json is taken as an Agent Plugins manifest. Rejected and duplicate manifests are logged at Debug through Resolver.Logger; their rows carry the error. A scan root that validates as a plugin keeps its instruction files, .mcp.json and other skill containers, but its skills/ belongs to the plugin and its plugin containers are not scanned. Symlinked plugin directories and plugin containers are skipped, as is a plugin or plugin skill directory whose path is longer than workspacesdk.MaxContextSourceBytes (1024), coderd's Source cap, or whose name is not valid UTF-8. The plugin root is canonicalized once and is the containment boundary for plugin.json, the skills/ container, each skill directory, and every SKILL.md: following spec section 4.1, each may be a symlink that resolves inside the plugin root. An escaping skills/ disables the skills component with a warning on the plugin resource, and an escaping skill directory or SKILL.md is reported as an invalid skill. Canonicalization falls back to the cleaned path when EvalSymlinks fails, which it does on Windows substituted drives, but only for an existing directory with no link between the scan root and the path. Validation follows spec section 5 by hand: name grammar (via workspacesdk.ValidatePluginName), optional string fields, keywords as strings, closed author object, extensions as object. Unknown top-level fields and a non-object extensions value are recorded in Resource.Error while the status stays OK; every other violation, an unsupported schema version, invalid JSON, or an oversize manifest rejects the plugin and loads no components. Version and description are sanitized and capped at 64 and 1024 runes before they reach the resource; the spec sets no limit on either, so the caps are Coder's. Skills are deduplicated by (PluginName, Name) so a plugin skill and a plain skill of the same name both survive. A plugin name already claimed by another plugin root makes later same-named plugins StatusInvalid with no components. After the caps, skills and MCP servers whose plugin is not StatusOK (for example one the count cap excluded) are dropped, since coderd rejects a push holding them. Resources are deduplicated on Source as they are found, across kinds, for the same reason: a skill directory that is also a scan-root plugin keeps the earlier scan root's row unless that row is invalid and the later one valid; skill directories are compared after resolving symlinks. A valid plugin whose row is not kept claims no name, emits no skills, and is still treated as a plugin root, so its plugin containers are not scanned. A rejected plugin that loses to an OK skill adds its error to the skill row as a warning. Resource gains PluginName, set on resources that belong to a plugin, and PluginVersion. The DRPC pusher maps KindPlugin to PluginBody and sets plugin_name on skill and MCP server bodies. When coderd rejects a push that carries plugin data, the pusher retries it once without plugin resources or plugin-attributed resources, with the aggregate hash recomputed and a notice naming the rejection in SnapshotError. If that push is accepted, the pusher omits plugin data for the rest of the connection. Older coderd versions reject the PluginBody variant, so context keeps syncing and the rejection is visible in the chat UI. Every resource error has invalid UTF-8 replaced and is truncated on a UTF-8 boundary to workspacesdk.MaxContextErrorBytes (4096). coderd's agentapi now uses that constant and workspacesdk.MaxContextSourceBytes in place of its own. Plugin discovery is always on in the agent; coderd drops plugin data when the agent-plugins experiment is off. Watcher syncs are serialized and each reads the current scan roots, so an older root set never replaces a newer one. The fsnotify watcher registers the plugin containers under each scan root (or the existing parent of a missing .agents/plugins), each immediate plugin directory, its skills/ container, and the skill directories the resolver would read, and treats plugin.json events as relevant. Skills are watched only for a directory holding a plugin.json, and in a plugin directory only plugin.json and skills events re-resolve. A scan root that holds a plugin.json has its skills/ watched the same way. Sync publishes each directory's watch role through an atomic pointer, and the event loop reads it without the watcher lock, so filtering an event never waits on a Sync that is blocked in fsnotify Add or Remove on Windows. testutil.FakeSink holds its lock across t.Log, so a goroutine logging as the test ends can no longer log after the test completed. The built-in ~/.claude/plugins/cache scan root is removed: Claude's .claude-plugin manifests are not read. When the manifest has no directory the agent logs, once at Info, a notice listing the working-directory entries it does not scan (instruction files, .mcp.json, skill containers, plugin.json and the plugin containers) and pointing at ~/.coder/.agents/plugins, since templates without coder_agent.dir otherwise give no signal. The agent socket's ContextResource and the HTTP API's SnapshotResource gain plugin_name and plugin_version, and `coder exp chat context show` prints a plugin column.