Files
Plexum-openai-provider/internal/runner/mcp_register.go
hzhang a27f6bd067 feat(codex): plexum-host MCP exposure + real consume mirror
Three-part wiring so codex agents get full dynamic-tool consume
support:

1. mcp-host subcommand in the plugin binary (routes argv[1] to
   mcpbridge.Run). Reads PLEXUM_MCP_SOCKET / PLEXUM_MCP_AGENT_ID
   from env, baked into codex's `mcp add --env` registration.

2. EnsureCodexMCPRegistered registers a per-agent stable server
   name (plexum-host-<sanitised>) lazily on first turn. Stable
   per-agent socket path via StableSocketPath so codex's
   registration entry and the per-turn bridge listener agree.

3. Post-turn ParseRolloutToolCalls + EmitCodexToolCalls walks the
   matching rollout-*-<thread_id>.jsonl and emits canonical events
   for every function_call + function_call_output pair using
   codex's REAL call_X ids. dynamic.jsonl now has block_ids that
   match codex's session, so consume mirror has real targets.

4. MutateCodexSession rewrites those response_item entries on
   consume: function_call.arguments → "{}" (heavy), output → marker.
   E2E verified: codex resume sees "...(tool called)" instead of
   the original output.
2026-06-01 20:14:06 +01:00

77 lines
2.4 KiB
Go

// MCP server registration. codex's `mcp add` writes into the user's
// global config; we use a per-agent stable name so multiple Plexum
// agents using codex don't collide. Registration is idempotent and
// performed lazily before the first turn that needs it.
package runner
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
var registered sync.Map // agentID → bool (registration done this process)
// EnsureCodexMCPRegistered makes sure codex's user config has an MCP
// server entry for this agent named plexum-host-<sanitized>. Returns
// the entry's tool-name namespace prefix so the caller can build
// req.Tools wire shapes if needed.
//
// The socket path baked into the env is StableSocketPath(agentID) —
// the same value the per-turn bridge listens on.
func EnsureCodexMCPRegistered(ctx context.Context, codexBinary, pluginBinary, agentID string) (string, error) {
name := codexServerName(agentID)
if _, ok := registered.Load(name); ok {
return name, nil
}
// Remove first so we don't get "already exists"; ignore error.
_ = exec.CommandContext(ctx, codexBinary, "mcp", "remove", name).Run()
args := []string{
"mcp", "add",
"--env", "PLEXUM_MCP_SOCKET=" + StableSocketPath(agentID),
"--env", "PLEXUM_MCP_AGENT_ID=" + agentID,
name, "--", pluginBinary, "mcp-host",
}
cmd := exec.CommandContext(ctx, codexBinary, args...)
if out, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("codex mcp add: %w: %s", err, strings.TrimSpace(string(out)))
}
registered.Store(name, true)
return name, nil
}
// StableSocketPath returns the per-agent unix socket path the bridge
// listens on AND codex's mcp-host subprocess dials into. Keeping it
// stable lets us register codex's MCP server once and reuse it.
func StableSocketPath(agentID string) string {
return filepath.Join(os.TempDir(), "plexum-codex-mcp-"+sanitize(agentID)+".sock")
}
// codexServerName produces the per-agent MCP server name codex stores
// in its global config. Names need to be filesystem-safe; we just
// reuse the sanitised agent id.
func codexServerName(agentID string) string {
return "plexum-host-" + sanitize(agentID)
}
func sanitize(s string) string {
out := make([]byte, 0, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z',
c >= '0' && c <= '9', c == '-', c == '_':
out = append(out, c)
default:
out = append(out, '_')
}
}
return string(out)
}