AOI-2: MCP env-merge at instance scope #731
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#731
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
As an operator running multiple instances of an agent type that need different MCP credentials (e.g. per-instance Penpot tokens or per-instance secrets paths), I want instance-scope
mcp_serverrows to merge theirenvmap into the inherited row, so that I can tweak per-instance secrets without redefiningcommand/args/transport/url— while forge MCP auth stays bound to type identity by construction.Acceptance criteria
Resolver
resolveMcpServers(apps/server/src/domain/agent-config/resolver.ts) detects when an instance-scope row'snamematches an inherited row fromagent_type/global/builtinand produces a merged record:env: instance keys override parent keys per-key; absent keys inheritcommand/args/transport/url: inherited from parent; instance row's values for these MUST be null OR identical to parent (validation rejects divergence)name) keeps current full-row behavior.enabled=falseinvariant still holds at instance scope.Forge MCP identity guardrail (option C)
name = "forge", formerly"forgejo") is special-cased: the resolver synthesizes its auth env from the agent's type-level identity (ResolvedAgent.token_file/forgejo_user) at render time, ignoring anyenvoverride at any scope for the auth keys (FORGEJO_ACCESS_TOKEN,FORGE_TOKEN_FILE, or whichever keys the binary reads).mcp-builtin-defs.json(e.g.mcp-builtin-locks.jsonmapping MCP name → array of locked env keys). Builtin layer is the source of truth; operator cannot edit at runtime.mcp_server.env(any scope ≥ global) that touch a locked key, with:"<env_key> on <mcp_name> is bound to type identity; remove from override".github,gitlabMCP names), they get the same lock treatment automatically by appearing inmcp-builtin-locks.json.Validation
namematches an inherited row AND whosecommand,args,transport, orurldiffers from inherited → reject with:"instance scope can only override env on inherited MCP; differing <field> rejected".mcp-builtin-locks.json) → rejected per forge-identity guardrail above.Render path
agent-env-sync.renderForInstance(apps/server/src/infrastructure/agent-env-sync/render-for-instance.ts) writes the merged env into the materialized.mcp.json(or the MCP config file the agent reads).ResolvedAgent.token_file/forgejo_user. Locked keys never come from the resolver's merged env.envis not written separately; the resolver's merged output (minus locked keys) is the single source for non-locked env.Tests
{LOG_LEVEL: "debug"}) merges with parent env (e.g.{LOG_LEVEL: "info", X: "y"}) → result{LOG_LEVEL: "debug", X: "y"}.commandrejected at validation.renderForInstance(agent).forgewithenv.FORGEJO_ACCESS_TOKENset → validation rejects.renderForInstancewrites the type-derived value, not the row's value.forge(e.g.LOG_LEVEL) still works as expected.Out of scope
agent_typeorglobalscope (those still replace today). Revisit if a use case appears; out of this story.github/gitlabnames without code changes.References
apps/server/src/infrastructure/agent-env-sync/render-for-instance.tsapps/server/src/domain/agent-config/resolver.tsresolveMcpServersconfig/mcp-builtin.json/config/mcp-builtin-defs.json(sibling: newmcp-builtin-locks.json)enabled=falseat instance scope).🦵 @charles kicked the queue — re-running implement on @code-lead.