feat(agents): per-instance plugin enablement #114
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/72"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements the service-side of #72 — the operator picks which baked plugins each agent instance loads at dispatch time, independent of its type, without rebuilding the image or bouncing the container.
pluginscolumn (+ forward ALTER migration for existingDBs). Resolution rule:
NULLinherits the type'splugins: [...]default, an explicit
[]disables every plugin (debug escape hatch),a non-null array fully overrides.
ResolvedAgent.pluginsis populated bymergeAgentfrom row override or type default;
AgentTypeConfig.default_pluginsparses the new
plugins: [...]array fromconfig/agents.jsonwithnon-string entries silently dropped.
writeInstanceSettings(new helper incontainer.ts): rewrites<credentialsHostDir>/settings.json.enabledPluginsbefore spawningquery(). Preserves every other top-level key (e.g.extraKnownMarketplacesfromclaude plugin install), idempotent(no write when byte-identical), fail-loud on malformed plugin ids.
runAgentTaskcallswriteInstanceSettingsin container moderight after
assertContainerRunningand before worktree setup sothe in-container CLI picks up the fresh file on the next dispatch.
§ Per-agent Claude Code pluginsgrows an"Install vs. enable" subsection with the resolution table and the
operator flow for per-instance overrides.
Ref #72. Does not close — the image-side bake-in (Dockerfile plugin
steps,
scripts/smoke-creds.shplugin-presence probe),config/agents.jsonplugins: [...]entries per type, and READMEone-liner are tracked separately and will close #72 when they land.
Test plan
bun x tsc --noEmit— clean.bun x biome check src/+bun x biome format src/— clean.bun test— 392 pass, 0 fail (new: 12 tests in db / container /webhook-config suites covering resolver inheritance, explicit
empty-set escape hatch, enable-set preservation vs. rewrite,
malformed id rejection, key preservation during rewrite).
just agent-env-sync+just agent-plugins-installon a test instance, flippluginscolumn to[], dispatch a task, assertsettings.json.enabledPlugins == {}andclaude plugin listinside the container reports every plugin as disabled.
🤖 Generated with Claude Code
6fe0277dde981471c8e8test
Review: feat(agents): per-instance plugin enablement
CI: green (run #1667, 2m55s, all jobs passed).
The service-side implementation is clean and well-structured. The SQLite migration, resolver semantics, writeInstanceSettings helper, and test coverage are all solid. One critical issue prevents merge: the PR body says "Closes #72" but several required acceptance criteria from that issue are not implemented, so merging would auto-close the issue and silently drop those items.
REQUEST_CHANGES: "Closes #72" is premature — will auto-close with unchecked ACs
"Closes #72" in the PR body causes Forgejo to auto-close issue #72 on merge. The following acceptance criteria from that issue are not implemented in this PR:
Image — bake in the approved plugin set:
Config:
Documentation:
Fix: Change "Closes #72" to e.g. "Implements the service-side of #72" so the issue stays open for the Dockerfile/smoke-creds.sh/config work. Or create a follow-on issue for those items before merging.
Informational: dispatch with no plugins: in agents.json silently writes enabledPlugins: {} on every task
File: src/agent-runner.ts (the if (credsDir && config.plugins !== undefined) guard), src/main.ts
main.ts always passes plugins: agent.plugins to WorkerConfig. Since ResolvedAgent.plugins is always string[] — including [] when agents.json has no plugins: key for a type — config.plugins is never undefined in production. The guard therefore never skips the write in production; it only matters in test harnesses that build WorkerConfig manually.
This is harmless today (no plugins are installed in the image per the issue context), but once just agent-plugins-install has been run, any operator-installed plugin NOT listed in agents.json will be silently disabled on the next dispatch. The comment says "Skipped when the agent has no plugin list at all" but the condition is never true in the production path. Not blocking, but the comment is misleading and worth fixing when agents.json defaults land.
What looks good
Thanks — addressed both points.
REQUEST_CHANGES: "Closes #72" auto-close — rewrote the PR body: header now reads "Implements the service-side of #72" and there's an explicit "Ref #72. Does not close — …" paragraph listing what still needs to land (Dockerfile bake-in,
scripts/smoke-creds.shprobe,agents.jsonplugins: [...]entries, README one-liner). Issue #72 stays open on merge.Informational: misleading comment on the
plugins !== undefinedguard — fixed in4a331ee. New comment explicitly states the guard never short-circuits in the production path (ResolvedAgent.pluginsis always a possibly-emptystring[]frommergeAgent), and attributes the!== undefinedcheck to test harnesses that build bareWorkerConfigobjects. Empty array →enabledPlugins: {}(explicit opt-out).No source behaviour change;
bun x tsc --noEmitandbun x biome check src/both clean.