Shared app servers: the PreToolUse hook leaves a ticket naming the calling session, the MCP server takes it per call (middleware), so two sessions of one app on one Harbor server act as themselves; i_am checks instead of binding and is allowed under a stop
This commit is contained in:
1 parent
e283e4cc54
commit
fdaa3b0806
4 files changed
+131
-18
No files matched your search
+40
-17
@@ -5,6 +5,7 @@ Run with the agent's identity fixed in its environment (it cannot be changed fro
|
||||
The project comes from $HARBOR_PROJECT or the .harbor.toml above the working directory. The human is refused.
|
||||
"""
|
||||
import asyncio
|
||||
import contextvars
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
@@ -14,8 +15,9 @@ os.environ.setdefault("FASTMCP_CHECK_FOR_UPDATES", "off") # no network call
|
||||
os.environ.setdefault("FASTMCP_SHOW_SERVER_BANNER", "false")
|
||||
from fastmcp import FastMCP # noqa: E402
|
||||
from fastmcp.exceptions import ToolError # noqa: E402
|
||||
from fastmcp.server.middleware import Middleware # noqa: E402
|
||||
|
||||
from . import projects # noqa: E402
|
||||
from . import projects, sessions # noqa: E402
|
||||
from .core import OPEN_ITEM_STATES, Harbor, HarborError # noqa: E402
|
||||
|
||||
INSTRUCTIONS = """Harbor is the team's shared coordination store: messages in threads, acknowledgements, claims, a
|
||||
@@ -55,6 +57,30 @@ ClaimKind = Literal["draft", "naming", "edit", "review", "band"]
|
||||
mcp = FastMCP("harbor", instructions=INSTRUCTIONS)
|
||||
_lock = threading.Lock()
|
||||
_state = {"seen": 0.0}
|
||||
_caller = contextvars.ContextVar("harbor_caller", default=None) # the session behind the current tool call
|
||||
|
||||
|
||||
class SessionIdentity(Middleware):
|
||||
"""One app may share this server between several sessions (Codex and Codex2). The app's PreToolUse hook names
|
||||
the calling session in a ticket; take it so this call runs as that agent (harbor.sessions)."""
|
||||
|
||||
async def on_call_tool(self, context, call_next):
|
||||
agent = None
|
||||
try:
|
||||
message = context.message
|
||||
with _lock:
|
||||
agent = sessions.take(harbor(), os.environ.get("HARBOR_HOST", ""), message.name,
|
||||
dict(message.arguments or {}))
|
||||
except Exception: # noqa: BLE001 - identity falls back to the server's own
|
||||
agent = None
|
||||
token = _caller.set(agent)
|
||||
try:
|
||||
return await call_next(context)
|
||||
finally:
|
||||
_caller.reset(token)
|
||||
|
||||
|
||||
mcp.add_middleware(SessionIdentity())
|
||||
|
||||
|
||||
def harbor():
|
||||
@@ -67,9 +93,9 @@ def harbor():
|
||||
|
||||
def me():
|
||||
h = harbor()
|
||||
# a session bound with i_am, else the agent its working folder names (when it runs in the same app as the
|
||||
# installed one: Codex and Codex2 share the Codex app), else the installed one
|
||||
agent = _state.get("bound") or h.project.session_identity(os.environ.get("HARBOR_AGENT"), os.getcwd()) or ""
|
||||
# the session its app's hook named for this call (several sessions can share this server), else the agent the
|
||||
# server's working folder names when it runs in the same app as the installed one, else the installed one
|
||||
agent = (_caller.get() or h.project.session_identity(os.environ.get("HARBOR_AGENT"), os.getcwd()) or "")
|
||||
if agent not in h.project.ai_names:
|
||||
raise HarborError(f"HARBOR_AGENT must be one of {', '.join(h.project.ai_names)} (set it in this server's MCP "
|
||||
"config)")
|
||||
@@ -125,22 +151,19 @@ def run(fn):
|
||||
|
||||
@mcp.tool
|
||||
def i_am(agent: str) -> dict:
|
||||
"""Say which of the project's agents this session is, when one app runs several (Codex and Codex2). Call it once
|
||||
at the start of the session if your instructions or your working folder name you; only an agent of the same
|
||||
app is accepted, and a server already bound to another agent refuses."""
|
||||
"""Check which of the project's agents Harbor takes this session to be, when one app runs several (Codex and
|
||||
Codex2). Call it at the start of the session with the name your instructions give you. Harbor knows the session
|
||||
from its working folder (through the app's hooks); if the answer is not you, stop and tell the user."""
|
||||
def do(h):
|
||||
pinned = os.environ.get("HARBOR_AGENT") or ""
|
||||
if agent not in h.project.ai_names:
|
||||
raise HarborError(f"{agent} is not one of {', '.join(h.project.ai_names)}")
|
||||
if pinned and h.project.host_of(agent) != h.project.host_of(pinned):
|
||||
raise HarborError(f"{agent} runs in another app ({h.project.host_of(agent) or 'unknown'}), not this one")
|
||||
bound = _state.get("bound")
|
||||
if bound and bound != agent:
|
||||
raise HarborError(f"this Harbor server is already {bound}: the app shares one server between sessions, "
|
||||
"so give each session its own app profile instead")
|
||||
_state["bound"] = agent
|
||||
return {"you_are": agent, "working_folder": os.getcwd(),
|
||||
"folder_says": h.project.identity_for(os.getcwd())}
|
||||
seen = me()
|
||||
out = {"you_are": seen, "matches": seen == agent}
|
||||
if seen != agent:
|
||||
out["fix"] = (f"Harbor sees this session as {seen}, not {agent}. Check that this session works in "
|
||||
f"{agent}'s own worktree and that the app's Harbor hooks are installed and trusted; do no "
|
||||
"work until it matches.")
|
||||
return out
|
||||
return run(do)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user